UnrealEngine

Unreal Container - TArray

Muji-in 2025. 3. 26. 23:35

Unreal Container

Unreal Engine에서는 내부에서 사용할 자료 구조를 이미 구현해 놓았다. 이러한 것을 Unreal Container Libaray라고 부르고 Unreal Engine에 특화되어 있다. 특히 UObject을 안정적으로 지원하는데 UPROPERTY를 통해서 에디터에서 조작이 가능하거나 포인터를 가지고 있어도 GC를 안정적으로 지원한다. 이러한 Unreal Container들은 동적으로 생성하지 않고 클래스 멤버 변수와 같이 값 타입으로 사용을 하게 된다.

Unreal Engine에는 많은 컨테이너들이 구현되어 있지만 가장 자주 사용하고 문서가 존재하는 TArray, TMap, TSet을 정리하기로 했다.

TArray

TArray는 언리얼 엔진에서 가장 기본적인 컨테이너이다. TArray는 C++의 vector와 유사하게 동질성 컨테이너로 같은 타입에 대해서 저장하며 연속된 메모리 공간에 정보를 저장한다. vector와 유사한 자료구조이므로 vector의 특징이 같으며 임의 접근이 가능하고 연속된 메모리라 캐시 적중률이 높지만 중간에 값을 삽입하거나 삭제하는 비용이 높다.

초기화

TArray(std::initializer_list<InElementType> InitList)
void Init(const ElementType& Element, SizeType Number);

초기화 리스트를 이용해서 생성자에서 초기화하거나 아니면 Init 함수를 통해서 값을 채워넣을 수 있다.

// 초기화 리스트를 이용한 초기화
TArray<int32> Arr = {1, 2, 3, 4, 5}; // 1, 2, 3, 4, 5
// Init 함수를 통한 초기화
TArray<int32> InitArr;
InitArr.Init(5, 10); // 5, 5, 5, 5, 5, 5, 5, 5, 5, 5

데이터 추가

데이터를 추가할 때는 값 타입에 대한 추가와 struct와 같은 생성자가 필요한 변수에 대한 함수로 나뉘게 된다.

  • 복사, 이동 생성 : Add, AddZeroed, AddUnique, Insert
  • 내부적으로 생성 : Emplace, AddDefaulted

Add

SizeType Add(const ElementType& Item);

배열의 끝에 새 오브젝트를 생성해서 삽입한다. 값을 복사해서 삽입을 하거나 rvalue의 경우 이동을 이용하기도 한다. 반환 값으로는 삽입된 원소의 인덱스를 반환한다. Struct와 같은 값을 삽입할 경우 복사 연산이 발생하므로 이러한 Emplace를 사용하도록 한다.

TArray<int32> InsertArray;
// return : 삽입된 원소의 인덱스
InsertArray.Add(1);
InsertArray.Add(2);
InsertArray.Add(3);
InsertArray.Add(4);
InsertArray.Add(5);
// 1, 2, 3, 4, 5

Emplace

FORCEINLINE SizeType Emplace(ArgsType&&... Args);

인자를 받아서 배열의 끝에 생성을 한다. Emplace를 이용하면 복사 연산 없이 바로 배열에 생성을 할 수 있어서 성능적으로 유리하다. 값 타입에서는 Add를 사용하고 그 외에서는 Emplace를 이용하면 된다.

TArray<int32> EmplaceArray;
EmplaceArray.Emplace(1);
EmplaceArray.Emplace(2);
EmplaceArray.Emplace(3);
EmplaceArray.Emplace(4);
EmplaceArray.Emplace(5);
// 1, 2, 3, 4, 5

AddZeroed

SizeType AddZeroed();
SizeType AddZeroed(SizeType Count);

배열의 끝에 0으로 초기화되어 있는 값 타입을 추가한다. 생성자를 호출하지 않으므로 생성자가 필요한 struct와 같은 타입은 AddDefaulted를 호출해야 한다. 반환값으로 추가된 원소의 가장 처음 인덱스를 반환한다.

TArray<int32> InsertZeroArray;
InsertZeroArray.AddZeroed(); // 0
InsertZeroArray.AddZeroed(5); // 0, 0, 0, 0, 0, 0

AddDefaulted

SizeType AddDefaulted()
SizeType AddDefaulted(SizeType Count)

