게임 엔진/Unreal

[Unreal Engine] Container Library

Henzee 2025. 6. 18. 12:45

 

 


 

 

1. Container Library

1) 언리얼 컨테이너 라이브러리

  • 언리얼 엔진이 자체 제작해 제공하는 자료구조 라이브러리
  • 줄여서 UCL(Unreal Container Library) 라고도 함
  • 언리얼 오브젝트를 안정적으로 지원하며 다수 오브젝트 처리에 유용하게 사용됨
  • 언리얼 C++은 다양한 자료구조 라이브러리를 직접 만들어 제공하고 있음
  • 실제 게임 제작에 유용하게 사용되는 라이브러리로 세 가지를 추천함
    • TArray
    • TMap
    • TSet

 

 

2) C++ STL과 언리얼 컨테이너 라이브러리의 차이점

  • C++ STL은 범용적으로 설계되어 있음
  • C++ STL은 표준이기 때문에 호환성이 높음
  • C++ STL은 많은 기능이 엮여 있어 컴파일 시간이 오래 걸림
  • 언리얼 컨테이너 라이브러리는 언리얼 엔진에 특화되어 있음
  • 언리얼 컨테이너 라이브러리는 언리얼 오브젝트 구조를 안정적으로 지원함
  • 언리얼 컨테이너 라이브러리는 가볍고 게임 제작에 최적화되어 있음

 

 

3) 언리얼 C++ 주요 컨테이너 라이브러리

 

  • 두 라이브러리의 이름과 용도는 유사하지만, 내부적으로 다르게 구현되어 있음
    • TArray: 오브젝트를 순서대로 담아 효율적으로 관리하는 용도로 사용
    • TSet: 중복되지 않는 요소로 구성된 집합을 만드는 용도로 사용
    • TMap: 키, 밸류 조합의 레코드를 관리하는 용도로 사용

 

 


 

 

2. TArray의 구조와 활용

문서 링크: TArray 컨테이너

 

1) TArray 개요

  • TArray는 가변 배열(Dynamic Array) 자료구조
  • STL의 vector와 동작 원리가 유사
  • 게임 제작에서는 가변 배열 자료구조를 효과적으로 활용하는 것이 좋음
    • 데이터가 순차적으로 모여있기 때문에 메모리를 효과적으로 사용할 수 있고 캐시 효율이 높음
    • 컴퓨터 사양이 좋아지면서 캐시 지역성(Locality)으로 인한 성능 향상은 굉장히 중요해짐
    • 임의 데이터의 접근이 빠르고, 고속으로 요소를 순회하는 것이 가능
  • 가변 배열의 단점
    • 맨 끝에 데이터를 추가하는 것은 가볍지만, 중간에 요소를 추가하거나 삭제하는 작업은 비용이 큼
  • 데이터가 많아질수록 검색, 삭제, 수정 작업이 느려지기 때문에 많은 수의 데이터에서 검색 작업이 빈번하게 일어난다면 TArray 대신 TSet을 사용하는 것이 좋음

 

TArray의 내부 구조

 

 

2) TArray를 활용한 예제 구현

// Cpp File

#include "TArrayGameInstance.h"
#include "Algo/Accumulate.h"

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

	const int32 ArrayNum = 10;
	TArray<int32> Int32Array;

	for (int32 i = 1; i <= ArrayNum; i++)
	{
		Int32Array.Add(i);
	}

	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); // 합의 초기값을 0으로 설정(두번째 인자)
	ensure(Sum == SumByAlgo);
}

 

 


 

 

3. TSet의 구조와 활용

문서 링크 : TSet 컨테이너

 

