쉽게 말하자면 게임 내 요소와 관련된 수치들이라고 할 수 있다.
이들은 FGameplayAttributeData라는 구조체로 존재한다.
그리고 Attribute Set에 저장되어 관리된다.
우리는 이 Attributes의 변경을 감지할 수 있고 그에 따른 Function을 호출할 수 있다.
이들은 코드에서 바로 세팅할 수 있지만, 더 선호되는 방식은 Gameplay Effect를 사용하는 방법이다.
Gameplay Effect를 사용하여 다양한 방식으로 Attributes에 영향을 주고 이를 predict할 수 있다.
여기서의
predict란 클라이언트가 서버의 허가를 받지 않고도 어떤 값을 바꿀 수 있다는 것이다.
값을 클라이언트 측에서 변경하고 이를 서버에 알린다.
(서버는 이 변경이 유효하지 않다면 다시 롤백해버린다.)
이 Prediction은 멀티플레이에서 아주 이점이 많다.
Prediction이 없는 경우, 클라이언트는 서버에게 값을 변경한 것을 알리고 서버는 이를 검사한 후 유효하다고 판단하면 그제서야 클라이언트들에게 알린다.
이 방식으로는 서버에 갔다가 클라이언트까지 다시 알려질 때 까지 delay가 생기게 된다.
하지만 Prediction을 사용하는 경우,
클라이언트에서 바로 값을 변경한다. 플레이어는 delay가 없는 게임 환경을 경험할 수 있다.
서버는 이 변경이 유효한지 확인하고 클라이언트에게 알릴 지 다시 롤백 시킬 지 판단하게 된다.
여전히 서버는 Authority를 갖고 있고, 클라이언트는 Delay에서 해방되는
그런 좋은 방식이다.
Attributes를 사용할 때에는 일종의 보일러 플레이트라고 불릴만한 것이 있다.
아래의 코드에서 Health라는 캐릭터의 체력에 해당하는 데이터를 만들어보겠다.
//AttributeSet Class의 헤더 파일
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UAuraAttributeSet();
//Replicated로써 등록하기 위해서 필요한 함수
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
//FGameplayAttributeData라는 구조체로 Attribute를 만들 수 있다.
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Character Attribute")
FGameplayAttributeData Health;
//Health가 Replicated 되었을 때 호출할 함수이다.
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldHealth) const; //Last Value가 들어가게됨
};
//구현 파일 (.cpp)
#include "AbilitySystem/AuraAttributeSet.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
#include "Net/UnrealNetwork.h"
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//Replicated로 지정하는 것, 현재 GAS를 사용하면서 값의 반응을 보고 싶기 때문에 REPNOTIFY_Always로 설정
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
}
void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
//서버에 알리고 뭐가 이상하면 원래 값, OldHealth로 롤백 된다.
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}
대충 이렇게 이루어져 있다.
GetLifetimeReplicatedProps
네트워크를 통해 복제되어야 하는, Replicated 변수들을 지정하는데 사용되는 함수이다. 내부에서 DOREPLIFETIME_CONDITION_NOTIFY를 사용하여 프로퍼티를 등록할 수 있다.
#include "Net/UnrealNetwork.h" 헤더파일을 추가해주어야 사용할 수 있다.
OnRep_Health
Health 프로퍼티가 Replicated 되었을 때 호출될 함수이다.
Attribute를 만들 때 UPROPERTY 매크로 내부에서 ReplicatedUsing = OnRep_Health 형식으로 지정할 수 있다.
함수 내에서는 GAMEPLAYATTRIBUTE_REPNOTIFY를 호출 해주어야한다.
앞에 말했던 Predict 시스템에서 필요한 것이다. 서버에 변경사항을 보내고, 서버는 이를 검증한 후 이를 다른 클라이언트에게 전달할지 그냥 롤백시킬 지 정한다.
Attribute에 접근하는 (Getter, Setter 등)을 쉽게 구현할 수 있는 방법이 있다.
바로 특별한 매크로 몇개를 추가해주는 것이다.
//Health로 예를 들어 보자
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Character Attribute")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health);
위 코드처럼 매크로ATTRIBUTE_ACCESSORS를 붙여준다면 해당 FGameplayAttributeData에 대한 Getter,Setter,Initter를 사용할 수 있다.
이 매크로가 무엇인고 정의를 찾아보면
/**
* This defines a set of helper functions for accessing and initializing attributes, to avoid having to manually write these functions.
* It would creates the following functions, for attribute Health
*
* static FGameplayAttribute UMyHealthSet::GetHealthAttribute();
* FORCEINLINE float UMyHealthSet::GetHealth() const;
* FORCEINLINE void UMyHealthSet::SetHealth(float NewVal);
* FORCEINLINE void UMyHealthSet::InitHealth(float NewVal);
*
* 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(UMyHealthSet, Health)
*/
#define GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
static FGameplayAttribute Get##PropertyName##Attribute() \
{ \
static FProperty* Prop = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
return Prop; \
}
.
.
.
GAMEPLAYATTRIBUTE_PROPERTY_GETTER를 비롯하여 Setter, Initter같은 매크로 정의가 존재하고 위에는 관련 주석을 확인할 수 있다.
주석을 요약하면
Getter, Setter, Init 함수를 ATTRIBUTE_ACCESSORS 매크로를 붙여줌으로써 쉽게 사용할 수 있게 도와준다고 한다.
그리고 이 매크로를 정의하는 법을 알려준다.
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
얘네를 코드에 붙여주면 된다.
(또한 이 정의는 AbilitySystemComponent에 존재하는 것이므로, AbilitySystemComponent 헤더를 꼭 include 할 것)
Getter가 두개인 이유는, 값인 Float를 반환하는 함수와 Attribute 자체를 반환하는 함수 두 개가 존재하기 때문이다.
그리고 Set과 Init이 뭐가 다른지 궁금한데, 정의에서 바로 찾을 수 있었다.
#define GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
FORCEINLINE void Set##PropertyName(float NewVal) \
{ \
UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); \
if (ensure(AbilityComp)) \
{ \
AbilityComp->SetNumericAttributeBase(Get##PropertyName##Attribute(), NewVal); \
}; \
}
#define GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \
FORCEINLINE void Init##PropertyName(float NewVal) \
{ \
PropertyName.SetBaseValue(NewVal); \
PropertyName.SetCurrentValue(NewVal); \
}
Setter : BaseValue만 수정
Initter : BaseValue와 CurrentValue 둘 다 수정
Setter가 Current Value를 못바꾸는 걸 봐서는 Setter로 Attribute에 영향을 주는 것은 불가능한가보다.
그래서 ATTRIBUTE_ACCESSORS를 붙여주면 어떤게 가능해지냐?
UAuraAttributeSet::UAuraAttributeSet()
{
InitHealth(100.f);
}
이런 식으로 정의하지 않아도 Getter Setter Initter를 프로퍼티 이름만 붙여서 사용할 수 있다는 것이다.
매우 아주 편리해보인다.
적용되는지 확인해보자.
콘솔 창에 showdebug abilitysystem을 입력하면 된다.

Health가 100인걸 확인할 수 있다.
그리고 이와 함께 위를 보면 아바타가 캐릭터이고 소유자가 플레이어 스테이트인 것을 확인할 수 있다.
원하는 대로 잘 설정이 되어 있다.
멋져..
Attribute를 사용할 때, 보일러 플래이트 코드로써 이것들을 기억하자.
1. GetLifetimeReplicatedProps에 Attribute를 추가하기 (DOREPLIFETIME_CONDITION_NOTIFY 사용)
2. On_Rep 함수를 추가하여 Attribute의 UPROPERTY 매크로 내에 추가해주기 (GAMEPLAYATTRIBUTE_REPNOTIFY 사용)
3. ATTRIBUTE_ACCESSORS 매크로 추가
1, 2번은 Replicated 관련 기능이긴 해도 멀티 게임은 싱글 플레이가 가능하지만 싱글 플레이는 멀티가 안되니까 아무튼 알아두는게 이득이다.