배열의 끝에 새로운 원소를 추가하고 생성자를 호출한다. 단순한 값 타입일 경우는 AddZeroed를 호출이 가능하다. 반환된 값으로 추가된 원소의 가장 처음 인덱스를 반환한다.

Insert

SizeType Insert(ElementType&& Item, SizeType Index)
SizeType Insert(const ElementType& Item, SizeType Index)

특정 Index에 원소를 삽입한다. 순서를 유지하기 떄문에 그 이후의 원소는 한 칸씩 밀리게 된다. 따라서 O(n)의 시간 복잡도가 소요된다. Index는 0-base를 기준으로 하고 가장 끝, STL로 치면 end 위치에 삽입을 하면 제일 마지막에 값을 삽입할 수 있다. 만약에 배열 범위를 벗어나면 크래시를 발생하므로 반드시 범위를 체크해야 한다.

반환값은 삽입한 원소의 인덱스이다.

TArray<int32> InsertAtArray;
InsertAtArray.Init(0, 5);
// 0번째 인덱스에 1을 삽입한다.
InsertAtArray.Insert(1, 0); // 1, 0, 0, 0, 0, 0
// 3번째 인덱스에 2를 삽입한다.
InsertAtArray.Insert(2, 3); // 1, 0, 0, 2, 0, 0
const int ToInsertIndex = 5;
if(InsertAtArray.IsValidIndex(ToInsertIndex))
{
  InsertAtArray.Insert(3, ToInsertIndex); // 1, 0, 0, 2, 0, 3, 0, 0
}

AddUnique

FORCEINLINE SizeType AddUnique(ElementType&& Item)
FORCEINLINE SizeType AddUnique(const ElementType& Item)

배열에 존재하지 않으면 Add를 한다. 모든 원소를 확인해야 하므로 O(n)의 시간이 필요하고 이러한 존재하지 않는 것을 판단하는 것은 Array에 적합하지 않으므로 신중하게 사용해야 한다. 하지만, 배열의 크기가 매우 작은 경우는 충분히 사용 가능하다고 생각한다. 내부적으로 Find 함수를 호출한다.

반환값은 추가된 원소의 인덱스이다.

TArray<int32> AddUniqueArray;
AddUniqueArray.AddUnique(1); // 1
AddUniqueArray.AddUnique(2); // 1, 2
AddUniqueArray.AddUnique(3); // 1, 2, 3
AddUniqueArray.AddUnique(4); // 1, 2, 3, 4
AddUniqueArray.AddUnique(5); // 1, 2, 3, 4, 5
AddUniqueArray.AddUnique(1); // 1, 2, 3, 4, 5 | 이미 1이 배열에 존재하므로 추가하지 않는다.

Append

void Append(const TArray<OtherElementType, OtherAllocatorType>& Source)
void Append(TArray<OtherElementType, OtherAllocator>&& Source)
void Append(RangeType&& Source)
void Append(const ElementType* Ptr, SizeType Count)
FORCEINLINE void Append(std::initializer_list<ElementType> InitList)
TArray& operator+=(TArray&& Other)
TArray& operator+=(const TArray& Other)
TArray& operator+=(std::initializer_list<ElementType> InitList)

Append는 다른 배열이나 초기화 리스트의 값들을 뒤에 추가하는 함수이다. +=도 Append를 사용하도록 구현되어 있기 때문에 Append를 호출하는 대신에 +=를 이용하는 것도 가능하다.

TArray<int32> AppendArray;
AppendArray.Append(InsertArray); // 1, 2, 3, 4, 5
AppendArray.Append(InsertAtArray); // 1, 2, 3, 4, 5, 1, 0, 0, 2, 0, 0
AppendArray.Append({5, 6, 7}); // 1, 2, 3, 4, 5, 1, 0, 0, 2, 0, 0, 5, 6, 7
AppendArray += {8, 9}; // 1, 2, 3, 4, 5, 1, 0, 0, 2, 0, 0, 5, 6, 7, 8, 9

데이터 제거

