[UE GAS] 캐릭터의 광역 스킬 구현

캐릭터에 다양한 기능을 가진 광역 공격 스킬을 부여하기
- 플레이어 캐릭터에 새로운 스킬 GA를 부여하고, 범위 공격을 추가하도록 설계를 확장하기
- 새로운 어트리뷰트 세트를 추가해 확장하기
- GA에 사용 비용과 쿨다운의 제약 사항 추가하기
- 게임플레이 이펙트 실행 계산 클래스를 사용해 거리에 따른 광역 데미지 감소 적용하기
인프런 이득우님의 '언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템' 강의를 참고하였습니다.
😎 [이득우의 언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템] 강의 들으러 가기!
목차
기획
플레이어 캐릭터에 신규 광역 스킬 부여
- 무기를획득하면 마우스 우클릭으로 스킬을 사용할 수 있는 기능을 추가
- 스킬 반경과 데미지 계산은 새로운 어트리뷰트 값을 사용 ( SkillRange, SkillAttackRate )
- 스킬은 광역스킬이며, 다수의 액터에게 피해를 전달하고 폭발장식 이펙트를 발생시킴
- 스킬에는 에너지를 소비하며, 에너지가 모자라면 발동이 안됨 ( SkillEnergy )
- 스킬 시전 후 쿨다운이 발동되며, 쿨다운이 끝날 때까지 발동이 안됨
- 스킬이 주는 데미지는공격자와의 거리에 따라 선형적으로 감소함.
핵심 컴포넌트


