[정리] GAS / 어트리뷰트 (Attribute)
태그: AttributeSet, Game Engine, GameAbilitySystem, GAS, Unreal
카테고리: UnrealDocs
어트리뷰트 세트 (Attribute Set)
- 단일 어트리뷰트 데이터인 GameplayAttributeData의 묶음을 어트리뷰트 ‘세트’라고 한다.
- GameplayAttributeData는 하나의 값이 아닌 두 가지 값으로 구성되어 있다.
- BaseValue : 기본 값. 영구히 적용되는 고정 스탯 값을 관리하는데에 사용함.
- CurrentValue : 변동 값. 버프스킬 등으로 임시적으로 변동된 값을 관리하는데에 사용함.
- 어트리뷰트 세트의 주요 함수 (오버라이드하여 사용할 수 있도록 제공함)
- PreAttributeChange : 어트리뷰트 변경 전에 호출
변경될 값을 레퍼런스(&)로 넘겨줘서, 이 값을 변경해도 되는지 체크하여 값을 바꾸거나 냅두도록 사용할 수 있음. - PostAttributeChange : 어트리뷰트 변경 후에 호출
변경 되기 전의 값과 변경 후의 값을 인자로 넘겨줘서 변화가 일어났다는 걸 알려줄 수 있음. log남길 때 씀. - PreGameplayEffectExecute : 게임플레이 이펙트 적용 전에 호출
- PostGameplayEffectExecute : 게임플레이 이펙트 적용 후에 호출
- PreAttributeChange : 어트리뷰트 변경 전에 호출
- 어트리뷰트 세트 접근자 매크로
- 많이 수행되는 기능에 대해 매크로를 만들어 제공
- ASC는 초기화될 때, 같은 액터에 있는(OwnerActor)
UAttributeSet
타입 객체를 찾아서 ‘자동으로’ 등록한다.
매크로
AttributeSet.h
에 들어가보면,
1
2
3
4
5
6
7
* To use this in your game you can define something like this, and then add game-specific functions as necessary:
*
* #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
이렇게 매크로에 대한 설명이 있다. 생성한 어트리뷰트 세트 클래스에 복붙하여 사용할 수 있다.
ATTRIBUTE_ACCESSORS
라는 매크로는 밑에 작성된 4개의 함수,
즉 특정 속성에 대해서 총 4개의 함수를 이 매크로 하나로 지정해준다 는 것이다.
- GAMEPLAYATTRIBUTE_PROPERTY_GETTER
- 어트리뷰트를 나타내는 클래스에 등록된 프로퍼티를 가져온다.
- 어트리뷰트를 처리할 때 이게 해당 어트리뷰트가 맞는지 비교할 때 사용할 수 있음.
- GAMEPLAYATTRIBUTE_VALUE_GETTER
- CurrentValue 값을 가져오는 매크로
- GAMEPLAYATTRIBUTE_VALUE_SETTER
- BaseValue 값을 변경해주는 매크로… BaseValue 값은 영구값이라서, 즉 내 스탯을 ‘확정’해주는 용도.
- GAMEPLAYATTRIBUTE_VALUE_INITTER
- BaseValue와 CurrentValue를 같은 값으로 지정해주는 역할.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ABCharacterAttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
// ...
class ARENABATTLEGAS_API UABCharacterAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UABCharacterAttributeSet();
ATTRIBUTE_ACCESSORS(UABCharacterAttributeSet, AttackRange);
protected:
UPROPERTY(BlueprintReadOnly, Category="Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData AttackRange;
}
1
2
3
4
5
6
7
// ABCharacterAttributeSet.cpp
// 생성자에서 초기값 이니셜라이저로 초기화 해주기!
UABCharacterAttributeSet::UABCharacterAttributeSet() :
AttackRange(100.0f)
{
}
ASC에서 Attribute를 추가하는 동작 원리
AbilitySystemComponent
클래스에 들어가보면,
InitializeComponent
라는 함수가 존재한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// in AbilitySystemComponent class
void UAbilitySystemComponent::InitializeComponent()
{
Super::InitializeComponent();
AActor *Owner = GetOwner();
InitAbilityActorInfo(Owner, Owner); // Default init to our outer owner
// cleanup any bad data that may have gotten into SpawnedAttributes
for (int32 Idx = SpawnedAttributes.Num()-1; Idx >= 0; --Idx)
{
if (SpawnedAttributes[Idx] == nullptr)
{
SpawnedAttributes.RemoveAt(Idx);
}
}
TArray<UObject*> ChildObjects;
GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::Garbage);
for (UObject* Obj : ChildObjects)
{
UAttributeSet* Set = Cast<UAttributeSet>(Obj);
if (Set)
{
SpawnedAttributes.AddUnique(Set);
bIsNetDirty = true;
}
}
SetSpawnedAttributesListDirty();
}
Owner가 가지고 있는 자식 오브젝트들을 TArray로 가져온 다음에
for문으로 돌려서 자식들 중에 타입이 UAttributeSet
인게 있다면
SpawnedAttributes
라고 하는 어트리뷰트 목록에 AddUnique로 추가를 해준다.
💡 SpawnedAttributes이 TArray로 선언되었기에 중복된 어트리뷰트셋을 2개 선언할 수 없다.
다만, ‘타입’이 다르면 여러 개를 가질 수 있다.
코드 예시
Player & NPC에 어트리뷰트 설정
1. AttributeSet 생성
AttributeSet를 상속 받은 클래스를 생성한다.
생성된 클래스에 AbilitySystemComponent.h
를 include하고, 필요한 어트리뷰트 데이터들을 작성한다.
필요할 경우 PreAttributeChange
나 PostAttributeChange
함수도 오버라이드 하여 작성해준다.
PreAttributeChange 의 경우 어트리뷰트 값 변경 전에 호출되어
변경될 값을 레퍼런스(&)로 넘겨줘서, 이 값을 변경해도 되는지 파악하여 값을 바꾸거나 냅둘 수 있으므로
구현해주어야 할 것이다.
그리고 헤더에 AbilitySystemComponent.h
를 인클루드하고
UAttributeSet 클래스에 있는 매크로를 복붙한 뒤 작성해준다.
매크로를 사용하는 이유는, 어트리뷰트를 만들면 이것에 접근할 Get, 변경할 Set 등
여러 함수가 필요한데, 매크로를 선언해줌으로써 코드를 줄일 수 있다.
ABCharacterAttributeSet.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include "AbilitySystemComponent.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 UABCharacterAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UABCharacterAttributeSet();
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);
protected:
UPROPERTY(BlueprintReadOnly, Category="Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData AttackRange;
UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxAttackRange;
UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData AttackRadius;
UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxAttackRadius;
UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData AttackRate;
UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxAttackRate;
UPROPERTY(BlueprintReadOnly, Category = "Health", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData Health;
UPROPERTY(BlueprintReadOnly, Category = "Health", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxHealth;
protected:
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
};
ABCharacterAttributeSet.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "ArenaBattleGAS.h"
UABCharacterAttributeSet::UABCharacterAttributeSet() :
AttackRange(100.0f),
AttackRadius(50.f),
AttackRate(30.0f),
MaxAttackRange(300.0f),
MaxAttackRadius(150.0f),
MaxAttackRate(100.0f),
MaxHealth(100.0f)
{
InitHealth(GetMaxHealth());
}
void UABCharacterAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
if (Attribute == GetHealthAttribute())
{
// 어트리뷰트가 현재 hp어트리뷰트일 때 hp값을 최소 0 혹은 최댓값으로 숫자의 범위를 지정
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
}
void UABCharacterAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
if (Attribute == GetHealthAttribute())
{
ABGAS_LOG(LogABGAS, Log, TEXT("Health : %f -> %f"), OldValue, NewValue);
}
}
2. Player에 어트리뷰트 추가
Player의 경우 ASC를 PlayerStat 에 추가했었다.
그러므로 어트리뷰트도 PlayerStat에 추가한다.
ABGASPlayerStat.h
1
2
3
4
5
6
7
8
9
10
11
12
#include "AbilitySystemInterface.h"
UCLASS()
class ARENABATTLEGAS_API AABGASPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
// ...
UPROPERTY()
TObjectPtr<class UABCharacterAttributeSet> AttributeSet;
};
ABGASPlayerStat.cpp
1
2
3
4
5
6
7
8
#include "Attribute/ABCharacterAttributeSet.h"
AABGASPlayerState::AABGASPlayerState()
{
ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("AttributeSet"));
}
3. Player의 공격탐지에 어트리뷰트 사용
기존에 하드코딩 되어있던 부분(const float AttackRange = 100.0f;
)을
어트리뷰트에 넣은 값으로 대체해주었다.
ABTA_Trace.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
{
ACharacter* Character = CastChecked<ACharacter>(SourceActor);
// ASC 포인터를 가져온다
UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
if (!ASC)
{
ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
return FGameplayAbilityTargetDataHandle();
}
// 어트리뷰트셋 포인터를 가져온다
const UABCharacterAttributeSet* AttributeSet = ASC->GetSet<UABCharacterAttributeSet>();
if (!AttributeSet)
{
ABGAS_LOG(LogABGAS, Error, TEXT("AttributeSet not found!"));
return FGameplayAbilityTargetDataHandle();
}
FHitResult OutHitResult;
const float AttackRange = AttributeSet->GetAttackRange();
const float AttackRadius = AttributeSet->GetAttackRadius();
// ...
}
4. Player의 공격 데미지를 NPC에 전달
공격 탐지에서 타겟이 있을 경우
각각의 SourceASC(Player)와 TargetASC(NPC) 에서 Attribute를 가져와서
SourceAttribute의 데미지 값을 TargetAttribute의 SetHealth
함수를 사용하여 hp를 변경함으로써 데미지를 전달한다.
이때 SourceASC는 GetAbilitySystemComponentFromActorInfo_Checked
함수를 호출하여 어빌리티를 발동한 액터의 ASC를 가져올 수 있다.
TargetASC는 UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
함수를 호출하여
탐지 결과에 들어있는 액터의 ASC를 가져올 수 있다. (#include "AbilitySystemBlueprintLibrary.h"
를 인클루드 해야한다.)
그리고 TargetAttribute를 사용하여 Target의 Health값을 변경해야 하는데,
GetSet 함수는 const로 반환하므로 값을 변경할 수 없다.
그래서 const_cast를 사용하여 const를 떼준다.
ABGA_AttackHitCheck.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
// ABAT_Trace가 끝나면 이 함수가 호출될 것.
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();
// 데미지 전달 (타겟 액터에게)
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
if (!SourceASC || !TargetASC)
{
ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
return;
}
const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();
UABCharacterAttributeSet* TargetAttribute = const_cast<UABCharacterAttributeSet*>(TargetASC->GetSet<UABCharacterAttributeSet>());
// Source는 데미지값을 읽어들이고, Target은 값을 '변경'해줘야 한다.
// 그런데 GetSet 함수는 const로 반환하므로 변경을 할 수 없다.
// 그래서 const_cast를 하여 const를 제거한다.
if (!SourceAttribute || !TargetAttribute)
{
ABGAS_LOG(LogABGAS, Error, TEXT("Attribute not found!"));
return;
}
const float AttackDamage = SourceAttribute->GetAttackRate();
TargetAttribute->SetHealth(TargetAttribute->GetHealth() - AttackDamage);
}
bool bReplicatedEndAbility = true;
bool bWasCancelled = true;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
// 어빌리티 종료
}
5. NPC에 어트리뷰트 추가
ABGAS_CharacterNonPlayer.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "AbilitySystemInterface.h"
UCLASS()
class ARENABATTLEGAS_API AABGAS_CharacterNonPlayer : public AABCharacterNonPlayer, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AABGAS_CharacterNonPlayer();
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
// AI 컨트롤러가 이 NPC 컨트롤러를 장악할 때 ASC를 초기화 하기 위해 이 함수 내부에 구현해줄 것임.
virtual void PossessedBy(AController* NewController) override;
protected:
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<class UAbilitySystemComponent> ASC;
UPROPERTY()
TObjectPtr<class UABCharacterAttributeSet> AttributeSet;
};
ABGAS_CharacterNonPlayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "AbilitySystemComponent.h"
#include "Attribute/ABCharacterAttributeSet.h"
AABGAS_CharacterNonPlayer::AABGAS_CharacterNonPlayer()
{
ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("AttributeSet"));
}
UAbilitySystemComponent* AABGAS_CharacterNonPlayer::GetAbilitySystemComponent() const
{
return ASC;
}
void AABGAS_CharacterNonPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// npc는 PlayerStat 을 사용하지 않아서 모두 자신(this)를 넣어주게됨.
ASC->InitAbilityActorInfo(this, this);
}
댓글남기기