순서를 유지하는 순서를 유지하지 않는 제거가 있다. 순서를 유지하는 제거 연산은 순서를 유지하기 위해 뒤의 원소로 채워야 하므로 O(n)의 시간 복잡도로 실행된다. 순서를 유지하지 않는 제거 연산은 원소를 제거하고 가장 뒤의 원소로 바꿔서 O(1)의 시간 복잡도로 실행된다. 순서가 중요하지 않고 데이터 유무가 중요하다면 순서를 고려하지 않는 제거 연산을 실행한다.

  순서를 유지하는 제거 순서를 유지하지 않는 제거
함수 Remove, RemoveAll, RemoveSingle, RemoveAt RemoveSwap, RemoveAllSwap, RemoveSingleSwap, RemoveAtSwap
시간 복잡도 O(n) O(1)
순서 유지됨 유지되지 않음
  • Remove : 특정한 값을 제거
  • RemoveAll : 특정한 조건을 만족하는 모든 값을 제거
  • RemoveSingle : 특정한 값 중 맨 앞의 하나를 제거
  • RemoveAt : 특정한 인덱스의 값을 제거거

Remove

SizeType Remove(const ElementType& Item)

배열 내 주어진 값과 동일한 모든 원소를 제거한다. 순서가 유지되지만 Index는 유지되지 않는다. 내부적으로 RemoveAll을 호출한다.

반환값으로는 삭제된 원소의 개수를 반환한다.

TArray<int32> RemoveValueArray = { 1, 1, 1, 2, 3 };
// Value : 삭제할 원소
RemoveValueArray.Remove(1); // 2, 3

RemoveAll

SizeType RemoveAll(const PREDICATE_CLASS& Predicate)

조건을 만족하는 모든 원소를 제거한다. 순서가 유지되지만 Index는 유지되지 않는다.

반환값으로는 삭제된 원소의 개수를 반환한다.

TArray<int32> RemoveAllArray = {1, 2, 3, 4, 5, 6, 7, 8};
RemoveAllArray.RemoveAll([](int32 Value) { return Value % 2 == 0; }); // 1, 3, 5, 7

RemoveSingle

SizeType RemoveSingle(const ElementType& Item)

특정한 값 중에서 처음 발견한 값만을 제거한다.

반환값은 삭제된 원소의 개수를 반환한다. 하나만 제거하기 때문에 0 혹은 1이다.

TArray<int32> RemoveSingleArray = { 1, 1, 1, 2, 3 };
RemoveSingleArray.RemoveSingle(1); // 1, 1, 2, 3

RemoveAt

void RemoveAt(SizeType Index, EAllowShrinking AllowShrinking = EAllowShrinking::Yes)

특정한 인덱스의 원소를 제거한다. Insert와 마찮가지로 사용하기 전에 반드시 인덱스의 유효성을 판단해야 한다.

TArray<int32> RemoveArray = { 1, 2, 3, 4, 5 };
// Index : 삭제할 인덱스, 0-based
if(RemoveArray.IsValidIndex(2))
{
  RemoveArray.RemoveAt(2); // 1, 2, 4, 5
}

순회하기

C++에서 사용할 수 있는 모든 방법으로 순회가 가능하다.

TArray<int32> IterateArray = { 1, 2, 3, 4, 5 };
//  range-based for loop
for (int32 Element : IterateArray)
{
  // 1, 2, 3, 4, 5
  UE_LOG(LogTemp, Display, TEXT("%d"), Element);
}

// index-based for loop
for (int32 Index = 0; Index < IterateArray.Num(); ++Index)
{
  // 1, 2, 3, 4, 5
  UE_LOG(LogTemp, Display, TEXT("%d"), IterateArray[Index]);
}

// iterator for loop
// CreateConstIterator : 읽기 전용 Iterator
// CreateIterator : 읽기, 쓰기 가능한 Iterator
for (auto Iterator = IterateArray.CreateConstIterator(); Iterator; ++Iterator)
{
  // 1, 2, 3, 4, 5
  UE_LOG(LogTemp, Display, TEXT("%d"), *Iterator);
}

참고

  1. TArray: 언리얼 엔진의 배열
  2. Array.h

'UnrealEngine' 카테고리의 다른 글

UCL - TMap  (0) 2025.04.21
Unreal Container - TArray(2)  (0) 2025.03.27
Delegate 추가 학습  (0) 2025.03.24
Network 채팅 프로그램 공부  (0) 2025.03.18
Ownership, Owning Connection  (0) 2025.03.14