3. Gameplay Tags

골두·2024년 7월 10일

Unreal GAS Framework

목록 보기
3/8
post-thumbnail

해당 글은 https://github.com/tranek/GASDocumentation 의 설명을 한글로 번역 후 첨언 및 요약 해 나 보려고 내 입맛대로 작성한 글이다.

Gameplay Tags

FGameplayTags는 계층 구조(ex. Parent.Child.GrandChild)같은 key 기반으로 설계되어 GameplayTagManager에서 저장하고 필요할 때 꺼내서 사용하는 태그 시스템이라고 보면 된다.

태그 시스템이라는 것은 객체의 상태를 분류하고 설명해주는 것에 굉장히 유리한 면을 가지고 있는데, 예를 들어 캐릭터의 기절 여부를 알기 위해서 bool 변수를 하나 만들어서 사용해도 되지만 태그를 사용해서 State.Debuff.Stun이라는 태그를 주입시키고 이 태그가 있을 때는 기절 상태다 라는 것을 나타낼 수 있다. 이렇게 하게 되는 경우 외부에서 기절 상태에 대해 인지할 때 기절 상태를 위해 매번마다 새로운 변수를 만들 필요도 없고 태그를 인지하는 공통 함수를 만들어 쉽게 처리할 수 있기에 자주 사용할 수 있다.

// 개인이 작성한 부가 설명
void IsStun(ACustomPawn* P) {
	// 매번 pawn마다 IsStun 같은 변수를 만들어줘야 한다. 관리 포인트가 늘어나고 메모리 사용량 또한 증가한다.
	return P->IsStun;
};

void IsStun(AcustomPawn* P) {
	// TagManager는 엔진에 내장된 함수가 아닌 임의로 만든 함수이니 오해하지 말자.
	return TagManager::GetTag(P, "State.Debuff.Stun");
}

태그를 객체에 제공할 때 GAS에서 상호작용을 할 수 있게 하기 위해서는 객체의 ASC에 태그를 추가해주는 것이 좋다. ASC는 GameplayTagAssetInterface를 상속(implements)받고 있기 때문에 넣어두기만 해도 알아서 접근이 가능하다 라는 것을 알아두면 좋다.

여러개의 Tag 관리

우리가 기본적으로 같은 타입을 여러개를 쓰려면 Array 타입을 사용한다. C++ 관점에서는 동적 배열 선언을 위해 포인터를 선언해 관리해주거나 하지만 Tag의 경우는 Array 타입이 아닌 FGameplayTagContainer 라는 타입을 사용하라고 GAS에서는 권장한다. FGameplayTagContainer는 배열은 아니고 별도의 구조체이지만(간혹 반복을 위해 GameplayTag의 배열 형태로 반환되는 경우도 있다) 일부의 효율성을 위해서 이 타입을 사용하라고 한다.

FGameplayTagContainer의 장점 중 하나는 태그들은 보통 FName 기반으로 되어있지만 Project Settings에서 태그들에 대해 빠른 복제(replicated) 여부를 활성화 시킨다면 이 태그들을 묶어서 사용하는 것이 효율적이기 때문이라고 한다. (빠른 복제(replicated) 옵션 말고도 최적화 관련 옵션이 많이 있지만 빠른 복사 옵션이 가장 우선적으로 써야하는 것 으로 보인다.)

자세한 것은 추후 더 조사해보려고 하지만 GameplayTagContainer.cpp의 소스코드를 대강 봐도 어느정도 최적화를 위한 고민이 보이는 것이 확인된다.

// path: /Engine/Source/Runtime/GameplayTags/Private/GameplayTagContainer.cpp
FGameplayTagContainer FGameplayTagContainer::Filter(const FGameplayTagContainer& OtherContainer) const
{
	SCOPE_CYCLE_COUNTER(STAT_FGameplayTagContainer_Filter);

	FGameplayTagContainer ResultContainer;

	for (const FGameplayTag& Tag : GameplayTags)
	{
		if (Tag.MatchesAny(OtherContainer))
		{
			ResultContainer.AddTagFast(Tag);
		}
	}

	return ResultContainer;
}

또한 빠른 복제 옵션을 제대로 사용하려면 서버와 클라이언트 간의 GameplayTag가 동일해야 하고, 일반적으로는 다를 일 또한 없기 때문에 활성화 해두는 것이 좋다.

GAS 관점에서

GAS에서 GameplayTags를 FGameplayTagCountContainer라는 공간에 저장하는 방식 또한 존재한다. FGameplayTagCountContainer는 Map 자료 구조를 가지고 있어 태그를 Key로, 현재 그 태그의 갯수를 Value로 설정해 사용하는 방식으로 운영하고 있다. FGameplayTagCountContainer 구조에서는 GameplayTag가 존재하더라도 Count는 0으로 반환되는 경우가 있다 (그 태그를 이전에 사용했다 지금은 없을 때). 이러한 결과는 가끔 디버깅할 때 ASC에서 태그가 있는 것 처럼 보이는 문제가 생길 수 있기 때문에 HasTag()HasMatchingTag()같은 함수를 사용해 Tag의 count 수를 확인하고 그것이 아에 존재하지 않거나 count가 0이라면 false를 반환하게 함으로써 해당 문제를 보완할 수 있다.

