비헤이비어 트리 기초
비헤이비어 트리는 언리얼 엔진에서 복잡한 AI 행동을 모델링하는 강력한 도구입니다.
이 절에서는 C++ 관점에서 비헤이비어 트리 시스템을 구현하고 활용하는 방법을 살펴보겠습니다.
비헤이비어 트리의 기본 개념과 구조
비헤이비어 트리는 노드의 계층 구조로 이루어져 있으며 주요 노드 유형은 다음과 같습니다.
- Composite Nodes : Sequence, Selector 등
- Decorator Nodes : Conditional, Repeater 등
- Task Nodes : 실제 행동을 수행하는 노드
C++에서 비헤이비어 트리 태스크 생성
비헤이비어 트리 태스크를 C++로 생성하는 방법
UCLASS()
class MYPROJECT_API UBTTask_FindRandomLocation : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_FindRandomLocation();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
UPROPERTY(EditAnywhere, Category = "Search")
float SearchRadius = 1000.0f;
};
UBTTask_FindRandomLocation::UBTTask_FindRandomLocation()
{
NodeName = "Find Random Location";
}
EBTNodeResult::Type UBTTask_FindRandomLocation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* AIController = OwnerComp.GetAIOwner();
if (AIController == nullptr)
{
return EBTNodeResult::Failed;
}
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
if (NavSys == nullptr)
{
return EBTNodeResult::Failed;
}
FNavLocation RandomLocation;
if (NavSys->GetRandomReachablePointInRadius(AIController->GetPawn()->GetActorLocation(), SearchRadius, RandomLocation))
{
OwnerComp.GetBlackboardComponent()->SetValueAsVector("TargetLocation", RandomLocation.Location);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
비헤이비어 트리와 블랙보드 연동
블랙보드는 비헤이비어 트리의 메모리 역할을 합니다.
UCLASS()
class MYPROJECT_API UBTTask_SetPatrolPoint : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_SetPatrolPoint();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
UPROPERTY(EditAnywhere, Category = "Blackboard")
struct FBlackboardKeySelector PatrolPointKey;
};
EBTNodeResult::Type UBTTask_SetPatrolPoint::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
TArray<AActor*> PatrolPoints;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APatrolPoint::StaticClass(), PatrolPoints);
if (PatrolPoints.Num() > 0)
{
int32 RandomIndex = FMath::RandRange(0, PatrolPoints.Num() - 1);
BlackboardComp->SetValueAsObject(PatrolPointKey.SelectedKeyName, PatrolPoints[RandomIndex]);
return EBTNodeResult::Succeeded;
}
}
return EBTNodeResult::Failed;
}
AI 로직 구현을 위한 비헤이비어 트리
- 모듈화 : 작은 재사용 가능한 서브트리 생성
- 계층적 구조 : 복잡한 행동을 더 작은 단위로 분해
- 상태 기반 전환 : 블랙보드 값을 사용한 동적 행동 전환
예시
UCLASS()
class MYPROJECT_API UBTComposite_StateSelector : public UBTCompositeNode
{
GENERATED_BODY()
public:
UBTComposite_StateSelector();
virtual int32 GetNextChildHandler(struct FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const override;
private:
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector StateKey;
};
int32 UBTComposite_StateSelector::GetNextChildHandler(struct FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
UBlackboardComponent* BlackboardComp = SearchData.OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
int32 CurrentState = BlackboardComp->GetValueAsEnum(StateKey.SelectedKeyName);
return CurrentState < GetChildrenNum() ? CurrentState : INDEX_NONE;
}
return INDEX_NONE;
}
비헤이비어 트리의 디버깅 및 최적화 기법
- 비주얼 디버깅
void AMyAIController::DebugBehaviorTree()
{
if (UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(BrainComponent))
{
UBehaviorTree* BT = BTComp->GetCurrentTree();
if (BT)
{
FString DebugInfo = FString::Printf(TEXT("Current Node: %s"), *BTComp->GetActiveNode()->GetNodeName());
DrawDebugString(GetWorld(), GetPawn()->GetActorLocation(), DebugInfo, nullptr, FColor::White, 0.0f, true);
}
}
}
- 프로파일링
DECLARE_CYCLE_STAT(TEXT("AI Behavior Tree"), STAT_AIBehaviorTree, STATGROUP_AI);
EBTNodeResult::Type UMyBTTaskNode::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
SCOPE_CYCLE_COUNTER(STAT_AIBehaviorTree);
// Task logic here
}
런타임에 비헤이비어 트리 조작
동적으로 비헤이비어 트리를 변경하는 방법
void AMyAIController::SwitchBehaviorTree(UBehaviorTree* NewBT)
{
if (UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(BrainComponent))
{
BTComp->StopTree();
BTComp->StartTree(*NewBT);
}
}
고급 AI 패턴 구현
순찰 패턴
UCLASS()
class MYPROJECT_API UBTTask_Patrol : public UBTTaskNode
{
GENERATED_BODY()
public:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
UPROPERTY(EditAnywhere, Category = "Patrol")
FBlackboardKeySelector PatrolPointKey;
};
EBTNodeResult::Type UBTTask_Patrol::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* AIController = OwnerComp.GetAIOwner();
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (AIController && BlackboardComp)
{
AActor* PatrolPoint = Cast<AActor>(BlackboardComp->GetValueAsObject(PatrolPointKey.SelectedKeyName));
if (PatrolPoint)
{
AIController->MoveToActor(PatrolPoint);
return EBTNodeResult::InProgress;
}
}
return EBTNodeResult::Failed;
}
협동 AI
UCLASS()
class MYPROJECT_API UBTTask_CoordinateAttack : public UBTTaskNode
{
GENERATED_BODY()
public:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
UPROPERTY(EditAnywhere, Category = "Team")
FBlackboardKeySelector TeamMembersKey;
};
EBTNodeResult::Type UBTTask_CoordinateAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
TArray<AActor*> TeamMembers = BlackboardComp->GetValueAsArray(TeamMembersKey.SelectedKeyName);
// Implement coordination logic here
}
return EBTNodeResult::Succeeded;
}
성능 고려사항
- 트리 깊이 제한 : 깊은 트리는 성능에 영향을 줄 수 있음
- 무거운 작업의 비동기 처리 : 장시간 실행되는 태스크는 비동기로 처리
UCLASS()
class MYPROJECT_API UBTTask_AsyncOperation : public UBTTaskNode
{
GENERATED_BODY()
public:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override;
private:
void PerformAsyncOperation(UBehaviorTreeComponent* OwnerComp);
};
EBTNodeResult::Type UBTTask_AsyncOperation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, &OwnerComp]()
{
PerformAsyncOperation(&OwnerComp);
});
return EBTNodeResult::InProgress;
}
void UBTTask_AsyncOperation::PerformAsyncOperation(UBehaviorTreeComponent* OwnerComp)
{
// Perform heavy operation here
// When done, finish the task
FinishLatentTask(*OwnerComp, EBTNodeResult::Succeeded);
}
AI 시스템 설계 시 비헤이비어 트리의 역할
- 재사용 가능한 서브트리 생성
UCLASS()
class MYPROJECT_API UBTComposite_SubtreeLoader : public UBTCompositeNode
{
GENERATED_BODY()
public:
virtual void OnNodeCreated(UBehaviorTree* OwnerTree) override;
private:
UPROPERTY(EditAnywhere, Category = "Subtree")
TSoftObjectPtr<UBehaviorTree> SubtreeAsset;
};
void UBTComposite_SubtreeLoader::OnNodeCreated(UBehaviorTree* OwnerTree)
{
Super::OnNodeCreated(OwnerTree);
if (SubtreeAsset.IsValid())
{
UBehaviorTree* Subtree = SubtreeAsset.Get();
for (auto& SubNode : Subtree->RootNode->Children)
{
Children.Add(SubNode);
}
}
}
- 동적 서브트리 로딩
void AMyAIController::LoadDynamicSubtree(FName SubtreeName)
{
UBehaviorTree* Subtree = LoadObject<UBehaviorTree>(nullptr, *SubtreeName.ToString());
if (Subtree)
{
UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(BrainComponent);
if (BTComp)
{
BTComp->InjectSubtree(Subtree);
}
}
}
비헤이비어 트리는 언리얼 엔진에서 복잡한 AI 로직을 구현하는 데 강력한 도구입니다.
C++를 사용하여 커스텀 태스크와 컴포지트 노드를 생성하고, 블랙보드와 연동하여 동적인 AI 행동을 구현할 수 있습니다.
복잡한 AI 로직을 구현할 때는 모듈화와 계층적 구조를 활용하여 관리하기 쉽고 확장 가능한 비헤이비어 트리를 설계해야 합니다.