Unreal Engine 5

Unreal Engine 컨테이너 자료구조 정리

둠치킨 2025. 7. 8. 18:45

STL 대신 자체 컨테이너 사용 이유

일단 UE는 왜 STL 대신 자체 컨테이너를 쓸까?라는 근본적인 질문부터 짚고 넘어가고자 한다. 구현 방식만 보면 사실 기본 STL과 방식은 크게 차이가 나는 것 같지 않다.

결론부터 말하자면 엔진 특화 요구 사항과 성능, 호환성 목적 때문이다. 단순히 STL이 느려서 그런게 아니라, 게임 엔진이라는 특수환 환경에 최적화된 설계가 필요해서 그렇다.

 

1. 엔진 전반적인 메모리 관리 통제

Unreal의 목적

  • 메모리 사용을 정밀하게 추적하고 관리해야 함 (디버깅, 최적화, 플랫폼별 제약 대응)
  • STL은 new, delete, malloc, free 등을 내부적으로 사용하므로 Unreal의 메모리 추적 시스템(FMemory 등)과 완전히 통합되지 않음

Unreal 방식

  • Unreal의 컨테이너는 FMemory::Malloc, FMemory::Free, FMemory::Realloc 등을 통해 엔진 커스텀 메모리 시스템과 직결
  • 메모리 풀, allocator 교체, 힙 디버깅 등에서 효과적

2. 가비지 컬렉션(GC) 및 리플렉션 시스템 연동

UPROPERTY 시스템 통합

  • Unreal은 UObject 기반 시스템에서 자동으로 GC 및 블루프린트 노출 등을 처리함
  • TArray<UObject*>와 같은 컨테이너는 GC 시스템에 의해 인식되도록 설계됨
  • STL 컨테이너는 Unreal의 리플렉션 및 GC 시스템에 완전히 통합될 수 없음

3. 플랫폼 호환성과 디버깅 편의성

  • Unreal은 여러 플랫폼(콘솔, 모바일, PC 등)을 타겟으로 함
  • STL은 컴파일러마다 구현 차이가 존재 (MSVC, Clang, GCC 등) → 바이너리 호환성 및 디버깅 어려움
  • Unreal 컨테이너는 일관된 디버깅 정보와 로깅 메시지 제공 가능 (check, ensure, 로그 메시지 등과 통합됨)

4. 템플릿 특화와 퍼포먼스 제어

  • TArray, TMap 등은 게임 특화 상황에서 trivially copyable 타입을 판단해 memcpy로 처리하는 등 퍼포먼스 최적화되어 있음
  • 불필요한 복사 제거, 이동 연산 강화, in-place 생성 (Emplace) 등 STL보다 적극적

5. 내부 디버그 및 에디터 툴 통합

  • 게임 에디터에서 TArray 등 컨테이너를 자동으로 시각화하거나 수정 가능
  • 디버그 시 메모리 상태, 순회 구조 확인 등에서 Unreal 컨테이너가 정보 제공에 유리

6. 게임 특화 기능 내장

기능 STL Unreal 컨테이너
GC-aware 지원 없음 지원
블루프린트/리플렉션 지원 없음 지원
메모리 추적/최적화 어렵거나 불완전 FMemory 통합으로 용이
플랫폼 일관성 컴파일러 의존 엔진 관리 일원화
In-place 생성 (Emplace) 일부 제공 널리 지원

Unreal Engine 컨테이너 자료구조 정리

1. TArray<T> – 동적 배열

  • 구조: T* Data; int32 ArrayNum; int32 ArrayMax;
  • 메모리 할당: ArrayMax가 부족하면 두 배로 확장하며 FMemory::Realloc으로 재할당
  • 삽입: Add, Insert, Emplace
  • 삭제: Remove, RemoveAt, RemoveAtSwap (후자 O(1))
  • 복사/이동 최적화: POD 타입은 FMemory::Memcpy 사용
  • Allocator: 기본은 FDefaultAllocator, 커스텀 가능

2. TMap<KeyType, ValueType> – 해시 기반 맵

  • 구조: TSet<TPair<Key, Value>, KeyFuncs>
  • 기반 자료구조: 내부적으로 TSet + TSparseArray로 구현
  • 삽입: 키에 대한 해시값 계산 → 중복 검사 → 존재하지 않으면 추가
  • 탐색: Find, Contains
  • 해시 함수: GetTypeHash(Key)가 필수 구현
  • KeyFuncs: KeyEquality 및 KeyHash를 정의하는 구조체

