Skip to content

자료구조 — 트리(Tree)와 이진 탐색 트리(BST)

개요 — 왜 트리를 알아야 하는가

Section titled “개요 — 왜 트리를 알아야 하는가”

배열이나 연결 리스트 같은 선형(Linear) 자료구조는 데이터를 한 줄로 나열합니다. 그러나 현실의 많은 데이터는 계층(Hierarchy) 관계를 갖습니다. 파일 시스템의 폴더 구조, 조직도, XML/JSON 문서의 중첩 구조, 그리고 게임 엔진의 씬 그래프(Scene Graph)가 대표적입니다.

**트리(Tree)**는 이러한 계층 관계를 표현하는 비선형 자료구조입니다. 그리고 트리의 특수한 형태인 **이진 탐색 트리(Binary Search Tree, BST)**는 정렬된 데이터에 대해 평균 O(log n)의 탐색·삽입·삭제를 제공합니다.

Unreal Engine 내부에서도 BVH(Bounding Volume Hierarchy), Octree, Behavior Tree 등 핵심 시스템이 모두 트리 구조 위에 구현되어 있습니다. UE5 C++ 개발자라면 트리 자료구조를 깊이 이해하는 것이 엔진 구조를 꿰뚫는 지름길입니다.


트리는 **노드(Node)**와 **간선(Edge)**으로 구성된 계층적 자료구조입니다. 다음 특성을 가집니다.

  • 하나의 루트(Root) 노드에서 시작합니다.
  • 각 노드는 0개 이상의 자식(Child) 노드를 가집니다.
  • 부모-자식 관계는 단방향이며, 사이클(Cycle)이 없습니다.
  • 루트를 제외한 모든 노드는 정확히 하나의 부모(Parent) 노드를 가집니다.
[A] ← 루트(Root)
/ \
[B] [C] ← A의 자식, B와 C는 형제(Sibling)
/ | \ |
[D] [E] [F] [G] ← 리프(Leaf): D, E, F, G는 자식 없음
용어정의
루트(Root)트리의 최상단 노드. 부모가 없음
노드(Node)데이터를 담는 기본 단위
리프(Leaf)자식이 없는 노드 (말단 노드)
내부 노드(Internal Node)자식이 하나 이상 있는 노드
부모(Parent)특정 노드의 바로 위 노드
자식(Child)특정 노드의 바로 아래 노드
형제(Sibling)같은 부모를 공유하는 노드들
서브트리(Subtree)특정 노드와 그 모든 자손으로 구성된 부분 트리
깊이(Depth)루트에서 해당 노드까지의 간선 수 (루트의 깊이 = 0)
높이(Height)해당 노드에서 가장 깊은 리프까지의 간선 수 (리프의 높이 = 0)
레벨(Level)같은 깊이에 있는 노드들의 집합
차수(Degree)한 노드가 가진 자식의 수
트리 높이와 깊이 예시:
[A] ← 깊이 0, 높이 3
/ \
[B] [C] ← 깊이 1, 높이 2 / 1
/ \ \
[D] [E] [F] ← 깊이 2, 높이 1 / 1 / 0
/
[G] ← 깊이 3, 높이 0 (리프)
전체 트리의 높이 = 루트의 높이 = 3
일반 트리: 이진 트리: 완전 이진 트리:
[A] [A] [A]
/ | \ / \ / \
[B][C][D] [B] [C] [B] [C]
/ \ \ / \ / \
[D] [E] [F] [D] [E][F] [G]
이진 트리: 각 노드의 자식이 최대 2개
완전 이진 트리: 마지막 레벨을 제외한 모든 레벨이 꽉 차고,
마지막 레벨은 왼쪽부터 채워짐
포화 이진 트리: 모든 레벨이 완전히 꽉 찬 이진 트리

2. 이진 트리 vs 이진 탐색 트리(BST)

Section titled “2. 이진 트리 vs 이진 탐색 트리(BST)”

이진 트리는 각 노드가 최대 2개의 자식을 갖는 트리입니다. 왼쪽 자식(Left Child)과 오른쪽 자식(Right Child)으로 구분하지만, 데이터의 순서에 관한 규칙이 없습니다.

// 이진 트리 노드 구조 (C++)
struct FBinaryTreeNode
{
int32 Value;
FBinaryTreeNode* Left; // 왼쪽 자식
FBinaryTreeNode* Right; // 오른쪽 자식
explicit FBinaryTreeNode(int32 InValue)
: Value(InValue), Left(nullptr), Right(nullptr)
{}
};

