본문으로 바로가기

Unreal Engine의 FText 내부까지 알아보기

category UnrealEngine/Impl 2024. 7. 21. 19:06

우리가 Unreal Engine으로 게임을 제작하다보면 UI를 만들거나, 혹은 현지화 작업을 할 때 FText를 만지게 되어있다.

현재 내가 속해 있는 게임 개발팀에서도 FText를 이용하여 L10N 작업중이다.

Unreal Engine이 FText에 정의한 내용은 다음과 같다.

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/ftext-in-unreal-engine

 

글만봐서는 '텍스트 현지화'를 제공한다고 하는데, 어떻게 사용하고 어떻게 동작하는지에 대해 모호하게 적혀있다.

 

이 문서는 내가 직접 FText에 대해 분석하고, 어떻게 사용하는지에 대해 공유하는 문서이다.

 

1. FText 타입 변수의 사용방법

사용 방법은 정말 간단하다.

우리가 어떠한 문자열을 정의하고, 그것을 언리얼 엔진에게 알려주고 우리는 사용만 하면된다.

// NSLOCTEXT(NamespaceName, KeyName, DefaultValue);
FText testText = NSLOCTEXT("A", "B", "C");

 

가장 기초적인 정의방법은 다음과 같다.

이것을 가장 간단하게 바라보면 다음과 같이 풀어 쓸 수 있다.

"A 라는 Namespace에서 B라는 Key를 가진 FText 값을 주세요. 만약 없다면 DefaultValue로 C를 사용하겠습니다."

 

모든 FText에 대해서 이렇게 작성하기가 귀찮을 수 있다.

그럴땐 Namespace를 미리 지정하고, Key Value만 지정해도 사용할 수 있다.

 

#define LOCTEXT_NAMESPACE "A"
FText testText1 = LOCTEXT("B", "C");
FText testText2 = LOCTEXT("D", "E");
#undef

 

이렇게 사용한다면 NSLOCTEXT를 사용하지 않고 Key와 Value만을 사용하여 FText를 정의할 수 있다.

 

(Stringtable에 대한것은 밑에서 계속 작성하겠다)

 

2. Localization 하기

우리는 위에서 A Namespace에 있는 B Key 값을 정의했다.

이제 이걸 Localization 해보자

 

언리얼 엔진을 켜고 Tools > Localization Dashboard를 클릭한다.

 

이제 이곳에서 GatherText를 해주어야 한다.

 

Gather from Text Files를 체크하고

Search Directories 에 다음과같이 최상위 폴더만 지정해주자

그리고 Gather Text를 누르고, Compile Text를 해준다.

 

그럼 내 프로젝트 Content > Localization > Game에 다음과 같은 파일들이 생성되게 된다.

우리가 정의한 FText를 보려면 en -> Game.po 파일을 메모장으로 열어보자

다음과 같이 정의되어 있을 것이다

msgctext에서 "A,B" 는 A namespace B key 라는 뜻이고

이때 value는 C가 된다는 뜻이다.

en 상태일때는 C였으니 ko 일때는 다르게 출력하고 싶다.

ko 일때의 Culture를 만들어주자

 

 

다시 Localization dashboard를 켜서 밑에 Add New Culture를 누르고 Korean을 추가해준다.

Gather Text를 해주자, 그 후 Export Text를 한 후, ko 에 있는 .po 파일을 열어보자

그리고 msgstr에 우리가 쓸 문자열을 추가해준 뒤 Word Count를 해주면 다음과 같이 정상적으로 나타나게 된다.

 

이제 우리가 게임의 Language를 EN -> KR 로 변환하게 되면 A.B 에 있는 FText의 Value는 "씨" 로 나타나게 된다.

 

3. 불편한데요?

누군가는 이렇게 말할 것이다. 

 

"결국 FText를 사용하려면 코드 어딘가에 NSLOCTEXT나 LOCTEXT로 (Key, Value) 쌍을 제공하고 컴파일 한 다음 에디터를 한번 켜야하네요? 불편한데.."

 

이를 위해 언리얼 엔진은 StringTable이라는 것을 제공한다.

 

Content browser > Miscellaneous > String Table을 눌러 새로운 String Table을 만들어주자

 

 

새로운 Key / Value 값을 만들어주자

이 때 Namespace는 우리가 만든 StringTable의 이름임을 기억하자.

(본인은 ST_TestString이라는 스트링 테이블 파일을 만들었으므로, ST_TestString이 Namespace가 된다. 이는 위에서 따로 지정해줄 수도 있다.)

 

