[ 🌱 Unreal Container Library (UCL) ]
📍UCL 이란? :
UE이 자체 제작해 제공하는 자료구조 라이브러리
Unreal C++은 다양한 자료구조 라이브러리를 직접 만들어 제공하고 있다.
UCL은 Unreal Objet 를 안정적으로 자원하며 다수의 오브젝트 처리에 유용하게 사용된다.
📍STL 과 UCL 의 차이점 :
STL은 범용적으로 설계되어 있기에 호환성이 높다.
하지만, 많은 기능이 엮여 있기에 컴파일 시간이 오래 걸린다.
UCL은 UE에 특화되어 있기에 Unreal Object를 안정적으로 지원하며 가볍다.
이러한 이유로 STL 보다 UCL이 게임 제작에 적합하며 대체하여 사용해야한다.
📍STL 과 UCL 비교 :
두 라이브러리의 용도는 유사하지만, 내부적으로 다르게 구현되어 있다.
UCL의 다양한 라이브러리중 게임 제작에 주로 사용되는 것은 TArray, TMap, TSet 이다.
접두사 "T"는 "Template" 을 의미한다.
- TArray : 오브젝트를 순서대로 담아 효율적으로 관리 용도
- TSet : 중복되지 않는 요소로 구성된 집합 생성 용도
- TMap : Key-Value 조합의 레코드를 관리 용도
STL의 vector, set, map 은 각각 UCL의 TArray, TSet, TMap 과 용도가 비슷하다.
하지만, TArray와 달리 TSet 과 TMap의 경우 내부적으로 다르게 구현되어 있다.
[ 🌱TArray ]
가변 배열(Dynamic Array) 자료구조로써 STL의 vector와 동작 원리가 거의 유사하다.
Array Containers in Unreal Engine
dev.epicgames.com
📍장점 :
- 데이터가 순차적으로 모여있기 때문에 메모리를 효과적으로 사용할 수 있고 캐시 효율이 좋다.
- "캐시 지역성(Locality)"으로 인한 성능 향상은 게임 프로그래밍에 굉장히 중요하다.
- 임의 데이터에 대한 접근(Random Access, RA)이 빠르고, 고속으로 요소를 순회하는 것이 가능하다.
📍단점 :
- 메모리의 맨 끝에 데이터를 추가하는 작업은 쉽지만, 중간 삽입/삭제에 대한 작업비용은 크다.
- 데이터가 많아질수록 검색/삭제/중간에 데이터를 추가하는 수정 작업은 느려진다.
- 많은 수의 데이터에대해 빈번한 검색 작업이 일어난다면 TArray 대신 TSet을 사용하는 것이 좋다.
📍내부구조 :
- 동일한 크기를 가진 데이터들이 빈틈없이 나열되어 있다.
- 배열의 시작 부분에 대한 주소(포인터)를 GetData() 함수를 통해 얻어온다.
- 배열의 끝에 데이터를 추가하는 함수 : Add(), Emplace(), Append(), opeartor+=
- 중간에 데이터를 추가하는 Insert() 함수의 경우 TArray의 메모리 변경이 발생하기에 비용이 크다.
- remove() 함수역시 많은 비용이 발생한다.
- operator[ ] 를 통한 데이터에 대한 임의접근은 엄청 빠르다.
Array Containers in Unreal Engine
dev.epicgames.com

시퀀스 = 연속적으로 나열되어 있음을 의미한다.
특정 요소에 대해 바로 접근하여 빠르게 가져올 수 있음을 말해주고 있다.

얼로케이터(allocator) = 메모리를 어떻게 관리하는지에 대한 설정, 일반적으로 사용하지 않는다.

얼로케이터는 대부분 기본값을 사용한다.