아래 트리는 이진 트리이지만 BST가 아닙니다. 노드 배치에 순서 규칙이 없기 때문입니다.

[5]
/ \
[9] [2] ← 오른쪽 자식(2)이 루트(5)보다 작음 → BST 위반
/
[1]

BST는 이진 트리에 **정렬 속성(BST Property)**을 추가한 것입니다.

BST 속성: 임의의 노드 N에 대해,

  • N의 왼쪽 서브트리의 모든 노드 값은 N의 값보다 작습니다.
  • N의 오른쪽 서브트리의 모든 노드 값은 N의 값보다 큽니다.
올바른 BST: 잘못된 BST (BST 속성 위반):
[8] [8]
/ \ / \
[3] [10] [3] [10]
/ \ \ / \ \
[1] [6] [14] [1] [9] [14]
/ \ ← [9]가 [8]의 오른쪽에 있어야 하는데
[4] [7] [3]의 오른쪽에 있으므로 위반
중위 순회 결과: 1 3 4 6 7 8 10 14 중위 순회: 1 3 8 9 10 14 → 정렬 불일치

BST의 핵심 장점은 정렬 속성 덕분에 이진 탐색(Binary Search)과 동일한 원리로 O(log n) 탐색이 가능하다는 것입니다. 매 단계마다 탐색 범위가 절반으로 줄어듭니다.

특성이진 트리BST
자식 수 제한최대 2개최대 2개
데이터 정렬 규칙없음왼쪽 < 루트 < 오른쪽
탐색 효율O(n) (전체 순회 필요)평균 O(log n)
중위 순회 결과임의 순서오름차순 정렬된 순서
주요 활용표현식 트리, 파스 트리탐색, 정렬, 범위 쿼리

3.1 BST 노드 구조 및 클래스 설계

Section titled “3.1 BST 노드 구조 및 클래스 설계”
#include <cstdint>
struct FBSTNode
{
int32 Value;
FBSTNode* Left;
FBSTNode* Right;
explicit FBSTNode(int32 InValue)
: Value(InValue), Left(nullptr), Right(nullptr)
{}
};
class FBinarySearchTree
{
public:
FBinarySearchTree() : Root(nullptr) {}
~FBinarySearchTree() { DestroyTree(Root); }
void Insert(int32 Value);
bool Search(int32 Value) const;
void Remove(int32 Value);
private:
FBSTNode* Root;
FBSTNode* InsertRecursive(FBSTNode* Node, int32 Value);
bool SearchRecursive(const FBSTNode* Node, int32 Value) const;
FBSTNode* RemoveRecursive(FBSTNode* Node, int32 Value);
FBSTNode* FindMin(FBSTNode* Node) const;
void DestroyTree(FBSTNode* Node);
};

3.2 탐색(Search) — 평균 O(log n), 최악 O(n)

Section titled “3.2 탐색(Search) — 평균 O(log n), 최악 O(n)”

BST 속성을 활용하여 매 단계마다 탐색 범위를 절반으로 줄입니다.

탐색 예시: 값 6을 찾아라 (BST 높이 = 3)
[8] → 6 < 8, 왼쪽으로 이동
/ \
[3] [10] → 6 > 3, 오른쪽으로 이동
/ \
[1] [6] → 6 == 6, 탐색 성공! (3단계만에 완료)
bool FBinarySearchTree::SearchRecursive(const FBSTNode* Node, int32 Value) const
{
if (Node == nullptr)
{
return false; // 탐색 실패: 값이 트리에 없음
}
if (Value == Node->Value)
{
return true; // 탐색 성공
}
else if (Value < Node->Value)
{
return SearchRecursive(Node->Left, Value); // 왼쪽 서브트리 탐색
}
else
{
return SearchRecursive(Node->Right, Value); // 오른쪽 서브트리 탐색
}
}
bool FBinarySearchTree::Search(int32 Value) const
{
return SearchRecursive(Root, Value);
}

시간복잡도

경우복잡도설명
평균 (균형 잡힌 트리)O(log n)매 단계 탐색 범위 절반 감소
최악 (편향 트리)O(n)한쪽으로만 치우쳐 선형 탐색과 동일
균형 BST (높이 ≈ log n): 편향 BST (높이 = n-1):
[5] [1]
/ \ \
[3] [7] → [2]
/ \ / \ \
[2][4][6][8] O(log n) [3]
\
[4] → O(n)