이제 Localization Dashboard를 키고 Gather from Packcages 에서 내가 만든 스트링 테이블의 위치 WildCards를 추가해주고 Gather Text를 실행해주자.

 

진행하면 Word Count가 증가한 것을 볼 수 있다.

이를 통해 알 수 있는 점은, 우리가 새로운 (Key, Value)를 지정하면 그것은 엔진의 Default Language와 같다는 것을 알 수있다.

 

아무튼, 또 Korean의 .po를 수정해주고, Translation Edit로 해당 값을 열면 

다음과 같이 나타난다.

이제 Count words를 해주고 Compile text를 해주자.

 

 

이제 코드에서 다음과 같이 작성하여 사용할 수 있다.

이를 공용적으로 사용하기 위해서는 TextManager 같은것을 하나 만들어놓고 그고세엇 StringTable 들을 다 긁어온 다음

그것을 사용하는것이 매우 공용적이고 합당하게 사용할 수 있을 것이다.

 

4. FText의 구현 

우리가 FText를 어떻게 사용하는지 알 수 있게 되었다.

그럼 실제로 언리얼 엔진에서 FText는 어떻게 구현되어 있을까?

 

이를 위해 엔진 코드에서 TextLocalizationManager.h 를 열어보자

여기서 TMap에 대한 코드를 찾으면 다음과 같이 typedef가 되어 있다.

/** Manages the currently loaded or registered text localizations. */
typedef TMap<FTextId, FDisplayStringEntry> FDisplayStringLookupTable;
    
FDisplayStringLookupTable DisplayStringLookupTable;

 

DisplayStringLookupTable에 우리가 사용하는 Key, Value를 저장하여 사용한다는 것을 알 수 있다.

근데 이 생각을 할 수 있다.

"아니 우리는 Key Value 뿐만 아니라 Namespace도 지정했는데?"

이는 TMap<FTextId, FDisplayStringEntry> 에서 FTextId를 보면 알 수 있다.

FTextId의 생성과 변수는 다음과 같다.

FTextId(const FTextKey& InNamespace, const FTextKey& InKey)
	: Namespace(InNamespace)
	, Key(InKey)
{
}

/** Namespace component of this text identity */
FTextKey Namespace;

/** Key component of this text identity */
FTextKey Key;

이를 통해 FTextId는 Namespace와 Key로 이루어졌다는 것을 알 수 있다.

 

TextLocalizationManager는 Singleton으로 작성되어 있다.

FTextLocalizationManager::GetDisplayString(const FTextKey& Namespace, const FTextKey& Key, const FString* const SourceString)

이제 우리가 실제로 FText를 정의하고 사용할 때, GetDisplayString함수로 넘어오게 된다.

 

	FDisplayStringEntry* LiveEntry = DisplayStringLookupTable.Find(TextId);

이 때 DisplayStringLooupTable에 데이터가 있으면 바로 Find로 찾아서 꺼내올 수 있게 된다.

 

FName 도 동일한 방법으로 정의 되는데

우리가 배울 때 FString에 비해 FName과 FText가 가볍다고 한 이유가 바로 이렇게 Key값으로 저장해두고 바로 찾을 수 있기 때문이다.

 

이제 실제로 FText를 정의하게 되면 어떻게 찾는지 플로우를 대략적으로 코드로만 설명하겠다.

 

FText testText = NSLOCTEXT("A", "B", "C");

APlayerController* controller = Cast<APlayerController>(GetController());
controller->GetHUD()->DrawText(testText.ToString(), FColor::White, 500, 400);

우리가 testText를 실제로 사용하게 될 때,

FText 객체가 들고있는 TextData->TextHistory를 통해서 Text를 새로 업데이트 한다.

void FTextHistory_Base::UpdateDisplayString()
{
	check(!TextId.IsEmpty()); // CanUpdateDisplayString should prevent UpdateDisplayString being called
	LocalizedString = FTextLocalizationManager::Get().GetDisplayString(TextId.GetNamespace(), TextId.GetKey(), &SourceString);
}

이 FTextHistory는 내부에서 FTextLocalizationManager의 GetDisplayString 함수를 호출한다.

 

FText에 대한 사용법과 구현에 대해서 간략하게 설명했습니다.

만약 잘못된 내용이 있거나, 이해가 안되는 부분이 있다면 이에 대해 댓글을 남겨주시면 업데이트 하겠습니다.

감사합니다.