1) TSet의 특징

  • STL의 set과 언리얼 TSet의 비교
    • STL set의 특징
      • 이진 트리로 구성되어 있어 정렬을 지원함
      • 메모리 구성이 효율적이지 않음
      • 요소가 삭제될 때 균형을 위한 재구축이 일어날 수 있음
      • 모든 자료를 순회하는데 적합하지 않음
    • 언리얼 TSet 특징
      • 해시테이블 형태로 키 데이터가 구축되어 있어 빠른 검색이 가능함
      • 동적 배열의 형태로 데이터가 모여 있음
      • 데이터를 빠르게 순회할 수 있음
      • 데이터를 삭제해도 재구축이 일어나지 않음
      • 비어있는 데이터가 있을 수 있음
  • 따라서 STL set과 언리얼 TSet의 활용 방법은 서로 다르기 때문에 주의할 것
  • STL의 unordered_set과 유사하게 동작하지만 동일하진 않음
  • TSet은 중복없는 데이터 집합을 구축하는데 유용하게 사용할 수 있음

 

TSet의 내부 구조

 

 

2) TSet을 활용한 예제 구현

// Cpp File

#include "TArrayGameInstance.h"

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

	const int32 ArrayNum = 10;
	TSet<int32> Int32Set;
	for (int32 i = 1; i <= ArrayNum; i++)
	{
		Int32Set.Add(i);
	}
	
    // 삭제 시 해당 인덱스는 Invalid, 추가 시 뒷자리부터 데이터 채워짐
	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);
}

 

 


 

 

4. UStruct

문서 링크: 언리얼 구조체

 

1) 언리얼 구조체 UStruct

  • 데이터 저장/전송에 특화된 가벼운 객체
  • 대부분 GENERATED_BODY 매크로를 선언해줌
    • 리플렉션, 직렬화와 같은 유용한 기능을 지원함
    • GENERATED_BODY를 선언한 구조체는 UScriptStruct 클래스로 구현됨
    • 이 경우 제한적으로 리플렉션을 지원함
      • 속성 UPROPERTY만 선언할 수 있고, 함수 UFUNCTION은 선언할 수 없음
  • 언리얼 엔진의 구조체 이름은  F로 시작함
    • 대부분 힙 메모리 할당(포인터 연산) 없이 스택 내 데이터로 사용됨
    • NewObject API를 사용할 수 없음

 

 

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

리플렉션에 관련된 언리얼 오브젝트의 계층 구조

 

 

3) 언리얼 구조체를 활용한 예제 구현

  • FStudentData 구조체를 생성 후 TArray와 TSet에 이름을 담아 개수 출력하기
// Header File

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "TArrayGameInstance.generated.h"

USTRUCT()
struct FStudentData // Field는 public이 기본
{
	GENERATED_BODY()

	FStudentData()
	{
		Name = TEXT("홍길동");
		Order = -1;
	}

	FStudentData(FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

	UPROPERTY() // 선택 사항 (언리얼 오브젝트 포인터를 멤버 변수로 가지는 경우는 필수)
	FString Name;

	UPROPERTY()
	int32 Order;
};

UCLASS()
class HELLOUNREAL_API UTArrayGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	virtual void Init() override;

private:
	// UPROPERTY() 필요 시 선언
	TArray<FStudentData> StudentsData;

	UPROPERTY() // 필수 선언
	TArray<TObjectPtr<class UStudent>> Students;
};


// Cpp File

#include "TArrayGameInstance.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 UTArrayGameInstance::Init()
{
	Super::Init();

	const int32 StudentNum = 300;
	for (int i = 1; i <= StudentNum; i++)
	{
		StudentsData.Emplace(FStudentData(MakeRandomName(), i)); // 복사를 하지 않고 생성
	}

	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());
}

 

출력 결과

 

 


 

 

5. TMap의 구조와 활용

문서 링크: TMap 컨테이너

 