편향 트리는 정렬된 순서로 데이터를 삽입할 때 발생합니다. 이를 해결하는 것이 AVL Tree, Red-Black Tree 등 균형 이진 트리입니다 (Section 5 참고).

3.3 삽입(Insert) — 평균 O(log n), 최악 O(n)

Section titled “3.3 삽입(Insert) — 평균 O(log n), 최악 O(n)”

탐색과 동일한 방식으로 삽입 위치를 찾은 뒤, 해당 위치에 새 노드를 생성합니다.

삽입 예시: 값 5를 삽입 (BST: 8, 3, 10, 1, 6)
[8] → 5 < 8, 왼쪽
/ \
[3] [10] → 5 > 3, 오른쪽
/ \
[1] [6] → 5 < 6, 왼쪽 → nullptr 발견, 여기에 삽입
/
[5] ← 삽입 완료
FBSTNode* FBinarySearchTree::InsertRecursive(FBSTNode* Node, int32 Value)
{
if (Node == nullptr)
{
// 삽입 위치 발견: 새 노드 생성
return new FBSTNode(Value);
}
if (Value < Node->Value)
{
Node->Left = InsertRecursive(Node->Left, Value);
}
else if (Value > Node->Value)
{
Node->Right = InsertRecursive(Node->Right, Value);
}
// Value == Node->Value인 경우: 중복 값은 무시 (또는 카운터 증가 등 정책 적용)
return Node;
}
void FBinarySearchTree::Insert(int32 Value)
{
Root = InsertRecursive(Root, Value);
}

3.4 삭제(Remove) — 평균 O(log n), 최악 O(n)

Section titled “3.4 삭제(Remove) — 평균 O(log n), 최악 O(n)”

삭제는 세 가지 케이스로 나뉩니다.

케이스 1 — 리프 노드 삭제 (자식 없음):
삭제: [1]
[8] [8]
/ \ / \
[3] [10] → [3] [10]
/ \ \
[1] [6] [6]
→ 그냥 제거
케이스 2 — 자식이 하나인 노드 삭제:
삭제: [10] (오른쪽 자식 [14]만 있음)
[8] [8]
/ \ / \
[3] [10] → [3] [14]
\
[14]
→ 자식 노드가 삭제된 노드 자리를 대체
케이스 3 — 자식이 둘인 노드 삭제:
삭제: [3] (왼쪽 [1], 오른쪽 [6] 모두 있음)
[8] [8]
/ \ / \
[3] [10] → [4] [10]
/ \ / \
[1] [6] [1] [6]
/
[4]
→ 오른쪽 서브트리의 최솟값(In-order Successor = [4])으로 대체 후,
해당 최솟값 노드를 원래 위치에서 삭제
FBSTNode* FBinarySearchTree::FindMin(FBSTNode* Node) const
{
// 왼쪽 끝 노드가 최솟값
while (Node->Left != nullptr)
{
Node = Node->Left;
}
return Node;
}
FBSTNode* FBinarySearchTree::RemoveRecursive(FBSTNode* Node, int32 Value)
{
if (Node == nullptr)
{
return nullptr; // 삭제 대상 없음
}
if (Value < Node->Value)
{
Node->Left = RemoveRecursive(Node->Left, Value);
}
else if (Value > Node->Value)
{
Node->Right = RemoveRecursive(Node->Right, Value);
}
else
{
// 삭제 대상 노드 발견
// 케이스 1: 리프 노드 또는 케이스 2: 자식 하나
if (Node->Left == nullptr)
{
FBSTNode* RightChild = Node->Right;
delete Node;
return RightChild; // 오른쪽 자식(또는 nullptr)으로 대체
}
else if (Node->Right == nullptr)
{
FBSTNode* LeftChild = Node->Left;
delete Node;
return LeftChild; // 왼쪽 자식으로 대체
}
// 케이스 3: 자식 둘 — In-order Successor로 대체
FBSTNode* Successor = FindMin(Node->Right);
Node->Value = Successor->Value; // 값 복사
Node->Right = RemoveRecursive(Node->Right, Successor->Value); // Successor 삭제
}
return Node;
}
void FBinarySearchTree::Remove(int32 Value)
{
Root = RemoveRecursive(Root, Value);
}
void FBinarySearchTree::DestroyTree(FBSTNode* Node)
{
if (Node == nullptr) return;
DestroyTree(Node->Left);
DestroyTree(Node->Right);
delete Node;
}
연산평균최악비고
탐색(Search)O(log n)O(n)편향 트리 시 최악
삽입(Insert)O(log n)O(n)편향 트리 시 최악
삭제(Remove)O(log n)O(n)편향 트리 시 최악
최솟값/최댓값O(log n)O(n)가장 왼쪽/오른쪽 노드
중위 순회 (전체)O(n)O(n)정렬된 순서 출력