새로운 범위 공격 스킬 부여
ABGASCharacterPlayer - 몽타주 에셋 추가
ABGASCharacterPlayer.h
#pragma once #include "CoreMinimal.h" #include "Character/ABCharacterPlayer.h" #include "AbilitySystemInterface.h" #include "Abilities/GameplayAbilityTypes.h" #include "ABGASCharacterPlayer.generated.h" UCLASS() class ARENABATTLEGAS_API AABGASCharacterPlayer : public AABCharacterPlayer, public IAbilitySystemInterface { GENERATED_BODY() public: AABGASCharacterPlayer(); FORCEINLINE virtual class UAnimMontage* GetSkillActionMontage() const { return SkillActionMontage; } virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override; virtual void PossessedBy(AController* NewController) override; virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; protected: void SetupGASInputComponent(); void GASInputPressed(int32 InputId); void GASInputReleased(int32 InputId); UFUNCTION() virtual void OnOutOfHealth(); void EquipWeapon(const FGameplayEventData* EventData); void UnequipWeapon(const FGameplayEventData* EventData); UPROPERTY(EditAnywhere, Category = GAS) TObjectPtr<class UAbilitySystemComponent> ASC; UPROPERTY(EditAnywhere, Category = GAS) TArray<TSubclassOf<class UGameplayAbility>> StartAbilities; UPROPERTY(EditAnywhere, Category = GAS) TMap<int32, TSubclassOf<class UGameplayAbility>> StartInputAbilities; UPROPERTY(VisibleAnywhere) TObjectPtr<class UABGASWidgetComponent> HpBar; UPROPERTY(EditAnywhere, Category = Weapon) TObjectPtr<class USkeletalMesh> WeaponMesh; UPROPERTY(EditAnywhere, Category = Weapon) float WeaponRange; UPROPERTY(EditAnywhere, Category = Weapon) float WeaponAttackRate; UPROPERTY(EditAnywhere, Category = GAS) TSubclassOf<class UGameplayAbility> SkillAbilityClass; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Animation) TObjectPtr<class UAnimMontage> SkillActionMontage; // 스킬 몽타주 };
인라인 함수 추가
- FORCEINLINE virtual class UAnimMontage* GetSkillActionMontage() const { return SkillActionMontage; }
스킬 몽타주 변수 추가
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Animation)
TObjectPtr<class UAnimMontage> SkillActionMontage;
ABGASCharacterPlayer.cpp
#include "Character/ABGASCharacterPlayer.h" #include "AbilitySystemComponent.h" #include "Player/ABGASPlayerState.h" #include "EnhancedInputComponent.h" #include "UI/ABGASWidgetComponent.h" #include "UI/ABGASUserWidget.h" #include "Attribute/ABCharacterAttributeSet.h" #include "Tag/ABGameplayTag.h" AABGASCharacterPlayer::AABGASCharacterPlayer() { ASC = nullptr; static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_ComboAttack.AM_ComboAttack'")); if (ComboActionMontageRef.Object) { ComboActionMontage = ComboActionMontageRef.Object; } HpBar = CreateDefaultSubobject<UABGASWidgetComponent>(TEXT("Widget")); HpBar->SetupAttachment(GetMesh()); HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f)); static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HpBar.WBP_HpBar_C")); if (HpBarWidgetRef.Class) { HpBar->SetWidgetClass(HpBarWidgetRef.Class); HpBar->SetWidgetSpace(EWidgetSpace::Screen); HpBar->SetDrawSize(FVector2D(200.0f, 20.f)); HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision); } static ConstructorHelpers::FObjectFinder<USkeletalMesh> WeaponMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/InfinityBladeWeapons/Weapons/Blunt/Blunt_Hellhammer/SK_Blunt_HellHammer.SK_Blunt_HellHammer'")); if (WeaponMeshRef.Object) { WeaponMesh = WeaponMeshRef.Object; } WeaponRange = 75.f; WeaponAttackRate = 100.0f; static ConstructorHelpers::FObjectFinder<UAnimMontage> SKillActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_SkillAttack.AM_SkillAttack'")); if (SKillActionMontageRef.Object) { SkillActionMontage = SKillActionMontageRef.Object; } } UAbilitySystemComponent* AABGASCharacterPlayer::GetAbilitySystemComponent() const { return ASC; } void AABGASCharacterPlayer::PossessedBy(AController* NewController) { Super::PossessedBy(NewController); AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>(); if (GASPS) { ASC = GASPS->GetAbilitySystemComponent(); ASC->InitAbilityActorInfo(GASPS, this); ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONEQUIP).AddUObject(this, &AABGASCharacterPlayer::EquipWeapon); ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONUNEQUIP).AddUObject(this, &AABGASCharacterPlayer::UnequipWeapon); const UABCharacterAttributeSet* CurrentAttributeSet = ASC->GetSet<UABCharacterAttributeSet>(); if (CurrentAttributeSet) { CurrentAttributeSet->OnOutOfHealth.AddDynamic(this, &ThisClass::OnOutOfHealth); } for (const auto& StartAbility : StartAbilities) { FGameplayAbilitySpec StartSpec(StartAbility); ASC->GiveAbility(StartSpec); } for (const auto& StartInputAbility : StartInputAbilities) { FGameplayAbilitySpec StartSpec(StartInputAbility.Value); StartSpec.InputID = StartInputAbility.Key; ASC->GiveAbility(StartSpec); } SetupGASInputComponent(); APlayerController* PlayerController = CastChecked<APlayerController>(NewController); PlayerController->ConsoleCommand(TEXT("showdebug abilitysystem")); } } void AABGASCharacterPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); SetupGASInputComponent(); } void AABGASCharacterPlayer::SetupGASInputComponent() { if (IsValid(ASC) && IsValid(InputComponent)) { UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 0); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AABGASCharacterPlayer::GASInputReleased, 0); EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 1); EnhancedInputComponent->BindAction(SkillAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 2); } } void AABGASCharacterPlayer::GASInputPressed(int32 InputId) { FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId); if (Spec) { Spec->InputPressed = true; if (Spec->IsActive()) { ASC->AbilitySpecInputPressed(*Spec); } else { ASC->TryActivateAbility(Spec->Handle); } } } void AABGASCharacterPlayer::GASInputReleased(int32 InputId) { FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId); if (Spec) { Spec->InputPressed = false; if (Spec->IsActive()) { ASC->AbilitySpecInputReleased(*Spec); } } } void AABGASCharacterPlayer::OnOutOfHealth() { SetDead(); // 사망 처리 } void AABGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData) { if (Weapon) { Weapon->SetSkeletalMesh(WeaponMesh); FGameplayAbilitySpec NewSkillSpec(SkillAbilityClass); NewSkillSpec.InputID = 2; if (!ASC->FindAbilitySpecFromClass(SkillAbilityClass)) { ASC->GiveAbility(NewSkillSpec); } const float CurrentAttackRange = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute()); const float CurrentAttackRate = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute()); ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange + WeaponRange); ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate + WeaponAttackRate); } } void AABGASCharacterPlayer::UnequipWeapon(const FGameplayEventData* EventData) { if (Weapon) { const float CurrentAttackRange = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute()); const float CurrentAttackRate = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute()); ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange - WeaponRange); ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate - WeaponAttackRate); FGameplayAbilitySpec* SkillAbilitySpec = ASC->FindAbilitySpecFromClass(SkillAbilityClass); if (SkillAbilitySpec) { ASC->ClearAbility(SkillAbilitySpec->Handle); } Weapon->SetSkeletalMesh(nullptr); } }
스킬 몽타주 에셋 설정
- AABGASCharacterPlayer::AABGASCharacterPlayer()
- static ConstructorHelpers::FObjectFinder<UAnimMontage> SKillActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_SkillAttack.AM_SkillAttack'"));
- if (SKillActionMontageRef.Object)
- SkillActionMontage = SKillActionMontageRef.Object;
무기 장착시 스킬 부여하기
- if (Weapon)
- FGameplayAbilitySpec NewSkillSpec(SkillAbilityClass);
- NewSkillSpec.InputID = 2;
- if (!ASC->FindAbilitySpecFromClass(SkillAbilityClass))
- ASC->GiveAbility(NewSkillSpec);
무기 탈착시 스킬 해제하기
- if (Weapon)
- FGameplayAbilitySpec* SkillAbilitySpec = ASC->FindAbilitySpecFromClass(SkillAbilityClass);
- if (SkillAbilitySpec)
- ASC->ClearAbility(SkillAbilitySpec->Handle);
BP_ABGASCharacterPlayer

