AI Control에 있어서 FSM이랑 Behavior tree가 가장 많이 쓰인다. 이 둘은 Modularity
와 Reactivity
측면에서 각각의 장단점이 존재한다. 일반적으로 BT는 Module면에서 강점을, FSM은 Reactive Behaviors를 구현하는데 강점을 가진다.
왼쪽은 BT, 오른쪽은 FSM의 사진이다. BT는 트리 형태, FSM은 순서도의 모습을 하고 있는 것을 알 수 있다. 수정면에서 어떠한 노드를 삽입한다고 했을때 FSM의 경우보다 BT의 경우에 수정 사항이 확실히 덜 한 것을 알 수 있다(빨간색 영역이 각각 수정해야 할 부분들) .
Reactive designing
의 측면에서 여러 노드간의 transition이 필요한 경우를 생각해보자. FSM의 경우 노드와 노드를 연결하면 그만이지만, BT는 기본적으로 DFS을 기반으로 한 트리 형태이기 때문에 이를 간단하게 구현하기가 쉽지 않다.
부모 노드에서 다수의 행동을 컨트롤 하는 것을 Composite
이라고 한다.
Composite
에는 Selector
, Sequence
, Parallel
3가지 종류가 있음.
Selector
: 자식 노드 중 성공한 노드가 나오면 종료.
Sequence
: 자식 노드 중 실패한 노드가 나올 때까지 진행.
Parallel
: 자식 노드들을 동시에 실행.
Decorator
: Composite 노드가 실행되는 조건을 지정.Service
: Composite 노드가 실행되는동안 Tick마다 Service에 해당하는 부가 명령을 함께 실행. Abort
: Decorator 조건에 부합되면 Composite 내 활동을 모두 중단하고 루트에서부터 BT를 재시작(Decorator과 필히 함께 사용되어야 함). BT 또한 tick에 의해서 update 되는데, tick마다 node는 밑의 4가지 상태 값 중 하나를 부모 노드에게 전달한다.
Succedded
: 행동의 성공Failed
: 행동의 실패Aborted
: 외부 요인으로 인한 행동의 실패InProgress
: 행동 결과를 홀딩모든 경우에 대해서 하드코딩하여 BT를 구성할 수 있지만, 언리얼에서는 BT가 참고하는 변수 시스템인 BlackBoard
를 통해 parametric subtree
를 만들 수도 있다.
예시 그림은 parametric subtree
인데 해당 flow를 설명해 보자면,
LocationQueue
를 참고하는데 이 queue에는 N개의 location 지점이 존재한다. 이 지점들은 뽑아서 AtLoc
에 하나씩 넣으면서 BT가 작동하게 된다. 위 그림은 우리가 구현할 NPC 캐릭터의 BT이다. BT에 대한 분석을 해보면,
우리가 캐릭터를 PlayerController로 조종하는 것처럼, NPC 캐릭터들도 일종의 Controller가 있어야 행동을 할 수 있다. 이 Controller를 AIController
라고 한다.
Step 1
: 에디터에서 BlackBoard
와 Behavior Tree
만들기
언리얼 editor로 들어가 Artificial Intelligence
항목에 있는 Blackboard
와 Behavior Tree
를 각각 만들어줘야 한다.
Behavior Tree
와 BlackBoard
의 관계는 긴밀하다. BT
에서 사용할 변수들을 BlackBoard에서 참고할 수 있는데, 이를 통해서 위에서 다루었던 것처럼 modular한 subtree를 생성할 수도 있다. 더 나아가 AI들이 level에 다양한 사물과 상호작용하고 이에 대한 값들을 BlackBoard
에 저장하면, 이 값들을 참고해 BT
상에서 수행할 로직을 바꾸는 행동이 가능하다.
Step 2
: Custom AIController.cpp
에 로직 구현
#include "AI/RyanAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"
ARyanAIController::ARyanAIController()
{
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBAssetRef(TEXT("/Game/AI/BB_RyanCharacter.BB_RyanCharacter"));
if (nullptr != BBAssetRef.Object)
{
BBAsset = BBAssetRef.Object;
}
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAssetRef(TEXT("/Game/AI/BT_RyanCharacter.BT_RyanCharacter"));
if (nullptr != BTAssetRef.Object)
{
BTAsset = BTAssetRef.Object;
}
}
void ARyanAIController::RunAI()
{
UBlackboardComponent* BlackboardPtr = Blackboard.Get();
if (UseBlackboard(BBAsset, BlackboardPtr))
{
bool RunResult = RunBehaviorTree(BTAsset);
ensure(RunResult);
}
}
void ARyanAIController::StopAI()
{
UBehaviorTreeComponent* BTComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
if (BTComponent)
{
BTComponent->StopTree();
}
}
// Controller에서 제공하는 OnPossess 함수 override
void ARyanAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
RunAI();
}
생성자에서 BlackBoard랑 BehaviorTree 둘 다 reference로 가져오기
Pawn이 possessed 되었는지를 체크하는 Onpossess
를 override하고 RunAI
함수를 호출
RunAI를 통해서 BT를 실행
종료시에는 StopAI
함수를 호출. AIController가 생성되면서 맴버 변수로 BrainComponent
라는 것이 같이 생성되는데 AI에 관련된 다양한 명령은 이 BrainComponent
를 통해서 가능하다. 지금 우리는 BT를 중지하고 싶은 것이므로, BrainComponent
를 UBehaviorTreeComponent로 casting해서 가져온 다음, StopTree
를 호출한다.
Step 3
: AIController
가 빙의할 NPC Character
에 등록
// RyanNPCCharacter.cpp
// AIController 설정 부분
AIControllerClass = ARyanAIController::StaticClass();
// Spawn된 Actor나 이미 level에 배치되었던 Actor 둘다에게 AIController를 빙의
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
AIController
를 상속해서 만든 custom 클래스 ARyanAIController
정보를 가져온다. 어떻게 world에 생성된 pawn들에 대해서 AIController를 적용할지를 설정. 위 예시에서는 level에 배치된 것 + spawned되어서 생성된 것 둘 다에게 적용하도록 한다.
위 예시는 level이 재생되는 Spawned된 NPC 캐릭터에 대해 AIController를 Behavior Tree로 실시간 관찰하는 모습이다.
Selector 노드 밑에 Wait만 걸어주었기 때문에 아무런 동작을 하지 않는다.
https://roboticseabass.com/2021/05/08/introduction-to-behavior-trees/