트리 순회는 트리의 모든 노드를 특정 순서로 방문하는 방법입니다. 이진 트리에서는 4가지 주요 순회 방식이 있습니다.

예제 트리:
[1]
/ \
[2] [3]
/ \
[4] [5]

4.1 전위 순회(Pre-order) — 루트 → 왼쪽 → 오른쪽

Section titled “4.1 전위 순회(Pre-order) — 루트 → 왼쪽 → 오른쪽”

현재 노드를 먼저 방문한 뒤, 왼쪽 서브트리, 오른쪽 서브트리 순으로 순회합니다.

방문 순서: 1 → 2 → 4 → 5 → 3

활용: 트리 복사, 직렬화(Serialization), 표현식 트리의 전위 표기법

void PreorderTraversal(const FBSTNode* Node)
{
if (Node == nullptr) return;
// 1. 현재 노드 방문 (Root)
UE_LOG(LogTemp, Log, TEXT("%d"), Node->Value);
// 2. 왼쪽 서브트리 순회
PreorderTraversal(Node->Left);
// 3. 오른쪽 서브트리 순회
PreorderTraversal(Node->Right);
}

4.2 중위 순회(In-order) — 왼쪽 → 루트 → 오른쪽

Section titled “4.2 중위 순회(In-order) — 왼쪽 → 루트 → 오른쪽”

BST에서 중위 순회를 하면 오름차순 정렬된 결과를 얻습니다.

방문 순서: 4 → 2 → 5 → 1 → 3

활용: BST에서 정렬된 출력, 오름차순 탐색

void InorderTraversal(const FBSTNode* Node)
{
if (Node == nullptr) return;
// 1. 왼쪽 서브트리 순회
InorderTraversal(Node->Left);
// 2. 현재 노드 방문 (Root)
UE_LOG(LogTemp, Log, TEXT("%d"), Node->Value);
// 3. 오른쪽 서브트리 순회
InorderTraversal(Node->Right);
}

4.3 후위 순회(Post-order) — 왼쪽 → 오른쪽 → 루트

Section titled “4.3 후위 순회(Post-order) — 왼쪽 → 오른쪽 → 루트”

자식 노드를 모두 처리한 뒤 현재 노드를 방문합니다.

방문 순서: 4 → 5 → 2 → 3 → 1

활용: 트리 메모리 해제, 파일 시스템 삭제(자식 폴더 먼저 삭제), 표현식 트리의 후위 표기법

void PostorderTraversal(const FBSTNode* Node)
{
if (Node == nullptr) return;
// 1. 왼쪽 서브트리 순회
PostorderTraversal(Node->Left);
// 2. 오른쪽 서브트리 순회
PostorderTraversal(Node->Right);
// 3. 현재 노드 방문 (Root) — 자식들을 모두 처리한 후
UE_LOG(LogTemp, Log, TEXT("%d"), Node->Value);
}

4.4 레벨 순회(Level-order / BFS) — 위에서 아래, 왼쪽에서 오른쪽

Section titled “4.4 레벨 순회(Level-order / BFS) — 위에서 아래, 왼쪽에서 오른쪽”

같은 레벨(깊이)의 노드를 왼쪽부터 오른쪽으로 방문한 뒤 다음 레벨로 이동합니다. **큐(Queue)**를 사용하여 구현합니다.

방문 순서: 1 → 2 → 3 → 4 → 5

활용: 최단 경로 탐색(BFS), 트리 직렬화, 완전 이진 트리 검증

