WinAPI환경에서 2D게임 개발을 할 시에는 자료구조가 필요할 시 STL에 있는 자료구조들을 자주 사용했습니다.

언리얼 엔진을 접하면서 STL의 자료구조들과 같이 언리얼 내부에 있는 자료구조들이 있는지에 대해 알아보았는데, 언리얼 컨테이너 라이브러리(Unreal Container Library)에 포함되어있는 TArray, TMap, TSet의 자료구조 외에도 다양한 클래스 및 구조체들이 있었습니다.

효율적인 리소스 사용을 위해서 어떤것들이 더 효율적인지 비교하며 학습한 내용들을 이번 포스팅을 통해 알아보겠습니다.

 

언리얼 컨테이너 라이브러리(UCL)

  • 언리얼에서 제공하는 대표 컨테이너 라이브러리
  • 언리얼 오브젝트를 안정적으로 지원하며, 다수의 오브젝트 처리에 효율적이다.
    • UCL의 라이브러리는 가비지컬렉션을 통해 메모리 관리가 가능하다.
  • 대표적인 라이브러리는 TMap, TSet, TArray가 존재한다.
  • 접두사 T는 Template Library를 의미한다.

 

UCL과 C++의 STL의 차이점

  • C++ STL은 게임외에도 사용할 수 있도록 범용적으로 설계되어 있다.
  • STL에는 많은 기능들이 포함되어 있어 컴파일 시 UCL보다 많은 시간이 걸린다.
  • STL은 런타임 오버헤드가 최소화 되어있으며, UCL은 STL보다 런타임 환경에서 오버헤드가 더 발생할 수 있다.
  • UCL은 언리얼 오브젝트에 특화되어있다. 즉, 게임 개발이라는 한정된 범위가 존재한다.
  • UCL은 게임 개발에 적합한 API와 기능들을 가지고 있어, 게임 제작에 최적화된 구조로 설계되어있다.
  • UCL은 디버거 및 로거와 연동할 수 있어 문제가 발생했을 때 원인을 쉽게 파악할 수 있다.
  • UCL은 UPROPERTY()를 사용하여 블루프린터와 연동하여 사용할 수 있다.

즉, 언리얼 엔진 환경에서의 게임 개발시에는 UCL을 사용하는것이 더 적합하다고 할 수 있습니다.

 

 

TArray

  • 동적 배열(Dynamic Array)클래스이다.
  • STL의 vector와 동작 원리가 유사하다.
  • 기존 Array의 특징과 같이, 데이터가 순차적으로 모여있기 때문에 메모리를 효과적으로 사용할 수 있다.
  • 임의 데이터를 접근하는데에 있어 빠른 속도를 보장한다. 시간복잡도 O(1)
  • 탐색, 삽입, 삭제시에는 시간복잡도 O(N)이다.
  • 중복된 값을 허용한다.

배열 만들고 채우기

	// 정의
	TArray<int32> IntArray;

	// Init : n의 값을 n번 채운다.
	IntArray.Init(10, 5);
	// IntArray == {10, 10, 10, 10, 10}

	
	// Add, Emplace : 배열의 가장 마지막 위치에 추가한다. (Dynamic Array이므로 최대 사이즈 고려 X)
	IntArray.Add(10);			// 값 타입의 경우 해당 자료형의 임시 값을 추가해 복사한다.)
	IntArray.Emplace(10);		// 임시 변수를 생성하지 않는다.
	

	FString Arr[] = { TEXT("TEXT1"), (TEXT("TEXT2")) };
	TArray<FString> StrArray;

	// Append : 해당 배열의 크기에 다수의 엘리먼트를 한꺼번에 추가한다.
	StrArray.Append(Arr, sizeof(Arr));
	// StrArray == {TEXT1, TEXT2};


	//Insert : 단일 엘리먼트 또는 엘리먼트 배열 사본을 주어진 인덱스에 추가한다.
	StrArray.Insert(TEXT("TEXT3"), 1);
	// StrArray == {TEXT1, TEXT3, TEXT2};

 

정렬 (Sort)

	TArray<FString> FStrArray = {TEXT("!"), TEXT("Hello"), TEXT("Apple"), TEXT("Boat"), TEXT("World")};
	FStrArray.Sort();		// 안정적이지 않다. FString의 경우 대소문자 구분없이 사전식 비교후 정렬을 실행한다.
	// FStrArray == {!, Apple, Boat, Hello, World};

	FStrArray.HeapSort();	// 안정적이지 않다. FString의 경우 FString의 길이별로 정렬하지만, 같은 크기의 경우 순서를 보장하지 않는다.
	// FStrArray == {!, Boat, Hello, Apple, World};

	FStrArray.StableSort();	// Merge Sort형태로 구현되어있다. 순서를 보장한다.(A->Z의 순서 정렬)
	// FStrArray == {!, Boat, Apple, Hello, World};

 

 

쿼리

	TArray<FString> StrArray = { TEXT("!"), TEXT("Hello"), TEXT("Apple"), TEXT("Boat"), TEXT("World") };

	// Num : 배열의 Element가 몇개인지 확인하기
	int32 Count = StrArray.Num();
	// Count == 5

	// GetData : 배열 내 Element에 대한 포인터를 반환받는다.
	FString* StrPtr = StrArray.GetData();
	// Index 별로 개별 접근이 가능하다.
	// StrPtr[0] == "!"
	// StrPtr[1] == "Hello"
	// ...
	// StrPtr[5] - undefined behavior

	// Element 값 받아오기
	FString Elem1 = StrArray[1];
	// Elem1 == "Hello"

	// IsValidIndex(n) : 특정 인덱스가 유효한지에 대한 검사
	bool isValid1 = StrArray.IsValidIndex(1);
	bool isValid6 = StrArray.IsValidIndex(6);
	// isValid1 == true
	// isValid6 == false

	// Contains : 배열에 특정 엘리먼트가 들어가있는지 조회하기
	bool bContain = StrArray.Contains(TEXT("Hello"));
	bool bContain2 = StrArray.Contains(TEXT("NotHello"));
	// bContain == true
	// bContain == false

 

 

