icon안동민 개발노트

비헤이비어 트리 기초


 비헤이비어 트리는 언리얼 엔진에서 복잡한 AI 행동을 모델링하는 강력한 도구입니다.

 이 절에서는 C++ 관점에서 비헤이비어 트리 시스템을 구현하고 활용하는 방법을 살펴보겠습니다.

비헤이비어 트리의 기본 개념과 구조

 비헤이비어 트리는 노드의 계층 구조로 이루어져 있으며, 주요 노드 유형은 다음과 같습니다.

  1. Composite Nodes : Sequence, Selector 등
  2. Decorator Nodes : Conditional, Repeater 등
  3. 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 로직 구현을 위한 비헤이비어 트리 설계 전략

  1. 모듈화 : 작은 재사용 가능한 서브트리 생성
  2. 계층적 구조 : 복잡한 행동을 더 작은 단위로 분해
  3. 상태 기반 전환 : 블랙보드 값을 사용한 동적 행동 전환

 예시

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;
}

비헤이비어 트리의 디버깅 및 최적화 기법

  1. 비주얼 디버깅
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);
        }
    }
}
  1. 프로파일링
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;
}

성능 고려사항

  1. 트리 깊이 제한 : 깊은 트리는 성능에 영향을 줄 수 있음
  2. 무거운 작업의 비동기 처리 : 장시간 실행되는 태스크는 비동기로 처리
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 시스템 설계 시 비헤이비어 트리의 역할

  1. 재사용 가능한 서브트리 생성
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);
        }
    }
}
  1. 동적 서브트리 로딩
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 로직을 구현할 때는 모듈화와 계층적 구조를 활용하여 관리하기 쉽고 확장 가능한 비헤이비어 트리를 설계해야 합니다. 또한, 디버깅과 프로파일링 도구를 활용하여 비헤이비어 트리의 성능을 최적화하고 문제를 신속하게 해결할 수 있습니다.

 런타임에 비헤이비어 트리를 동적으로 조작하는 기능을 통해 상황에 따라 AI의 행동을 유연하게 변경할 수 있으며, 이는 적응형 AI 시스템 구현에 매우 유용합니다.