#include <queue>
void LevelOrderTraversal(const FBSTNode* Root)
{
if (Root == nullptr) return;
std::queue<const FBSTNode*> NodeQueue;
NodeQueue.push(Root);
while (!NodeQueue.empty())
{
const FBSTNode* Current = NodeQueue.front();
NodeQueue.pop();
// 현재 노드 방문
UE_LOG(LogTemp, Log, TEXT("%d"), Current->Value);
// 자식 노드를 큐에 추가 (왼쪽 → 오른쪽 순)
if (Current->Left != nullptr) NodeQueue.push(Current->Left);
if (Current->Right != nullptr) NodeQueue.push(Current->Right);
}
}
순회 방식방문 순서예제 결과주요 활용
전위(Pre-order)루트 → 좌 → 우1 2 4 5 3트리 복사, 직렬화
중위(In-order)좌 → 루트 → 우4 2 5 1 3BST 정렬 출력
후위(Post-order)좌 → 우 → 루트4 5 2 3 1트리 해제, 파일 삭제
레벨(Level-order)레벨별 좌→우1 2 3 4 5BFS, 최단 경로

5. 균형 이진 트리 — AVL Tree와 Red-Black Tree

Section titled “5. 균형 이진 트리 — AVL Tree와 Red-Black Tree”

일반 BST는 삽입 순서에 따라 편향 트리가 될 수 있어 O(n) 최악 케이스가 발생합니다. **균형 이진 트리(Balanced BST)**는 트리의 높이를 항상 O(log n)으로 유지하여 이 문제를 해결합니다.

AVL Tree(Adelson-Velsky and Landis Tree)는 **균형 인수(Balance Factor)**를 사용하여 트리 균형을 유지합니다.

균형 인수(Balance Factor) = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이

모든 노드의 균형 인수는 반드시 -1, 0, +1 중 하나여야 합니다.

균형이 깨지면 회전(Rotation) 연산으로 복구합니다.

Left-Left 케이스 (우회전): Left-Right 케이스 (좌-우 회전):
[5] BF=+2 [5] BF=+2
/ /
[3] BF=+1 → [3] BF=-1 → [4]
/ 우회전 전 좌회전 / \
[1] \ [3] [5]
[4]
↓ 우회전(Right Rotation)
[3]
/ \
[1] [5]
Right-Right 케이스 (좌회전): Right-Left 케이스 (우-좌 회전):
[1] BF=-2 [1] BF=-2
\ \
[3] BF=-1 → [5] BF=+1 → [3]
\ 좌회전 전 우회전 / \
[5] / [1] [5]
↓ 좌회전(Left Rotation) [3]
[3]
/ \
[1] [5]

AVL Tree 특징

  • 모든 연산의 시간복잡도: O(log n) 보장
  • 삽입·삭제마다 균형 인수를 갱신하고 최대 O(log n) 번의 회전 수행
  • 엄격한 균형 유지로 탐색 성능이 Red-Black Tree보다 우수
  • 삽입·삭제 시 회전 횟수가 많아 쓰기 작업이 빈번한 경우 Red-Black Tree보다 느릴 수 있음

Red-Black Tree는 각 노드에 색상(빨강/검정) 속성을 추가하여 5가지 규칙으로 균형을 유지합니다.

Red-Black Tree 5가지 규칙

  1. 모든 노드는 빨강 또는 검정이다.
  2. 루트 노드는 항상 검정이다.
  3. 모든 리프(NIL 노드)는 검정이다.
  4. 빨강 노드의 자식은 반드시 검정이다 (빨강이 연속으로 올 수 없다).
  5. 루트에서 임의의 리프까지 경로에 있는 검정 노드의 수는 모두 동일하다.
Red-Black Tree 예시:
(B=검정, R=빨강)
[8:B]
/ \
[3:R] [10:B]
/ \ \
[1:B] [6:B] [14:R]
/ \
[4:R] [7:R]

Red-Black Tree 특징

  • 모든 연산의 시간복잡도: O(log n) 보장
  • AVL Tree보다 덜 엄격한 균형 — 탐색은 다소 느리지만 삽입·삭제 시 회전 횟수 적음
  • C++ STL의 std::map, std::set, std::multimap, std::multiset이 내부적으로 Red-Black Tree를 사용
  • Linux 커널의 프로세스 스케줄러(CFS)에서도 활용