ABGA_Skill + BPGA_Skill생성
GameAbility - ABGA_Skill 생성


ABGA_Skill.h
#pragma once #include "CoreMinimal.h" #include "Abilities/GameplayAbility.h" #include "ABGA_Skill.generated.h" UCLASS() class ARENABATTLEGAS_API UABGA_Skill : public UGameplayAbility { GENERATED_BODY() public: UABGA_Skill(); public: virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override; virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override; protected: UFUNCTION() void OnCompleteCallback(); UFUNCTION() void OnInterruptedCallback(); protected: UPROPERTY() TObjectPtr<class UAnimMontage> ActiveSkillActionMontage; };
ABGA_Skill.cpp
BPGA_Skill 생성

발동방지를 위해 Gameplay Tag 추가
BPGA_Attack / BPGA_CharacterJump
- Activation Blocked Tags : 지정한 태그일 때 해당 GA를 발동시키지 않는다


실행화면

애니메이션 노티파이 이벤트설정
AM_SkillAttack에 AnimNotify_GASAttackHitCheck 추가

Trigger Gameplay Tag
- Event.Character.Action.SkillHitCheck로 설정
BPGA_AttackHitCheck에 Ability Trigger 추가 +
BPGA_SkillHitCheck에 Ability Trigger 추가


각각의 Trigger - Ability Triggers
- Trigger Tag : Event.Character.Action.AttackHitCheck / Event.Character.Action.SkillHitCheck 태그 추가
BP_ABGASCharacterPlayer에 BPGA_SkillHitCheck 등록하

GAS - Start Abilities
- BPGA_SkillHitCheck 추가
실행화면

