Attributes 추가해보기 (GameAbilitySystem)

Lee Raccoon·2024년 9월 7일
0

게임플레이 어트리뷰트

Attributes

Attributes란

쉽게 말하자면 게임 내 요소와 관련된 수치들이라고 할 수 있다.
이들은 FGameplayAttributeData라는 구조체로 존재한다.
그리고 Attribute Set에 저장되어 관리된다.
우리는 이 Attributes의 변경을 감지할 수 있고 그에 따른 Function을 호출할 수 있다.

이들은 코드에서 바로 세팅할 수 있지만, 더 선호되는 방식은 Gameplay Effect를 사용하는 방법이다.
Gameplay Effect를 사용하여 다양한 방식으로 Attributes에 영향을 주고 이를 predict할 수 있다.

Prediction?

여기서의 predict란 클라이언트가 서버의 허가를 받지 않고도 어떤 값을 바꿀 수 있다는 것이다.
값을 클라이언트 측에서 변경하고 이를 서버에 알린다.
(서버는 이 변경이 유효하지 않다면 다시 롤백해버린다.)

이 Prediction은 멀티플레이에서 아주 이점이 많다.
Prediction이 없는 경우, 클라이언트는 서버에게 값을 변경한 것을 알리고 서버는 이를 검사한 후 유효하다고 판단하면 그제서야 클라이언트들에게 알린다.
이 방식으로는 서버에 갔다가 클라이언트까지 다시 알려질 때 까지 delay가 생기게 된다.

하지만 Prediction을 사용하는 경우,
클라이언트에서 바로 값을 변경한다. 플레이어는 delay가 없는 게임 환경을 경험할 수 있다.
서버는 이 변경이 유효한지 확인하고 클라이언트에게 알릴 지 다시 롤백 시킬 지 판단하게 된다.
여전히 서버는 Authority를 갖고 있고, 클라이언트는 Delay에서 해방되는
그런 좋은 방식이다.

Attributes Data 추가하기

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 Accessor

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 관련 기능이긴 해도 멀티 게임은 싱글 플레이가 가능하지만 싱글 플레이는 멀티가 안되니까 아무튼 알아두는게 이득이다.

profile
영차 영차

0개의 댓글