Skip to content

UE5 C++ Behavior Tree 구현 가이드

UE5의 Behavior Tree(BT)는 AI 행동을 트리 구조로 모델링하는 시스템입니다. 세 가지 노드 타입으로 구성됩니다.

노드 타입클래스역할
TaskUBTTaskNode실제 동작 수행 (이동, 공격, 대기)
DecoratorUBTDecorator조건 판단 — 실행 허용/차단
ServiceUBTService주기적 업데이트 (감지, Blackboard 갱신)

Blackboard는 BT 노드들이 공유하는 데이터 저장소입니다. 노드 간 직접 통신 대신 Blackboard를 통해 데이터를 교환합니다.


BTTask_MoveToPatrolPoint.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_MoveToPatrolPoint.generated.h"
UCLASS()
class MYGAME_API UBTTask_MoveToPatrolPoint : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_MoveToPatrolPoint();
// Task 실행 진입점
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) override;
// 비동기 Task 완료 처리 (이동 완료 후 호출)
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) override;
virtual FString GetStaticDescription() const override;
// 에디터에서 설정 가능한 Blackboard 키
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector PatrolPointKey;
UPROPERTY(EditAnywhere, Category = "Movement")
float AcceptanceRadius = 50.f;
};
BTTask_MoveToPatrolPoint.cpp
#include "BTTask_MoveToPatrolPoint.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include "Navigation/PathFollowingComponent.h"
UBTTask_MoveToPatrolPoint::UBTTask_MoveToPatrolPoint()
{
NodeName = TEXT("Move To Patrol Point");
// 비동기 Task 선언 — 이동 완료 전까지 InProgress 유지
bNotifyTaskFinished = true;
}
EBTNodeResult::Type UBTTask_MoveToPatrolPoint::ExecuteTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* AIController = OwnerComp.GetAIOwner();
if (!AIController) return EBTNodeResult::Failed;
UBlackboardComponent* Blackboard = OwnerComp.GetBlackboardComponent();
if (!Blackboard) return EBTNodeResult::Failed;
// Blackboard에서 목표 위치 읽기
FVector TargetLocation = Blackboard->GetValueAsVector(PatrolPointKey.SelectedKeyName);
// AI 이동 요청
FAIMoveRequest MoveRequest;
MoveRequest.SetGoalLocation(TargetLocation);
MoveRequest.SetAcceptanceRadius(AcceptanceRadius);
FNavPathSharedPtr NavPath;
AIController->MoveTo(MoveRequest, &NavPath);
// 이동 완료 델리게이트 등록
AIController->GetPathFollowingComponent()->OnRequestFinished.AddUObject(
this, &UBTTask_MoveToPatrolPoint::OnMoveCompleted);
// 이동 중 — InProgress 반환
return EBTNodeResult::InProgress;
}
void UBTTask_MoveToPatrolPoint::OnMoveCompleted(FAIRequestID RequestID,
const FPathFollowingResult& Result)
{
// 이동 완료 시 BT에 결과 통보 — FinishLatentTask 사용
// (실제 구현에서는 OwnerComp 참조를 멤버로 캐싱해 호출)
}
EBTNodeResult::Type UBTTask_MoveToPatrolPoint::AbortTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
if (AAIController* AIController = OwnerComp.GetAIOwner())
{
AIController->StopMovement();
}
return EBTNodeResult::Aborted;
}
FString UBTTask_MoveToPatrolPoint::GetStaticDescription() const
{
return FString::Printf(TEXT("Move to: %s\nRadius: %.0f"),
*PatrolPointKey.SelectedKeyName.ToString(),
AcceptanceRadius);
}
BTTask_UpdatePatrolIndex.h
UCLASS()
class MYGAME_API UBTTask_UpdatePatrolIndex : public UBTTaskNode
{
GENERATED_BODY()
public:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) override;
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector PatrolIndexKey;
};
EBTNodeResult::Type UBTTask_UpdatePatrolIndex::ExecuteTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (!BB) return EBTNodeResult::Failed;
AAIController* AIC = OwnerComp.GetAIOwner();
if (!AIC) return EBTNodeResult::Failed;
// 순찰 인덱스 순환 증가
AMyAIController* MyAIC = Cast<AMyAIController>(AIC);
if (MyAIC)
{
int32 NextIndex = MyAIC->AdvancePatrolIndex();
BB->SetValueAsInt(PatrolIndexKey.SelectedKeyName, NextIndex);
}
// 즉시 완료
return EBTNodeResult::Succeeded;
}

