언리얼 구조체의 특징을 이해하고, 다양한 컨테이너 라이브러리에서 구조체를 활용하기

  • 언리얼 구조체의 선언과 특징 이해
  • 언리얼 대표 컨테이너 라이브러리 TMap의 내부 구조 이해
  • 세 컨테이너 라이브러리의 장단점을 파악하고 알맞게 활용하기

 


인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다. 

😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!

 

 

 

목차

     

     


     

     

    언리얼 구조체


     

     

    USTRUCT

     

    USTRUCT(BlueprintType)
    struct FMyStruct
    {
    	GENERATED_BODY()
    	
        // BP에서 접근 가능
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Test Variables") 
        int32 Num;
        
        int32 AnotherNum; // BP 접근 불가능
        
        // UPPROPERTY를 붙여야만 자동으로 메모리 관리를 할 수 있다.
        // 리플렉션, 스마트 포인터, 가비지 컬렉션 시스템에 표시된다.
    	UPROPERTY() 
    	UObject* Ptr;
    }

     

     

    언리얼 구조체 USTRUCT

    C++ 구조체와 같이 기본 접근 지정자는 public 이다.

    데이터 저장/전송에 특화된 가벼운 객체

    대부분 GENERATED_BODY 매크로를 선언해주는 것이 일반적이다.

    • GENERATED_BODY 선언은 필수는 아니지만, 선언하지 않으면 리플렉션, 직렬화와 같은 기능을 사용할 수 없다.
    • GENERATED_BODY 매크로 는 리플렉션, 직렬화와 같은 유용한 기능들을 지원한다.
    • GENERATED_BODY 를 선언한 구조체는 UScriptStruct 클래스로 자동으로 구현됨.
      • ⇒ 그래서 엄밀히 따지면 우리가 사용하는 언리얼 구조체는 UStruct가 아닌 UScriptStruct 클래스라고 한다.
      • 이 경우 제한적으로 리플렉션을 지원함
      • 속성 UPROPERTY 만 선언할 수 있고, 함수 UFUNCTION 은 선언할 수 없음. (Why? A. 데이터 저장과 전송에 특화된 가벼운 객체로만 우리가 사용하기 때문에 따로 힙의 메모리에 할당할 이유가 거의 없다.)
      • 이것이 UStruct 와 UObject 의 중요한 차이점이다.
    • 언리얼 엔진의 구조체 이름은 F 로 시작. 언리얼 엔진이 메모리를 관리해주지 않는다.
      • 데이터 저장과 전송에 특화된 가벼운 객체로만 사용하기 때문에, 힙에 메모리를 할당할 일이 거의 없다.
        • 따라서, 클래스의 멤버 변수나 스택 내 데이터로 사용되는 것이 일반적이다.
      • NewObject API 를 제공하지 않음. (반면, UObject는 NewObject를 이용해서 생성 가능).

     

     

    언리얼 구조체 USTRUCT의 특징

    USTRUCT는 언리얼 엔진의 스마트 포인터 및 Garbage Collection(GC) 시스템을 사용하여 GC에 의해 UObjects가 제거되는 것을 방지할 수 있다.

    구조체는 단순히 데이터 타입에 적합하므로 UObjects 와는 다르다.

    • 프로젝트 내부에서 보다 인터랙션을 하기 위해서는, UObject 또는 AActor 서브 클래스를 만드는 것이 좋다.

    USTRUCT 는 리플리케이션용으로 간주되지 않습니다.

    • UPROPERTY를 붙이면 변수를 리플리케이션용을으로 간주하고, 안 붙이면 C++ 멤버 변수로 간주한다.

    구조체에 UObject*를 사용하면, 반드시 UPROPERTY 를 붙여야 언리얼 엔진에서 제공해주는 자동 메모리 관리가 가능하다.

    • UPROPERTY 매크로를 통해 UE의 GC(가비지컬렉션)기능을 사용하면, 약간의 리소스를 지불하고 안전한 메모리 관리를 할 수 있다.

    언리얼 엔진 구조체의 경우 F 접두사가 붙는데, 리플리케이션 기능은 사용하지 못하나, 구조체에 선언된 UPROPERTY 변수는 리플리케이션 기능이 제공된다.

     

     


     

     

    언리얼 리플렉션 관련 계층 구조

     

     

    UClassUStruct를 상속받아 만들어지고 UFunctionComposition으로 넣어 관리한다.

     


     

     

    객체의 동적 배열 관리를 위한 예제 다이어그램

     

    언리얼 오브젝트 학생의 동적 배열 관리 방법

    언리얼 구조체 학생 정보의 동적 배열 관리 방법

     

    	UPROPERTY()
        TArray<TObjectPtr<UStudent>> Students;
        // TArray<UStudent*> Students; // 위와 동일
        UPROPERTY()
        TMap<int32, TObjectPtr<UStudent>> StudentsInfo;
        
    	TArray<FStudentData> StudentsData;
        TMap<int32, FString> StudentsMap;

     

    Key나 Value에 언리얼 오브젝트 포인터가 들어가면, UPROPERTY를 반드시 선언해줘야 한다.


     

     

    TMap


     

     

    TMap의 특징

     

    STL map 과 TMap 의 비교

    • STL map 의 특징
      • STL map 은 STL set 과 동일하게 이진 트리로 구성되어 있음.
      • 정렬은 지원하나, 메모리 구성이 효율적이지 않으며, 데이터 삭제시 재구축이 일어날 수 있음.
      • 모든 자료를 순회하기에 적합하진 않다.
    • 언리얼 TMap 의 특징
      • TSet과 차이점은 튜플(Tuple) 데이터로 구성되어 있단 것 하나! 나머진 동일.
        • Key, Value 구성의 튜플(Tuple) 데이터로 구성되어 있다.
        • 그래서 Key, Value 튜플(Tuple)로 구성된 TSet 이라 할 수 있다.
      • 해시테이블 형태로 구축되어 있어 빠른 검색이 가능함.
      • (이 빠진) 동적 배열의 형태로 데이터가 모여있음.
      • 데이터를 빠르게 순회할 수 있음.
      • 데이터는 삭제해도 재구축이 일어나지 않음.
      • 비어있는 데이터가 있을 수 있음.
      • TMultiMap을 사용하면 중복 데이터를 관리할 수 있음.

    동작 원리는 오리혀 STL unordered_map 과 유사하다.

    Key, Value 쌍이 필요한 자료구조에 광범위하게 사용됨.


     

     

    TMap의 자료구조

     

     

     

    TPair의 자료 구조를 기본으로 채택하고 있다는 점을 제외하고는 TSet과 동일하다.


     

     

     

    언리얼 공식문서

     

    • TMap 에서 Key-Value 짝은 마치 개별 오브젝트인 것처럼 맵의 엘리먼트 유형으로 정의된다.
    • 맵의 유형은 두 가지:  TMapTMultiMap
      • 이 둘의 차이점은 TMap 키는 고유한 반면, TMultiMap 은 다수의 동일한 키 저장을 지원한다.
      • 기존 짝과 일치하는 키로 새 키-값 짝을 TMap 에 추가하면 기존 것이 대체되고, TMultiMap 에 추가하면 새로 저장한다.
    • TMap 에서  Key-Value 짝은 마치 개별 오브젝트인 것처럼 맵의 엘리먼트 유형으로 정의된다. 엘리먼트 유형은 실제 TPair< KeyType, ElementType> 다.
    • TSet과 동일한 해시 컨테이너라서 Key 유형에는 GetTypeHash를 지원하고, Key의 동일성을 비교하기 위한 operator==을 제공해야 한다.

     

    https://dev.epicgames.com/documentation/ko-kr/unreal-engine/map-containers-in-unreal-engine?application_version=5.2

     


     

     


    예제


     

     

    예제 코드

     

    MyGameInstance.h

    #pragma once
    #include "CoreMinimal.h"
    #include "Engine/GameInstance.h"
    #include "MyGameInstance.generated.h"
    
    USTRUCT()
    struct FStudentData 
    {
    	GENERATED_BODY() // GENERATED_BODY 매크로: 리플렉션 하기
    	
    	FStudentData() 
    	{ 
    		Name = TEXT("홍길동");
    		Order = -1;
    	}
    
    	// UObject 상속이 아니기에, NewObject를 이용해서 생성 불가능.
    	// 따라서 UObject와 달리 생성자를 오버로딩할 수 있다.
    	FStudentData(FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}
    
    	bool operator==(const FStudentData& InOther) const
    	{
    		return Order == InOther.Order;
    	}
    
    	friend FORCEINLINE uint32 GetTypeHash(const FStudentData& InStudentData)
    	{
    		return GetTypeHash(InStudentData.Order);
    	}
    
    	// 언리얼 오브젝트 포인터를 넣는 경우에는 꼭 UPROPERTY를 넣어야 한다.
    	UPROPERTY()
    	FString Name;
    
    	UPROPERTY()
    	int32 Order;
    };
    
    UCLASS()
    class UNREALCONTAINER_API UMyGameInstance : public UGameInstance
    {
    	GENERATED_BODY()
    	
    public:
    
    	virtual void Init() override;
    	
    private:
    	TArray<FStudentData> StudentsData;
    
    	UPROPERTY()
    	TArray<TObjectPtr<class UStudent>> Students;
    
    	TMap<int32, FString> StudentsMap;
    };

     

     


    MyGameInstance.cpp

    #include "MyGameInstance.h"
    #include "Algo/Accumulate.h"
    
    FString MakeRandomName()
    {
    	TCHAR FirstChar[] = TEXT("김이박최");
    	TCHAR MiddleChar[] = TEXT("상혜지성");
    	TCHAR LastChar[] = TEXT("수은원연");
    	
    	TArray<TCHAR> RandArray;
    	RandArray.SetNum(3); // 기본값으로 채워진다.
    	RandArray[0] = FirstChar[FMath::RandRange(0, 3)]; 	// 랜덤 인덱스 뽑기
    	RandArray[1] = MiddleChar[FMath::RandRange(0, 3)];  // 랜덤 인덱스 뽑기
    	RandArray[2] = LastChar[FMath::RandRange(0, 3)]; 	// 랜덤 인덱스 뽑기
    
    	return RandArray.GetData(); // FString*형 반환
    }
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    
    	const int32 ArrayNum = 10;
    	TArray<int32> Int32Array;
    
    	for (int32 ix = 1; ix <= ArrayNum; ++ix)
    	{
    		Int32Array.Add(ix);
    	}
    
    	Int32Array.RemoveAll(
    		[](int32 Val)
    		{
    			return Val % 2 == 0;
    		}
    	);
    
    	Int32Array += {2, 4, 6, 8, 10};
    
    	TArray<int32> Int32ArrayCompare;
    	int32 CArray[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8 , 10 };
    	Int32ArrayCompare.AddUninitialized(ArrayNum);
    	FMemory::Memcpy(Int32ArrayCompare.GetData(), CArray, sizeof(int32) * ArrayNum);
    
    	ensure(Int32Array == Int32ArrayCompare);
    
    	int32 Sum = 0;
    	for (const int32& Int32Elem : Int32Array)
    	{
    		Sum += Int32Elem;
    	}
    
    	ensure(Sum == 55);
    
    	int32 SumByAlgo = Algo::Accumulate(Int32Array, 0);
    	ensure(Sum == SumByAlgo);
    
    	TSet<int32> Int32Set;
    	for (int32 ix = 1; ix <= ArrayNum; ++ix)
    	{
    		Int32Set.Add(ix);
    	}
    
    	Int32Set.Remove(2);
    	Int32Set.Remove(4);
    	Int32Set.Remove(6);
    	Int32Set.Remove(8);
    	Int32Set.Remove(10);
    	Int32Set.Add(2);
    	Int32Set.Add(4);
    	Int32Set.Add(6);
    	Int32Set.Add(8);
    	Int32Set.Add(10);
    
    	const int32 StudentNum = 300;
    	for (int32 ix = 1; ix <= StudentNum; ++ix)
    	{
    		StudentsData.Emplace(FStudentData(MakeRandomName(), ix));
    	}
    
    	TArray<FString> AllStudentsNames;
    	Algo::Transform(StudentsData, AllStudentsNames,
    		[](const FStudentData& Val)
    		{
    			return Val.Name;
    		}
    	);
    
    	UE_LOG(LogTemp, Log, TEXT("모든 학생 이름의 수 : %d"), AllStudentsNames.Num());
    
    	TSet<FString> AllUniqueNames;
    	Algo::Transform(StudentsData, AllUniqueNames,
    		[](const FStudentData& Val)
    		{
    			return Val.Name;
    		}
    	);
    
    	UE_LOG(LogTemp, Log, TEXT("중복 없는 학생 이름의 수 : %d"), AllUniqueNames.Num());
    
    
    	Algo::Transform(StudentsData, StudentsMap,
    		[](const FStudentData& Val)
    		{
    			return TPair<int32, FString>(Val.Order, Val.Name);
    		}
    	);
    
    	UE_LOG(LogTemp, Log, TEXT("순번에 따른 학생 맵의 레코드 수 : %d"), StudentsMap.Num());
    
    	TMap<FString, int32> StudentsMapByUniqueName;
    
    	Algo::Transform(StudentsData, StudentsMapByUniqueName,
    		[](const FStudentData& Val)
    		{
    			return TPair<FString, int32>(Val.Name, Val.Order);
    		}
    	);
    
    	UE_LOG(LogTemp, Log, TEXT("이름에 따른 학생 맵의 레코드 수 : %d"), StudentsMapByUniqueName.Num());
    
    	TMultiMap<FString, int32> StudentMapByName;
    	Algo::Transform(StudentsData, StudentMapByName,
    		[](const FStudentData& Val)
    		{
    			return TPair<FString, int32>(Val.Name, Val.Order);
    		}
    	);
    
    	UE_LOG(LogTemp, Log, TEXT("이름에 따른 학생 멀티맵의 레코드 수 : %d"), StudentMapByName.Num());
    
    	const FString TargetName(TEXT("이혜은"));
    	TArray<int32> AllOrders;
    	StudentMapByName.MultiFind(TargetName, AllOrders);
    
    	UE_LOG(LogTemp, Log, TEXT("이름이 %s인 학생 수 : %d"), *TargetName, AllOrders.Num());
    
    	TSet<FStudentData> StudentsSet;
    	for (int32 ix = 1; ix <= StudentNum; ++ix)
    	{
    		StudentsSet.Emplace(FStudentData(MakeRandomName(), ix));
    	}
    }

     


     

     

    주의사항

    :\Epic Games\UE_5.3\Engine\Source\Runtime\Core\Public\Containers\Set.h(78): error C2665: 'GetTypeHash': no overloaded function could convert all the argument types 1>D:\Epic Games\UE_5.3\Engine\Source\Runtime\Engine\Classes\PhysicsEngine\RigidBodyIndexPair.h(64): note: could be 'uint32 GetTypeHash(const FRigidBodyIndexPair)'

    위와 같은 에러가 발생했었다.

     

    에러 발생 이유는? 

    에러가 뜨는 원인은 지정한 커스텀 구조체에 대한 GetTypeHash 함수가 지정되어 있지 않아서 해시값을 만들 수가 없다는 에러 메시지.

     

    해결방안

    구조체 내에 함수 2개 추가

    USTRUCT()
    struct FStudentData {
    	GENERATED_BODY()
    	...
    	bool operator==(const FStudentData& InOther) const
    	{
    		return Order == InOther.Order; // 순번이 같으면 같다
    	}
    
    	friend FORCEINLINE uint32 GetTypeHash(const FStudentData& InStudentData)
    	{
    		return GetTypeHash(InStudentData.Order); // 해시값을 리턴해서 Student에 대한 해시값을 지정한다
    	}
    	
    }

     

     

    실행화면

     


     

     

    정리


     

     

    자료구조 비교

     

      TArray TSet TMap TMultiMap
    접근 O(1) O(1) O(1) O(1)
    검색 O(N) O(1) O(1) O(1)
    삽입 O(N) O(1) O(1) O(1)
    삭제 O(N) O(1) O(1) O(1)
      빈틈없는 메모리
    가장 높은 접근성능
    가장 높은 순회성능
    빠른 중복 감지


    중복 불허
    Key, Value 관리

    중복 허용
    Key, Value 관리

     

     


     

     

    구조체와 언리얼 컨테이너 라이브러리

     

    TArray, TSet, TMap 컨테이너 라이브러리 내부 구조와 활용 방법

    언리얼 구조체의 선언 방법

    TSet과 TMap에서 언리얼 구조체를 사용하기 위해 필요한 함수의 선언과 구현 방법