TArray 타입은 TArray* 와 같이 동적할당을 하지 않으며, 여러 변수가 공유하지 않는다.
[ 🌱TArray 사용법 ]
📍TArray 만들기 :
TArray<타입> 변수명;
기본 형태는 이와 같으며, 만약 정수 값들을 저장하는 배열을 만들고자 한다면 "TArray<int32> IntArray" 와 같이 선언한다.
📍배열 채우기 :
🔹Init()
TArray::Init ( element value, element count );
Init() 함수를 사용하여 배열을 element 의 사본 여러 개로 채울 수 있다.
TArray<int32> IntArray;
IntArray.Init(10, 5); // Init(value, cnt)
🔹Add(), Emplace()
TArray::Add( element value );
TArray::Emplace( element value );
Add 와 Emplace는 같은 결과 값을 보이지만, 다르게 동작한다.
- Add : 엘리먼트를 외부에서 생성하여 TArray에 복사하여 넣어준다.
- Emplace : TArray 내부에서 엘리먼트를 생성한다.
즉, Add()는 임시 변수를 생성하여 복사과정을 거쳐 값을 대입하지만, Emplace()는 이러한 과정이 없다.
때문에 Emplace 가 Add 보다 효율이 좋지만, 가독성 측면에선 Add 가 나을 수 있다.
자주 호출되는 반복문이나 크기가 큰 자료형(구조체,...)의 경우 Emplace() 를 사용하고 그 외에 사소한 경우 Add() 를 사용하자
// case1
TArray<int32> IntArray;
IntArray.Add(1);
IntArray.Emplace(2);
// case2
TArray<FString> StrArray;
StrArray(TEXT("Hello"));
StrArray(TEXT("World"));
🔹Append()
TArray::Append( TArray );
TArray::Append( T* );
TArray::Append( T*, element count )
배열에 다수의 객체를 한번에 추가하는 경우 사용하는 기능
TArray<int32> IntArray;
// case 1
int32 arr[] = { 1,2,3,4,5 };
IntArray.Append(arr);
// case2
int32 arr[] = { 1,2,3,4,5 };
IntArray.Append(arr, 3);
// case3
TArray<int32> temp = {1,2,3};
IntArray.Append(temp);
🔹AddUnique()
검색 기능이 들어있는 기능으로 기존 컨테이너에 동일한 값이 존재하지 않는 경우에만 값을 추가해준다.
하지만, TSet 을 활용하는것이 더 효율적이다.
FArray<FString> StrArray;
FString StrArr[] = {TEXT("Hello"), TEXT("World"), TEXT("!")};
StrArray.Append(StrArr);
StrArray.AddUnique(TEXT("!"));
🔹Insert()
TArray::Insert(value, idx);
단일 엘리먼트나 배열 사본을 주어진 인덱스의 위치에 추가한다.
전체적인 메모리 구조가 변경되기에 비용이 발생한다.
TArray<int32> IntArray = { 1,2,3,4,5 };
IntArray.Insert(10, 1);
🔹SetNum()
Num() 함수를 통해서 배열의 크기를 받아올 수 있고, SetNum() 함수를 통해 배열의 엘리먼트 개수를 직접 설정할 수 있다.
설정된 값이 현재 배열의 크기보다 큰 경우 기본 생성자의 엘리먼트 유형을 사용하여 새로운 엘리먼트를 추가해준다.
현재 배열의 크기보다 작은 경우 엘리먼트를 제거하기도 한다.
📍반복처리(iterator) :
반복처리 하는 방법에는 여러가지가 있지만, C++의 범위 기반 for문을 사용하는것이 추천된다.
// case1
FString Result;
FString Source = TEXT("Hello World!");
for (int32 i = 0; i < Source.Len(); i++)
{
Result += Source[i];
}
// case2
FString Result2;
TArray<FString> StrArray = { TEXT("Hello"), TEXT("World!") };
for (int32 i = 0; i < StrArray.Num(); i++)
{
Result2 += StrArray[i];
Result2 += TEXT(" ");
}
Iterator를 사용하여 순회를 하는 경우 CreateIterator(읽기-쓰기) 와 CreateConstIterator(읽기전) 가 지원된다.
FString Result;
StrArray = { TEXT("Hello"), TEXT("World!") };
for (TArray<FString>::TConstIterator It = StrArray.CreateConstIterator(); It; ++It)
{
Result += *It;
Result += TEXT(" ");
}
📍정렬 (Sort) :
🔹Sort()
간단히 정렬할 수 있는 기능을 제공하며, 인자로 람다 함수를 넘겨줘서 커스텀하는것도 가능하다.
TArray<int32> IntArray = { 4,10,2,1,7,9,5 };
// case 1
IntArray.Sort();
// case 2
IntArray.Sort([](int32 a, int32 b) {return a > b; });
이 외에도 HeapSort(), StableSort() 를 지원한다.
📍쿼리 :
🔹Num
Num 함수를 사용해 배열에 값이 몇개 들어있는지 확인할 수 있다. (배열의 크기 확인)
🔹GetData
배열의 첫번째 엘리먼트에 대한 포인터를 반환한다.
🔹GetTypeSize
컨테이너에 들어있는 엘리먼트의 사이즈를 얻어올 수 있다.
🔹IsValidIndex
유요하지 않은 인덱스 (0 > Idx || Num() <= Idx ) 를 전해주면 런타임 에러가 발생한다.
컨터이너에 특정 인덱스가 유효한지 IsValidIndex 함수를 통해 확인할 수 있다.
🔹operator[ ]
레퍼런스를 반환하기 때문에, TArray가 const로 선언되지 않았다는 가정하에 엘리먼트 값을 변형시키는 데 사용할 수 있다.
🔹Top(), Last()
해당 함수들을 사용하여 배열 끝에서부터 역순으로 인덱스를 사용할 수 있다.
Top, Last 둘다 배열의 마지막 값에 접근할 수 있으며, Last 의 경우 인덱스 값을인자로 받을 수 있고 기본값은 0 이다.
🔹Contains
배열에 특정 엘리먼트가 들어있는지 확인할 수 있다.
람다를 통해 좀더 세밀한 조건을 추가할 수 있다.
🔹Find , FindLast
검색 기능의 경우 빈번하게 빠르게 찾아야한다면 Set 컨테이너를 활용하는 것이 좋다.
element 의 존재여부를 검사해서 존재하면 해당 인덱스를 두번째 인자에 넣어주며 true 값을 반환한다.
배열 내에 처음 찾은 엘리먼트의 인덱스를 반환값으로 설정한다.
중복된 엘리먼트가 있는 상태에서 마지막 엘리먼트의 인덱스를 찾고자 하는 경우, FindLast 함수를 사용한다.
인덱스를 찾지 못하였다면 두번째 인자에 INDEX_NONE(= -1) 을 넣어주고 false를 반환한다.
📍제거 (Remove) :
배열에서 엘리먼트를 지울 수 있다.
Insert()와 마찬가지로 값을 메모리 구조에 변동이 생기기 때문에 비용이 많이 발생하여 효율이 좋지 않다.
🔹Remove()
Remove() 함수는 인자로 제공된 값과 동일한 것으로 간주는 element 를 배열에서 모두 지운다.
🔹RemoveSingle()
배열에서 처음 일치한 엘리먼트만 지울 수 있다.
배열에 중복된 값들이 있는데 하나만 지우고자 한다거나, 배열에 해당 element가 하나만 있는 것이 확실한 경우 최적화 차원에서 유용하다.
🔹RemoveAt()
제거할 element의 인덱스를 인자로 지정할 수 있다.
Find() 혹은 IsValidIndex() 함수와 함께 사용하여 제거하고자 하는 값 또는 인덱스가 배열에 포함되는지 확인하는게 좋다.
유효하지 않은 인덱스를 전달하면 런타임 에러가 발생한다.
TArray<int32> IntArray = { 4,10,2,1,7,9,5 };
int32 TargetIdx = -1;
if (IntArray.Find(4, TargetIdx))
{
IntArray.RemoveAt(TargetIdx);
}
🔹RemoveAll()
이자로 들어가는 람다를 통해 특정 조건을 만족하는 모든 element 를 배열에서 제거할 수 있다.
TArray<int32> IntArray = { 4,10,2,1,7,9,5 };
IntArray.RemoveAll([](int a) {return a % 2 == 0; });
🔹Empty()
배열에서 모든 것을 제거한다.
📍연산자 :
🔹operator=
배열은 일반적인 값 유형으로, 생성자 복사나 할당 연산자를 통해 복사할 수 있다.
🔹operator!=
두 배열을 연결시킬 수 있다.
🔹operator==, operator!=
operator== 또는 operator!= 를 사용해서 두 배열을 비교할 수 있다.
해당 경우 엘리먼트의 순서가 중요한데, 두 배열이 동등한 경우는 엘리먼트의 수와 순서가 같은 경우다.
🔹MoveTemp
배열의 값을 다른 곳으로 이동시킬 수 있다.
이동 이후 원본 배열은 공백으로 남는다.
📍슬랙 (slack) :
GetSlack = Max - Num
// GetSlack = slack 의 개수
// Max = 배열에 최대로 저장할 수 있는 엘리먼트 개수 (현재 엘리멘트 개수 + slack)
// Num = 현재 배열에 들어있는 엘리먼트 개수
TArray<int32> IntArray;
IntArray.Reserve(10);
IntArray = { 1,2,3,4,5 };
int32 Num = IntArray.Num(); // 실제 데이터 개수
int32 Slack = IntArray.GetSlack(); // 여유분 개수
int32 Max = IntArray.Max(); // 담을 수 있는 총 데이터 개수
📍실습 :
값이 들어가는 걸 직접 확인하기 위해선 빌드 모드를 변경해줘야한다.
변경 : Development Editor → DebugGame Editor
코드 :
#pragma region TArray
UE_LOG(LogTemp, Log, TEXT("==========================="));
const int32 ArrayNum = 10;
TArray<int32> Int32Array;
// 1~10 대입
for (int32 i = 0; i < ArrayNum; ++i)
{
// int32 타입과 같은 간단한 정수 값의 경우 Add 와 Emplace 에 큰 차이가 없다.
Int32Array.Emplace(i);
}
// 짝수 제거
// 람다 함수 적용
Int32Array.RemoveAll(
[](int32 Val)
{
return Val % 2 == 0;
});
// 짝수 추가
Int32Array += {2, 4, 6, 8, 10};
// C스타일
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);
// #include "Algo/Accumulate.h"
int32 SumByAlgo = Algo::Accumulate(Int32Array, 0);
ensure(Sum == SumByAlgo);
UE_LOG(LogTemp, Log, TEXT("==========================="));
#pragma endregion
[ 🌱TSet ]
Part1.10 언리얼 컨테이너 라이브러리 Ⅰ - Array & Set_2
TArray 포스팅 : Part1.10 언리얼 컨테이너 라이브러리 Ⅰ - Array & Set_1 강의 목표 : TArray, TSet 의 내부 구조 이해 각 컨테이너 라이르버리의 장단점을 파악하고, 알맞게 활용하는 방법 학습 강의 내용 :
coder-qussong.tistory.com
================================
개인 공부 기록용 포스팅입니다.
댓글, 질문 환영해요~!
Unreal Engine Ver : 5.1.1
Refer : Inflearn_이득우의 언리얼 프로그램
================================
'Unreal > Inflearn' 카테고리의 다른 글
Part1.10 언리얼 컨테이너 라이브러리 Ⅰ - Set (0) | 2024.04.11 |
---|---|
Part1.9 언리얼 C++ 설계 Ⅲ - 델리게이트(Delegate)⭕ (2) | 2024.04.11 |
Part1.8 언리얼 C++ 설계 Ⅱ - 컴포지션⭕ (0) | 2024.04.09 |
Part1.7 언리얼 C++ 설계 Ⅰ - 인터페이스⭕ (0) | 2024.04.09 |
Part1.6 언리얼 오브젝트 리플렉션 시스템 Ⅱ⭕ (0) | 2024.04.08 |