스킬 공격을 하면 아래에 SkillHitCheck 로그가 찍힌다.
범위 공격 감지하기
ABGA_AttackHitCheck
ABGA_AttackHitCheck.h
#pragma once #include "CoreMinimal.h" #include "Abilities/GameplayAbility.h" #include "ABGA_AttackHitCheck.generated.h" UCLASS() class ARENABATTLEGAS_API UABGA_AttackHitCheck : public UGameplayAbility { GENERATED_BODY() public: UABGA_AttackHitCheck(); virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override; protected: UFUNCTION() void OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle); UPROPERTY(EditAnywhere, Category = "GAS") TSubclassOf<class UGameplayEffect> AttackDamageEffect; UPROPERTY(EditAnywhere, Category = "GAS") TSubclassOf<class UGameplayEffect> AttackBuffEffect; float CurrentLevel; // CurveTable의 현재레벨 변수 UPROPERTY(EditAnywhere, Category = "GAS") TSubclassOf<class AABTA_Trace> TargetActorClass; };
변수 추가
- UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class AABTA_Trace> TargetActorClass;
ABGA_AttackHitCheck.cpp
#include "GA/ABGA_AttackHitCheck.h" #include "ArenaBattleGAS.h" #include "AbilitySystemBlueprintLibrary.h" #include "GA/AT/ABAT_Trace.h" #include "GA/TA/ABTA_Trace.h" #include "Attribute/ABCharacterAttributeSet.h" #include "ArenaBattleGAS.h" #include "Tag/ABGameplayTag.h" UABGA_AttackHitCheck::UABGA_AttackHitCheck() { InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor; } void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { ABGAS_LOG(LogABGAS, Log, TEXT("%s"), *TriggerEventData->EventTag.GetTagName().ToString()); Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); CurrentLevel = TriggerEventData->EventMagnitude; UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass); AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback); AttackTraceTask->ReadyForActivation(); } void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle) { // TargetDataHandle는 배열로 들어오기 때문에 0번 인덱스(=첫번째 데이터)에 이것이 있는지 확인 if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0)) // 있다면 { FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0); ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName())); UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked(); // 소스에서 타겟으로 넘겨야 하므로 체크로 확인한 AbilitySystemComponent를 가져옴 UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor()); // 물리 판정을 감지한 타겟 액터 const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>(); FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel); if (EffectSpecHandle.IsValid()) { //EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttribute->GetAttackRate()); ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle); FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle); CueContextHandle.AddHitResult(HitResult); FGameplayCueParameters CueParam; CueParam.EffectContext = CueContextHandle; TargetASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam); } FGameplayEffectSpecHandle BuffEffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackBuffEffect); if (BuffEffectSpecHandle.IsValid()) { ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, BuffEffectSpecHandle); } } bool bReplicatedEndAbility = true; bool bWasCancelled = false; EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled); }
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
- 변경 전: UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
- 변경 후: UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass);
ABTA_SphereMultiTrace 생성
ABTA_Trace - ABTA_SphereMultiTrace 생성


ABTA_SphereMultiTrace.h
#pragma once #include "CoreMinimal.h" #include "GA/TA/ABTA_Trace.h" #include "ABTA_SphereMultiTrace.generated.h" UCLASS() class ARENABATTLEGAS_API AABTA_SphereMultiTrace : public AABTA_Trace { GENERATED_BODY() protected: virtual FGameplayAbilityTargetDataHandle MakeTargetData() const override; };
ABTA_SphereMultiTrace.cpp
#include "GA/TA/ABTA_SphereMultiTrace.h" #include "ArenaBattleGAS.h" #include "Abilities/GameplayAbility.h" #include "GameFramework/Character.h" #include "AbilitySystemBlueprintLibrary.h" #include "Physics/ABCollision.h" #include "DrawDebugHelpers.h" #include "Attribute/ABCharacterSkillAttributeSet.h" FGameplayAbilityTargetDataHandle AABTA_SphereMultiTrace::MakeTargetData() const { ACharacter* Character = CastChecked<ACharacter>(SourceActor); UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor); if (!ASC) { ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!")); return FGameplayAbilityTargetDataHandle(); } TArray<FOverlapResult> Overlaps; // Sphere와 겹친 액터들의 정보를 담는 배열 const float SkillRadius = 600.f; // 탐지할 Sphere의 반지름 FVector Origin = Character->GetActorLocation(); FCollisionQueryParams Params(SCENE_QUERY_STAT(AABTA_SphereMultiTrace), false, Character); GetWorld()->OverlapMultiByChannel(Overlaps, Origin, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(SkillRadius), Params); // 겹침연산 후 결과가 Overlaps 배열 변수에 저장됨 TArray<TWeakObjectPtr<AActor>> HitActors; // 겹친 연산이 일어난 액터들을 담는 배열. 아래의 ActorData->SetActors()의 인자로 넘길 때 TWeakObjectPtr 형태여야 해서 TWeakObjectPtr를 사용하였다. for (const FOverlapResult& Overlap : Overlaps) { AActor* HitActor = Overlap.OverlapObjectHandle.FetchActor<AActor>(); if (HitActor && !HitActors.Contains(HitActor)) { HitActors.Add(HitActor); } } // 다수의 액터를 보관하는 TargetData 타입인 FGameplayAbilityTargetData_ActorArray 변수 생성 후 HitActors를 담는다 FGameplayAbilityTargetData_ActorArray* ActorsData = new FGameplayAbilityTargetData_ActorArray(); ActorsData->SetActors(HitActors); #if ENABLE_DRAW_DEBUG if (bShowDebug) { FColor DrawColor = HitActors.Num() > 0 ? FColor::Green : FColor::Red; DrawDebugSphere(GetWorld(), Origin, SkillRadius, 16, DrawColor, false, 5.0f); } #endif return FGameplayAbilityTargetDataHandle(ActorsData); }
BPGA_SkillHitCheck에 ABTA_SphereMultiTrace 지정