특성AVL TreeRed-Black Tree
균형 기준균형 인수 (-1, 0, +1)색상 규칙 5가지
균형 엄격도더 엄격덜 엄격
탐색 성능상대적으로 빠름상대적으로 느림
삽입·삭제 성능상대적으로 느림 (더 많은 회전)상대적으로 빠름 (적은 회전)
시간복잡도O(log n) 보장O(log n) 보장
주요 사용처탐색 중심 DB, 파일시스템std::map/set, Linux 스케줄러

5.4 C++ STL에서의 Red-Black Tree 활용

Section titled “5.4 C++ STL에서의 Red-Black Tree 활용”
#include <map>
#include <set>
// std::map — Red-Black Tree 기반 Key-Value 탐색 (O(log n) 보장)
void STLMapExample()
{
// 내부적으로 Red-Black Tree
std::map<int32, std::string> ScoreMap;
// O(log n) 삽입
ScoreMap[100] = "Diamond";
ScoreMap[75] = "Platinum";
ScoreMap[50] = "Gold";
ScoreMap[25] = "Silver";
// O(log n) 탐색
auto It = ScoreMap.find(75);
if (It != ScoreMap.end())
{
// "Platinum"
UE_LOG(LogTemp, Log, TEXT("Rank: %s"), It->second.c_str());
}
// 중위 순회 → 오름차순 자동 출력 (25, 50, 75, 100)
for (const auto& [Score, Rank] : ScoreMap)
{
UE_LOG(LogTemp, Log, TEXT("%d: %s"), Score, Rank.c_str());
}
}
// std::set — Red-Black Tree 기반 정렬된 유일값 집합
void STLSetExample()
{
std::set<int32> UniqueIDs;
UniqueIDs.insert(42);
UniqueIDs.insert(17);
UniqueIDs.insert(88);
UniqueIDs.insert(42); // 중복 무시
// 정렬된 순서: 17, 42, 88
for (int32 ID : UniqueIDs)
{
UE_LOG(LogTemp, Log, TEXT("ID: %d"), ID);
}
}

트리 자료구조는 Unreal Engine의 핵심 시스템 곳곳에서 사용됩니다. 원리를 이해하면 엔진 내부 동작과 성능 특성을 훨씬 명확하게 파악할 수 있습니다.

6.1 BVH (Bounding Volume Hierarchy) — 충돌 감지

Section titled “6.1 BVH (Bounding Volume Hierarchy) — 충돌 감지”

BVH는 물리 시뮬레이션과 레이캐스트(Raycast)에서 사용하는 가속 구조입니다. 씬의 모든 오브젝트를 경계 볼륨(AABB 또는 OBB)으로 감싸고, 이를 계층적 트리 구조로 구성합니다.

BVH 구조 예시 (씬에 오브젝트 A, B, C, D가 있는 경우):
[전체 씬 AABB]
/ \
[그룹1 AABB] [그룹2 AABB]
/ \ / \
[A] [B] [C] [D]

레이캐스트 과정:

  1. 레이가 루트 AABB와 교차하는지 검사
  2. 교차하면 자식 AABB와 검사 재귀 진행
  3. 교차하지 않는 서브트리 전체를 즉시 제외 (가지치기, Pruning)
// UE5에서 BVH 기반 물리 시스템 활용
// Engine/Source/Runtime/Engine/Public/Physics/PhysicsInterfacePhysX.h 참조
// 레이캐스트 (BVH를 내부적으로 사용)
FHitResult HitResult;
FVector Start = GetActorLocation();
FVector End = Start + GetActorForwardVector() * 5000.0f;
bool bHit = GetWorld()->LineTraceSingleByChannel(
HitResult,
Start,
End,
ECC_Visibility
);
// 내부적으로 PhysX/Chaos의 BVH 트리를 순회하여 후보 오브젝트만 정밀 검사
// 전체 오브젝트 순회(O(n))가 아닌 O(log n) 수준으로 성능 개선
if (bHit)
{
UE_LOG(LogTemp, Log, TEXT("Hit: %s"), *HitResult.GetActor()->GetName());
}

Octree는 3D 공간을 재귀적으로 8개의 자식 셀로 분할하는 트리 구조입니다. Unreal Engine은 Octree를 다양한 용도로 활용합니다.

