오늘은 AI를 한 단계 더 똑똑하게 만드는 행동 트리(Behavior Tree)와 블랙보드(Blackboard)에 대해 공부했다. 단순히 타이머로 정해진 행동만 반복하는 게 아니라, AI가 스스로 상황을 '기억'하고 그 기억을 토대로 '판단'해서 행동하게 만드는 방식이다. AI의 감각(Perception)으로 얻은 정보를 블랙보드라는 데이터베이스에 저장하고, 행동 트리가 이 정보를 읽어 추격, 조사, 순찰 등 다양한 행동을 결정하는 흐름을 직접 구현해봤다. 코드가 복잡해 보였지만, 역할을 분리하니 오히려 구조가 더 명확해지는 느낌이었다. 🧠
Key-Value 형태로 저장하는 데이터베이스 역할을 한다. (예: CanSeeTarget = true)AI의 각 컴포넌트는 다음과 같은 순서로 유기적으로 동작한다.
Perception (감각) → AI Controller (정보 처리) → Blackboard (기억) → Behavior Tree (판단) → Character (행동)
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "SpartaAIController.generated.h"
class UBehaviorTree;
class UBlackboardComponent;
UCLASS()
class SPARTAPROJECT_API ASpartaAIController : public AAIController
{
GENERATED_BODY()
public:
ASpartaAIController();
void StartBehaviorTree();
protected:
virtual void BeginPlay() override;
// AI의 기억 장치
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
UBlackboardComponent* BlackboardComp;
// AI의 의사결정 로직 에셋
UPROPERTY(EditDefaultsOnly, Category = "AI")
UBehaviorTree* BehaviorTreeAsset;
private:
// 감각 정보가 업데이트될 때 호출될 함수
UFUNCTION()
void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};
#include "SpartaAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTree.h"
#include "Perception/AIPerceptionComponent.h"
#include "Kismet/GameplayStatics.h"
// BeginPlay: 게임 시작 시 블랙보드 초기화 및 행동 트리 실행
void ASpartaAIController::BeginPlay()
{
Super::BeginPlay();
if (BlackboardComp)
{
// 블랙보드 값 초기화
BlackboardComp->SetValueAsBool(TEXT("CanSeeTarget"), false);
BlackboardComp->SetValueAsBool(TEXT("IsInvestigating"), false);
}
// 행동 트리 실행
StartBehaviorTree();
}
// OnPerceptionUpdated: 감각 시스템이 정보를 감지했을 때 블랙보드를 업데이트
void ASpartaAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
if (Actor != PlayerPawn || !BlackboardComp) return;
if (Stimulus.WasSuccessfullySensed())
{
// 봤을 때: 타겟 정보 저장
BlackboardComp->SetValueAsObject(TEXT("TargetActor"), Actor);
BlackboardComp->SetValueAsBool(TEXT("CanSeeTarget"), true);
BlackboardComp->SetValueAsVector(TEXT("TargetLastKnownLocation"), Actor->GetActorLocation());
BlackboardComp->SetValueAsBool(TEXT("IsInvestigating"), false);
}
else
{
// 놓쳤을 때: 조사 모드 시작
BlackboardComp->SetValueAsBool(TEXT("CanSeeTarget"), false);
BlackboardComp->SetValueAsBool(TEXT("IsInvestigating"), true);
}
}
// StartBehaviorTree: 지정된 행동 트리 에셋을 실행
void ASpartaAIController::StartBehaviorTree()
{
if (BehaviorTreeAsset)
{
RunBehaviorTree(BehaviorTreeAsset);
}
}
StartBehaviorTree() 함수만 작성하고 호출하지 않음: SpartaAIController 클래스에 StartBehaviorTree() 함수를 멋지게 구현했지만, 정작 BeginPlay() 함수에서 이 함수를 호출하는 코드를 빼먹었다. 그래서 AI가 게임 시작 시 아무런 행동도 하지 않고 가만히 서 있었다. 코드가 아무리 잘 짜여 있어도 실제로 호출해주지 않으면 아무 소용이 없다는 것을 뼈저리게 느꼈다. 😅Simple Parallel 노드를 사용해야 했는데, 실수로 Selector 노드를 사용했다. 그 결과, 플레이어를 감지하면 추격 행동만 하고 다른 행동은 시도조차 하지 않는 문제가 발생했다. 각 노드의 정확한 역할과 사용법을 숙지하는 것이 얼마나 중요한지 다시 한번 깨달았다.Abort Mode 설정을 잘못하면, 플레이어를 발견해도 기존의 순찰 행동을 계속하거나, 예상치 못한 시점에 행동이 중단되는 문제가 발생할 수 있다. 각 Abort Mode (Self, Lower Priority, Both)의 의미를 정확히 이해하고 상황에 맞게 설정하는 것이 중요하다는 것을 배웠다.| 개념 | 설명 | 비고 |
|---|---|---|
| Blackboard | AI의 기억 저장소. 모든 상황 판단의 근거가 되는 데이터를 저장한다. | Key-Value 방식 |
| Behavior Tree | AI의 의사결정 로직. 블랙보드의 데이터를 보고 어떤 행동을 할지 결정한다. | 우선순위에 따라 행동 결정 |
| Perception | AI의 감각 기관. 플레이어를 보거나 소리를 듣는 등 외부 정보를 감지한다. | 이벤트 기반으로 컨트롤러에 알림 |
| BT Task | 행동 트리를 구성하는 가장 작은 행동 단위. (예: 이동, 공격, 대기) | C++ 코드로 직접 기능 확장 가능 |
| Decorator | 행동 트리의 조건문. 블랙보드의 값을 체크하여 자식 노드의 실행 여부를 결정한다. | 'If'문과 비슷한 역할 (CanSeeTarget?) |