1) TMap의 특징

  • STL map과 TMap의 비교
    • STL map의 특징
      • STL map은 STL set과 동일하게 이진 트리로 구성되어 있음
      • 정렬은 지원하지만, 메모리 구성이 효율적이지 않으며 데이터 삭제 시 재구축이 일어날 수 있음
      • 모든 자료를 순회하는데 적합하진 않음
    • 언리얼 TMap의 특징
      • 키, 밸류 구성의 튜플 데이터의 TSet 구조로 구현되어 있음
      • 해시테이블 형태로 구축되어 있어 빠른 검색이 가능함
      • 동적 배열의 형태로 데이터가 모여있음
      • 데이터는 빠르게 순회할 수 있음
      • 데이터는 삭제해도 재구축이 일어나지 않음
      • 비어있는 데이터가 있을 수 있음
      • TMultiMap을 사용하면 중복 데이터를 관리할 수 있음
  • 동작 원리는 STL unordered_map과 유사
  • 키, 밸류 쌍이 필요한 자료구조에 광범위하게 사용

 

TMap의 내부 구조

 

 

2) TMap을 활용한 예제 구현

// Header File

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "TArrayGameInstance.generated.h"

USTRUCT()
struct FStudentData // Field는 public이 기본
{
	GENERATED_BODY()

	FStudentData()
	{
		Name = TEXT("홍길동");
		Order = -1;
	}

	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() // 선택 사항 (언리얼 오브젝트 포인터를 멤버 변수로 가지는 경우는 필수)
	FString Name;

	UPROPERTY()
	int32 Order;
};

UCLASS()
class HELLOUNREAL_API UTArrayGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	virtual void Init() override;

private:
	TArray<FStudentData> StudentsData;

	UPROPERTY() // 필수 선언
	TArray<TObjectPtr<class UStudent>> Students;

	TMap<int32, FString> StudentsMap; // Key or Value에 오브젝트 포인터가 들어가면 UPROPERTY 필수 선언
};


// Cpp File

#include "TArrayGameInstance.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 UTArrayGameInstance::Init()
{
	Super::Init();

	const int32 StudentNum = 300;
	for (int32 i = 1; i <= StudentNum; i++)
	{
		StudentsData.Emplace(FStudentData(MakeRandomName(), i)); // 복사를 하지 않고 생성
	}

	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> StudentsMapByName;
	Algo::Transform(StudentsData, StudentsMapByName,
		[](const FStudentData& Val)
		{
			return TPair<FString, int32>(Val.Name, Val.Order);
		}
	);

	UE_LOG(LogTemp, Log, TEXT("이름에 따른 학생 멀티맵의 레코드 수 : %d"), StudentsMapByName.Num());

	const FString TargetName(TEXT("이혜은"));
	TArray<int32> AllOrders;
	StudentsMapByName.MultiFind(TargetName, AllOrders);

	UE_LOG(LogTemp, Log, TEXT("이름이 %s인 학생 수 : %d"), *TargetName, AllOrders.Num());

	TSet<FStudentData> StudentsSet; // operator==와 GetTypeHash 함수를 지정해줘야 함
	for (int32 i = 1; i <= StudentNum; i++)
	{
		StudentsSet.Emplace(FStudentData(MakeRandomName(), i)); // 복사를 하지 않고 생성
	}
}

 

출력 결과

 

 


 

 

6. 자료구조의 시간 복잡도 비교

  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)
  • TArray
    • 빈틈없는 메모리
    • 가장 높은 접근 성능
    • 가장 높은 순회 성능
  • TSet
    • 빠른 중복 감지
  • TMap
    • 중복 불허
    • 키, 밸류 관리
  • TMultiMap
    • 중복 허용
    • 키, 밸류 관리

 

 

 


 

 

 

'게임 엔진 > Unreal' 카테고리의 다른 글

[Unreal Engine] 게임 제작 기초  (0) 2025.06.18
[Unreal Engine] 메모리 관리  (1) 2025.06.18
[Unreal Engine] Delegate  (0) 2025.06.16
[Unreal Engine] Composition  (0) 2025.06.16
[Unreal Engine] Interface  (3) 2025.06.16