내배캠/GAS(Gameplay Ability System)
GAS_13_피격 대상에게 GameEffect 적용
동그래님
2025. 5. 9. 21:57
📌 FGameEffectSpecHandle 생성
앞서 진행했던 흐름은 WeaponBase 클래스에서 Collision에 OverlapEvent가 발생하면 델리게이트로 BroadCasting을 하고,
이를 구독하고 있는 CombatComponent가 감지된 객체 정보를 Event Tag 기반으로 GameplayAbility로 Event를 송신하였고, 이를 Melee Attack 관련 Gameplay Ability에서 Event를 수신 받아 현재 피격된 대상이 누구인지까지 알게 되었다.
이제 무기 데미지, 공격 타입, 콤보 횟수 등의 요소를 저장하는 FGamaEffectSpecHandle을 생성하고 추후 최종 피해량을 계산하고 데미지를 적용시키는 것을 구현하려한다.
FGameplayEffectSpecHandle에 무기 데미지, 공격 타입, 콤보 횟수 등의 요소들을 담고 ExecutionCalculation에게 넘겨주어 계산을 하는 흐름이다.
✅ FGameplayEffectSpecHandle 반환 함수 구현
✔️ RPGGameplayTags
더보기
🔹RPGGameplayTags.h
#pragma once
#include "NativeGameplayTags.h"
namespace RPGGameplayTags
{
/** 생략 */
/** Shared Tags */
RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Shared_Event_MeleeHit);
RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Shared_SetByCaller_BaseDamage); // 기본 데미지 Tag 추가
}
🔹RPGGameplayTags.cpp
#include "RPGGameplayTags.h"
namespace RPGGameplayTags
{
/** 생략 */
/** Shared Tags */
UE_DEFINE_GAMEPLAY_TAG(Shared_Event_MeleeHit, "Shared.Event.MeleeHit");
UE_DEFINE_GAMEPLAY_TAG(Shared_SetByCaller_BaseDamage, "Shared.SetByCaller.BaseDamage");
}
외부에서 지정해주는 기본 데미지를 뜻하는 "Shared.SetByCaller.BaseDamage" Tag를 정의한다.
✔️ WarriorHeroGameplayAbility
더보기
🔹WarriorHeroGameplayAbility.h
#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/RPGGameplayAbility.h"
#include "WarriorHeroGameplayAbility.generated.h"
class AWarriorHeroCharacter;
class AWarriorHeroController;
class UHeroCombatComponent;
UCLASS()
class RPG_API UWarriorHeroGameplayAbility : public URPGGameplayAbility
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "RPGAbility")
AWarriorHeroCharacter* GetHeroCharacterFromActorInfo();
UFUNCTION(BlueprintPure, Category = "RPGAbility")
AWarriorHeroController* GetWarriorHeroControllerFromActorInfo();
UFUNCTION(BlueprintPure, Category = "RPGAbility")
UHeroCombatComponent* GetHeroCombatComponentFromActorInfo();
// GameplayEffectSpecHandle 반환 함수 추가
UFUNCTION(BlueprintPure, Category = "RPGAbility")
FGameplayEffectSpecHandle MakeHeroDamageEffectSpecHandle(
TSubclassOf<UGameplayEffect> EffectClass, float InWeaponBaseDamage,
FGameplayTag InCurrentAttackTypeTag, int32 InUsedComboCount);
private:
TWeakObjectPtr<AWarriorHeroCharacter> CachedWarriorHeroCharacter;
TWeakObjectPtr<AWarriorHeroController> CachedWarriorHeroController;
};
🔹WarriorHeroGameplayAbility.cpp
#include "AbilitySystem/Abilities/WarriorHeroGameplayAbility.h"
#include "Characters/WarriorHeroCharacter.h"
#include "Controllers/WarriorHeroController.h"
#include "AbilitySystem/RPGAbilitySystemComponent.h"
#include "RPGGameplayTags.h"
/** 생략 */
FGameplayEffectSpecHandle UWarriorHeroGameplayAbility::MakeHeroDamageEffectSpecHandle(
TSubclassOf<UGameplayEffect> EffectClass, float InWeaponBaseDamage,
FGameplayTag InCurrentAttackTypeTag, int32 InUsedComboCount)
{
check(EffectClass);
// 컨텍스트 생성: 누가, 어떤 상황에서 발동 했는지
FGameplayEffectContextHandle ContextHandle = GetRPGAbilitySystemComponentFromActorInfo()->MakeEffectContext();
ContextHandle.SetAbility(this);
ContextHandle.AddSourceObject(GetAvatarActorFromActorInfo());
ContextHandle.AddInstigator(GetAvatarActorFromActorInfo(), GetAvatarActorFromActorInfo());
// 스펙 생성:
FGameplayEffectSpecHandle EffectSpecHandle = GetRPGAbilitySystemComponentFromActorInfo()->MakeOutgoingSpec(
EffectClass,
GetAbilityLevel(),
ContextHandle
);
// 무기 기본 데미지 값 전달
EffectSpecHandle.Data->SetSetByCallerMagnitude(
RPGGameplayTags::Shared_SetByCaller_BaseDamage,
InWeaponBaseDamage
);
// 공격 유형 태그를 키로, 콤보 수를 값으로 전달
if (InCurrentAttackTypeTag.IsValid())
{
EffectSpecHandle.Data->SetSetByCallerMagnitude(InCurrentAttackTypeTag, InUsedComboCount);
}
return EffectSpecHandle;
}
"FGameplayEffectSpecHandle"은 적용할 GameplayEffect와 그 세부정보를 담은 객체이다.
쉽게 말해 GE 클래스를 어떤 상황(Context)에, 어떤 수치(Magnitude)로 적용할지 담고 있는 객체를 반환하는 함수이다.
또한 이 객체는 GE를 실질적으로 Taget에게 적용시키려면 반드시 필요하다.
✅ Make Hero Damage Effect Spec Handle에 할당할 데이터 생성
- Effect Class: ExecutionCalculation에서 실제 데미지 계산
- In Weapon Base Damage: 무기 기본 공격력 → SetByCallerMagnitude()로 전달되어 데미지 계산에 사용
- In Current Attack Type Tag: 강공격, 약공격 등 어떤 종류의 공격인지 ExecutionCalculation에서 분기 처리에 사용
- In Used Combo Count: 콤보 수 → 공격 데미지 증폭에 반영
에디터로 돌아와서 GameplayAbility에서 C++에서 구현했던 SpecHandle을 생성하는 함수를 BP로 사용할 수 있게 되었다.
이제 각 데이터를 채워 넣기 위한 구현을 한다.
✔️ 데미지 계산 GameplayEffect 클래스 생성
"GameplayEffectExecutionCalculation"를 상속 받은 C++ 클래스를 생성해주고 "GEExecCalc_Damage"으로 명명한다.
구현은 나중에 하고 우선 클래스만 먼저 생성하도록 한다.
✔️ 플레이어와 Enemy 모두에게 적용 가능한 Damage 관련 GE 생성
데미지 계산을 처리하는 GameplayEffect를 생성하고, 앞서 생성했던 "GEExecCalc_Damage"를 Calculation Class로 설정한다.
✔️ RPGStructTypes
더보기
🔹RPGStructTypes.h
USTRUCT(BlueprintType)
struct FRPGHeroWeaponData
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UWarriorHeroLinkedAnimLayer> WeaponAnimLayerToLink;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UInputMappingContext* WeaponInputMappingContext;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (TitleProperty = "InputTag"))
TArray<FWarriorHeroAbilitySet> DefaultWeaponAbilities;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FScalableFloat WeaponBaseDamage; // BaseDamage 선언
};
무기 클래스가 가지고 있는 Data 구조체에 WeaponBaseDamage를 선언한다.
FScalableFloat은 언리얼 GAS에서 데이터를 Curve Table 기반으로 동적으로 조정 가능한 수치로 만들기 위해 사용하는 구조체이다.
단순한 float와 달리 레벨, 난이도, 상황에 따라 수치를 조정할 수 있는 기능을 제공한다.
에디터에서 Curve Table을 생성해서 BaseDamage 데이터를 정의한다. 그리고 BP_Axe에 WeaponBaseDamage를 저장해준다.
✔️ HeroCombatComponent
더보기
🔹HeroCombatComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/Combat/PawnCombatComponent.h"
#include "HeroCombatComponent.generated.h"
class AWarriorHeroWeapon;
UCLASS()
class RPG_API UHeroCombatComponent : public UPawnCombatComponent
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "RPG|Combat")
AWarriorHeroWeapon* GetHeroCarriedWeaponByTag(FGameplayTag InWeaponTag) const;
// 현재 장착 중인 무기 반환
UFUNCTION(BlueprintCallable, Category = "RPG|Combat")
AWarriorHeroWeapon* GetHeroCurrentEquippedWeapon() const;
// 현재 장착중인 무기 데미지 반환
UFUNCTION(BlueprintCallable, Category = "RPG|Combat")
float GetHeroCurrentEquippedWeaponDamageAtLevel(float InLevel) const;
virtual void OnHitTargetActor(AActor* HitActor) override;
virtual void OnWeaponPulledFromTargetActor(AActor* InteractedActor) override;
};
🔹HeroCombatComponent.cpp
#include "Components/Combat/HeroCombatComponent.h"
#include "Items/Weapons/WarriorHeroWeapon.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "RPGGameplayTags.h"
#include "WarriorDebugHelper.h"
AWarriorHeroWeapon* UHeroCombatComponent::GetHeroCurrentEquippedWeapon() const
{
return Cast<AWarriorHeroWeapon>(GetCharacterCurrentEquippedWeapon());
}
float UHeroCombatComponent::GetHeroCurrentEquippedWeaponDamageAtLevel(float InLevel) const
{
return GetHeroCurrentEquippedWeapon()->HeroWeaponData.WeaponBaseDamage.GetValueAtLevel(InLevel);
}
WeaponData를 하고 나서 현재 Level에 비례한 무기 데미지 반환 함수 구현한다.
✔️ RPGGameplayTags
더보기
🔹RPGGameplayTags.h
#pragma once
#include "NativeGameplayTags.h"
namespace RPGGameplayTags
{
/** 생략 */
/** Player Tags */
RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_SetByCaller_AttackType_Light);
RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_SetByCaller_AttackType_Heavy);
}
🔹RPGGameplayTags.cpp
#include "RPGGameplayTags.h"
namespace RPGGameplayTags
{
/** 생략 */
/** Player Tags */
UE_DEFINE_GAMEPLAY_TAG(Player_SetByCaller_AttackType_Light, "Player.SetByCaller.AttackType.Light");
UE_DEFINE_GAMEPLAY_TAG(Player_SetByCaller_AttackType_Heavy, "Player.SetByCaller.AttackType.Heavy");
}
GameplayEffectSpec에 SetByCallerMagnitude 방식으로 공격 타입 정보를 담아 전달하기 위해, Tag를 선언한다.
🔔 GE Spec Handle 반환 함수에 데이터 연결
FGameplayEffectSpecHandle UWarriorHeroGameplayAbility::MakeHeroDamageEffectSpecHandle(
TSubclassOf<UGameplayEffect> EffectClass, float InWeaponBaseDamage,
FGameplayTag InCurrentAttackTypeTag, int32 InUsedComboCount)
{
check(EffectClass);
// 컨텍스트 생성: 누가, 어떤 상황에서 발동 했는지
FGameplayEffectContextHandle ContextHandle = GetRPGAbilitySystemComponentFromActorInfo()->MakeEffectContext();
ContextHandle.SetAbility(this);
ContextHandle.AddSourceObject(GetAvatarActorFromActorInfo());
ContextHandle.AddInstigator(GetAvatarActorFromActorInfo(), GetAvatarActorFromActorInfo());
// 스펙 생성:
FGameplayEffectSpecHandle EffectSpecHandle = GetRPGAbilitySystemComponentFromActorInfo()->MakeOutgoingSpec(
EffectClass,
GetAbilityLevel(),
ContextHandle
);
// 무기 기본 데미지 값 전달
EffectSpecHandle.Data->SetSetByCallerMagnitude(
RPGGameplayTags::Shared_SetByCaller_BaseDamage,
InWeaponBaseDamage
);
// 공격 유형 태그를 키로, 콤보 수를 값으로 전달
if (InCurrentAttackTypeTag.IsValid())
{
EffectSpecHandle.Data->SetSetByCallerMagnitude(InCurrentAttackTypeTag, InUsedComboCount);
}
return EffectSpecHandle;
}
EffectClass: 실행할 GE_Shared_DealDamage
InWeaponBaseDamage: Curve Table에서 레벨에 매칭되는 Damage
InCurrentAttackTypeTag: "Player.SetByCaller.AttackType.Light" Tag
InUsedComboCount: GameplayAbility의 현재 Combo Count 변수 값
위와 같이 데이터를 함수에 전달하였다.
📌 FGameEffectSpecHandle로 대상에게 Damage 로직 구현
앞에서 생성한 GameplayEffectHandle을 대상 Actor에게 적용 시키는 기능을 하는 함수를 구현한다.
플레이어와 Enemy 모두 사용할 수 있도록 BaseGameplayAbility 클래스에 구현하도록 한다.
✔️ RPGEnumTypes.h
#pragma once
/** 생략 */
// 성공 여부 EnumType 구현
UENUM(BlueprintType)
enum class ERPGSuccessType : uint8
{
Successful,
Failed
};
Effect가 대상에게 실제로 적용되었는지 결과를 반환하기 위해 Enum 생성
✔️ RPGGameplayAbility
더보기
🔹RPGGameplayAbility.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "RPGTypes/RPGEnumTypes.h"
#include "RPGGameplayAbility.generated.h"
class UPawnCombatComponent;
class URPGAbilitySystemComponent;
UENUM(BlueprintType)
enum class ERPGAbilityActivationPolicy : uint8
{
OnTriggered,
OnGiven
};
UCLASS()
class RPG_API URPGGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
protected:
/** 생략 */
// GameplayEffectSpecHandle을 대상 액터에게 적용하는 헬퍼 함수
FActiveGameplayEffectHandle NativeApplyEffectSpecHandleToTarget(AActor* TargetActor, const FGameplayEffectSpecHandle& InSpecHandle);
// 위와 기능 동일하고 Effect 적용 결과를 Enum으로 반환하는 BP 전용 함수
UFUNCTION(BlueprintCallable, Category = "RPGAbility", meta = (DisplayName = "Apply Gameplay Effect Spec Handle To Target", ExpandEnumAsExecs = "OutSuccessType"))
FActiveGameplayEffectHandle BP_ApplyEffectSpecHandleToTarget(AActor* TargetActor, const FGameplayEffectSpecHandle& InSpecHandle, ERPGSuccessType& OutSuccessType);
};
🔹RPGGameplayAbility.cpp
#include "AbilitySystem/Abilities/RPGGameplayAbility.h"
#include "AbilitySystem/RPGAbilitySystemComponent.h"
#include "Components/Combat/PawnCombatComponent.h"
#include "AbilitySystemBlueprintLibrary.h"
/** 생략 */
FActiveGameplayEffectHandle URPGGameplayAbility::NativeApplyEffectSpecHandleToTarget(AActor* TargetActor, const FGameplayEffectSpecHandle& InSpecHandle)
{
// Effect가 처리될 대상의 ASC
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
check(TargetASC && InSpecHandle.IsValid());
return GetRPGAbilitySystemComponentFromActorInfo()->ApplyGameplayEffectSpecToTarget(
*InSpecHandle.Data, // SpecHandle에서 실제 FGameplaySpecHandle 전달
TargetASC // Effect 적용 대상의 ASC 전달
);
}
FActiveGameplayEffectHandle URPGGameplayAbility::BP_ApplyEffectSpecHandleToTarget(AActor* TargetActor, const FGameplayEffectSpecHandle& InSpecHandle, ERPGSuccessType& OutSuccessType)
{
FActiveGameplayEffectHandle ActiveGameplayEffectHandle = NativeApplyEffectSpecHandleToTarget(TargetActor, InSpecHandle);
OutSuccessType = ActiveGameplayEffectHandle.WasSuccessfullyApplied() ? ERPGSuccessType::Successful : ERPGSuccessType::Failed;
return ActiveGameplayEffectHandle;
}
GameplayEffect를 대상 Actor의 ASC에 적용시키는 함수 구현
에디터로 돌아와 GA_LightAttack에서 방금 구현한 함수로 GameplayEffect를 적용시키는 로직을 추가한다.
🔎로직 흐름
- Wait Gameplay Event 수신
- Shared.Event.MeleeHit 태그를 가진 이벤트를 수신
- 적이 무기에 피격 되었을 때, SendGameplayEventToActor( ) 를 통해 호출
- Payload로 GameplayEventData를 전달
- Gameplay Event Data에서 Target 추출
- 여기서 추출한 Target은 피격 판정된 적 캐릭터(AActor)
- Make Hero Damage Effect Spec 호출
- 데미지 계산 수행하는 클래스 포함한 GameplayEffect
- Weapon클래스 데이터에 있는 BaseDamage
- 데미지 계산을 위한 공격 Type Tag
- 현재 Combo Count
- 위의 데이터를 SpecHandle에 저장하고 반환
- Apply Gameplay Effect Spec Handle To Target 호출
- 피격 처리할 대상(Target)에게 SpecHandle 적용
- Apply 시점에 SpecHandle에 저장된 정보를 바탕으로 데미지 계산
- Target의 ASC를 찾아 GameplayEffect 적용
- FActiveGameplayEffectHandle 반환 및 성공 여부 핀으로 전달
📌 Attribute 캡처 및 피해량 계산
앞서 데미지 계산을 수행하는 클래스(Execution Calculation)를 포함한 GameEffect를 만들었고, 이 GE를 대상 Actor에게 적용시키는 로직을 구현하였다.
하지만 클래스 생성만 해놓고, 데미지 계산 로직을 비워둔 상태로 GE 적용만 시켰기 때문에 현재는 피격당하더라도 데미지를 입을 수 없다.
따라서 이제 데미지 계산 로직을 수행하는 클래스를 구현하도록 한다.
GameplayEffectExecutionCalculation 클래스는 단순히 GameplayEffect에 설정된 Modifier만으로는 할 수 없는 동적인 계산(공격력 - 방어력 등)을 처리하기 위한 클래스이다.
이 클래스는 실제 계산을 할 때, 지정된 Attribute를 "캡처"해서 값으로 사용할 수 있도록 설정해야한다.
즉, Execute 함수 내부에서 값을 가져오려면, 사전에 어떤 값을 사용할 것인지 명시적으로 등록해야 하며, 이 등록이 RelevantAttributesToCapture 배열에 추가되는 "Attribute Capture Definition"이다.
캡처하는 방법에는 "직접 수동으로 캡처를 하는 방법" 과 "매크로 + 구조체를 사용한 방법"이 있다.
당연히 전자 쪽이 코드 작성이 더 많고 구현하는데 오래걸리므로 후자 쪽 방향으로 구현할 것이다.
🔎캡처 정의 2가지 방법
더보기
🔴 수동 캡쳐 정의
UGEExecCalc_Damage::UGEExecCalc_Damage()
{
/** 수동 캡처 */
FProperty* AttackPowerProperty = FindFieldChecked<FProperty>(
URPGAttributeSet::StaticClass(),
GET_MEMBER_NAME_CHECKED(URPGAttributeSet, AttackPower)
);
FGameplayEffectAttributeCaptureDefinition AttackPowerCaptureDefinition(
AttackPowerProperty,
EGameplayEffectAttributeCaptureSource::Source,
false
);
FProperty* DefensePowerProperty = FindFieldChecked<FProperty>(
URPGAttributeSet::StaticClass(),
GET_MEMBER_NAME_CHECKED(URPGAttributeSet, DefensePower)
);
FGameplayEffectAttributeCaptureDefinition DefensePowerCaptureDefinition(
DefensePowerProperty,
EGameplayEffectAttributeCaptureSource::Target,
false
);
RelevantAttributesToCapture.Add(AttackPowerCaptureDefinition);
RelevantAttributesToCapture.Add(DefensePowerCaptureDefinition);
}
🟢 매크로 + 구조체 사용한 정의
#include "AbilitySystem/GEExecCalc/GEExecCalc_Damage.h"
#include "AbilitySystem/RPGAttributeSet.h"
struct FRPGDamageCaputre
{
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower)
DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower)
FRPGDamageCaputre()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, false)
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false)
}
};
static const FRPGDamageCaputre& GetRPGDamageCaputre()
{
static FRPGDamageCaputre RPGDamageCaputre;
return RPGDamageCaputre;
}
UGEExecCalc_Damage::UGEExecCalc_Damage()
{
RelevantAttributesToCapture.Add(GetRPGDamageCaputre().AttackPowerDef);
RelevantAttributesToCapture.Add(GetRPGDamageCaputre().DefensePowerDef);
}
여러 값을 캡처한다고 했을 때, 매크로 + 구조체를 사용하는 것이 더 코드가 간결해질 것이라는 것을 확인할 수 있다.
자세한 매크로 내부는 "UGameplayEffectExecutionCalculation.h"에 정의되어있다.
✅ 데미지 계산 클래스 기능 구현
✔️ RPGAttributeSet
더보기
🔹 RPGAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystem/RPGAbilitySystemComponent.h"
#include "RPGAttributeSet.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 RPG_API URPGAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
URPGAttributeSet();
/** 생략 */
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData AttackPower;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, AttackPower)
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData DefensePower;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, DefensePower)
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData DamageTaken;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, DamageTaken) // 최종 계산된 데미지 저장할 Attribute
};
ExecutionCalculation에서 계산된 최종 데미지를 저장하는 용도의 Attribute인 "DamageTaken" 선언한다.
실제 체력 감소 처리 로직에서 이 속성 값이 사용된다.
✔️ GEExecCalc_Damage
더보기
🔹 GEExecCalc_Damage.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "GEExecCalc_Damage.generated.h"
UCLASS()
class RPG_API UGEExecCalc_Damage : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
// 생성자에서 Attribute를 계산을 위해 캡처
// 매크로 + 구조체 방식으로 캡처
UGEExecCalc_Damage();
// 계산 실행 함수
virtual void Execute_Implementation(
const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
🔹 GEExecCalc_Damage.cpp
#include "AbilitySystem/GEExecCalc/GEExecCalc_Damage.h"
#include "AbilitySystem/RPGAttributeSet.h"
#include "RPGGameplayTags.h"
#include "WarriorDebugHelper.h"
struct FRPGDamageCaputre
{
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower)
DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower)
DECLARE_ATTRIBUTE_CAPTUREDEF(DamageTaken)
FRPGDamageCaputre()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, false)
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false)
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DamageTaken, Target, false)
}
};
static const FRPGDamageCaputre& GetRPGDamageCaputre()
{
static FRPGDamageCaputre RPGDamageCaputre;
return RPGDamageCaputre;
}
UGEExecCalc_Damage::UGEExecCalc_Damage()
{
RelevantAttributesToCapture.Add(GetRPGDamageCaputre().AttackPowerDef);
RelevantAttributesToCapture.Add(GetRPGDamageCaputre().DefensePowerDef);
RelevantAttributesToCapture.Add(GetRPGDamageCaputre().DamageTakenDef);
}
void UGEExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
// 1.EffectSpec 준비
const FGameplayEffectSpec& EffectSpec = ExecutionParams.GetOwningSpec();
/** Effect Spec이 가지고 있는 ContextHandle에서 정보 가져올 수 있음 */
/*EffectSpec.GetContext().GetSourceObject();
EffectSpec.GetContext().GetAbility();
EffectSpec.GetContext().GetInstigator();
EffectSpec.GetContext().GetEffectCauser();*/
// 2.EvaluateParameters 생성(속성 계산에 사용할 태그 정보)
FAggregatorEvaluateParameters EvaluateParameters;
EvaluateParameters.SourceTags = EffectSpec.CapturedSourceTags.GetAggregatedTags();
EvaluateParameters.TargetTags = EffectSpec.CapturedTargetTags.GetAggregatedTags();
// 3.Source(공격자)의 AttackPower 추출
float SourceAttackPower = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(GetRPGDamageCaputre().AttackPowerDef, EvaluateParameters, SourceAttackPower);
Debug::Print(TEXT("SourceAttackPower"), SourceAttackPower);
// 4.SetByCaller 값(BaseDamage, Light/Heavy Attack Combo Count) 추출
// Ability에서 SetSetByCallerMagnitude로 전달한 값을 TMap에서 추출하는 것
float BaseDamage = 0.0f;
int32 UsedLightAttackComboCount = 0;
int32 UsedHeavyAttackComboCount = 0;
for(const TPair<FGameplayTag, float>& TagMagnitude : EffectSpec.SetByCallerTagMagnitudes)
{
if (TagMagnitude.Key.MatchesTagExact(RPGGameplayTags::Shared_SetByCaller_BaseDamage))
{
BaseDamage = TagMagnitude.Value;
Debug::Print(TEXT("BaseDamage"), BaseDamage);
}
if (TagMagnitude.Key.MatchesTagExact(RPGGameplayTags::Player_SetByCaller_AttackType_Light))
{
UsedLightAttackComboCount = TagMagnitude.Value;
Debug::Print(TEXT("UsedLightAttackComboCount"), UsedLightAttackComboCount);
}
if (TagMagnitude.Key.MatchesTagExact(RPGGameplayTags::Player_SetByCaller_AttackType_Heavy))
{
UsedHeavyAttackComboCount = TagMagnitude.Value;
Debug::Print(TEXT("UsedHeavyAttackComboCount"), UsedHeavyAttackComboCount);
}
}
// 5.Target(피격자)의 DefensePower 추출
float TargetDefensePower = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(GetRPGDamageCaputre().DefensePowerDef, EvaluateParameters, TargetDefensePower);
Debug::Print(TEXT("TargetDefensePower"), TargetDefensePower);
// 6.Combo Count에 따른 Damage 보정
if (UsedLightAttackComboCount != 0)
{
const float DamageIncreasePercentLight = (UsedLightAttackComboCount - 1) * 0.05 + 1.0f;
BaseDamage *= DamageIncreasePercentLight;
Debug::Print(TEXT("ScaledBaseDamageLight"), BaseDamage);
}
if (UsedHeavyAttackComboCount != 0)
{
const float DamageIncreasePercentHeavy = UsedHeavyAttackComboCount * 0.15f + 1.0f;
BaseDamage *= DamageIncreasePercentHeavy;
Debug::Print(TEXT("ScaledBaseDamageHeavy"), BaseDamage);
}
// 7.최종 피해량 계산
const float FinalDamageDone = BaseDamage * SourceAttackPower / TargetDefensePower;
Debug::Print(TEXT("FinalDamageDone"), FinalDamageDone);
// 8.피해량 적용
// 캡처한 DamageTaken Attribute에 Override 방식으로 최종 피해량 적용
if (FinalDamageDone > 0.0f)
{
OutExecutionOutput.AddOutputModifier(
FGameplayModifierEvaluatedData(
GetRPGDamageCaputre().DamageTakenProperty,
EGameplayModOp::Override,
FinalDamageDone
)
);
}
}
GameplaySpec에서 전달 받은 데이터(기본 데미지, 공격력, 방어력 등)을 캡처하여 최종 피해량 계산 후, "DamageTaken" 이라는 Attribute에 데이터 Override 하였다.
🔔 데미지 계산하여, 최종 데미지 도출 결과
내부적으로 GE가 Apply될 때, 정상적으로 데미지 계산이 이루어지는 것을 확인할 수 있었다.
📌 현재 체력에 데미지 적용
앞서 계산된 최종 데미지를 현제 체력에 적용하고, 캐릭터가 데미지를 입고 죽는지 판단하는 로직을 "Attribute Set" 내부에서 처리하도록 할 것이다.
✔️ RPGAttributeSet
더보기
🔹 RPGAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystem/RPGAbilitySystemComponent.h"
#include "RPGAttributeSet.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 RPG_API URPGAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
URPGAttributeSet();
// GE에 의해 Attribute 값에 Modifier가 적용되면, 자동으로 호출되는 콜백 함수
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
/** 생략 */
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData AttackPower;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, AttackPower)
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData DefensePower;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, DefensePower)
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData DamageTaken;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, DamageTaken)
};
🔹 RPGAttributeSet.cpp
#include "AbilitySystem/RPGAttributeSet.h"
#include "GameplayEffectExtension.h"
#include "WarriorDebugHelper.h"
URPGAttributeSet::URPGAttributeSet()
{
InitCurrentHealth(1.0f);
InitMaxHealth(1.0f);
InitCurrentRage(1.0f);
InitMaxRage(1.0f);
InitAttackPower(1.0f);
InitDefensePower(1.0f);
}
void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
// GE 실행 결과로 Modifier가 적용된 Attribute가 CurrentHealth라면 해당 로직 실행
if (Data.EvaluatedData.Attribute == GetCurrentHealthAttribute())
{
const float NewCurrentHealth = FMath::Clamp(GetCurrentHealth(), 0.0f, GetMaxHealth());
SetCurrentHealth(NewCurrentHealth);
}
if (Data.EvaluatedData.Attribute == GetCurrentRageAttribute())
{
const float NewCurrentRage = FMath::Clamp(GetCurrentRage(), 0.0f, GetMaxRage());
SetCurrentRage(NewCurrentRage);
}
if (Data.EvaluatedData.Attribute == GetDamageTakenAttribute())
{
const float OldHealth = GetCurrentHealth();
const float DamageDone = GetDamageTaken();
const float NewCurrentHealth = FMath::Clamp(OldHealth - DamageDone, 0.0f, GetMaxHealth());
SetCurrentHealth(NewCurrentHealth);
const FString DebugString = FString::Printf(
TEXT("Old Health %f, Damage Done: %f, NewCurrentHealth: %f"),
OldHealth,
DamageDone,
NewCurrentHealth
);
Debug::Print(DebugString, FColor::Green);
// TODO::사망 처리
if (NewCurrentHealth == 0.0f)
{
// 사망 관련 태그 추가 -> DeathAbility 실행 등
}
}
}
GameplayEffect가 Modifier를 Attribute에 적용시키면 자동으로 호출되며 해당 로직들이 동작한다.
추후 현재 체력이 0이라면 사망관련 Tag를 부여하고 ASC가 이를 감지해 DeathAbility를 실행시키는 구조로 간다면 자연스럽게 사망로직이 구현될 것이다.
🔔 데미지 적용 테스트 결과
LogTemp: Warning: FinalDamageDone: 16.0
LogTemp: Warning: Old Health 75.000000, Damage Done: 16.000000, NewCurrentHealth: 59.000000
LogTemp: Warning: FinalDamageDone: 16.799999
LogTemp: Warning: Old Health 59.000000, Damage Done: 16.799999, NewCurrentHealth: 42.200001
LogTemp: Warning: FinalDamageDone: 17.600002
LogTemp: Warning: Old Health 42.200001, Damage Done: 17.600002, NewCurrentHealth: 24.599998
LogTemp: Warning: FinalDamageDone: 20.799999
LogTemp: Warning: Old Health 24.599998, Damage Done: 20.799999, NewCurrentHealth: 3.799999
LogTemp: Warning: FinalDamageDone: 16.0
LogTemp: Warning: Old Health 3.799999, Damage Done: 16.000000, NewCurrentHealth: 0.000000
LogTemp: Warning: FinalDamageDone: 16.799999
LogTemp: Warning: Old Health 0.000000, Damage Done: 16.799999, NewCurrentHealth: 0.000000
LogTemp: Warning: FinalDamageDone: 18.4
LogTemp: Warning: Old Health 0.000000, Damage Done: 18.400000, NewCurrentHealth: 0.000000
AI의 체력이 FinalDamage만큼 감소하고, 체력이 0이 되었을 때 그 이하로 떨어지지 않는 것을 확인할 수 있었다.