행동 트리 (Behavior Tree) 이해하기
행동 트리란 무엇인가?
행동 트리(Behavior Tree, BT)는 인공지능(AI)에서 캐릭터나 시스템의 행동을 모듈식으로 구성하고 제어하는 데 사용되는 강력한 도구입니다. 특히 게임 개발에서 NPC(Non-Player Character)의 복잡한 의사 결정을 체계적으로 구현하는 데 널리 사용됩니다. 트리는 계층 구조를 가지며, 각 노드는 특정 작업이나 제어 흐름을 나타냅니다. 실행은 루트 노드에서 시작하여 자식 노드로 전달되는 '틱(tick)' 신호를 통해 이루어집니다.
핵심 개념: 노드와 틱(Tick)
행동 트리의 모든 연산은 '틱'이라는 단위로 이루어집니다. 루트 노드에 틱이 전달되면, 해당 노드는 자신의 로직을 실행하고, 필요에 따라 자식 노드에게 틱을 전달합니다. 각 노드는 틱에 대한 응답으로 다음 세 가지 상태 중 하나를 반환합니다:
- Success: 작업이 성공적으로 완료되었습니다.
- Failure: 작업을 완료할 수 없거나 조건이 충족되지 않았습니다.
- Running: 작업이 아직 진행 중이며, 완료되려면 시간이 더 필요합니다. 다음 틱에서 상태가 변경될 수 있습니다.
주요 노드 유형 및 다이어그램
행동 트리는 목적에 따라 다양한 노드로 구성됩니다. 주요 노드 유형은 다음과 같습니다.
1. 복합 노드 (Composite Nodes)
자식 노드들의 실행 흐름을 제어합니다.
-
시퀀스 (Sequence) -
->
자식 노드를 순서대로 실행합니다. 하나라도 Failure를 반환하면 즉시 중단하고 Failure를 반환합니다. 모든 자식이 Success를 반환해야 Success를 반환합니다.
graph TD subgraph Sequence Success Case S_Root["Sequence (->)"] --> S_C1["Child 1 (Success)"] S_C1 --> S_C2["Child 2 (Success)"] S_C2 --> S_C3["Child 3 (Success)"] end style S_Root fill:#ccf,stroke:#333; style S_C1 fill:#cfc,stroke:#333; style S_C2 fill:#cfc,stroke:#333; style S_C3 fill:#cfc,stroke:#333;graph TD subgraph Sequence Failure Case F_Root["Sequence (->)"] --> F_C1["Child 1 (Success)"] F_C1 --> F_C2["Child 2 (Failure)"] F_C2 -.-> F_Stop((Stop!)) F_Root -.-> F_C3["Child 3 (Not Run)"] end style F_Root fill:#ccf,stroke:#333; style F_C1 fill:#cfc,stroke:#333; style F_C2 fill:#fcc,stroke:#333; style F_C3 fill:#eee,stroke:#aaa,stroke-dasharray: 5 5; style F_Stop fill:none,stroke:none,color:red; -
셀렉터 (Selector) -
?
(Fallback)자식 노드를 순서대로 실행합니다. 하나라도 Success 또는 Running을 반환하면 즉시 중단하고 해당 상태를 반환합니다. 모든 자식이 Failure를 반환해야 Failure를 반환합니다.
graph TD subgraph Selector Success Case S_Root["Selector (?)"] --> S_C1["Child 1 (Failure)"] S_Root --> S_C2["Child 2 (Success)"] S_C2 -.-> S_Stop((Stop!)) S_Root -.-> S_C3["Child 3 (Not Run)"] end style S_Root fill:#f9f,stroke:#333; style S_C1 fill:#fcc,stroke:#333; style S_C2 fill:#cfc,stroke:#333; style S_C3 fill:#eee,stroke:#aaa,stroke-dasharray: 5 5; style S_Stop fill:none,stroke:none,color:green;graph TD subgraph Selector Failure Case F_Root["Selector (?)"] --> F_C1["Child 1 (Failure)"] F_Root --> F_C2["Child 2 (Failure)"] F_Root --> F_C3["Child 3 (Failure)"] end style F_Root fill:#f9f,stroke:#333; style F_C1 fill:#fcc,stroke:#333; style F_C2 fill:#fcc,stroke:#333; style F_C3 fill:#fcc,stroke:#333; -
병렬 (Parallel) -
=>
자식 노드를 동시에 실행합니다. 성공/실패 조건은 정책에 따라 다릅니다 (예: M개 성공 시 성공, N개 실패 시 실패).
graph TD P_Root["Parallel (=>)"] P_Root --> P_C1["Child 1"] P_Root --> P_C2["Child 2"] P_Root --> P_C3["Child 3"] P_Info((Ticks run concurrently)) --> P_Root style P_Root fill:#e9d8fd,stroke:#333; style P_C1 fill:#fff,stroke:#333; style P_C2 fill:#fff,stroke:#333; style P_C3 fill:#fff,stroke:#333; style P_Info fill:none,stroke:none,color:#555,font-style:italic;
2. 데코레이터 노드 (Decorator Nodes)
하나의 자식 노드를 가지며, 자식의 실행 조건이나 결과를 변경합니다.
데코레이터 유형 | 설명 |
---|---|
인버터 (Inverter) | 자식의 Success는 Failure로, Failure는 Success로 반전시킵니다 (Running은 유지). |
서크시더 (Succeeder) | 자식의 결과와 상관없이 항상 Success를 반환합니다. |
리피터 (Repeater) | 자식 노드를 지정된 횟수만큼 또는 무한히 반복 실행합니다. |
언틸 페일 (Until Fail) / 언틸 석세스 (Until Success) | 자식이 특정 결과(Failure / Success)를 반환할 때까지 반복 실행합니다. |
조건 (Condition Decorator) | 연결된 특정 조건이 참일 때만 자식 노드를 실행합니다. (실제로는 리프 노드의 조건 노드와 유사하게 사용되거나, 리프 노드와 조합됩니다.) |
3. 리프 노드 (Leaf Nodes)
트리의 가장 말단에서 실제 행동이나 조건을 검사합니다. 자식 노드를 가지지 않습니다.
-
액션 (Action)
구체적인 행동을 수행합니다 (예: 이동, 공격, 상호작용). Success, Failure, 또는 Running 상태를 반환할 수 있습니다.
-
조건 (Condition)
특정 상태나 환경을 검사합니다 (예: 체력 확인, 적 감지). 보통 Success(참) 또는 Failure(거짓)를 반환합니다.
행동 트리 구성 예시 (Mermaid 그래프)
다음은 간단한 NPC 행동 로직을 시각적인 그래프로 표현한 행동 트리 예시입니다.
NPC 기본 로직"] --> SeqAttack["Sequence (->)
공격 로직"]; Root --> ActPatrol["Action
순찰하기"]; SeqAttack --> CondSeeEnemy{"Condition
적이 시야에 있는가?"}; SeqAttack --> CondInRange{"Condition
공격 가능 거리인가?"}; SeqAttack --> ActAttack["Action
공격 실행"]; %% 스타일 정의 (선택 사항) style Root fill:#f9f,stroke:#333,stroke-width:2px; style SeqAttack fill:#ccf,stroke:#333,stroke-width:1px; style ActPatrol fill:#cfc,stroke:#333,stroke-width:1px; style CondSeeEnemy fill:#ffc,stroke:#333,stroke-width:1px; style CondInRange fill:#ffc,stroke:#333,stroke-width:1px; style ActAttack fill:#cfc,stroke:#333,stroke-width:1px;
실행 흐름 설명:
- 루트 Selector가 틱을 받습니다.
- 첫 번째 자식인 Sequence (공격 로직)에게 틱을 전달합니다.
- Sequence는 첫 번째 자식 Condition (적이 시야에 있는가?)에게 틱을 전달합니다.
- 만약 적이 시야에 없다면(Failure), Sequence는 즉시 Failure를 반환합니다. 루트 Selector는 다음 자식인 Action (순찰하기)에게 틱을 전달합니다.
- 만약 적이 시야에 있다면(Success), Sequence는 다음 자식 Condition (공격 가능한 거리인가?)에게 틱을 전달합니다.
- 거리가 멀면(Failure), Sequence는 Failure를 반환하고, 루트 Selector는 Action (순찰하기)에게 틱을 전달합니다.
- 거리가 가깝다면(Success), Sequence는 마지막 자식 Action (공격 실행)에게 틱을 전달합니다.
- Action (공격 실행)이 Success 또는 Running을 반환하면, Sequence도 같은 상태를 반환하고, 루트 Selector도 해당 상태를 반환합니다. (공격이 성공했거나 진행 중)
- Action (공격 실행)이 Failure를 반환하면, Sequence도 Failure를 반환하고, 루트 Selector는 Action (순찰하기)에게 틱을 전달합니다.
장점
- 모듈성: 행동 로직을 재사용 가능한 작은 단위(노드)로 나눌 수 있습니다.
- 가독성: 트리 구조는 복잡한 행동 흐름을 시각적으로 이해하기 쉽게 만듭니다.
- 유지보수성: 특정 행동을 수정하거나 추가하기 용이합니다.
- 확장성: 새로운 노드 유형을 정의하여 기능을 확장할 수 있습니다.
'프로그래밍' 카테고리의 다른 글
인터페이스의 접근 제한자 (0) | 2025.02.17 |
---|---|
Github 협업 전략 [Github Desktop] (0) | 2025.02.06 |