3. TSet<ElementType> – 중복 없는 해시 집합

  • 구조: TSparseArray<ElementType> Elements; TBitArray<> AllocationFlags;
  • 특징:
    • 중복을 허용하지 않음
    • 해시 기반 검색
    • 내부적으로 TSparseArray 사용
  • 해시 충돌 처리: open addressing
  • KeyFuncs: 기본 해시 및 비교 함수 커스터마이징 가능

4. TQueue<T> – 선입선출 큐

  • 기반 구조: 단순 연결 리스트
  • 클래스 내부: struct FElement { T Value; FElement* Next; }; FElement* Head; FElement* Tail;
  • 연산:
    • Enqueue – Tail에 추가
    • Dequeue – Head에서 제거
  • 특징:
    • O(1) 삽입 및 제거
    • Non-copyable 타입도 이동 연산 가능

5. TStack<T> – 후입선출 스택

  • 구조: TArray<T> 기반
  • 연산:
    • Push() – 배열의 마지막에 추가
    • Pop() – 마지막 요소 제거
    • Top() – 마지막 요소 참조
  • 특징:
    • 메모리 재사용
    • 유연한 크기 조절

6. TSparseArray<T> – 희소 배열 (Sparse Array)

  • 구조: TArray<T> Data; TBitArray<> AllocationFlags; TArray<int32> FreeList;
  • 목적:
    • 중간 요소 삭제 시 빈 슬롯(hole)을 유지
    • 빈 슬롯을 추적하여 재사용 가능
  • 성능 특징:
    • O(1) 삽입 및 제거 (최적화된 free list 사용)
    • Random access 가능하지만, 인덱스의 연속성은 보장되지 않음
  • 사용처:
    • TSet, TMap, UWorld 내 actor 목록 등에서 내부적으로 사용

7. TBitArray<> – 비트 배열

  • 구조: TArray<uint32> Data; int32 NumBits;
  • 특징:
    • 1비트 단위로 메모리 효율적인 플래그 저장
    • SetBit, ClearBit, TestBit 등의 함수 제공
    • 연산자 오버로드 (|, &, ^, ~) 제공

8. TLinkedList<T> – 양방향 연결 리스트

  • 구조: struct TDoubleLinkedListNode<T> { T Value; TDoubleLinkedListNode<T>* Next; TDoubleLinkedListNode<T>* Prev; };
  • 특징:
    • 삽입/삭제가 O(1)
    • IsValid, GetNextNode, GetPrevNode 제공
    • Unreal에서는 일반적으로 사용 빈도 낮음 (캐시 효율 낮음)

9. TRingBuffer<T> – 고정 크기 원형 버퍼

  • 구조: TArray<T> Buffer; int32 Capacity; int32 ReadIndex; int32 WriteIndex;
  • 특징:
    • 메모리를 고정 크기로 할당한 순환 버퍼
    • 사용자가 ReadIndex, WriteIndex를 직접 관리
    • 캐시 효율성 높고 lock-free 구조에 적합
    • 순환 큐처럼 동작하지만, 요소 수(count)는 내부에 없음 (직접 관리 필요)
    • 오디오 버퍼, 네트워크 스트림, 생산자-소비자 큐 등에 적합
  • 동작:
    • WriteIndex 위치에 데이터를 쓰고 한 칸 이동
    • ReadIndex 위치에서 데이터를 읽고 한 칸 이동
    • 양쪽 인덱스가 Capacity를 기준으로 순환 (% Capacity)

공통적인 내부 최적화 요소

최적화 기법 설명
트리비얼 타입 최적화 TIsTriviallyDestructible 체크 후 memcpy/memmove로 처리
Custom Allocator TInlineAllocator, FDefaultAllocator 등 선택 가능
Move Semantics 지원 MoveTemp()로 비용 최소화
템플릿 메타프로그래밍 SFINAE, 타입 traits 등을 사용한 내부 분기 최적화
Assert + 체크 매크로 개발 중 잘못된 접근 방지 (check, ensure)

 

이외에도 사실상 STL에 존재하는 컨테이너는 다 존재한다. 근데 std::map 같은 레드 블랙 트리 기반 컨테이너는 못 찾은 것인지 모르겠지만 잘 안 사용되는 것 같다. 아마 이유는 게임 상에서는 순서 기반보다는 빠르게 탐색/삽입/삭제 같은 것을 할 일이 많아서 그런 것 같다. 그래서 key 순서를 보장해야하는 경우는 직접 구현하거나 TArray를 써서 정렬해야 하는 것 같다.