언리얼 C++의 기본 타입과 문자열 다루는 방법 소개

  • 언리얼 환경에서 알아두어야 할 기본 타입과 고려할 점
  • 캐릭터 인코딩 시스템에 대한 이해
  • 언리얼 C++이 제공하는 다양한 문자열 처리 방법과 내부 구성의 이해

 

인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!

 

 

목차

     

     


     

     

    언리얼 C++ 기본 타입


     

    언리얼은 C++의 기본 타입

     

    1970년대에 개발된 C++언어는 기기들마다 C++ 언어를 구현하는 방법이 달라서 C++ 주요 함수가 특정 플랫폼에서 다르게 작동하는 플랫폼 파편화 (Platform Fragmentation) 문제가 발생했다.

     

    게임 제작에서는

    • 데이터 정보가 명확해야 한다.
    • 단일 컴퓨터에서 최대 퍼포먼스를 뽑아내야 한다.
    • 네트웍 상에서 데이터 통신이 효율적이고 안정적이어야 한다.

    그렇기 때문에 언리얼 엔진은 데이터 타입의 모호함을 없애기 위해 언리얼 자체 기본 타입을 만들었다.

     

     

    데이터 타입의 애매 모호함은 게임 개발 시 문제를 일으킬 수 있음


     

    bool 타입의 선언

     

    • 데이터 전송을 고려한 참, 거짓 데이터의 지정
    • `.h` 헤더에서는 가급적 `bool` 대신 `uint8` 타입을 사용하되 Bit Field 오퍼레이터를 사용해서 데이터 크기를 제한해 최소화시킬 수 있다.
    • 일반 `uint8` 과의 구분을 위해, `b` 접두사를 사용하도록 코딩 표준에 명시되어 있다. 
    • .cpp 로직에서는 자유롭게 bool을 사용해도 무방하다.

    UPROPERTY()
    uint8 bNetTemporary:1;
    
    uint8 bNetStartup:1;
    
    UPROPERTY(Category=Replication, EditDefaultOnly, BlueprintReadOnly)
    uint8 bOnlyRelevantToOwner:1;
    
    UPROPERTY(Category=Replication, EditDefaultOnly, BlueprintReadWrite)
    uint8 bAlwaysRelevant:1;

     

     

    언리얼의 문자열


     

     

    언리얼이 문자열을 따로 지정하는 이유는?

     

    • 1990년대 후반에 Unicode라는 새로운 표준을 통해 모든 국가의 문자를 표현할 수 있게 되었다.
    • 하지만, 컴퓨터는 그 전에 보급되었다.
    • 문자열 처리의 종류
      • Single byte
      • Multibyte
      • Unicode

     

    • Unicode보다 먼저 등장한 Multibyte 표현방법이 아직도 사용이 되고 있다.
      • C++ STL은 ASCII, UTF-8, UTF-16만 지원함

     

    이 상황을 해결하기 위해서 언리얼에서는 TCHAR을 이용한다.
    언리얼은 내부적으로 UTF-16을 사용한다.


     

     

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

     

    • 유니코드(Unicode)를 사용해 문자열 처리를 통일
      • 이 중에서 2byte로 사이즈가 균일한 UTF-16을 사용
      • 유니코드를 위한 언리얼 표준 캐릭터 타입: TCHAR

     

    • 문자열은 언제나 TEXT 매크로를 사용해 지정
      • TEXT 매크로로 감싼 문자열은 TCHAR 배열로 지정됨

     

    • 문자열을 다루는 클래스로 FString을 제공함
      • FStringTCHAR 배열을 포함하는 헬퍼 클래스

     

    #include "MyGameInstance.h"
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    	TCHAR LogCharArray[] = TEXT("Hello Unreal!");
    	UE_LOG(LogTemp, Log, TEXT("%s"), LogCharArray);
    
    	FString LogCharString = LogCharArray;
    	UE_LOG(LogTemp, Log, TEXT("%s"), *LogCharString); // *연산자를 붙여줘야 FString이 감싸고 있는 실제 문자열 데이터를 가져올 수 있다.
    }

     

     

    FString의 구조와 활용

     

    • 다른 타입에서 FString으로의 변환
      • FString::Printf
      • FString::SanitizeFloat
      • FString::FromInt

     

    • FCString 래퍼 클래스로 C 런타임 수준에서 문자열을 처리하는 함수를 사용할 수 있다.
      • ex. 문자열을 찾는 strstr, 문자열 복사 strcpy, strcmp, 등등

     

    • FString에서 다른 타입으로의 변환 (안전하지 않으므로 주의)
      • FCString::Atoi
      • FCString::Atof

     

     

     

    ※ 참고사항:  직렬화(Serialization) 후 변환되는 

    • 직렬화(Serialization)을 거친 모든 오브젝트와 변수들은 '0, 1로 이루워진 String(= 즉, bit array)'으로 변경된다.
      • 직렬화된 정보들은 나열된 정보로 저장된다.
      • CoreNet.h 내의 UPackageMap을 사용해서 직렬화된 정보를 사용


     

     

     

     

    TCHAR와 FString 이용한 문자열 처리

     

    #include "MyGameInstance.h"
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    	
    	const TCHAR* LogCharPtr = *LogCharString;
    
    	// std::string::data() 같은 느낌인듯 하다.
    	TCHAR* LogCharDataPtr = LogCharString.GetCharArray().GetData();
    
    	// 대소문자 구분 없이 "Unreal"이란 문자열을 찾음. 리턴 값은 bool형
    	if (LogCharString.Contains(TEXT("Unreal"), ESearchCase::IgnoreCase))
    	{
    		// 대소문자 구분 없이 "Unreal"이란 문자열이 시작하는 위치를 찾음.
    		int32 Index = LogCharString.Find(TEXT("Unreal"), ESearchCase::IgnoreCase);
    
    		// Index 위치부터 끝까지의 부분문자열을 리턴. substr
    		FString EndString = LogCharString.Mid(Index);
    
    		UE_LOG(LogTemp, Log, TEXT("Find Test: %s"), *EndString);
    	}
    }

     


     

     

    문자열 코드 출력하기 코드

     

    #include "MyGameInstance.h"
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    	
    	FString LeftString, RightString;
    	
    	if (LogCharString.Split(" ", &LeftString, &RightString)) // " "(공백)을 기준으로 문자열을 두개의 부분 문자열로 분리
    	{
    		// 한글을 사용하기 위해선 UTF-8(Unicode 65001)로 인코딩해야 함.
    		UE_LOG(LogTemp, Log, TEXT("Split Test: %s 와 %s"), *LeftString, *RightString);
    	}
    
    	int32 IntValue = 32;
    	float FloatValue = 3.141592;
    
    	// FString::SanitizeFloat, FString::FromInt를 이용해 정수형, 실수형 변수를 문자열로 전환
    	// FString::Printf의 경우, 출력하는 함수가 아닌 FString을 반환하는 함수다
    	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);
    }

     

     


     

     

     

    FName의 활용

     


     

     

    언리얼이 제공하는 다양한 문자열 처리

     

       
     FName  애셋 관리를 위해 사용되는 문자열 체계
    • 대소문자 구분 없음
    • 한번 선언되면 바꿀 수 없음 (선언되면 key로 만들어지는 것이기 때문에 int로 변환된다.)
    • 가볍고 빠름
    • 문자를 표현하는 용도가 아닌 애셋 키를 지정하는 용도로 사용. 빌드시 해시값으로 변환됨.
     FText  다국어 지원을 위한 문자열 관리 체계
    • 일종의 로 작용함
    • 별도의 문자열 테이블 정보가 추가로 요구됨
    • 게임 빌드 시 자동으로 다양한 국가별 언어로 변환

     


     

     

    FName 구조와 활용

     

    • 언리얼은 FName과 관련된 글로벌 Pool 자료구조를 가지고 있음
    • FName과 글로벌 Pool
      • 문자열이 들어오면 해시 값을 추출해 Key를 생성해 FName에서 보관
      • FName 값에 저장된 값을 사용해 전역 Pool에서 원하는 자료를 검색해 반환
      • 문자 정보는 대소문자를 구분하지 않고 저장함. (Ignore Case)
    • FName의 형성
      • 생성자에 문자열 정보를 넣으면 풀을 조사해 적당한 키로 변환하는 작업이 수반됨
      • Find or Add

    문자가 들어왔을때 문자열 정보가 저장되는 것이 아니라 Key값이 저장됨. 문자열은 다른곳에 따로 저장됨.


     

     

    FName 예시 코드

     

    #include "MyGameInstance.h"
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    	
    	// 문자열이 해시테이블을 통해 key값으로 변환
    	FName key1(TEXT("PELVIS"));
    	FName key2(TEXT("pelvis"));
    	UE_LOG(LogTemp, Log, TEXT("FName 비교 결과: %s"), key1 == key2 ? TEXT("같음") : TEXT("다름"));
    
    
        // 아래와 같이 매번 실행하게 하면, 오버헤드가 발생한다.
        // Tick과 같이 자주 실행되는 곳에 FName을 사용하는 경우, 오버헤드가 발생할 수 있다.
    	for (int i = 0; i < 10000; i++)
    	{
    		FName SearchInNamePool = FName(TEXT("pelvis")); // 생성자에 문자열을 넣음. pelvis
    	}
        
         // StationOnlyOnce로 처음 초기화할 때 저장한 뒤에는 그 후에는 찾을 일이 없다.
    	for (int i = 0; i < 10000; i++)
    	{
    		FName SearchInNamePool = FName(TEXT("pelvis")); // 생성자에 문자열을 넣음. pelvis
    		const static FName StaticOnlyOnce(TEXT("pelvis"));
    	}
    }