실행화면

다수 NPC에 데미지 전달 및 장식 이펙트 부여
ABGA_AttackHitCheck
ABGA_AttackHitCheck.h
#pragma once #include "CoreMinimal.h" #include "Abilities/GameplayAbility.h" #include "ABGA_AttackHitCheck.generated.h" UCLASS() class ARENABATTLEGAS_API UABGA_AttackHitCheck : public UGameplayAbility { GENERATED_BODY() public: UABGA_AttackHitCheck(); virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override; protected: UFUNCTION() void OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle); UPROPERTY(EditAnywhere, Category = "GAS") TSubclassOf<class UGameplayEffect> AttackDamageEffect; UPROPERTY(EditAnywhere, Category = "GAS") TSubclassOf<class UGameplayEffect> AttackBuffEffect; float CurrentLevel; // CurveTable의 현재레벨 변수 UPROPERTY(EditAnywhere, Category = "GAS") TSubclassOf<class AABTA_Trace> TargetActorClass; };
변경사항 없음
ABGA_AttackHitCheck.cpp
#include "GA/ABGA_AttackHitCheck.h" #include "ArenaBattleGAS.h" #include "AbilitySystemBlueprintLibrary.h" #include "GA/AT/ABAT_Trace.h" #include "GA/TA/ABTA_Trace.h" #include "Attribute/ABCharacterAttributeSet.h" #include "ArenaBattleGAS.h" #include "Tag/ABGameplayTag.h" UABGA_AttackHitCheck::UABGA_AttackHitCheck() { InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor; } void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { ABGAS_LOG(LogABGAS, Log, TEXT("%s"), *TriggerEventData->EventTag.GetTagName().ToString()); Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); CurrentLevel = TriggerEventData->EventMagnitude; UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass); AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback); AttackTraceTask->ReadyForActivation(); } void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle) { // TargetDataHandle는 배열로 들어오기 때문에 0번 인덱스(=첫번째 데이터)에 이것이 있는지 확인 if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0)) // 있다면 { FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0); ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName())); UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked(); // 소스에서 타겟으로 넘겨야 하므로 체크로 확인한 AbilitySystemComponent를 가져옴 UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor()); // 물리 판정을 감지한 타겟 액터 const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>(); FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel); if (EffectSpecHandle.IsValid()) { //EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttribute->GetAttackRate()); ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle); FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle); CueContextHandle.AddHitResult(HitResult); FGameplayCueParameters CueParam; CueParam.EffectContext = CueContextHandle; TargetASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam); } FGameplayEffectSpecHandle BuffEffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackBuffEffect); if (BuffEffectSpecHandle.IsValid()) { ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, BuffEffectSpecHandle); } } else if (UAbilitySystemBlueprintLibrary::TargetDataHasActor(TargetDataHandle, 0)) { UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked(); FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel); if (EffectSpecHandle.IsValid()) { ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle); FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle); CueContextHandle.AddActors(TargetDataHandle.Data[0].Get()->GetActors(), false); FGameplayCueParameters CueParam; CueParam.EffectContext = CueContextHandle; SourceASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam); } } bool bReplicatedEndAbility = true; bool bWasCancelled = false; EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled); }
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
- else if (UAbilitySystemBlueprintLibrary::TargetDataHasActor(TargetDataHandle, 0))
- UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
if (EffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle);
CueContextHandle.AddActors(TargetDataHandle.Data[0].Get()->GetActors(), false);
FGameplayCueParameters CueParam;
CueParam.EffectContext = CueContextHandle;
SourceASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam);
- UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
ABGC_AttackHit
ABGC_AttackHit.h
#pragma once #include "CoreMinimal.h" #include "GameplayCueNotify_Static.h" #include "ABGC_AttackHit.generated.h" UCLASS() class ARENABATTLEGAS_API UABGC_AttackHit : public UGameplayCueNotify_Static { GENERATED_BODY() public: UABGC_AttackHit(); virtual bool OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const override; protected: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=GameplayCue) TObjectPtr<class UParticleSystem> ParticleSystem; };
변경사항 없음
ABGC_AttackHit.cpp
#include "GC/ABGC_AttackHit.h" #include "Particles/ParticleSystem.h" #include "Kismet/GameplayStatics.h" UABGC_AttackHit::UABGC_AttackHit() { static ConstructorHelpers::FObjectFinder<UParticleSystem> ExplosionRef(TEXT("/Script/Engine.ParticleSystem'/Game/StarterContent/Particles/P_Explosion.P_Explosion'")); if (ExplosionRef.Object) { ParticleSystem = ExplosionRef.Object; // 파티클 이펙트 지정 } } bool UABGC_AttackHit::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const { const FHitResult* HitResult = Parameters.EffectContext.GetHitResult(); if (HitResult) { UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, HitResult->ImpactPoint, FRotator::ZeroRotator, true); // 충돌 위치에 스폰 } else // HitResult가 없고, 다수의 액터 정보가 들어올 때(=범위 공격 피격들) { for (const auto& TargetActor : Parameters.EffectContext.Get()->GetActors()) // 피격된 모든 TargetActor들에 대해 { if (TargetActor.Get()) { UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, TargetActor.Get()->GetActorLocation(), FRotator::ZeroRotator, true); // TargetActor에 이펙트 스폰 } } } return false; }
bool UABGC_AttackHit::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const
- else // HitResult가 없고, 다수의 액터 정보가 들어올 때(=범위 공격 피격들)
- for (const auto& TargetActor : Parameters.EffectContext.Get()->GetActors())
- if (TargetActor.Get())
- UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, TargetActor.Get()->GetActorLocation(), FRotator::ZeroRotator, true);
- if (TargetActor.Get())
- for (const auto& TargetActor : Parameters.EffectContext.Get()->GetActors())
실행화면

