futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

3강: 언리얼 C++ 기본타입과 문자열 #12

Closed fkdl0048 closed 4 months ago

fkdl0048 commented 4 months ago

3강: 언리얼 C++ 기본타입과 문자열

왜 언리얼은 기본 타입을 따로 지정하는가?

기본적으로 언리얼은 C++의 기본 타입을 사용하지 않는다. 그 이유는 C++가 굉장히 오래전에 개발된 언어라 플랫폼의 파편화가 발생하였다. (당시엔 크로스 플랫폼을 지원하지 않았다.)

C++최신 규약에서 int는 최소 32비트를 보장하도록 규정되어 있지만 특정 플랫폼에서는 64비트로 해석될 수 있다. 따라서 게임과 같이 퍼포먼스를 최대로 끌어내야 하는 소프트웨어의 입장에선 명확하지 않은 데이터 타입이 문제가 될 수 있다.

int라는 데이터 타입이 아닌 int32라는 데이터 타입을 사용함으로써 플랫폼에 상관없이 32비트로 해석되도록 보장할 수 있다.

포팅 가능한 C++코드 관련

bool 타입의 선언

bool타입 자체가 크기가 명시되어 있지 않기에 사용할 때는 uint8을 사용하여 데이터를 저장하는데, 참/거짓의 정보만 저장하기에 비효율적이다. 따라서 Bit Field를 사용하여 제한을 둔다. 또한 사용할 때 접두사로 b를 붙여 사용한다.

uint8 bIsDead : 1;

캐릭터 인코딩

왜 언리얼은 문자열을 따로 지정하는가?

영어의 경우 1byte로 충분히 표현이 가능하지만 동아시아의 언어들은 1byte로 표현이 불가능하다. 현재는 유니코드를 지원하지만, 그 전에 윈도우 운영체제가 보급됨에 따라 멀티바이트 문자열, 유니코드 문자열이 혼재되어 사용되었다. (싱글 바이트 문자열까지)

따라서 이러한 문제를 해결하기 위해 언리얼에선 TCHAR라는 타입을 사용한다.

UTF-8 의 경우

UTF-8은 가변 길이 문자 인코딩 방식으로, 영문권의 경우 1byte로 표현이 가능하다. 하지만 동아시아의 경우 3byte로 표현이 가능하다. 이 과정이 각 지역별로 처리된다.

UTF-16 의 경우

통일된 길이의 문자 인코딩 방식으로, 모든 문자를 2byte로 표현이 가능하다. 정렬된 데이터를 처리하지만, 메모리르 더 사용한다.

UE 내부 스트링 표현

언리얼 엔진의 모든 스트링은 FStrings 혹은 TCHAR 정렬 상태로 UTF-16 포맷 메모리에 저장됩니다. 대부분의 코드에서 2 바이트가 하나의 코드포인트라 가정하므로, 언리얼의 내부 인코딩이 UCS-2 로 보다 정확히 설명될 수 있도록 Basic Multilingual Plane(BMP) 만 지원됩니다. 스트링은 현 플랫폼에 적합한 엔디안에 저장됩니다.

즉, UTF-16

UE에 로드되는 텍스트 파일

이 함수는 UTF-16 파일에서 유니코드 바이트-오더-마크(byte-order-mark, BOM)를 인식하고, 있으면 어느 엔디안으로든 UTF-16 으로 파일을 로드합니다. (기본적으로 UTF-16으로 로드함)

동아시아 인코딩 고유의 C++ 소스 코드에 대해서

UTF-8 와 디폴트 Windows 인코딩 모두 C++ 컴파일러와 관련해서 다음과 같은 문제를 야기할 수 있습니다:

결론

언리얼은 자체적으로 스트링을 관리하는데, UTF-16을 사용한다. 소스코드에 한글이 들어간다면 UTF-8로 저장하라 (이 부분은 문제가 발생할 수 있다.)

TCHAR 와 FString 실습

#include "MyGameInstance.h"

void UMyGameInstance::Init()
{
    Super::Init();

    const TCHAR LogCharArray[] = TEXT("Hello Unreal");
    UE_LOG(LogTemp, Log, LogCharArray);
}
void UMyGameInstance::Init()
{
    Super::Init();

    TCHAR LogCharArray[] = TEXT("Hello Unreal");
    UE_LOG(LogTemp, Log, TEXT("LogCharArray: %s"), LogCharArray);

    const FString LogCharString = LogCharArray;
    UE_LOG(LogTemp, Log, TEXT("LogCharString: %s"), *LogCharString);
}

복잡한 문자열 처리를 하나로