FGameplayTagCountContainer(줄여서 GTCC라고 임의로 부른다)랑 FGameplayTagContainer(줄여서 GTC라고 임의로 부른다)의 차이에 대해 설명이 미흡해 조금 더 보완하자면 각 2개의 struct는 별개의 역할을 수행한다. 얼핏 내용을 읽어보면 GTCC가 GTC보다 더 좋다라는 의미 처럼 적혀있는 듯한 느낌을 받았지만 GTCC의 소스코드를 보면 메소드에서 GTC를 기반으로 수행하는 내용들이 많고, GTCC는 GAS에 GTC는 Unreal Engine에 내장되어 있는 각각의 요소이기에 기본적으로는 GTC를 쓰면서 GTCC랑 연계해서 사용하는 방향으로 가면 될 것 같다.

GameplayTag 만들기

GameplayTag의 정보는 DefaultGameplayTags.ini라는 config 디렉토리 안의 파일에서 관리가 된다. UE에서의 .ini 파일은 보통 Unreal Editor에서 조작이 가능한 값이기에 Tag를 만들때는 Editor에서 조작과 사용이 가능하다. (매번마다 직접 적을 필요 없이 .ini 기반으로 선택이 가능하다라는 이점이 존재한다라는 것이 핵심이다)

GameplayTag는 특정 상황에서 replicated 되는 케이스가 존재하는데 대표적으로는 GameplayEffect에 들어갈 때 Replicated가 진행된다. 그렇기 때문에 혹시 몰라 ASC에서는 LooseGameplayTags 옵션을 이용해서 수동으로 replicate를 관리할 수 있게 한다. 이 옵션을 사용하게 되면 기본적으로 replicated가 되지 않기에 수동으로 replicated를 실행해줘야한다. 그렇기 때문에 보통 클라이언트 내부에서 사용하는 tag에서 사용해 이 태그가 존재할 때의 반응 이후 서버에 replicated할 내용만 자동으로 전달되게 처리가 가능해진다 라는 것이 핵심이다.

LooseGameplayTags를 사용하기 위해서는 UAbilitySystemComponent::AddLooseGameplayTag()UAbilitySystemComponent::RemoveLooseGameplayTag()로 관리한다.

GameplayTag 사용하기

본격적으로 GameplayTag를 사용할텐데 사용하는 방법은 크게 참조, 관리, 필터 로 나눠서 정리한다.

  1. 참조
    아래의 코드와 같이 사용한다. 태그 Name의 경우 FName에 계층 구조의 key string을 넣어주면 된다. (상황에 따라 별도의 namespace에 만들어서 관리해도 무방함)

    FGameplayTag::RequestGameplayTag(FName("Your.GameplayTag.Name"))
  2. 관리
    GameplayTag에서 단순히 특정 태그만 가져오는 것이 아닌 특정 태그의 상위 및 하위 태그, 특정 그룹 태그를 가져오고 싶을 수 있는데 이런 경우에서는 GameplayTagManager 매니저를 통해서 관리가 가능하다. 이 곳에서 관련된 메소드들을 찾아볼 수 있으니 참고하면 좋을 것 같다.

  3. 필터
    GameplayTag와 TagContainer의 경우 UPROPERTY 옵션과 Meta = Category 옵션도 사용이 가능하다. 이 옵션을 통해서 특정 Category의 태그 정보 들만 필터링 해서 가져오는 것 또한 가능하다.

ex. Meta(Category=TestCat) 인 경우 Category가 TestCat인 태그들만 가져오는 것이 가능하다는 것을 알면된다.

자세한 코드는 Engine\Plugins\Editor\GameplayTagsEditor\Source\GameplayTagsEditor\Private\SGameplayTagContainerGraphPin.cpp에 존재하니 참고하면 좋다.

GameplayTag의 변경에 따른 반응 처리

ASC에서 GameplayTag의 변경에 대한 delegate를 제공해준다. (추가나 삭제의 경우)

이 Delegate는 Tag가 추가 및 제거 되는 경우, 그리고 GameplayTagCount가 변경되는 경우를 EGameplayTagEventType에서 찾아 할당해 상황에 따라 반응하게한다.

AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);

virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);

위의 코드를 번역해보자면 RegisterGameplayTagEventState.Debuff.Stun라는 태그의 변경이 있을 때 EGameplayTagEventType에 따라(현재는 NewOrRemoved니까 추가 및 제거의 경우) AddUObject로 DynamicDelegate에 현재 객체 주소(주로 pawn이나 character)와 반응에 대해 실행할 함수를 사용한다.

GameplayTag의 외부 플러그인 참조 시

외부 플러그인에서 별도의 gameplaytag가 담긴 .ini 파일이 생성될 때에 대해서는 외부 플러그인 모듈에서 파일을 불러와 사용하는 방식을 활용하면 된다.

void FCommonConversationRuntimeModule::StartupModule()
{
	TSharedPtr<IPlugin> ThisPlugin = IPluginManager::Get().FindPlugin(TEXT("CommonConversation"));
	check(ThisPlugin.IsValid());
	
	UGameplayTagsManager::Get().AddTagIniSearchPath(ThisPlugin->GetBaseDir() / TEXT("Config") / TEXT("Tags"));

	//...
}

CommonConversation이라는 플러그인이 존재하는지 탐색하고 그 플러그인이 존재한다는 check 매크로를 통해 검증이 완료되면 UGameplayTagsManager에서 해당 플러그인에서 config와 Tags 디렉토리를 탐색해 태그.ini 정보를 가져오게 처리하면 된다.

정확하게 어떻게 사용하면 된다라는 말은 없지만 아마 처음에 플러그인을 불러온 후 태그 정보를 다 가져와 외부 메모리에 저장하고 사용하면 유용하게 사용할 것 같다.

profile
나 볼려고 만든 블로그 (블로그 이전: https://goldfrosch.tistory.com/)

0개의 댓글