제거

	TArray<int32> ValArr;
	int32 Temp[] = { 10, 20, 30, 5, 10, 15, 20, 25, 30 };
	ValArr.Append(Temp, UE_ARRAY_COUNT(Temp));

	// Remove : 배열에서 일치하는 엘리먼트를 모두 지운다.
	ValArr.Remove(20);
	// ValArr == {10,30,5,10,15,25,30}

	// RemoveSingle : 배열에서 처음으로 일치하는 엘리먼트를 하나 지운다.
	ValArr.RemoveSingle(10);
	// ValArr == {10,5,10,15,25,30}

	// RemoveAt : 인덱스N의 엘리먼트를 지운다.
	ValArr.RemoveAt(2);
	// ValArr == {10,5,15,25,30}


	// RemoveAll : 일치하는 엘리먼트를 모두 지운다.
	ValArr.RemoveAll([](int32 Val) {
		return Val % 3 == 0;
		});
	// ValArr == {10,5,25}

	// Empty : 배열에 있는 모든 것을 제거한다.
	ValArr.Empty();
	// ValArr == []

 

힙(Heap)

  • TArray는 이진 힙 데이터 구조체를 지원하는 함수가 존재하며, 이진 트리 유형의 구조로 되어있다.
  • Heapify 함수룰 사용하여 기존 배열을 Heap으로 변환할 수 있다.
	TArray<int32> HeapArr;
	for (int32 Val = 10; Val != 0; --Val)
		HeapArr.Add(Val);
	// HeapArr == [10,9,8,7,6,5,4,3,2,1]
	

	// Heapify : 이진 힙으로 변경시킨다.
	HeapArr.Heapify();
	// HeapArr == [1,2,4,3,6,5,8,10,7,9]

 

Heapify() 결과 - 이진 트리 형태의 Heap으로 변경되었다.

 

 

 

	// HeapPust : 힙에 새로운 엘리먼트를 추가하며, 내부적으로 노드 순서가 변경된다.
	HeapArr.HeapPush(4);
	// HeapArr == [1,2,4,3,4,5,8,10,7,9,6]

HeapPush를 통해 Element 4를 추가한 경우

 

 

슬랙

  • 배열이 추가될 때마다 매번 재할당을 피하기 위해 얼로케이터는 요청보다 더 넉넉한 메모리를 제공하여 Add 호출시 재할당에 드는 퍼포먼스 비용을 물지 않도록 한다.
  • 엘리먼트를 삭제한다고해도 메모리가 해제되지는 않으며, 배열에 slack(여유분)을 가지고 있는다.
  • 초기 슬랙의 값은 0이며, GetSlack함수를 사용해 해당 배열의 슬랙 크기가 얼마나 되는지 알 수 있다.
  • Max 함수를 사용해 재할당이 일어나기 전까지 배열에 저장할 수 있는 최대 엘리먼트를 알 수 있다.
	TArray<int32> SlackArray;
	// SlackArray.GetSlack() == 0
	// SlackArray.Num()      == 0
	// SlackArray.Max()      == 0

	SlackArray.Add(1);
	// SlackArray.GetSlack() == 3
	// SlackArray.Num()      == 1
	// SlackArray.Max()      == 4

	SlackArray.Add(2);
	SlackArray.Add(3);
	SlackArray.Add(4);
	SlackArray.Add(5);
	// SlackArray.GetSlack() == 17
	// SlackArray.Num()      == 5
	// SlackArray.Max()      == 22

 

원시메모리

  • TArray는 할당된 메모리를 둘러싼 포장 단위
  • AddUninitialized, InsertUninitialized : 배열에 초기화 되지 않은 공간에 Element를 추가한다.
    • 해당 엘리먼트 타입의 생성자는 호출하지 않는다.
    • Memcpy 호출로 전체 구조체를 덮어 쓰려는 경우 생성자 호출을 피할 떄 효율적이다.

 

 

 

이상으로 언리얼 컨테이너 라이브러리가 어떤것인지, 어떤 특징을 가지고 있는지

그리고 STL과 어떤 차이점이 있는지 비교하며 알아보았습니다.

 

또한 언리얼 C++의 주요 컨테이너 라이브러중 하나인 TArray와 해당 라이브러리의 주요 함수들이 어떤것들이 있는지에 대해 알아보았습니다.

 

다음 포스팅에서는 이어서 TSet, TMap이 어떤 특징을 가지고 있는지, 그리고 STL의 어떤 자료구조와 유사한지에 대해 알아보겠습니다.

 


https://dev.epicgames.com/documentation/ko-kr/unreal-engine/array-containers-in-unreal-engine

https://designerd.tistory.com/entry/UE-%EC%96%B8%EB%A6%AC%EC%96%BC-C%EC%9D%98-TArray-TSet-TMap-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%99%80-%ED%99%9C%EC%9A%A9%EB%B0%A9%EB%B2%95

+ Recent posts