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

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의 구조와 활용
1) TSet의 특징
- STL의 set과 언리얼 TSet의 비교
- STL set의 특징
- 이진 트리로 구성되어 있어 정렬을 지원함
- 메모리 구성이 효율적이지 않음
- 요소가 삭제될 때 균형을 위한 재구축이 일어날 수 있음
- 모든 자료를 순회하는데 적합하지 않음
- 언리얼 TSet 특징
- 해시테이블 형태로 키 데이터가 구축되어 있어 빠른 검색이 가능함
- 동적 배열의 형태로 데이터가 모여 있음
- 데이터를 빠르게 순회할 수 있음
- 데이터를 삭제해도 재구축이 일어나지 않음
- 비어있는 데이터가 있을 수 있음
- STL set의 특징
- 따라서 STL set과 언리얼 TSet의 활용 방법은 서로 다르기 때문에 주의할 것
- STL의 unordered_set과 유사하게 동작하지만 동일하진 않음
- 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의 구조와 활용
1) TMap의 특징
- STL map과 TMap의 비교
- STL map의 특징
- STL map은 STL set과 동일하게 이진 트리로 구성되어 있음
- 정렬은 지원하지만, 메모리 구성이 효율적이지 않으며 데이터 삭제 시 재구축이 일어날 수 있음
- 모든 자료를 순회하는데 적합하진 않음
- 언리얼 TMap의 특징
- 키, 밸류 구성의 튜플 데이터의 TSet 구조로 구현되어 있음
- 해시테이블 형태로 구축되어 있어 빠른 검색이 가능함
- 동적 배열의 형태로 데이터가 모여있음
- 데이터는 빠르게 순회할 수 있음
- 데이터는 삭제해도 재구축이 일어나지 않음
- 비어있는 데이터가 있을 수 있음
- TMultiMap을 사용하면 중복 데이터를 관리할 수 있음
- STL map의 특징
- 동작 원리는 STL unordered_map과 유사
- 키, 밸류 쌍이 필요한 자료구조에 광범위하게 사용

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
- 중복 허용
- 키, 밸류 관리