AttributeSet
이전 포스팅에서는 공격 판정에 대한 내용을 다뤘다.
공격 판정이 완료된 후, 결과를 타깃에게 적용해야 한다.
보통 공격에 맞은 대상은 체력이 깎이게 된다.
또한, 얼마나 피해를 줄지에 대한 기준이 되는 정보가 필요하다.
이러한 Stat같은 데이터들을 쉽게 다룰 수 있게 해주는 AttriubuteSet이다.
이번 포스팅에서는 AttributeSet을 통해 공격에 적중된 Actor에게 피해를 입히는 예시를 구현해 보겠다.
AttributeSet 설명
AttributeSet은 단일 어트리뷰트 데이인 GameplayAttributeData의 묶음이다.
GameplayAttributeData는 두 가지 값으로 구성되어 있다.
- BaseValue: 기본 값, 영구히 적용되는 값
- CurrentValue: 변동 값, 버프 같은 어떠한 영향에 의해 일시적으로 변경되는 값
AttributeSet에서는 이러한 데이터를 변경하는데 몇 가지 함수를 사용한다.
- PreAttributeChange: 어트리뷰트 변경 전에 호출된다
- PostAttributeChange: 어트리뷰트 변경 후에 호출된다
- PreGameplayEffectExecute: 게임플레이 이펙트 적용 전에 호출된다
- PostGameplayEffectExecute: 게임플레이 이펙트 적용 후에 호출된다
또한, 게임이 커지면서 액터에 필요한 데이터들이 많아질 수 있다.
예를 들어, 캐릭터의 경우 체력, 공격력, 사거리 등과 같이 많은 데이터가 필요할 수 있다.
모든 변수를 public으로 연다면 캡슐화가 깨지기 때문에 좋지 않으므로 Get, Set과 같은 함수들이 필요하다.
하지만, 데이터가 많기 때문에 Get, Set같은 함수들을 모두 작성하기는 쉽지 않다.
AttributeSet에서는 이를 간편하게 생성해주는 매크로를 지원한다.
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
AttributeSet의 헤더에 있는 매크로이다.
이 매크로를 헤더파일에 선언한 후, 다음과 같이 사용하면 Get, Set 같은 함수들을 간편하게 사용할 수 있다.
ATTRIBUTE_ACCESSORS(클래스이름, 변수이름);
...
AttributeSet 구현
AttributeSet을 상속받아 자신만의 AttributeSet을 구현할 수 있다.
//.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 Project_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UMyAttributeSet();
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRange);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxAttackRange);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRadius);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxAttackRadius);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRate);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxAttackRate);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, Health);
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, MaxHealth);
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data) { return true; }
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
protected:
UPROPERTY(BlueprintReadOnly, Category=Attack, meta=(AllowPrivateAccess=true))
FGameplayAttributeData AttackRange;
UPROPERTY(BlueprintReadOnly, Category = Attack, meta = (AllowPrivateAccess = true))
FGameplayAttributeData AttackRadius;
UPROPERTY(BlueprintReadOnly, Category = Attack, meta = (AllowPrivateAccess = true))
FGameplayAttributeData AttackRate;
UPROPERTY(BlueprintReadOnly, Category = Health, meta = (AllowPrivateAccess = true))
FGameplayAttributeData Health;
UPROPERTY(BlueprintReadOnly, Category = Attack, meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxAttackRange;
UPROPERTY(BlueprintReadOnly, Category = Attack, meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxAttackRadius;
UPROPERTY(BlueprintReadOnly, Category = Attack, meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxAttackRate;
UPROPERTY(BlueprintReadOnly, Category = Health, meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxHealth;
};
앞에서 언급했듯이 변수가 많아지면 Get, Set 함수들을 작성하고 관리하기 어렵기 때문에 매크로를 사용하였다.
각 변수들의 Max값을 같이 관리해주는 것이 좋다.
그럼 AttributeSet에 있는 변수들을 변경하는 법을 알아보자.
앞에서 언급한 4가지 함수 중, 두 개는 GameplayEffect를 사용해야 하기 때문에 다음 포스팅에서 다루기로 하자.
...
UMyAttributeSet::UMyAttributeSet():
AttackRange(100.f),
MaxAttackRange(300.f),
AttackRadius(50),
MaxAttackRadius(150.f),
AttackRate(30.f),
MaxAttackRate(100.f),
MaxHealth(100.f),
Damage(0.f)
{
InitHealth(GetMaxHealth());
}
void UMyAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
}
void UMyAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
}
//Effect
void UMyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
}
//Effect
void UMyAttributeSet:PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
}
PreAttributeChange는 Attribute가 변경되기 전에 실행되는 함수이다.
해당 부분에서 올바른 값이 아니라면 교정해주는 부분이 들어가 주면 좋다.
지금은 변경되는 값이 0~최댓값 사이에 있게 조절해주었다.
Post AttributeChange는 Attribute가 변경된 후 실행되는 함수이다.
이미 변경된 Attribute를 다시 변경할 수는 없기 때문에 보통 Log를 남기는 식으로 사용된다.
이렇게 제작된 AttributeSet을 필요한 Actor에 넣어주어야 ASC에서 이를 관리할 수 있다.
...
protected:
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<class UAbilitySystemComponent> ASC;
UPROPERTY()
TObjectPtr<class UABCharacterAttributeSet> AttributeSet;
...
헤더 파일에 넣어 놓기만 하면 사용할 준비가 된 것이다.
다른 함수나 설정을 통해 등록할 필요가 없다.
ASC에서는 Component가 초기화될 때, AttributeSet을 찾아 자동으로 등록하기 때문이다.
그럼 공격 판정에 적중한 NPC에게 피해를 주는 부분을 구현해 보자.
NPC에게 피해를 주기 위해서는 NPC에도 AttributeSet이 있어야 한다.
//NPC .h
...
protected:
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<UAbilitySystemComponent> ASC;
UPROPERTY()
TObjectPtr<class UMyAttributeSet> AttributeSet;
...
그리고 이전 포스팅에서 구현한 GA의 공격 판정의 결과를 얻어오는 부분에서 피해를 적용시키면 된다.
GA를 실행한 액터(Player, Source)의 AttributeSet과 공격에 적중한 액터(NPC, Target)의 AttributeSet이 모두 필요한다.
AttribuetSet에 접근하기 위해서는 ASC가 필요하기 때문에 ASC를 먼저 받아온다.
Source의 경우에는 GA에 이미 설정되어 있기 때문에 GetAbilitySystemComponentFromActorInfo로 받아올 수 있다.
지금의 경우에는 Checked까지 진행하여 안전하게 진행한 것이다.
Target의 경우에는 TargetDataHandle로 넘어오는 HitResult에 Actor안에 들어 있을 것이다.
이를 통해 ASC를 받아오는데 이때, UAbilitySystemBlueprintLibrary 라이브러리를 통해 받아올 수 있다.
#include "AbilitySystemBlueprintLibrary.h"
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
두 ASC가 유효한지 체크한 뒤, ASC로부터 AttributeSet을 받아올 수 있다.
하지만, 이때 주의할 점은 AttributeSet은 const로 받아올 수밖에 없다.
즉, 임의로 데이터를 건드릴 수 없다는 뜻이다.
데이터를 변경하기 위해서는 GameplayEffect라는 클래스를 통해 변경할 수 있지만 지금은 간단한 예제이니 const_cast를 통해 const를 제거해 주고 진행하겠다.
const UMyAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();
UMyAttributeSet* TargetAttribute = const_cast<UMyAttributeSet*>(TargetASC->GetSet<UMyAttributeSet>());
if (!SourceAttribute || !TargetAttribute) return;
이제 SourceAttribute로부터 공격력을 얻어온 뒤, TargetAttribute의 Health값을 변경해 주면 된다.
const float AttackDamage = SourceAttribute->GetAttackRate();
TargetAttribute->SetHealth(TargetAttribute->GetHealth() - AttackDamage);
전체적인 코드는 다음과 같다.
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
{
FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
if (!SourceASC || !TargetASC) return;
const UMyAttributeSet* SourceAttribute = SourceASC->GetSet<UMyAttributeSet>();
UMyAttributeSet* TargetAttribute = const_cast<UMyAttributeSet*>(TargetASC->GetSet<UMyAttributeSet>());
if (!SourceAttribute || !TargetAttribute) return;
const float AttackDamage = SourceAttribute->GetAttackRate();
TargetAttribute->SetHealth(TargetAttribute->GetHealth() - AttackDamage);
}
bool bReplicateEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}