Octree 공간 분할 (2D 예시로는 Quadtree):
루트: 전체 공간
├── 셀 0 (왼쪽 위 앞)
│ ├── 서브셀 0-0
│ └── 서브셀 0-1 (오브젝트 A, B 포함)
│ ├── ...
├── 셀 1 (오른쪽 위 앞)
├── 셀 2 (왼쪽 아래 앞)
...
└── 셀 7 (오른쪽 아래 뒤)
└── 서브셀 7-X (오브젝트 C, D 포함)
// UE5의 FPrimitiveSceneInfo는 Octree에 등록되어 렌더링 가시성 쿼리에 사용됨
// Engine/Source/Runtime/Engine/Public/SceneManagement.h 참조
// 예시: Overlap 쿼리 (내부적으로 Octree 활용)
TArray<FOverlapResult> OverlapResults;
FCollisionShape SphereShape = FCollisionShape::MakeSphere(500.0f);
GetWorld()->OverlapMultiByChannel(
OverlapResults,
GetActorLocation(),
FQuat::Identity,
ECC_Pawn,
SphereShape
);
// Octree로 해당 구체와 겹치는 셀만 탐색 → 후보 Actor만 정밀 검사
// 씬 전체 순회 없이 근처 Actor만 효율적으로 탐색
for (const FOverlapResult& Result : OverlapResults)
{
if (ACharacter* Nearby = Cast<ACharacter>(Result.GetActor()))
{
UE_LOG(LogTemp, Log, TEXT("Nearby: %s"), *Nearby->GetName());
}
}

Unreal Engine의 씬은 Actor → Component 계층 구조로 구성됩니다. 특히 USceneComponentAttachToComponent를 통해 트리 구조를 형성하며, 부모-자식 Transform이 계층적으로 전파됩니다.

씬 그래프 예시:
ACharacter (Actor)
└── UCapsuleComponent (RootComponent)
├── USkeletalMeshComponent (캐릭터 메시)
│ └── UParticleSystemComponent (이펙트, 소켓 부착)
├── UCameraComponent (카메라)
└── USpringArmComponent (카메라 붐)
└── UCameraComponent (3인칭 카메라)
// AttachToComponent: 씬 그래프 트리에 노드 추가
// Engine/Source/Runtime/Engine/Classes/Components/SceneComponent.h 참조
UCLASS()
class AMyWeapon : public AActor
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere)
TObjectPtr<UStaticMeshComponent> WeaponMesh;
public:
void AttachToCharacterSocket(USkeletalMeshComponent* CharacterMesh, FName SocketName)
{
// 씬 그래프 트리에 자식으로 추가
WeaponMesh->AttachToComponent(
CharacterMesh,
FAttachmentTransformRules::SnapToTargetIncludingScale,
SocketName
);
// 이후 CharacterMesh의 Transform이 변경되면
// WeaponMesh에도 자동으로 전파됨 (트리 구조 덕분)
}
};
// 씬 그래프 순회 — PostorderTraversal과 동일한 원리
void TraverseComponentTree(USceneComponent* Component, int32 Depth = 0)
{
if (!IsValid(Component)) return;
FString Indent = FString::ChrN(Depth * 2, ' ');
UE_LOG(LogTemp, Log, TEXT("%s%s"), *Indent, *Component->GetName());
// 자식 컴포넌트 순회 (전위 순회 방식)
for (USceneComponent* Child : Component->GetAttachChildren())
{
TraverseComponentTree(Child, Depth + 1);
}
}

Unreal Engine의 Behavior Tree는 AI 캐릭터의 행동을 트리 구조로 정의하는 시스템입니다. 루트에서 자식 노드를 순회하며 조건에 따라 행동을 결정합니다.