신규 어트리뷰트의 추가 및 활용
ABCharacterSkillAttributeSet 생성
AttributeSet - ABCharacterSkillAttributeSet 생성


ABCharacterSkillAttributeSet.h
#pragma once #include "CoreMinimal.h" #include "AttributeSet.h" #include "AbilitySystemComponent.h" #include "ABCharacterSkillAttributeSet.generated.h" #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) UCLASS() class ARENABATTLEGAS_API UABCharacterSkillAttributeSet : public UAttributeSet { GENERATED_BODY() public: UABCharacterSkillAttributeSet(); ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, SkillRange); ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, MaxSkillRange); ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, SkillAttackRate); ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, MaxSkillAttackRate); ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, SkillEnergy); ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, MaxSkillEnergy); virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override; protected: UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true)) FGameplayAttributeData SkillRange; UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true)) FGameplayAttributeData MaxSkillRange; UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true)) FGameplayAttributeData SkillAttackRate; UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true)) FGameplayAttributeData MaxSkillAttackRate; UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true)) FGameplayAttributeData SkillEnergy; UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true)) FGameplayAttributeData MaxSkillEnergy; };
ABCharacterSkillAttributeSet.cpp
#include "Attribute/ABCharacterSkillAttributeSet.h" UABCharacterSkillAttributeSet::UABCharacterSkillAttributeSet() : SkillRange(800.0f), MaxSkillRange(1200.0f), SkillAttackRate(150.0f), MaxSkillAttackRate(300.0f), SkillEnergy(100.0f), MaxSkillEnergy(100.0f) { } void UABCharacterSkillAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { Super::PreAttributeChange(Attribute, NewValue); if (Attribute == GetSkillRangeAttribute()) { NewValue = FMath::Clamp(NewValue, 0.1f, GetMaxSkillRange()); } else if (Attribute == GetSkillAttackRateAttribute()) { NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxSkillAttackRate()); } }
ABGASPlayerState에 SkillAttributeSet 추가
ABGASPlayerState.h
#pragma once #include "CoreMinimal.h" #include "GameFramework/PlayerState.h" #include "AbilitySystemInterface.h" #include "ABGASPlayerState.generated.h" UCLASS() class ARENABATTLEGAS_API AABGASPlayerState : public APlayerState, public IAbilitySystemInterface { GENERATED_BODY() public: AABGASPlayerState(); virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override; protected: UPROPERTY(EditAnywhere, Category = GAS) TObjectPtr<class UAbilitySystemComponent> ASC; UPROPERTY() TObjectPtr<class UABCharacterAttributeSet> AttributeSet; UPROPERTY() TObjectPtr<class UABCharacterSkillAttributeSet> SkillAttributeSet; };
변수 추가
- UPROPERTY()
TObjectPtr<class UABCharacterSkillAttributeSet> SkillAttributeSet;
ABGASPlayerState.cpp
#include "Player/ABGASPlayerState.h" #include "AbilitySystemComponent.h" #include "Attribute/ABCharacterAttributeSet.h" #include "Attribute/ABCharacterSkillAttributeSet.h" AABGASPlayerState::AABGASPlayerState() { ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC")); //ASC->SetIsReplicated(true); AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("AttributeSet")); SkillAttributeSet = CreateDefaultSubobject<UABCharacterSkillAttributeSet>(TEXT("SkillAttributeSet")); } UAbilitySystemComponent* AABGASPlayerState::GetAbilitySystemComponent() const { return ASC; }
AABGASPlayerState::AABGASPlayerState()
- SkillAttributeSet = CreateDefaultSubobject<UABCharacterSkillAttributeSet>(TEXT("SkillAttributeSet"));
ABTA_SphereMultiTrace
ABTA_SphereMultiTrace.h
#pragma once #include "CoreMinimal.h" #include "GA/TA/ABTA_Trace.h" #include "ABTA_SphereMultiTrace.generated.h" UCLASS() class ARENABATTLEGAS_API AABTA_SphereMultiTrace : public AABTA_Trace { GENERATED_BODY() protected: virtual FGameplayAbilityTargetDataHandle MakeTargetData() const override; };
변경사항 없음
ABTA_SphereMultiTrace.cpp
#include "GA/TA/ABTA_SphereMultiTrace.h" #include "ArenaBattleGAS.h" #include "Abilities/GameplayAbility.h" #include "GameFramework/Character.h" #include "AbilitySystemBlueprintLibrary.h" #include "Physics/ABCollision.h" #include "DrawDebugHelpers.h" #include "Attribute/ABCharacterSkillAttributeSet.h" FGameplayAbilityTargetDataHandle AABTA_SphereMultiTrace::MakeTargetData() const { ACharacter* Character = CastChecked<ACharacter>(SourceActor); UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor); if (!ASC) { ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!")); return FGameplayAbilityTargetDataHandle(); } const UABCharacterSkillAttributeSet* SkillAttribute = ASC->GetSet<UABCharacterSkillAttributeSet>(); if (!SkillAttribute) { ABGAS_LOG(LogABGAS, Error, TEXT("SkillAttribute not found!")); return FGameplayAbilityTargetDataHandle(); } TArray<FOverlapResult> Overlaps; // Sphere와 겹친 액터들의 정보를 담는 배열 const float SkillRadius = SkillAttribute->GetSkillRange(); // 탐지할 Sphere의 반지름을 SkillAttribute에서 가져옴 FVector Origin = Character->GetActorLocation(); FCollisionQueryParams Params(SCENE_QUERY_STAT(AABTA_SphereMultiTrace), false, Character); GetWorld()->OverlapMultiByChannel(Overlaps, Origin, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(SkillRadius), Params); // 겹침연산 후 결과가 Overlaps 배열 변수에 저장됨 TArray<TWeakObjectPtr<AActor>> HitActors; // 겹친 연산이 일어난 액터들을 담는 배열. 아래의 ActorData->SetActors()의 인자로 넘길 때 TWeakObjectPtr 형태여야 해서 TWeakObjectPtr를 사용하였다. for (const FOverlapResult& Overlap : Overlaps) { AActor* HitActor = Overlap.OverlapObjectHandle.FetchActor<AActor>(); if (HitActor && !HitActors.Contains(HitActor)) { HitActors.Add(HitActor); } } // 다수의 액터를 보관하는 TargetData 타입인 FGameplayAbilityTargetData_ActorArray 변수 생성 후 HitActors를 담는다 FGameplayAbilityTargetData_ActorArray* ActorsData = new FGameplayAbilityTargetData_ActorArray(); ActorsData->SetActors(HitActors); #if ENABLE_DRAW_DEBUG if (bShowDebug) { FColor DrawColor = HitActors.Num() > 0 ? FColor::Green : FColor::Red; DrawDebugSphere(GetWorld(), Origin, SkillRadius, 16, DrawColor, false, 5.0f); } #endif return FGameplayAbilityTargetDataHandle(ActorsData); }
FGameplayAbilityTargetDataHandle AABTA_SphereMultiTrace::MakeTargetData() const
- const UABCharacterSkillAttributeSet* SkillAttribute = ASC->GetSet<UABCharacterSkillAttributeSet>(); // SkillAttribute 가져오기
- const float SkillRadius = SkillAttribute->GetSkillRange();
BPGE_SkillCost 생성


