
해당 글은 https://github.com/tranek/GASDocumentation 의 설명을 한글로 번역 후 첨언 및 요약 해 나 보려고 내 입맛대로 작성한 글이다.
Abiliy System Component (줄여서 ASC)는 GAS의 심장이라고 해도 과언이 아닐정도로 중요하다.
UAbilitySystemComponent라는 Actor Component를 액터(폰이나 캐릭터도 이에 포함)에 삽입시켜 GAS 시스템과 상호작용 시킬 수 있고 이 과정이 GAS 시스템을 활용하는 시작점이 된다. GameplayAbility(줄여서 GA)와 GameplayEffects(줄여서 GE)를 사용하기 위해서는 이 ASC가 액터에 내장(Attach)되어 있어야만 사용이 가능하다. ASC 오브젝트 만이 Attributes를 관리가 가능하기 때문에 꼭 넣어줘야하고 선언 시 굳이 TSubclass 선언 없이 바로 type을 선언하고 attach 시키면 된다.
// IAbilitySystemInterface-> 뒤에 나올 내용이니 그냥 이게 들어간다만 알면 된다.
class TestCharacter : public ACharacter, public IAbilitySystemInterface {
public:
UPROPERTY()
UAbilitySystemComponent* AbilitySystemComponent;
}
조금 더 깊게 들어가서 생각해보자. ASC가 내장되어 있는 액터를 이제 ASC의 OwnerActor라고 부르고 그 액터와 물리적인 반응을 일으키는(물리적이라기 보단 상호작용이라고 생각하면 마음이 편하다.) 액터를 AvatarActor라고 정의해보자.
MOBA 장르의 간단한 AI를 보유한 미니언 같은 경우는 이 OwnerActor와 AvatarActor가 동일할 수 있다. (같은 class에서 관리할 수 있기 때문에) 다만 플레이어 처럼 유저가 컨트롤 하는 액터의 경우는 OwnerActor는 PlayerState의 역할을 AvatarActor는 Character의 역할을 수행하게 된다. 즉 플레이를 진행하는 클라이언트의 주인공 시점에서는 OwnerActor != AvatarActor 가 된다라는 말이다. 이말은 즉슨 대부분의 Actor는 이 ASC를 자신한테 내장하고 있기에 액터가 리스폰 되는 경우 정보가 초기화될 수 있기 때문에 정보가 저장되어야 한다면 별도의 저장하는 공간이 필요하기에 "계속해서 정보를 저장하고 리스폰이 되는 액터" 즉 플레이어 같은 액터는 별도의 공간에 저장하는게 좋고 그것이 PlayerState가 된다라는 것이다.
NetUpdateFrequency
PlayerState에 ASC가 내장되어 있는 경우 PlayerState에서 액터의 속성을 복제하고 전달하는 replcation에서 업데이트 빈도 수를 늘리는 NetUpdateFrequency의 값을 늘려주는 것이 필요하다. PlayerState의 경우 기본값 자체가 매우 낮게 설정되어 있어 Attribute와 GameplayTags가 변경될 때 딜레이가 되거나 인식 지연으로 렉이 발생할 수 있는 가능성이 있기에 빈도를 늘려주는 것이 좋다. Fortnite 라는 선례가 있기 때문에 이렇게 먼저 해두는 게 좋다. (나중에 더 나은 방향이 있다면 그 부분을 연구해봐도 좋을 것 같다)
GetAbilitySystemComponent
위의 예시코드를 보면 ASC를 사용하기 위해서 IAbilitySystemInterface라는 interface를 상속시킨다. OwnerActor와 AvatarActor가 다른 경우 두 군데서 다 해당 interface를 상속시키는 상황이 발생할텐데 ASC의 주소값을 return 시키는 UAbilitySystemComponent* GetAbilitySystemComponent() const 함수는 시스템 내부적으로 찾아서 서로 상호작용 한다라는 것을 알면 좋다. 즉 알아서 동일한 ASC는 서로를 참조한다고 보면 좋다. (오역 있을 수 있음)
GameplayEffects의 관리
ASC에서는 현재 활성화 중인 GameplayEffect를 FActiveGameplayEffectsContainer ActiveGameplayEffects라는 값에서 관리한다.
GameplayAbility의 관리
ASC에서는 Actor에게 부여된 Ability들을 FGameplayAbilitySpecContainer ActivatableAbilities에서 관리한다.
만약에 특정 어빌리티의 아이템을 반복해서 사용할 일이 있다면 그 어빌리티에 ABILITYLIST_SCOPE_LOCK() 매크로를 부여해줘서 FScopedAbilityListLock라는 타입으로 변경하는 작업을 수행한다. lock을 걸어버린다면 루프가 돌 때 ability가 없어지면서 생기는 문제를 방지할 수 있다.
모든 ABILITYLIST_SCOPE_LOCK() 매크로를 설정한 값은 루프가 돌 때 마다 AbilityScopeLockCount가 증가하고 스코프에 접근할 일이 없어지면 점점 그 count가 감소한다. 그렇기에 ABILITYLIST_SCOPE_LOCK매크로가 걸린 어빌리티는 제거하려고 시도하면 안된다.
소스를 직접 보는 방법을 몰라 개인 예측으로 적는 말이라 참고할 필요가 없지만, 아마 연속해서 사용하는 값의 경우 계속해서 ASC에 접근해서 불러오는 방식보다 캐싱을 통해서 바로 접근을 하고 루프가 멈췄을 때 시간이 지나면 캐싱을 없애서 연산 속도를 증가시키는 방식을 위해 락을 거는 것으로 보인다. 그렇기 때문에 직접 제거해버리면 붕 뜬 메모리가 되어 메모리 누수의 원인이 되기에 이런 경고를 한 것이 아닐까 싶다.
(아님 말고...)
ASC에서도 멀티플레이 혹은 다른 객체와 replication을 통해 최신 데이터를 유지해주는 것이 좋다. 보통 GameplayEffect, Tags 그리고 GameplayCues를 replication을 해줘야할텐데 이 것을 내가 어떤 게임을 하냐에 따라 옵션을 다르게 설정할 수 있다.
| Replication Mode | 사용 시점 | 설명 |
|---|---|---|
Full | 싱글 플레이어 | 모든 GameplayEffect를 replicated한다. |
Mixed | 멀티 플레이어, 플레이어가 별도의 액터를 조종할 때 | 주체가 되는 클라이언트(조종 시점의 클라이언트)만이 GameplayEffect를 replicated 하고, GameplayTags와 GameplayCues는 모두에게 replicated 처리를 한다. |
Minimal | 멀티 플레이어, AI가 액터를 조종할 때 | GameplayEffects는 모두에게 replicated하지 않고, Only GameplayTags와 GameplayCues 만 모두에게 replicated한다. |
Mixed 사용 시점에 대한 이유: 이렇게 하는 이유는 멀티 플레이어 즉 다른 플레이어의 수치 변경에 대해서 계속해서 서버와 통신해 변경할 필요는 없기에 받은 값만 본인의 클라이언트 내부에서 관리하지만, 태그나 cues의 경우는 나 뿐만이 아닌 모두에게 중요한 데이터기 때문에 모두에게 replicated 처리를 해야한다.
Minimal 사용 시점에 대한 이유: 특히 AI가 액터를 조종한다면 GameplayEffect에 대한 replicated보다는 이후의 attributes에 대한 정보만 replicated 받으면 되는 문제거나 혹은 볼 필요도 없어 그 쪽에 갔을 때 수동으로 replicated되도 되는 문제로 보이기에 저렇게 처리하는 것 같다. 역시 다만 tag나 cues의 경우는 계속 갱신이 되어야 하기 때문에 모두에게 replicated한다.
Mixed Replication 모드일 때 OwnerActor의 소유자 자체는 Controller가 될 것으로 예상된다. PlayerState의 소유자의 경우 기본적으로 컨트롤러가 소유하지만 캐릭터 자체의 소유 꼭 Controller가 된다는 보장은 없기 때문에 PlayerState가 아닌 OwnerActor와 함께 Mixed Replication 모드를 사용한다면 OwnerActor에서 SetOwner를 호출해 유효한 컨트롤러를 탐색해야한다.
참고로 4.24 이상의 버전부터는 PossessedBy()를 사용해 Pawn에 새로운 컨트롤러를 부여한다.
이전의 설명에서도 언급했지만 ASC는 반드시 C++에서 사용해야한다. (Blueprint가 가능한 것은 gameplay effect, cue, ability다) 즉 initialize도 반드시 C++에서 진행되어야 함을 알 수 있다.
// Player 기준으로는 PlayerState, 다른 것들은 캐릭터나 Pawn 기준으로 하면 된다.
AGDPlayerState::AGDPlayerState()
{
// CreateDefaultSubobject로 기본 객체를 생성하고 replicated 옵션을 키는 작업을 수행한다.
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
//...
}
ASC는 서버와 클라이언트에서 전부 initialize는 OwnerActor와 AvatarActor를 사용해 초기화 해야한다. 그 이후에 PossessedBy를 통해서 서버에서 ASC 초기화 작업을 수행하고 클라이언트에서는 플레이어 컨트롤러의 AcknowledgePossession에서 설정한다.
// APACharacterBase.cpp
// 싱글 플레이 게임에서는 신경쓰지 않아도 되는 작업이다.
void APACharacterBase::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
// Mixed Mode replication인 경우에 contorller를 새로 할당해줘야하는 것을 잊지 말아야 한다.
SetOwner(NewController);
}
// APAPlayerControllerBase.cpp
// 클라이언트 기준으로 플레이어 컨트롤러에서 해당 작업을 수행해 Pawn들의 초기화 할당을 진행한다.
void APAPlayerControllerBase::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);
APACharacterBase* CharacterBase = Cast<APACharacterBase>(P);
if (CharacterBase)
{
CharacterBase->GetAbilitySystemComponent()->InitAbilityActorInfo(CharacterBase, CharacterBase);
}
//...
}
그리고 플레이어가 직접 조종하는 캐릭터 (PlayerState와 PlayerController를 보유한 캐릭터)의 경우 캐릭터에 직접 ASC 정보를 초기화 해주는 로직을 추가해주면 된다.
// 서버 기준 코드. PossessedBy
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// 위에서 설명했던 대로 플레이어의 경우는 Owner가 GameState, Avatar가 Pawn이 되면 된단.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
}
}
// 클라이언트 기준 코드
void AGDHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
}
}