BTDecorator_IsPlayerInRange.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_IsPlayerInRange.generated.h"
UCLASS()
class MYGAME_API UBTDecorator_IsPlayerInRange : public UBTDecorator
{
GENERATED_BODY()
public:
UBTDecorator_IsPlayerInRange();
protected:
// 조건 평가 — true면 브랜치 허용, false면 차단
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) const override;
virtual FString GetStaticDescription() const override;
UPROPERTY(EditAnywhere, Category = "Detection")
float DetectionRange = 1000.f;
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector TargetActorKey;
};
UBTDecorator_IsPlayerInRange::UBTDecorator_IsPlayerInRange()
{
NodeName = TEXT("Is Player In Range");
// 조건이 바뀌면 즉시 재평가 (Abort 동작 설정과 연동)
bNotifyBecomeRelevant = true;
}
bool UBTDecorator_IsPlayerInRange::CalculateRawConditionValue(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
const UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (!BB) return false;
AActor* TargetActor = Cast<AActor>(BB->GetValueAsObject(TargetActorKey.SelectedKeyName));
if (!IsValid(TargetActor)) return false;
const AAIController* AIC = OwnerComp.GetAIOwner();
if (!AIC || !AIC->GetPawn()) return false;
float Distance = FVector::Dist(AIC->GetPawn()->GetActorLocation(),
TargetActor->GetActorLocation());
return Distance <= DetectionRange;
}

BTService_DetectPlayer.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_DetectPlayer.generated.h"
UCLASS()
class MYGAME_API UBTService_DetectPlayer : public UBTService
{
GENERATED_BODY()
public:
UBTService_DetectPlayer();
protected:
// Interval마다 호출
virtual void TickNode(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory,
float DeltaSeconds) override;
UPROPERTY(EditAnywhere, Category = "Detection")
float SightRange = 1500.f;
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector TargetActorKey;
};
UBTService_DetectPlayer::UBTService_DetectPlayer()
{
NodeName = TEXT("Detect Player");
// 0.2초마다 감지 갱신
Interval = 0.2f;
RandomDeviation = 0.05f; // 다수의 AI 동시 틱 분산
}
void UBTService_DetectPlayer::TickNode(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory,
float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
AAIController* AIC = OwnerComp.GetAIOwner();
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (!AIC || !BB || !AIC->GetPawn()) return;
APawn* AIPawn = AIC->GetPawn();
AActor* BestTarget = nullptr;
float ClosestDist = SightRange;
// 모든 플레이어 폰 탐색
for (FConstPlayerControllerIterator It = AIPawn->GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
if (APlayerController* PC = It->Get())
{
APawn* PlayerPawn = PC->GetPawn();
if (!IsValid(PlayerPawn)) continue;
float Dist = FVector::Dist(AIPawn->GetActorLocation(),
PlayerPawn->GetActorLocation());
if (Dist < ClosestDist)
{
ClosestDist = Dist;
BestTarget = PlayerPawn;
}
}
}
// Blackboard 갱신
BB->SetValueAsObject(TargetActorKey.SelectedKeyName, BestTarget);
}

// AIController에서 Blackboard 접근
void AMyAIController::SetTargetActor(AActor* Target)
{
if (UBlackboardComponent* BB = GetBlackboardComponent())
{
BB->SetValueAsObject(TEXT("TargetActor"), Target);
}
}
// Blackboard 키 타입별 접근
void AMyAIController::ReadBlackboardValues()
{
UBlackboardComponent* BB = GetBlackboardComponent();
if (!BB) return;
// Object 타입
AActor* Target = Cast<AActor>(BB->GetValueAsObject(TEXT("TargetActor")));
// Vector 타입
FVector PatrolPoint = BB->GetValueAsVector(TEXT("PatrolLocation"));
// Float 타입
float Threat = BB->GetValueAsFloat(TEXT("ThreatLevel"));
// Bool 타입
bool bIsAlerted = BB->GetValueAsBool(TEXT("IsAlerted"));
// Int 타입
int32 PatrolIndex = BB->GetValueAsInt(TEXT("PatrolIndex"));
// Enum 타입 (uint8)
uint8 AIState = BB->GetValueAsEnum(TEXT("AIState"));
}

MyAIController.h
UCLASS()
class MYGAME_API AMyAIController : public AAIController
{
GENERATED_BODY()
public:
virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override;
int32 AdvancePatrolIndex();
private:
UPROPERTY(EditDefaultsOnly, Category = "AI")
TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
int32 PatrolIndex = 0;
};
void AMyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTreeAsset)
{
// BT 실행 — Blackboard도 자동 초기화
RunBehaviorTree(BehaviorTreeAsset);
}
}
void AMyAIController::OnUnPossess()
{
// BT 중지
BrainComponent->StopLogic(TEXT("Unpossessed"));
Super::OnUnPossess();
}
int32 AMyAIController::AdvancePatrolIndex()
{
PatrolIndex = (PatrolIndex + 1) % PatrolPoints.Num();
return PatrolIndex;
}

노드 타입상속 클래스주요 오버라이드
TaskUBTTaskNodeExecuteTask, AbortTask
DecoratorUBTDecoratorCalculateRawConditionValue
ServiceUBTServiceTickNode
  • EBTNodeResult::Succeeded / Failed / InProgress / Aborted 로 BT에 결과 전달
  • 비동기 Task는 FinishLatentTask(OwnerComp, Result) 호출 필수
  • Service의 RandomDeviation으로 다수 AI의 틱 분산 처리