Behavior Tree 구조:
Root
└── Selector (자식 중 하나가 성공하면 OK)
├── Sequence (모든 자식이 성공해야 OK)
│ ├── BTTask_FindEnemy ← 적 탐지
│ ├── BTTask_MoveToEnemy ← 적에게 이동
│ └── BTTask_Attack ← 공격
└── Sequence
├── BTTask_FindPatrolPoint ← 순찰 포인트 탐색
└── BTTask_MoveToPoint ← 순찰
노드 유형:
- Composite: Selector(OR), Sequence(AND), Simple Parallel
- Decorator: 조건 검사 (Is Blackboard Value Set, Cooldown 등)
- Task: 실제 행동 (Move To, Play Animation, Custom Task 등)
// 커스텀 Behavior Tree Task 구현
// Engine/Source/Runtime/AIModule/Classes/BehaviorTree/BTTaskNode.h 참조
#include "BehaviorTree/BTTaskNode.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
UCLASS()
class UBTTask_FindAndSetTarget : public UBTTaskNode
{
GENERATED_BODY()
// Blackboard에서 읽어올 Target 키
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector TargetKey;
public:
UBTTask_FindAndSetTarget()
{
NodeName = TEXT("Find And Set Target");
}
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override
{
AAIController* AIController = OwnerComp.GetAIOwner();
if (!IsValid(AIController))
{
return EBTNodeResult::Failed;
}
APawn* ControlledPawn = AIController->GetPawn();
if (!IsValid(ControlledPawn))
{
return EBTNodeResult::Failed;
}
// 반경 내 적 탐색 (내부적으로 Octree/BVH 활용)
TArray<FOverlapResult> OverlapResults;
FCollisionShape SearchSphere = FCollisionShape::MakeSphere(2000.0f);
bool bFound = ControlledPawn->GetWorld()->OverlapMultiByChannel(
OverlapResults,
ControlledPawn->GetActorLocation(),
FQuat::Identity,
ECC_Pawn,
SearchSphere
);
if (bFound)
{
// Blackboard에 Target 설정
UBlackboardComponent* Blackboard = OwnerComp.GetBlackboardComponent();
Blackboard->SetValueAsObject(TargetKey.SelectedKeyName, OverlapResults[0].GetActor());
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
};
트리 시스템트리 종류활용 목적관련 클래스/파일
BVH이진 트리 계열충돌 감지, 레이캐스트 가속FPhysScene, Chaos Physics
Octree8진 트리공간 분할, Overlap 쿼리FPrimitiveSceneInfo, UWorld
Scene Graph일반 트리오브젝트 계층, Transform 전파USceneComponent
Behavior Tree일반 트리AI 의사결정UBehaviorTreeComponent, UBTTaskNode
Widget Tree일반 트리UMG UI 계층UWidgetTree, UUserWidget
Animation TreeDAG(방향 비순환 그래프)애니메이션 블렌딩UAnimGraph, FAnimNode_Base

트리 (Tree)
├── 기본 개념: 루트, 노드, 리프, 깊이(Depth), 높이(Height)
├── 이진 트리: 각 노드 자식 최대 2개, 순서 규칙 없음
└── BST (Binary Search Tree)
├── BST 속성: 왼쪽 < 현재 < 오른쪽
├── 탐색/삽입/삭제: 평균 O(log n), 최악 O(n)
└── 균형 BST
├── AVL Tree: 균형 인수(-1,0,+1), 엄격한 균형, 탐색 우수
└── Red-Black Tree: 색상 규칙 5개, 삽입·삭제 우수
→ std::map, std::set 내부 구조
순회 방식
├── 전위(Pre-order): 루트→좌→우, 트리 복사/직렬화
├── 중위(In-order): 좌→루트→우, BST 정렬 출력
├── 후위(Post-order): 좌→우→루트, 트리 해제
└── 레벨(Level-order): BFS, 최단 경로
UE5 트리 활용
├── BVH: 충돌 감지 O(log n) 가속
├── Octree: 공간 분할, Overlap 쿼리 최적화
├── Scene Graph: 컴포넌트 계층, Transform 전파
└── Behavior Tree: AI 의사결정 구조
  • BST 속성(왼쪽 < 현재 < 오른쪽)을 손으로 트리를 그리며 검증할 수 있는가?
  • 삭제 3가지 케이스(리프, 자식 하나, 자식 둘)를 설명하고 코드로 구현할 수 있는가?
  • 정렬된 데이터를 BST에 순서대로 삽입하면 왜 편향 트리가 되는지 설명할 수 있는가?
  • 중위 순회가 BST에서 정렬된 결과를 내는 이유를 이해하고 있는가?
  • AVL Tree와 Red-Black Tree의 트레이드오프를 상황에 맞게 설명할 수 있는가?
  • std::map/std::set이 Red-Black Tree 기반임을 알고, O(log n) 특성을 감안하여 사용하는가?
  • UE5의 LineTraceSingleByChannel이 내부적으로 BVH를 활용함을 이해하는가?
  • USceneComponent::AttachToComponent가 씬 그래프 트리에 자식 노드를 추가하는 것임을 이해하는가?
  • Behavior Tree의 Composite 노드(Selector/Sequence)가 트리 순회 방식으로 실행됨을 이해하는가?