스킬을 사용할 때마다 30씩 줄어든다
BPGE_SkillCooldown 생성


BPGA_Skill에 Cost와 Cooldown 지정

최종 데미지의 수동 계산
ABSkillDamageExecutionCalc 생성
GameplayEffectExecutionCalculation - ABSkillDamageExecutionCalc 생성


ABSkillDamageExecutionCalc.h
#pragma once #include "CoreMinimal.h" #include "GameplayEffectExecutionCalculation.h" #include "ABSkillDamageExecutionCalc.generated.h" UCLASS() class ARENABATTLEGAS_API UABSkillDamageExecutionCalc : public UGameplayEffectExecutionCalculation { GENERATED_BODY() public: virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override; };
ABSkillDamageExecutionCalc.cpp
#include "GE/ABSkillDamageExecutionCalc.h" #include "AbilitySystemComponent.h" #include "Attribute/ABCharacterSkillAttributeSet.h" #include "Attribute/ABCharacterAttributeSet.h" void UABSkillDamageExecutionCalc::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const { Super::Execute_Implementation(ExecutionParams, OutExecutionOutput); UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent(); UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent(); if (SourceASC && TargetASC) { AActor* SourceActor = SourceASC->GetAvatarActor(); AActor* TargetActor = TargetASC->GetAvatarActor(); if (SourceActor && TargetActor) { const float MaxDamageRange = SourceASC->GetNumericAttributeBase(UABCharacterSkillAttributeSet::GetSkillRangeAttribute()); const float MaxDamage = SourceASC->GetNumericAttributeBase(UABCharacterSkillAttributeSet::GetSkillAttackRateAttribute()); const float Distance = FMath::Clamp(SourceActor->GetDistanceTo(TargetActor), 0.0f, MaxDamageRange); // 최대 반경으로 갈수록 데미지가 세짐 const float InvDamageRatio = 1.0f - Distance / MaxDamageRange; // 최대 반경일수록 더 적은 데미지로 뒤집기위해 ratio 구하기 float Damage = InvDamageRatio * MaxDamage; // ratio를 곱해 최대 반경일수록 더 적고 중앙일수록 더 큰 데미지 적용 OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(UABCharacterAttributeSet::GetDamageAttribute(), EGameplayModOp::Additive, Damage)); // FGameplayModifierEvaluatedData 형태로 값을 넣어서 전달 } } }
BPGE_SkillDamage 수정

