[UE5] Behavior Tree로 순찰, 추적 AI 만들기
NPC의 행동을 담당하는 AI는 언리얼엔진에서 Behavior Tree를 사용하여 구현할 수 있다.
코드 예시는 아래 공식가이드에 자세하기 잘 나와있으므로 Behavior Tree의 각 요소에 대해 알아보겠다.
예시로 든 Behavior Tree를 실행하기 위해 추가로 세팅이 필요한 클래스들이 있다.
AIController: NPC Pawn에 Possess하여 세팅한 Behavior Tree를 실행하는 Controller
Blackboard: 구현하려는 AI에서 알아야 하는 모든 정보를 담고 있는 '뇌'를 담당하는 클래스
NavMeshBoundsVolume: UE의 Navigation System은 경로탐색 능력을 Agent에게 제공, 이를 가능하도록 World의 Collision 정보를 담고 있는 클래스
언리얼엔진의 Behavior Tree는 아래 사진과 같이 트리구조로 각 Node를 연결하여 행동의 흐름을 한눈에 볼 수 있도록 되어있다.
가장 윗 노드인 ROOT는 Behavior Tree에서 필요로 하는 정보인 Key를 가지고 있는 클래스로 Blackboard를 연결한 노드이다.
Vector, Boolean, Object 등 다양한 자료형을 모두 사용할 수 있다.
추적 AI를 구현하기 위해 추적 대상인 Character 객체를 가리키는 Target을 추가하였고
현재 위치인 HomePos, 순찰하고자 하는 위치인 PatrolPos를 Vector 자료형으로 키를 추가하였다.
AI에게 요구할 로직은 다음과 같다.
1. 매 순간 주변에 추적하고자 하는 대상이 있나 확인한다 <- Detect
2. 추적 대상을 찾지 못하면 대기(Wait) -> 다음 순찰 위치 파악(FindPatrolPos) -> 순찰 위치로 이동(MoveTo)
3. 추적 대상을 찾으면
- 3.1 대상이 공격 가능 범위에 있지 않으면 대상의 위치로 이동 (MoveTo)
- 3.2 대상이 공격 가능 범위에 있으면 공격(Attack) 및 대상을 향해 Pawn 회전(TurnToTarget)
Composities Node
Bahavior Tree에서 회색으로 표시된 노드는 Composities 노드로 흐름을 컨트롤하고 연결된 하위(child) Branch를 어떻게 동작시킬 것인가를 정하는 Node이다. 언리얼 엔진은 기본으로 Selector, Sequence, Simple Parallel 3종류의 Composities 노드를 제공한다.
Selector: Child Branches를 왼쪽부터 오른쪽으로 실행시키는 노드로 Child Branch 중 하나가 성공적으로 실행될 경우 해당 Branch에 머무른다. (= 노드의 실행에 실패할 경우 다른 Child Branch를 실행한다)
Sequence: Child Branches를 왼쪽부터 오른쪽으로 실행시키는 노드로 Child Branch 에서 실행에 실패한 노드가 있을 경우 다시 거슬러 올라가 이를 반복하고 Child Branch 전체 실행에 성공할 경우 다음 Child Branch로 이동한다.
Simple Parallel: Main Task 노드, Background Branch를 연결할 수 있으며 Background Branch는 Main Task와 동시에 실행된다. 설정에 따라 Main Task가 끝나거나 Background Branch의 실행이 끝나면 종료된다.
Decorator & Service SubNode
노드에 추가할 수 있는 서브노드로 Decorator, Service 두 종류가 있다.
Decorator: 조건을 담당하는 서브노드로 어떤 Branch, 노드를 실행할지 결정하는 노드이다.
Service: Task 노드, Composite 노드에 모두 부착가능하며, 정의된 주기에 따라 Branch가 실행되는 동안 실행된다. 주로 Balckboard의 값을 확인하거나 업데이트 하는데 사용된다.
Task Node
Task 노드는 Behavior Tree가 실행할 Task를 가리키는 노드이다. BTTask 클래스의 ExecuteNode 함수를 Override하여 로직을 구현하고 실행하며 반환값인 실행 결과는 enum으로 정의된 EBTNodeResult 자료형을 따른다.
EBTNodeResult::Type UBTTaskNode::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
return EBTNodeResult::Succeeded;
}
UENUM(BlueprintType)
namespace EBTNodeResult
{
// keep in sync with DescribeNodeResult()
enum Type : int
{
// finished as success
Succeeded,
// finished as failure
Failed,
// finished aborting = failure
Aborted,
// not finished yet
InProgress,
};
}
노드 선택 및 구현
AI에서 앞서 구현하고자 하는 로직을 다시보자.
1. 매 순간 주변에 추적하고자 하는 대상이 있나 확인한다 <- Detect
2. 추적 대상을 찾지 못하면 대기(Wait) -> 다음 순찰 위치 파악(FindPatrolPos) -> 순찰 위치로 이동(MoveTo)
3. 추적 대상을 찾으면
- 3.1 대상이 공격 가능 범위에 있지 않으면 대상의 위치로 이동 (MoveTo)
- 3.2 대상이 공격 가능 범위에 있으면 공격(Attack) 및 대상을 향해 Pawn 회전(TurnToTarget)
우선 Task를 결정하는 큰 분기는 추적대상을 찾았는가, 찾지 못했는가이다.
추적 대상을 찾았을 경우 추적, 공격을 계속해서 진행해야 하기 때문에 Composite 노드로는 Sequence가 아닌 Selector가 필요하다.
다음으로 Behavior Tree에서 계속해서 반복되어야 하는 추적 대상 탐색(Detect)는 방금 정한 Selector에 Service Subnode로 추가한다.
Detect에는 추적 대상을 찾게 되면 BlackBoard의 Target을 해당 추적 대상 객체로 업데이트하는 코드가 아래와 같이 들어있다.
OwnerComp.GetBlackboardComponent()->SetValueAsObject(AExampleAIController::TargetKey, nullptr);
Target Key가 업데이트되는 것을 조건으로 하는 SubNode가 필요하므로 조건을 담당하는 Decorator를 Selector 아래 두 Child Branch에 각각 추가하여야 한다. (Target이 설정되면 TaregetOn, 그렇지 않으면 NoTarget)
Target을 찾지 못했을 경우 앞서 정의한 순찰 로직을 실행해야 한다.
대기 -> 다음 순찰 위치 파악 -> 이동이 순차적으로 진행되어야 하기 때문에 이들의 Parent 노드로는 Composites 노드인 Sequence가 필요하다.
대기의 경우 기본으로 제공하는 Wait Task 노드를 사용하고 이동 또한 Location만 입력하면 되는 언리얼에서 제공하는 Move To Task 노드를 사용한다.
다음 순찰 위치를 파악하는 노드의 로직은 직접 구현이 필요하다. NavigtionSystem을 이용하여 Pawn을 기준으로 원 모양을 그리고 원 내부 랜덤 위치를 다음 순찰위치로 설정하여 Blackboard의 키를 업데이트 하는 Pseudo code다.
if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, PatrolRadius, NextPatrol))
{
OwnerComp.GetBlackboardComponent()->SetValueAsVector(AExampleAIController::PatrolPosKey, NextPatrol.Location);
return EBTNodeResult::Succeeded;
}
Sequence를 사용하였기 때문에 순차적으로 실행되어 다음 순찰 위치 파악 -> 해당 순찰위치로 이동의 로직의 구현이 가능하다.
추적 도중에는 공격 범위에 들어왔는가, 아닌가를 조건으로 행동이 나누어지므로 Selector와 공격범위에 있는가를 판단하는 Decorator가 필요하다.
공격과 공격대상을 향해 회전은 동시에 진행하여야 자연스럽게 플레이어를 향해 공격하는 동작이 구성되기 때문에 Simple Parallel을 사용하였고 Main Task로는 공격 Task인 Attak Task 노드를 지정하였다.
게임과 AI
많은 게이머에게 호평을 받은 에이리언: 아이솔레이션에서 AI는 단순 추적 AI와 플레이어의 예상 경로를 제공하는 AI 함께 활용하여 이용하여 실제로 적이 플레이어의 동선을 예측하며 쫓아온다는 느낌을 받을 수 있도록 하였다.
모든 필드에서의 데이터가 수집되는 게임의 특성상 Real World에 비해 복잡도가 크게 낮아 강화학습의 적용 또한 용이하다.
딥러닝을 처음 공부할 때, 2013년에 공개된 딥마인드의 "Playing Atari with Deep Reinforcement Learning"를 보고 Deep Q-Leaning(DQN)으로 Cartpole과 같이 간단한 강화학습을 테스트했었다.
언리얼엔진에 대한 이해가 깊어지면 딥러닝을 활용한 간단한 강화학습 AI를 구현해 볼 계획이다.
Reference
[1] 이득우의 언리얼 C++ 게임개발의 정석