
해당 글은 https://github.com/tranek/GASDocumentation 의 설명을 한글로 번역 후 첨언 및 요약 해 나 보려고 내 입맛대로 작성한 글이다.
Attribute(속성)란 GAS의 핵심 시스템 중 하나로 FGameplayAttributeData라는 struct(구조체)를 기반으로 사용하는 float형식의 수치를 의미한다. 이 float 기반의 속성은 캐릭터에게 필요한 수치를 관리하는 것에 도움이 되는데 예를 들어 플레이어의 체력부터 시작해, 레벨에 따른 플레이어 최대 체력의 수치, 골드 등을 Actor에게 배치해 수치를 기반으로 행동하고 플레이할 수 있도록 해주는 시스템이다.
GAS에서는 특히 이 속성에 대해서 변경 유무와 변경에 대한 대응을 원활하게 하기 위해서 GameplayEffect에 의해서만 수정해달라고 권고한다. (사실 이 부분은 프레임워크 들의 주요한 특성이기도 한 것 같다. 굳이 자체적인 시스템을 구축해 변경에 대한 대응을 할 이유도 없고, 일반 이용자가 이 대응을 직접 다루기에는 버그가 많을 가능성이 높다)
이 속성은 AttributeSet라는 UAttributeSet 기반의 class 안에서 정의되고 존재해야한다. AttributeSet의 역할은 표시된 속성을 Replicated하는 것에 초점을 갖고 있기 때문에 이곳에서 사용하는 것을 권장한다. 해당 내용은 추후 AttributeSet에서 속성을 정의할 때 더 자세히 설명할 것이다.
속성을 정의할 때 .h 파일에서 특정 속성의 UPROPERTY 매크로 안에서 Meta = (HideInDetailsView)라는 값을 넣어준다면 다른 곳에서는 보이지 않는다. 소스 코드 내부에서만 돌릴 속성한테 좋은 팁이라 할 수 있다.
속성 내부의 값은 여러가지가 있지만 실제로 우리가 수치로써 활용하는 값은 2개가 포함되어 있다. 그것이 BaseValue와 CurrentValue라는 것인데 둘의 역할이 조금 달라 차이를 알아보자
BaseValue라는 것은 해당 속성의 영구적인 값을 의미하고, CurrentValue는 상대적인 값을 의미한다. 이렇게만 말하면 뭔가 애매하게 느껴질 것 같아 부연설명을 추가하자면,
체력 설정이 500으로 되어 있는 Actor(미니언)가 있다고 가정해보자, 이런 경우 BaseValue는 500으로 고정된다. (설정을 500으로 했기에), 여기서 CurrentValue 또한 500이다. 당장보면 큰 차이는 없다. 하지만 CurrentValue는 외부 상황에 따라 변화하게 된다.
플레이어가 액터를 공격한다고 가정해보자. 그 액터를 공격할 때는 여러 함수를 이용해 공격하겠지만 GAS에서는 GameplayEffect를 통해 액터의 체력 수치(50만큼 깎았다고 가정해보자)를 깎을 것이다. 이렇게 된다면 우리가 생각한 액터의 체력은 550이어야 한다.
하지만 BaseValue는 영구적인 값이기에 600 그대로 유지된다. CurrentValue에서 이 50만큼 제거된 550으로 값이 변화한다.
그리고 이후에 아군 플레이어가 액터를 치유한다고 가정해보자. GameplayEffect를 통해 아군 액터를 40만큼 치유하게 된다면 BaseValue는 여전히 600이지만 CurrentValue는 550 + 40 = 590 이라는 결과 값을 가지게 된다.
즉 어찌보면 BaseValue는 최대 체력, CurrentValue는 현재 체력으로 생각하게 된다만, BaseValue를 최대 체력으로 보면 절대 안된다. BaseValue가 영구적인 값이라고 말했었다. 이말은 즉슨 절대로 변하지 않는 값이라는 것이 된다. 하지만 최대 체력으로 빗대서 생각해볼 때 최대 체력이 변하지 않은 적이 있었나에 대해 생각해보아야 한다.
위에서 체력을 예시로 들었으니 체력에 대해 더 생각해본다면, 특정 아이템이나 버프를 통해서 최대 체력이 증가하는 케이스가 존재한다. 하지만 이 최대 체력이 증가해도 BaseValue는 고정 값이기에 변하지 않는다. 더 구체적으로 예시를 들어보자
LOL을 예시로 들어보자. LOL에서는 워모그의 갑옷 이라는 아이템이 존재한다. 이 아이템에는 부가적인 효과도 있지만 우리가 생각해봐야할 옵션은 최대 체력만 생각해보자.
내 현재 최대 체력은 2000이다. 그리고 워모그(워모그의 갑옷을 줄여 워모그라 부르자)에서 최대 체력 1000을 올려준다고 가정해보자. 이렇게 된다면 내 현재 최대 체력은 3000이 된다. 이걸 단순한 하나의 Attribute에서 생각해보게 된다면 BaseValue는 2000이지만, CurrentValue는 3000이 된다. 왜냐하면 BaseValue는 바뀌지 않으니까.
BaseValue는 바뀌지 않은 상태에서 내가 공격을 당해 체력이 1200이 되었다. 그리고 집에 귀환해서 체력을 회복한다.
내가 생각한 최대 체력은 3000이다. 하지만 우리가 최대 체력으로 가정하기로 한 값은 BaseValue다. 이렇게 된다면 최대 체력만큼 체력을 회복하면 2000까지 밖에 회복하지 못한다. 3000까지 회복하기 위해서는 BaseValue + 워모그의 추가 체력(1000)만큼 회복하게 처리를 해줘야 한다.
당장에 보았을 때는 그렇다면 BaseValue를 기본 최대 체력으로 하고 아이템(워모그)의 추가 체력을 계산해서 회복하면 되지 않겠냐 할 수 있지만 간단하게 해서 워모그만 있지만 이 체력에 관련된 내용 들이 늘어나게 되면 문제가 커지게 된다.
레벨에 따른 최대 체력 상승률, 일시 버프로 인한 최대 체력 상승, 다양한 아이템들의 최대 체력 상승 등 외부 요인들로 인해 최대 체력이 매번마다 변하게 될 텐데 계산할 때 마다 이 모든 것들을 매번 계산하는 것은 연산 낭비이며 이렇게 해야할 이유 또한 존재하지 않는다.
그렇기에 BaseValue를 특정 수치의 최대로 사용하지 않고 기본 값 정도로만 생각해두는 것이 좋다. 최대 수치에 대한 것들은 별도의 변수를 만들어 활용하는 것을 권장한다.
그나마 레벨이나 특정 스텟에 따른 최대 값을 설정해주기 위해 DataTable을 설정해 그거에 맞게 변경해주는 작업이 있다라고는 하지만
WIP(Work In Progress - 진행 중이고 실무에서는 사용 불가함) 상태기 때문에 별도의 변수를 만들어주는 것이 가장 이상적이라고 할 수 있다.
물론 BaseValue를 바꾸는 방법 또한 존재하지만 사용하지 않는 것이 정신 건강에 더 이롭다.
CurrentValue 변경 방법: PreAttributeChange();
BaseValue 변경 방법: PostGameplayEffectExecute();
BaseValue의 변경 사례: 즉시 적용되고 영구적인 효과를 지닌 Instance(순간)과 Periodic(주기적인) GameplayEffects에서 사용하면 좋다. (레벨 업 할 때 마다 최대 체력의 BaseValue를 변경해 처리하는 방법)
CurrentValue의 변경 사례: 일정 시간(혹은 무한적으로 - 영구적이 아닌 것이 핵심) 지속되는 GameplayEffect에서 사용하면 좋다. (특정 시간 만큼의 버프)
Meta 속성이라는 것은 일종의 Placeholder의 역할을 수행한다. Placeholder라는 것이 다양한 분야에서 여러가지 의미로 쓰이는데 게임쪽에서는 일시적으로 사용할 상수라고 생각하면 편하다.
Placeholder는 여러 분야에서 여러 의미로 사용된다.
1. 웹: input에 아무것도 기록하지 않았을 경우에 대한 안내문
2. 게임:
2-1. mincraft 사례: 특정 도전과제를 위한 몬스터 잡은 수, 돈 액수 등을 이 곳에 임시로 저장해 외부 플러그인이나 인게임에서 꺼내 쓸 때 사용하는 계층 구조(Unreal Engine에서는 Gameplay Tag 같이) 형태로 사용하는 string key
2-2. GAS에서 원하는 사례: 나 자신의 기본 damage를 Meta Attribute에 저장해 내가 상대방에게 damage를 줄 때 이 데미지가 저장된 Placeholder(Meta Attribute)에서 꺼내서 상대방에게 줄 데미지를 계산한다.
이러한 Meta 속성은 GAS에서 GameplayEffectExecuteCalculation라는 메소드를 통해서 값이 주로 변경된다. (주로 버프, 디버프를 통한 이펙트 효과) 그리고 AttributeSet에서 추가 조작이 가능한데 예를 들어 방어력 속성 값을 이용해 내가 받는 데미지에서 방어력 만큼 값을 제외한 결과 값을 내 체력 소모에 사용하는 방식으로 활용한다.
메타 속성은 위에서 이미 설명했지만 게임 내에서 데미지 그리고 치유 같은 체력 속성 같이 속성을 변화시키기 위한 값들을 저장하고 처리하는 것에 굉장히 효과적이다. 이러한 메타속성이 얼마나 많은 데미지를 입었는가, 그리고 어떤 로직을 통해 이 데미지를 처리할 것인가의 로직을 분리시켜 GameplayEffect의 역할과 ExecuteCalculation의 역할을 분리시킬 수 있다.
-> 데미지를 주는건 GameplayEffect에서 전달하면 ExecuteCalculation에서 데미지를 받을 때 계산하는 로직을 실행시키는 것
특히 이 방식은 Subclass 기반으로 동작하는 AttributeSet에서도 유리하게 동작할 수 있는데 특정 MetaSet이 있는지 없는지에 대해서도 처리가 가능하니 필요에 따라 Meta 속성을 하위 클래스에 집어넣고 사용하는 것도 가능해진다라는 것이다. 그렇기에 전투에 필요한 Meta 속성만 추가로 넣은 Subclass에서 관련된 effect들을 계산하면 되기에 로직 분리에도 도움이 될 수 있다.
Meta 속성은 굉장히 편하고 좋은 것은 사실이나 필수는 아니다라는 것이 중요하다. GA(GameplayAbility), GE(GameplayEffect), ASC, Attribute 등은 해당 프레임워크의 코어 기능이지만 Meta 속성은 Attribute에서의 일부기 때문에 상황에 따라 사용하면 된다.
단순한 게임이라면 굳이 무리해서 사용할 필요가 없고, 복잡한 데미지나 속성 계산이 필요하다면 사용하는 것이 좋다.
UI나 플레이를 통해서 속성 값이 변한다면 변하는 것에 대해서 전체적인 변화가 필요할 때가 있다. (ex. 체력이 소모되어 0이 되면 죽는다 등)
GAS에서는 이 부분에 대해서 UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute) 함수를 사용해 등록한다. 해당 함수는 Delegate 등록 함수로 자동으로 값 변화에 따라 자동으로 호출되는 Delegate를 반환해주는데 이 Delegate에 필요한 함수를 등록하면서 사용하면 된다.
샘플 프로젝트에서는 아래와 같은 방식으로 바인딩한다.
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);
추가로
FGameplayEffectModCallbackData라는 함수는 서버에서만 동작한다.
참고로 샘플 프로젝트에서는 Blueprint에서 사용하기 위한 비동기 이벤트하나를 만들었다. AsyncTaskAttributeChanged.cpp을 만들어서 그 안에서 변경되는 attribute 설정에 따라 함수를 실행할 수 있도록 처리하는 작업을 거쳤기에 BP로 작업을 해야하는 경우라면 직접 Delegate 함수를 만들어서 적용시켜 사용하는 것도 방법인 것 같다. (주로 UI 블루프린트 내부에서 쓰일 가능성이 높음)
파생 속성은 다른 하나 이상의 속성을 활용해 일부 혹은 전체의 값을 결정시키는 속성을 의미한다.
ex. 대표적으로는 데미지가 있다. 이 역시도 LOL로 비유를 해보자면, 기본 공격력이 64라고 가정할 때 방어 관통력, 추가 공격력, 버프를 통한 공격력 상승 등을 기반으로 최종 데미지를 계산할 수 있다.
이러한 파생 속성의 구현 방식은 무한으로 적용되는 GameplayEffect, 그리고 추후 다룰 속성 기반의 MMC Modifiers(Modified Math Calculation)를 이용해 사용하고, 의존 속성이 변경되면 새롭게 값이 갱신되는 방식을 사용한다.