BPGA_SkillHitCheck 수정

실행화면

가까울수록 더 큰 데미지가 멀수록 더 작은 데미지가 적용된다.
정리
- 무기를 들 때 발동가능한, 마우스 우클릭으로 발동하는 범위 스킬의 구현
- 다중 충돌 감지 기능을 수행하는 타겟 액터의 구현과 활용
- 명중한 다수의 타겟에 데미지를 전달하고 장식 이펙트를 발동하는 기능 구현
- 스킬 전용 어트리뷰트 클래스를 활용한 스킬 사용 비용과 쿨다운 기능 구현
- 이펙트 실행 계산 클래스를 활용한 거리에 따른 데미지 감소 구현
'⭐ Unreal Engine > UE Game Ability System(GAS)' 카테고리의 다른 글
[UE GAS] 아이템 상자 구현 (0) | 2024.03.10 |
---|---|
[UE GAS] 어트리뷰트와 UI 연동 Integration with Attribute and UI (0) | 2024.03.09 |
[UE GAS] 게임플레이 이펙트의 활용 Applying Gameplay Effect (0) | 2024.03.08 |
[UE GAS] 캐릭터 어트리뷰트 설정 (0) | 2024.03.07 |
[UE GAS] 공격 판정 시스템의 구현 (0) | 2024.03.06 |
댓글
이 글 공유하기
다른 글
-
[UE GAS] 아이템 상자 구현
[UE GAS] 아이템 상자 구현
2024.03.10 -
[UE GAS] 어트리뷰트와 UI 연동 Integration with Attribute and UI
[UE GAS] 어트리뷰트와 UI 연동 Integration with Attribute and UI
2024.03.09 -
[UE GAS] 게임플레이 이펙트의 활용 Applying Gameplay Effect
[UE GAS] 게임플레이 이펙트의 활용 Applying Gameplay Effect
2024.03.08 -
[UE GAS] 캐릭터 어트리뷰트 설정
[UE GAS] 캐릭터 어트리뷰트 설정
2024.03.07
댓글을 사용할 수 없습니다.