첫 번째 게시글에서 Attribute가 무엇인지 간단하게 알아보았었는데 이번 게시글에선 조금 더 자세히 알아보도록 하겠다.
Attribute는 FGameplayAttributeData라는 구조체 안에 정의되어 있다.
이 내부에는 BaseValue 와 CurrentValue가 있다.
BaseValue는 아무런 영향도 받지 않는 기본 수치이고, CurrentValue는 버프, 디버프 등의 여러 영향을 고려하여 현재 플레이어에게 적용되고 있는 수치인 것 같다.
예제 프로젝트에 적힌 설명을 보자.
먼저, 아래는 Attribute에 대한 설명이다.
Attribute는 FGameplayAttributeData라는 구조체에 의해 정의된 실수값이다. 이것은 캐릭터의 HP 부터 캐릭터의 Level, 캐릭터가 보유한 포션의 개수까지 모든 것을 대표할 수 있다. 만약 이것이 액터에 속해있는 게임플레이와 관련된 수치라면, Attribute를 사용하는 것을 고려해야 한다. Attribute는 GameplayEffects에 의해서만 수정되어야 한다. 그래야 ASC에서 변화를 예측할 수 있기 때문이다.
Attribute는 AttributeSet 내부에서 정의되어 존재한다. AttributeSet은 리플리케이트되기로 한 Attribute들을 리플리케이트 하는 것에 대한 책임을 가지고 있다. 어떻게 Attribute를 정의하는지는 AttributeSets섹션을 확인해라.
TIP : 만약, 데이터의 Attribute목록에서 특정 Attribute가 보여지지 않게 하고 싶다면, Meta = (HideInDetailsView) 지정자를 사용하면 된다.
위의 설명을 보면 Atrribute는 FGameplayAttributeData 라는 구조체 내부에 정의된 실수값이라고 표현할 수 있다.
FGameplayAttributeData내부를 보면 아래와 같은 2개의 float형 변수가 선언되어 있다.
위와 같이 BaseColor과 CurrentValue 두개의 float 변수가 선언되어 있다.
아무래도 BaseValue는 기본 수치인 것 같고, CurrentValue는 여러가지 영향에 의해 최종적으로 계산되어 Actor에게 직접적으로 적용되고 있는 수치가 아닐까 싶다. 예제 프로젝트 깃허브의 설명은 아래와 같다.
Attribute는 BaseValue와 CurrentValue 두 값으로 구성되어 있다. BaseValue는 Attribute의 영구적인 값이고, CurrentValue는 GameplayEffects에 의해 일시적으로 수정된 수치가 BaseValue에 더해진 것이다.
예시를 위해, 캐릭터가 600units/second 라는 BaseValue를 가진 Attribute로 이동하고 있다고 가정해보자. 아직 GameplayEffects에 의해 수정된 이동속도가 없기 때문에 CurrentValue 또한 600u/s 일 것이다. 만약, 너의 캐릭터가 50u/s만큼의 이동속도가 증가하는 버프를 얻었다면, BaseValue는 여전히 600u/s 이지만, CurrentValue는 600u/s + 50u/s의 합으로 설정되어 있을 것이다. 이동속도 증가 버프가 사라진다면, CurrentValue는 다시 BaseValue인 600u/s로 돌아오게 된다.
GAS 초심자들은 가끔 BaseValue가 Attribute의 최대값이라고 착각하는 경우가 있다. 이것은 잘못된 접근이다. abilities나 UI에 의해 참조되는 Attribute의 최대값은 별도의 Attribute로 생성해주어야 한다. 최대값과 최소값을 하드코딩 하기 위해서는 최대, 최소에 대한 값을 가진 FAttributeMetaData 으로 DataTable을 정의하는 방법이 있다. 그러나 해당 구조체에 적혀있는 에픽게임즈의 코멘트를 보면 WIP(아직 개발중)이라고 한다. 더 많은 정보를 얻고 싶다면 Attribute.h 를 참고하자. 혼동을 피하기 위해서, Attribute의 최대 수치는 별도의 Attribute로 만들어주고 현재 수치에 대한 Attribute를 최대, 최소의 정보를 가진 Attribute를 이용해서 clamp하는 것을 추천한다. Attribute의 값을 Clamp하는 것은 Current Value를 바꾸고 싶다면 PreAttributeChange() 항목을 참조하면 되고, GameplayEffect로부터 BaseValue를 바꾸고 싶다면 PoseGameplayEffectExecute()항목을 참조하면 된다.
BaseValue에 대한 영구적인 변경은 인스턴스 GameplayEffect로부터 오는 반면, CurrentValue의 변경은 영구적인 GameplayEffect와 지속시간으로부터 온다. 주기적인 GameplayEffect는 인스턴스 GameplayEffect와 동일하게 취급되며 BaseValue를 바꾸게 된다.
(이 마지막 문장은 이해가 잘 안된다. 이 부분은 GameplayEffect에 대한 지식이 있어야 제대로 이해할 수 있을 듯 하다.)
일단, 설명은 위와 같다. BaseValue는 말 그대로 어떠한 영향도 없는 기본 수치이며 CurrentValue는 어떠한 영향에 의해 증가(감소)된 상태의 수치인 것이다.
BaseValue는 Attribute의 최대값이 아니라고 설명하는 부분이 있다. CurrentHP라는 이름으로 현재 HP에 대한 Attribute를 정의하였다면, 이 Attribute는 정말 현재 HP에 대해서만 관여해야 한다.
만약 MaxHP가 100이고, CurrentHP가 50이라고 해보자. 이 때, HP를 100채워주는 물약을 먹었다면, CurrentHP는 150이 되고, MaxHP를 초과하기 때문에 100으로 clamping되어야 한다. 이런 clamping을 할 때, Attribute내부의 BaseValue를 이용해서 clamp 하는 것이 아니라는 것이다. BaseValue는 누군가에게 피해를 입지도 않았고, 어떠한 버프나 디버프도 없는 상태의 값일 뿐이기 때문이다. CurrentValue 는 BaseValue보다 클 수도 있고 작을 수도 있다. BaseValue를 CurrentValue의 범위 기준으로 삼는 것은 매우 잘못되었다는 것이다.
그렇기 때문에 아예 별도로 MaxHP에 대한 Attribute를 정의한 뒤, 이 값에 의해 HP Attribute를 Clamp 하는 것이 좋은 방법이라고 한다. 보통 게임 내부의 HP창을 보면 최대 체력 / 현재 체력 이런식으로 표시가 되는데, 이 것을 BaseValue / CurrentValue로 하는 것이 아니라, MaxHP의 CurrentValue / CurrentHp의 CurrentValue 이렇게 사용하라는 것 같다.
이렇게 정의된 Attribute가 변경될 때마다, 특정 함수를 실행하면서 대응하고 싶다면 UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute) 함수를 사용하여 델리게이트에 원하는 함수를 바인딩하면 된다고 한다.
아래는 예제 프로젝트의 PlayerState 파생클래스에서 콜백함수를 바인딩하고 있는 코드이다.
HealthChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
MaxHealthChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetMaxHealthAttribute()).AddUObject(this, &AGDPlayerState::MaxHealthChanged);
HealthRegenRateChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthRegenRateAttribute()).AddUObject(this, &AGDPlayerState::HealthRegenRateChanged);
ManaChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetManaAttribute()).AddUObject(this, &AGDPlayerState::ManaChanged);
MaxManaChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetMaxManaAttribute()).AddUObject(this, &AGDPlayerState::MaxManaChanged);
ManaRegenRateChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetManaRegenRateAttribute()).AddUObject(this, &AGDPlayerState::ManaRegenRateChanged);
StaminaChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetStaminaAttribute()).AddUObject(this, &AGDPlayerState::StaminaChanged);
MaxStaminaChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetMaxStaminaAttribute()).AddUObject(this, &AGDPlayerState::MaxStaminaChanged);
StaminaRegenRateChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetStaminaRegenRateAttribute()).AddUObject(this, &AGDPlayerState::StaminaRegenRateChanged);
XPChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetXPAttribute()).AddUObject(this, &AGDPlayerState::XPChanged);
GoldChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetGoldAttribute()).AddUObject(this, &AGDPlayerState::GoldChanged);
CharacterLevelChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetCharacterLevelAttribute()).AddUObject(this, &AGDPlayerState::CharacterLevelChanged);
이러한, Attribute들은 FGameplayAttributeData라는 구조체에 1대1로 대응한다.
4개의 Attribute를 만들고 싶다면, 4개의 FGameplayAttributeData 구조체를 생성해야 하는 것이다.
Attribute가 많아질수록 관리가 힘들어지기 때문에, Attribute를 한 번에 관리하기 위한 AttributeSet 클래스도 존재한다.
예제 프로젝트에선 아래와 같이 AttributeSet을 상속받은 클래스를 만든 뒤 내부에 Attribute들을 선언해주었다.
참고로 Attribute는 블루프린트로 선언할 수 없고 무조건 C++로 선언해야 한다고 한다.
또한 AttributeSet 내부엔 아래와 같은 함수들도 선언되어 있다.
정의는 아래와 같이 되어있는데, Attribute를 제대로 리플리케이팅하기 위해 있는 함수라고 한다.
GAMEPLAYATTRIBUTE_REPNOTIFY는 기본적으로 Attribute를 제대로 Replicate 하기 위한 역할을 한다고 한다.
첫번째 게시글에서도 작성하였지만, Attribute의 변화에 대응하기 위해 바인딩한 콜백함수들은 해당 매크로 내부에서 broadcast해주고 있다.
AttributeSet은 여러 방식으로 정의하여, 하나의 ASC가 여러개의 AttributeSet을 관리하도록 할 수도 있다고 한다.
플레이어 스탯관련 AttributeSet을 만들어, 그 안에는 Hp, Mp 등을 정의하고
커뮤니티 관련 AttributeSet을 만들어, 친밀도, 친구의 수 등을 정의하였다면 ASC에 두개의 AttributeSet을 연결하여 하나의 ASC가 여러개의 AttributeSet을 관리하도록 할 수 있다고 한다.
그런데, 동일한 AttributeSet을 여러개 연결할 수는 없다고 한다. 예를 들어 스탯관련 AttributeSet을 FPlayerStatAttributeSet 라는 클래스로 만들었다면, ASC에는 FPlayerStatAttributeSet 인스턴스는 1개만 연결할 수 있다는 것이다. 실제로는 여러개의 인스턴스를 연결하더라도 연결 자체가 안되는 것은 아닌데, Attribute를 변경하거나 탐색할 때 어느 인스턴스로부터 정보를 참조해야 하는지 알 수가 없어 의도와는 다른 결과를 초래할 수 있다고 한다.
Attribute들의 값을 초기화하는 방법은 여러가지가 있다고 한다.
위에서 AttributeSet 내부에서 Attribute를 선언할 때, ATTRIBUTE_ACCESSORS 매크로를 사용하였다면
InitHealth, InitMana 등의 함수를 호출할 수 있게 된다. 이를 이용해서 C++에서 초기화 해도 된다.
하지만, 예제 프로젝트의 경우 블루프린트를 이용하여 초기화해주고 있다.
GameplayEffect를 상속받은 블루프린트 클래스이다.
내부의 Detail창을 보면 아래와 같은 Modifier들이 있다.
확장해보면 아래와 같다.
이렇게 초기화 세팅을 해주고 있다.
그런데 보면 모든 Attribute를 여기서 초기화해주고 있는 것이 아니라, Health, Mana, Stamina등은 초기화해주고 있지 않다.
예를 들어, Health(현재 체력)의 경우, 최초에는 최대체력(MaxHealth)로 초기화가 되어야 할 것이다.
그런데, MaxHealth와 Health를 둘 다 숫자를 입력하여 초기화하게 되면, MaxHealth수치를 바꿀 때마다 Health의 초기화 수치도 수정해야 한다.
예제 코드에선 아래와 같이 Max로 설정한 수치로 해당 Attribute를 초기화해주고 있다.
위의 코드는 Pawn이 Contoller에 의해 possess될 때 실행되고 있다.
첫 번째 게시글에서 알아봤던 것과 겹치는 부분도 많지만, Attribute를 조금 더 자세하게 알아보았고 사용법에 대해서도 조금 알아보았다. 복습하면서 더 이해되는 부분도 있었던 것 같다. 이렇게 야금야금 파헤치다 보면 언젠가 고수가 될 수 있을 거라 믿어 의심치 않는다!
'언리얼 엔진 > GAS' 카테고리의 다른 글
언리얼 엔진 - GAS ( Game Ability System ) [ 5 ] (0) | 2024.04.21 |
---|---|
언리얼 엔진 - GAS ( Game Ability System ) [ 4 ] (0) | 2024.04.17 |
언리얼 엔진 - GAS ( Game Ability System ) [ 2 ] (0) | 2024.04.16 |
언리얼 엔진 - GAS ( Game Ability System ) [ 1 ] (0) | 2024.04.14 |