UE5 C++ Behavior Tree 구현 가이드
개요 — Behavior Tree 아키텍처
Section titled “개요 — Behavior Tree 아키텍처”UE5의 Behavior Tree(BT)는 AI 행동을 트리 구조로 모델링하는 시스템입니다. 세 가지 노드 타입으로 구성됩니다.
| 노드 타입 | 클래스 | 역할 |
|---|---|---|
| Task | UBTTaskNode | 실제 동작 수행 (이동, 공격, 대기) |
| Decorator | UBTDecorator | 조건 판단 — 실행 허용/차단 |
| Service | UBTService | 주기적 업데이트 (감지, Blackboard 갱신) |
Blackboard는 BT 노드들이 공유하는 데이터 저장소입니다. 노드 간 직접 통신 대신 Blackboard를 통해 데이터를 교환합니다.
1. BTTask 구현 — 행동 노드
Section titled “1. BTTask 구현 — 행동 노드”1.1 순찰 지점으로 이동하는 Task
Section titled “1.1 순찰 지점으로 이동하는 Task”#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;};#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);}1.2 단순 즉시 완료 Task
Section titled “1.2 단순 즉시 완료 Task”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;}2. BTDecorator 구현 — 조건 판단
Section titled “2. BTDecorator 구현 — 조건 판단”#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;}3. BTService 구현 — 주기적 갱신
Section titled “3. BTService 구현 — 주기적 갱신”#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);}4. Blackboard 키 읽기·쓰기 패턴
Section titled “4. Blackboard 키 읽기·쓰기 패턴”// 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"));}5. AI Controller — BT 시작
Section titled “5. AI Controller — BT 시작”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;}| 노드 타입 | 상속 클래스 | 주요 오버라이드 |
|---|---|---|
| Task | UBTTaskNode | ExecuteTask, AbortTask |
| Decorator | UBTDecorator | CalculateRawConditionValue |
| Service | UBTService | TickNode |
EBTNodeResult::Succeeded/Failed/InProgress/Aborted로 BT에 결과 전달- 비동기 Task는
FinishLatentTask(OwnerComp, Result)호출 필수 - Service의
RandomDeviation으로 다수 AI의 틱 분산 처리