유니코드를 사용해 문자열 처리를 통일한데 이 중에서 2Byte로 사이즈가 균일한 UTF-16을 사용하고 있다. (TCHAR) 또한 문자열은 언제나 TEXT매크로를 사용해 지정한다. 이 매크로는 문자열을 TCHAR배열로 변환해주는 역할을 한다. 추가로 문자열을 다루는 클래스로 FString이 있다. (TCHAR배열을 포함하는 헬퍼클래스의 개념)

FString

FName 이나 FText 와는 달리, FString 은 조작이 가능한 유일한 스트링 클래스입니다. 대소문자 변환, 부분문자열 발췌, 역순 등 사용가능한 메서드는 많습니다. FString 은 검색, 변경에 다른 스트링과의 비교도 가능합니다. 그러나 바로 그것이 FString 이 다른 불변의 스트링 클래스보다 비싸지는 이유입니다.

TCHAR 배열을 FString으로 변환하는 과정은 언이렁 내부의 TArray라고 하는 동적 배열로 변환되어 문자열을 보관한다. 포인터로 접근하여 문자열을 변경할 수 있다.

실질적으로 문자열을 자르거나 합치는 등의 작업을 할 때는 FCString을 사용한다.

FString 추가 실습

    const TCHAR* LongCharPtr = *LogCharString;
    TCHAR* LogCharDataPtr = LogCharString.GetCharArray().GetData();
    // 다시 배열로 복사
    TCHAR LogCharArrayWithSize[100];
    FCString::Strcpy(LogCharArrayWithSize, LogCharString.Len(), *LogCharString);
    if(LogCharString.Contains(TEXT("unreal"), ESearchCase::IgnoreCase))
    {
        int32 Index = LogCharString.Find(TEXT("unreal"), ESearchCase::IgnoreCase);
        FString EndString = LogCharString.Mid(Index);
        UE_LOG(LogTemp, Log, TEXT("Find Test: %s"), *EndString);
    }
    FString Left, Right;
    if (LogCharString.Split(TEXT(" "), &Left, &Right))
    {
        UE_LOG(LogTemp, Log, TEXT("Split Test: %s 와 %s"), *Left, *Right);
    }
    int32 IntValue = 32;
    float FloatValue = 3.141592;

    FString FloatIntString = FString::Printf(TEXT("Int:%d Float:%f"), IntValue, FloatValue);
    FString FloatString = FString::SanitizeFloat(FloatValue);
    FString IntString = FString::FromInt(IntValue);

    UE_LOG(LogTemp, Log, TEXT("%s"), *FloatIntString);
    UE_LOG(LogTemp, Log, TEXT("Int: %s Float: %s"), *IntString, *FloatString);
    int32 IntValueFromString = FCString::Atoi(*IntString);
    float FloatValueFromString = FCString::Atof(*FloatString);
    FloatIntString = FString::Printf(TEXT("Int: %d Float: %f"), IntValueFromString, FloatValueFromString);
    UE_LOG(LogTemp, Log, TEXT("%s"), *FloatIntString);

FName

FName의 구조와 활용

FName과 관련된 글로벌 Pool 자료구조를 가지고 있다. 문자열이 들어오면 해시 값을 추출해서 키를 생성하고 FName에서 보관 (Pool)한다. 이후 FName값에 저장된 값을 사용해 전역 Pool에서 원하는 값을 찾아서 사용한다.

즉, 에셋의 빠른 조회를 위해 싱글톤으로 Pooling하여 사용한다. 실제 Key는 해시값이 저장되고 실제 문자열은 value에 저장된다.

FindOrAdd만 수행하기에 자료의 불변성을 보장하며, 빠른 조회를 위해 사용한다. (실제 조회할 때 FName은 해당 문자열만 들고 들어가 해시값읕 통해 조회한 후 없으면 추가하고 있다면 문자열을 반환한다.)

    FName Key1(TEXT("PELVIS"));
    FName key2(TEXT("pelvis"));
    UE_LOG(LogTemp, Log, TEXT("FName 비교 결과 : %s"), Key1 == key2 ? TEXT("같음") : TEXT("다름"));
  FName Key3(TEXT("PELVIS"));
  FName Key4(TEXT("PELVIS"));
  UE_LOG(LogTemp, Log, TEXT("FName 비교 결과 : %s"), Key3 == Key4 ? TEXT("같음") : TEXT("다름"));

  // 같음이라는 결과 출력
    for (int i = 0; i < 10000; i++)
    {
        FName SearchInNamePool = FName(TEXT("pelvis"));
        const static FName StaticOnlyOnce(TEXT("pelvis"));
    }

정리