ue-ai-navigation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

UE AI and Navigation

UE AI与导航系统

You are an expert in Unreal Engine's AI and navigation systems.
你是Unreal Engine AI与导航系统领域的专家。

Context

上下文信息

Read
.agents/ue-project-context.md
for project AI plugins, subsystem configs, enabled modules (AIModule, NavigationSystem, GameplayStateTreeModule, SmartObjectsModule), and existing AI frameworks.
请阅读
.agents/ue-project-context.md
,了解项目AI插件、子系统配置、已启用模块(AIModule、NavigationSystem、GameplayStateTreeModule、SmartObjectsModule)以及现有AI框架。

Information Gathering

信息收集

Before implementing, clarify: AI complexity, navigation needs (ground/fly/swim, dynamic obstacles, streaming), perception senses required, Behavior Tree vs. State Tree preference, multiplayer authority model, and agent count for budget planning.

在开始实现前,请明确以下内容:AI复杂度、导航需求(地面/飞行/水下、动态障碍物、流加载)、所需的感知类型、Behavior Tree与State Tree的偏好选择、多人游戏权限模型,以及用于预算规划的Agent数量。

AI Architecture

AI架构

APawn
  └── AAIController (server-only in multiplayer)
        ├── UBehaviorTreeComponent  (UBrainComponent subclass)
        │     └── UBehaviorTree asset → UBlackboardData
        ├── UBlackboardComponent    (AI knowledge store)
        ├── UAIPerceptionComponent  (sight, hearing, damage)
        └── UPathFollowingComponent (NavMesh path execution)
Build.cs modules:
AIModule
,
NavigationSystem
,
GameplayTasks

APawn
  └── AAIController (server-only in multiplayer)
        ├── UBehaviorTreeComponent  (UBrainComponent subclass)
        │     └── UBehaviorTree asset → UBlackboardData
        ├── UBlackboardComponent    (AI knowledge store)
        ├── UAIPerceptionComponent  (sight, hearing, damage)
        └── UPathFollowingComponent (NavMesh path execution)
Build.cs模块
AIModule
,
NavigationSystem
,
GameplayTasks

AIController

AIController

cpp
// MyAIController.h
UCLASS()
class AMyAIController : public AAIController
{
    GENERATED_BODY()
public:
    AMyAIController();
    UPROPERTY(EditDefaultsOnly, Category = AI)
    TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
protected:
    virtual void OnPossess(APawn* InPawn) override;
    UFUNCTION()
    void OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};

// MyAIController.cpp
AMyAIController::AMyAIController()
{
    bStartAILogicOnPossess = true;
    bStopAILogicOnUnposses = true;
    // PerceptionComponent declared in AAIController; configure senses here or in BP defaults
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    if (BehaviorTreeAsset)
        RunBehaviorTree(BehaviorTreeAsset); // calls UseBlackboard internally
    if (UAIPerceptionComponent* PC = GetAIPerceptionComponent())
        PC->OnTargetPerceptionUpdated.AddDynamic(this, &AMyAIController::OnTargetPerceptionUpdated);
}
cpp
// MyAIController.h
UCLASS()
class AMyAIController : public AAIController
{
    GENERATED_BODY()
public:
    AMyAIController();
    UPROPERTY(EditDefaultsOnly, Category = AI)
    TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
protected:
    virtual void OnPossess(APawn* InPawn) override;
    UFUNCTION()
    void OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};

// MyAIController.cpp
AMyAIController::AMyAIController()
{
    bStartAILogicOnPossess = true;
    bStopAILogicOnUnposses = true;
    // PerceptionComponent declared in AAIController; configure senses here or in BP defaults
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    if (BehaviorTreeAsset)
        RunBehaviorTree(BehaviorTreeAsset); // calls UseBlackboard internally
    if (UAIPerceptionComponent* PC = GetAIPerceptionComponent())
        PC->OnTargetPerceptionUpdated.AddDynamic(this, &AMyAIController::OnTargetPerceptionUpdated);
}

Key AAIController API

AAIController核心API

cpp
// Navigation
EPathFollowingRequestResult::Type MoveToActor(AActor* Goal, float AcceptanceRadius = -1,
    bool bStopOnOverlap = true, bool bUsePathfinding = true, bool bCanStrafe = true,
    TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);

EPathFollowingRequestResult::Type MoveToLocation(const FVector& Dest, float AcceptanceRadius = -1,
    bool bStopOnOverlap = true, bool bUsePathfinding = true,
    bool bProjectDestinationToNavigation = false, bool bCanStrafe = true,
    TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);

void StopMovement();
bool HasPartialPath() const;
EPathFollowingStatus::Type GetMoveStatus() const;

// Focus
void SetFocus(AActor* NewFocus, EAIFocusPriority::Type Priority = EAIFocusPriority::Gameplay);
void SetFocalPoint(FVector NewFocus, EAIFocusPriority::Type Priority = EAIFocusPriority::Gameplay);
void ClearFocus(EAIFocusPriority::Type Priority);

// Brain / Blackboard
bool RunBehaviorTree(UBehaviorTree* BTAsset);
bool UseBlackboard(UBlackboardData* BlackboardAsset, UBlackboardComponent*& BlackboardComponent);
UBlackboardComponent* GetBlackboardComponent();

// Team (IGenericTeamAgentInterface)
void SetGenericTeamId(const FGenericTeamId& NewTeamID);

// Delegate: FAIMoveCompletedSignature ReceiveMoveCompleted (RequestID, Result)
On Pawn:
AIControllerClass = AMyAIController::StaticClass(); AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

cpp
// Navigation
EPathFollowingRequestResult::Type MoveToActor(AActor* Goal, float AcceptanceRadius = -1,
    bool bStopOnOverlap = true, bool bUsePathfinding = true, bool bCanStrafe = true,
    TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);

EPathFollowingRequestResult::Type MoveToLocation(const FVector& Dest, float AcceptanceRadius = -1,
    bool bStopOnOverlap = true, bool bUsePathfinding = true,
    bool bProjectDestinationToNavigation = false, bool bCanStrafe = true,
    TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);

void StopMovement();
bool HasPartialPath() const;
EPathFollowingStatus::Type GetMoveStatus() const;

// Focus
void SetFocus(AActor* NewFocus, EAIFocusPriority::Type Priority = EAIFocusPriority::Gameplay);
void SetFocalPoint(FVector NewFocus, EAIFocusPriority::Type Priority = EAIFocusPriority::Gameplay);
void ClearFocus(EAIFocusPriority::Type Priority);

// Brain / Blackboard
bool RunBehaviorTree(UBehaviorTree* BTAsset);
bool UseBlackboard(UBlackboardData* BlackboardAsset, UBlackboardComponent*& BlackboardComponent);
UBlackboardComponent* GetBlackboardComponent();

// Team (IGenericTeamAgentInterface)
void SetGenericTeamId(const FGenericTeamId& NewTeamID);

// Delegate: FAIMoveCompletedSignature ReceiveMoveCompleted (RequestID, Result)
在Pawn中设置
AIControllerClass = AMyAIController::StaticClass(); AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

Blackboard

Blackboard(黑板)

TypeGetSet
Object
GetValueAsObject
SetValueAsObject
Vector
GetValueAsVector
SetValueAsVector
Bool
GetValueAsBool
SetValueAsBool
Float
GetValueAsFloat
SetValueAsFloat
Int
GetValueAsInt
SetValueAsInt
Enum
GetValueAsEnum
SetValueAsEnum
Name
GetValueAsName
SetValueAsName
Rotator
GetValueAsRotator
SetValueAsRotator
String
GetValueAsString
SetValueAsString
Class
GetValueAsClass
SetValueAsClass
cpp
UBlackboardComponent* BB = GetBlackboardComponent();
BB->SetValueAsObject(TEXT("TargetActor"), SomeActor);
BB->SetValueAsVector(TEXT("LastKnownLocation"), Location);
BB->ClearValue(TEXT("TargetActor"));
bool bSet = BB->IsVectorValueSet(TEXT("PatrolLocation"));

// Observer (called when key changes)
FBlackboard::FKey KeyID = BB->GetKeyID(TEXT("TargetActor"));
FDelegateHandle H = BB->RegisterObserver(KeyID, this,
    FOnBlackboardChangeNotification::CreateUObject(this, &AMyAIController::OnBBKeyChanged));
BB->UnregisterObserver(KeyID, H);

// High-perf cached accessor (avoids repeated name lookups):
FBBKeyCachedAccessor<UBlackboardKeyType_Bool> BBInCombat;
// Init: BBInCombat = FBBKeyCachedAccessor<...>(*BBComp, KeyID);
// Use: bool b = BBInCombat.Get(); BBInCombat.SetValue(*BB, true);
Mark keys Instance Synced to share values across all AI using the same
UBlackboardData
(squad-wide alerts via
UAISystem
propagation).

类型获取方法设置方法
Object
GetValueAsObject
SetValueAsObject
Vector
GetValueAsVector
SetValueAsVector
Bool
GetValueAsBool
SetValueAsBool
Float
GetValueAsFloat
SetValueAsFloat
Int
GetValueAsInt
SetValueAsInt
Enum
GetValueAsEnum
SetValueAsEnum
Name
GetValueAsName
SetValueAsName
Rotator
GetValueAsRotator
SetValueAsRotator
String
GetValueAsString
SetValueAsString
Class
GetValueAsClass
SetValueAsClass
cpp
UBlackboardComponent* BB = GetBlackboardComponent();
BB->SetValueAsObject(TEXT("TargetActor"), SomeActor);
BB->SetValueAsVector(TEXT("LastKnownLocation"), Location);
BB->ClearValue(TEXT("TargetActor"));
bool bSet = BB->IsVectorValueSet(TEXT("PatrolLocation"));

// Observer (called when key changes)
FBlackboard::FKey KeyID = BB->GetKeyID(TEXT("TargetActor"));
FDelegateHandle H = BB->RegisterObserver(KeyID, this,
    FOnBlackboardChangeNotification::CreateUObject(this, &AMyAIController::OnBBKeyChanged));
BB->UnregisterObserver(KeyID, H);

// 高性能缓存访问器(避免重复名称查找):
FBBKeyCachedAccessor<UBlackboardKeyType_Bool> BBInCombat;
// 初始化: BBInCombat = FBBKeyCachedAccessor<...>(*BBComp, KeyID);
// 使用: bool b = BBInCombat.Get(); BBInCombat.SetValue(*BB, true);
将标记为Instance Synced的键,可在所有使用同一
UBlackboardData
的AI之间共享值(通过
UAISystem
传播实现小队范围内的警报)。

Behavior Tree Nodes

Behavior Tree(行为树)节点

Custom Task

自定义任务节点

cpp
UCLASS()
class UMyBTTask_Attack : public UBTTaskNode
{
    GENERATED_BODY()
public:
    UMyBTTask_Attack() { NodeName = TEXT("Attack"); INIT_TASK_NODE_NOTIFY_FLAGS(); }
    UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey;
protected:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

EBTNodeResult::Type UMyBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AActor* Target = Cast<AActor>(
        OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetKey.SelectedKeyName));
    if (!IsValid(Target)) return EBTNodeResult::Failed;
    // Start async work → return InProgress; call FinishLatentTask() when done
    return EBTNodeResult::InProgress;
}

void UMyBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); // or Failed
}

EBTNodeResult::Type UMyBTTask_Attack::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    return EBTNodeResult::Aborted; // cleanup; or InProgress + FinishLatentAbort()
}
cpp
UCLASS()
class UMyBTTask_Attack : public UBTTaskNode
{
    GENERATED_BODY()
public:
    UMyBTTask_Attack() { NodeName = TEXT("Attack"); INIT_TASK_NODE_NOTIFY_FLAGS(); }
    UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey;
protected:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

EBTNodeResult::Type UMyBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AActor* Target = Cast<AActor>(
        OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetKey.SelectedKeyName));
    if (!IsValid(Target)) return EBTNodeResult::Failed;
    // Start async work → return InProgress; call FinishLatentTask() when done
    return EBTNodeResult::InProgress;
}

void UMyBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); // or Failed
}

EBTNodeResult::Type UMyBTTask_Attack::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    return EBTNodeResult::Aborted; // cleanup; or InProgress + FinishLatentAbort()
}

Custom Decorator

自定义装饰器节点

cpp
UCLASS()
class UMyBTDecorator_CanSee : public UBTDecorator
{
    GENERATED_BODY()
public:
    UMyBTDecorator_CanSee()
    {
        INIT_DECORATOR_NODE_NOTIFY_FLAGS();
        bAllowAbortLowerPri = true; bAllowAbortChildNodes = true;
        FlowAbortMode = EBTFlowAbortMode::Both;
    }
    UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey;
protected:
    virtual bool CalculateRawConditionValue(
        UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override
    {
        AActor* Target = Cast<AActor>(
            OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetKey.SelectedKeyName));
        return IsValid(Target) && OwnerComp.GetAIOwner()->LineOfSightTo(Target);
    }
};
cpp
UCLASS()
class UMyBTDecorator_CanSee : public UBTDecorator
{
    GENERATED_BODY()
public:
    UMyBTDecorator_CanSee()
    {
        INIT_DECORATOR_NODE_NOTIFY_FLAGS();
        bAllowAbortLowerPri = true; bAllowAbortChildNodes = true;
        FlowAbortMode = EBTFlowAbortMode::Both;
    }
    UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey;
protected:
    virtual bool CalculateRawConditionValue(
        UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override
    {
        AActor* Target = Cast<AActor>(
            OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetKey.SelectedKeyName));
        return IsValid(Target) && OwnerComp.GetAIOwner()->LineOfSightTo(Target);
    }
};

Custom Service

自定义服务节点

cpp
UCLASS()
class UMyBTService_UpdateTarget : public UBTService
{
    GENERATED_BODY()
public:
    UMyBTService_UpdateTarget() { Interval = 0.5f; RandomDeviation = 0.1f; INIT_SERVICE_NODE_NOTIFY_FLAGS(); }
protected:
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override
    {
        TArray<AActor*> Hostiles;
        OwnerComp.GetAIOwner()->GetAIPerceptionComponent()->GetPerceivedHostileActors(Hostiles);
        AActor* Best = nullptr; float BestDist = FLT_MAX;
        FVector MyLoc = OwnerComp.GetAIOwner()->GetPawn()->GetActorLocation();
        for (AActor* A : Hostiles)
        {
            float D = FVector::Dist(MyLoc, A->GetActorLocation());
            if (D < BestDist) { BestDist = D; Best = A; }
        }
        OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("TargetActor"), Best);
    }
};
cpp
UCLASS()
class UMyBTService_UpdateTarget : public UBTService
{
    GENERATED_BODY()
public:
    UMyBTService_UpdateTarget() { Interval = 0.5f; RandomDeviation = 0.1f; INIT_SERVICE_NODE_NOTIFY_FLAGS(); }
protected:
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override
    {
        TArray<AActor*> Hostiles;
        OwnerComp.GetAIOwner()->GetAIPerceptionComponent()->GetPerceivedHostileActors(Hostiles);
        AActor* Best = nullptr; float BestDist = FLT_MAX;
        FVector MyLoc = OwnerComp.GetAIOwner()->GetPawn()->GetActorLocation();
        for (AActor* A : Hostiles)
        {
            float D = FVector::Dist(MyLoc, A->GetActorLocation());
            if (D < BestDist) { BestDist = D; Best = A; }
        }
        OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("TargetActor"), Best);
    }
};

Built-in Nodes (reference)

内置节点(参考)

Tasks:
BTTask_MoveTo
,
BTTask_MoveDirectlyToward
,
BTTask_Wait
,
BTTask_WaitBlackboardTime
,
BTTask_RunEQSQuery
,
BTTask_PlayAnimation
,
BTTask_MakeNoise
,
BTTask_RotateToFaceBBEntry
,
BTTask_RunBehavior
,
BTTask_RunBehaviorDynamic
,
BTTask_FinishWithResult
Decorators:
BTDecorator_Blackboard
,
BTDecorator_CompareBBEntries
,
BTDecorator_Cooldown
,
BTDecorator_TagCooldown
,
BTDecorator_Loop
,
BTDecorator_TimeLimit
,
BTDecorator_DoesPathExist
,
BTDecorator_IsAtLocation
,
BTDecorator_CheckGameplayTagsOnActor
,
BTDecorator_ForceSuccess
Composites:
Selector
(first success),
Sequence
(first failure),
SimpleParallel
(main task + background subtree; main task drives completion). UE does not ship a general-purpose Parallel node —
SimpleParallel
is the built-in option. For true parallel execution of N branches, implement a custom
UBTCompositeNode
or chain multiple
SimpleParallel
nodes.

任务节点
BTTask_MoveTo
,
BTTask_MoveDirectlyToward
,
BTTask_Wait
,
BTTask_WaitBlackboardTime
,
BTTask_RunEQSQuery
,
BTTask_PlayAnimation
,
BTTask_MakeNoise
,
BTTask_RotateToFaceBBEntry
,
BTTask_RunBehavior
,
BTTask_RunBehaviorDynamic
,
BTTask_FinishWithResult
装饰器节点
BTDecorator_Blackboard
,
BTDecorator_CompareBBEntries
,
BTDecorator_Cooldown
,
BTDecorator_TagCooldown
,
BTDecorator_Loop
,
BTDecorator_TimeLimit
,
BTDecorator_DoesPathExist
,
BTDecorator_IsAtLocation
,
BTDecorator_CheckGameplayTagsOnActor
,
BTDecorator_ForceSuccess
组合节点
Selector
(首个成功节点),
Sequence
(首个失败节点),
SimpleParallel
(主任务+后台子树;主任务决定完成状态)。UE未提供通用Parallel节点——
SimpleParallel
是内置的可选方案。若要实现N个分支的真正并行执行,需自定义
UBTCompositeNode
或链式组合多个
SimpleParallel
节点。

AI Perception

AI感知

cpp
// In AIController constructor:
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AISenseConfig_Hearing.h"
#include "Perception/AISenseConfig_Damage.h"

UAIPerceptionComponent* AIP = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetPerceptionComponent(*AIP);

UAISenseConfig_Sight* Sight = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight"));
Sight->SightRadius = 2000.f;
Sight->LoseSightRadius = 2500.f;
Sight->PeripheralVisionAngleDegrees = 60.f;
Sight->AutoSuccessRangeFromLastSeenLocation = 400.f;
Sight->DetectionByAffiliation.bDetectEnemies = true;
AIP->ConfigureSense(*Sight);
AIP->SetDominantSense(Sight->GetSenseImplementation());

UAISenseConfig_Hearing* Hearing = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("Hearing"));
Hearing->HearingRange = 3000.f;
Hearing->DetectionByAffiliation.bDetectEnemies = true;
Hearing->DetectionByAffiliation.bDetectNeutrals = true;
AIP->ConfigureSense(*Hearing);
cpp
// Perception handler:
void AMyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
    UBlackboardComponent* BB = GetBlackboardComponent();
    if (Stimulus.WasSuccessfullySensed())
    {
        BB->SetValueAsObject(TEXT("TargetActor"), Actor);
        BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
    }
    else
    {
        // Lost target — keep last known location for investigation
        BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
    }
}

// Report noise manually (e.g., gunshot):
UAISense_Hearing::ReportNoiseEvent(GetWorld(), Location, Loudness, Instigator, MaxRange, Tag);

// Report damage:
UAISense_Damage::ReportDamageEvent(GetWorld(), DamagedActor, Instigator, Amount, EventLoc, HitLoc);
cpp
// On perceived actors — add UAIPerceptionStimuliSourceComponent:
#include "Perception/AIPerceptionStimuliSourceComponent.h"
UAIPerceptionStimuliSourceComponent* Source =
    CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("StimuliSource"));
Source->bAutoRegister = true;
Source->RegisterForSense(TSubclassOf<UAISense>(UAISense_Sight::StaticClass()));
Source->RegisterForSense(TSubclassOf<UAISense>(UAISense_Hearing::StaticClass()));
cpp
// Query perception state:
UAIPerceptionComponent* PC = GetAIPerceptionComponent();
TArray<AActor*> Visible; PC->GetCurrentlyPerceivedActors(UAISense_Sight::StaticClass(), Visible);
TArray<AActor*> Hostiles; PC->GetPerceivedHostileActors(Hostiles);
bool bCanSee = PC->HasActiveStimulus(*Target, UAISense::GetSenseID<UAISense_Sight>());
PC->ForgetActor(Target);
PC->ForgetAll();
// Per-actor delegate (shown above):
// OnTargetPerceptionUpdated — fires once per actor whose perception state changed
// Batch delegate — fires once per frame with all updated actors:
// OnPerceptionUpdated — signature: void(const TArray<AActor*>& UpdatedActors)
// Also: OnTargetPerceptionForgotten, OnTargetPerceptionInfoUpdated
Additional sense configs:
UAISenseConfig_Touch
(fires on physical contact with perceived actor),
UAISense_Team
(propagates enemy awareness across teammates via
IGenericTeamAgentInterface
),
UAISense_Prediction
(predicts future location — call
UAISense_Prediction::RequestPawnPredictionEvent
).

cpp
// In AIController constructor:
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AISenseConfig_Hearing.h"
#include "Perception/AISenseConfig_Damage.h"

UAIPerceptionComponent* AIP = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetPerceptionComponent(*AIP);

UAISenseConfig_Sight* Sight = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight"));
Sight->SightRadius = 2000.f;
Sight->LoseSightRadius = 2500.f;
Sight->PeripheralVisionAngleDegrees = 60.f;
Sight->AutoSuccessRangeFromLastSeenLocation = 400.f;
Sight->DetectionByAffiliation.bDetectEnemies = true;
AIP->ConfigureSense(*Sight);
AIP->SetDominantSense(Sight->GetSenseImplementation());

UAISenseConfig_Hearing* Hearing = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("Hearing"));
Hearing->HearingRange = 3000.f;
Hearing->DetectionByAffiliation.bDetectEnemies = true;
Hearing->DetectionByAffiliation.bDetectNeutrals = true;
AIP->ConfigureSense(*Hearing);
cpp
// Perception handler:
void AMyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
    UBlackboardComponent* BB = GetBlackboardComponent();
    if (Stimulus.WasSuccessfullySensed())
    {
        BB->SetValueAsObject(TEXT("TargetActor"), Actor);
        BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
    }
    else
    {
        // Lost target — keep last known location for investigation
        BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
    }
}

// Report noise manually (e.g., gunshot):
UAISense_Hearing::ReportNoiseEvent(GetWorld(), Location, Loudness, Instigator, MaxRange, Tag);

// Report damage:
UAISense_Damage::ReportDamageEvent(GetWorld(), DamagedActor, Instigator, Amount, EventLoc, HitLoc);
cpp
// On perceived actors — add UAIPerceptionStimuliSourceComponent:
#include "Perception/AIPerceptionStimuliSourceComponent.h"
UAIPerceptionStimuliSourceComponent* Source =
    CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("StimuliSource"));
Source->bAutoRegister = true;
Source->RegisterForSense(TSubclassOf<UAISense>(UAISense_Sight::StaticClass()));
Source->RegisterForSense(TSubclassOf<UAISense>(UAISense_Hearing::StaticClass()));
cpp
// Query perception state:
UAIPerceptionComponent* PC = GetAIPerceptionComponent();
TArray<AActor*> Visible; PC->GetCurrentlyPerceivedActors(UAISense_Sight::StaticClass(), Visible);
TArray<AActor*> Hostiles; PC->GetPerceivedHostileActors(Hostiles);
bool bCanSee = PC->HasActiveStimulus(*Target, UAISense::GetSenseID<UAISense_Sight>());
PC->ForgetActor(Target);
PC->ForgetAll();
// Per-actor delegate (shown above):
// OnTargetPerceptionUpdated — fires once per actor whose perception state changed
// Batch delegate — fires once per frame with all updated actors:
// OnPerceptionUpdated — signature: void(const TArray<AActor*>& UpdatedActors)
// Also: OnTargetPerceptionForgotten, OnTargetPerceptionInfoUpdated
其他感知配置:
UAISenseConfig_Touch
(与感知对象发生物理接触时触发)、
UAISense_Team
(通过
IGenericTeamAgentInterface
在队友间传播敌方感知信息)、
UAISense_Prediction
(预测未来位置——调用
UAISense_Prediction::RequestPawnPredictionEvent
)。

Navigation System

导航系统

cpp
#include "NavigationSystem.h"
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());

// Random reachable point
FNavLocation ResultLoc;
bool bOk = NavSys->GetRandomReachablePointInRadius(Origin, Radius, ResultLoc);

// Project onto NavMesh
FNavLocation Projected;
NavSys->ProjectPointToNavigation(WorldLoc, Projected, FVector(500, 500, 500));

// Sync path query
FPathFindingQuery Query; Query.StartLocation = Start; Query.EndLocation = End;
FPathFindingResult Result = NavSys->FindPathSync(Query);
if (Result.IsSuccessful()) { TArray<FNavPathPoint>& Pts = Result.Path->GetPathPoints(); }

// Async path query
FNavAgentProperties NavAgent = GetPawn()->GetNavAgentPropertiesRef();
NavSys->FindPathAsync(NavAgent, Query,
    FNavPathQueryDelegate::CreateUObject(this, &AMyAI::OnPathFound));

// Dynamic obstacle on actor:
#include "NavModifierComponent.h"
UNavModifierComponent* Mod = CreateDefaultSubobject<UNavModifierComponent>(TEXT("NavMod"));
Mod->AreaClass = UNavArea_Obstacle::StaticClass();

// Custom nav filter (prefer certain areas):
UCLASS() class UMyNavFilter : public UNavigationQueryFilter { ... };
MoveToActor(Target, -1.f, true, true, true, UMyNavFilter::StaticClass());

// Runtime NavMesh rebuild (after procedural generation):
NavSys->Build();

// Access RecastNavMesh for agent config (mirrors Project Settings > Navigation)
ARecastNavMesh* RNM = Cast<ARecastNavMesh>(NavSys->GetDefaultNavDataInstance());
// Key properties: AgentRadius, AgentHeight, AgentMaxStepHeight, AgentMaxSlope
// ANavMeshBoundsVolume must exist in level — without it, no tiles generate.
// For streaming/open-world: use UNavigationInvokerComponent on AI pawns instead.
Off-mesh links: Use
ANavLinkProxy
in-level, or implement
INavLinkCustomInterface
for traversal callbacks.

cpp
#include "NavigationSystem.h"
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());

// Random reachable point
FNavLocation ResultLoc;
bool bOk = NavSys->GetRandomReachablePointInRadius(Origin, Radius, ResultLoc);

// Project onto NavMesh
FNavLocation Projected;
NavSys->ProjectPointToNavigation(WorldLoc, Projected, FVector(500, 500, 500));

// Sync path query
FPathFindingQuery Query; Query.StartLocation = Start; Query.EndLocation = End;
FPathFindingResult Result = NavSys->FindPathSync(Query);
if (Result.IsSuccessful()) { TArray<FNavPathPoint>& Pts = Result.Path->GetPathPoints(); }

// Async path query
FNavAgentProperties NavAgent = GetPawn()->GetNavAgentPropertiesRef();
NavSys->FindPathAsync(NavAgent, Query,
    FNavPathQueryDelegate::CreateUObject(this, &AMyAI::OnPathFound));

// Dynamic obstacle on actor:
#include "NavModifierComponent.h"
UNavModifierComponent* Mod = CreateDefaultSubobject<UNavModifierComponent>(TEXT("NavMod"));
Mod->AreaClass = UNavArea_Obstacle::StaticClass();

// Custom nav filter (prefer certain areas):
UCLASS() class UMyNavFilter : public UNavigationQueryFilter { ... };
MoveToActor(Target, -1.f, true, true, true, UMyNavFilter::StaticClass());

// Runtime NavMesh rebuild (after procedural generation):
NavSys->Build();

// Access RecastNavMesh for agent config (mirrors Project Settings > Navigation)
ARecastNavMesh* RNM = Cast<ARecastNavMesh>(NavSys->GetDefaultNavDataInstance());
// Key properties: AgentRadius, AgentHeight, AgentMaxStepHeight, AgentMaxSlope
// ANavMeshBoundsVolume must exist in level — without it, no tiles generate.
// For streaming/open-world: use UNavigationInvokerComponent on AI pawns instead.
Off-mesh links:可在关卡中使用
ANavLinkProxy
,或实现
INavLinkCustomInterface
以处理遍历回调。

EQS (Environment Query System)

EQS(环境查询系统)

cpp
#include "EnvironmentQuery/EnvQueryManager.h"

UPROPERTY(EditDefaultsOnly) TObjectPtr<UEnvQuery> FindCoverQuery;

void AMyAIController::RunCoverQuery()
{
    FEnvQueryRequest Request(FindCoverQuery, this);
    Request.Execute(EEnvQueryRunMode::SingleResult,
        FQueryFinishedSignature::CreateUObject(this, &AMyAIController::OnCoverDone));
}

void AMyAIController::OnCoverDone(TSharedPtr<FEnvQueryResult> Result)
{
    if (Result.IsValid() && Result->IsSuccessful())
        GetBlackboardComponent()->SetValueAsVector(TEXT("CoverLocation"),
            Result->GetItemAsLocation(0));
}
Run modes:
SingleResult
(cheapest),
RandomBest5Pct
,
RandomBest25Pct
,
AllMatching
BTTask_RunEQSQuery: set
EQSRequest.QueryTemplate
,
BlackboardKey
,
RunMode
. Async lifecycle managed via
FBTEnvQueryTaskMemory.RequestID
.
Custom context (resolve BB actor for use in tests):
cpp
UCLASS()
class UEnvQueryContext_Enemy : public UEnvQueryContext
{
    GENERATED_BODY()
    virtual void ProvideContext(FEnvQueryInstance& QI, FEnvQueryContextData& CD) const override
    {
        AAIController* C = Cast<AAIController>(Cast<APawn>(QI.Owner.Get())->GetController());
        AActor* Enemy = Cast<AActor>(C->GetBlackboardComponent()->GetValueAsObject(TEXT("TargetActor")));
        if (IsValid(Enemy)) UEnvQueryItemType_Actor::SetContextHelper(CD, Enemy);
    }
};
See
references/eqs-reference.md
for generator and test configurations.

cpp
#include "EnvironmentQuery/EnvQueryManager.h"

UPROPERTY(EditDefaultsOnly) TObjectPtr<UEnvQuery> FindCoverQuery;

void AMyAIController::RunCoverQuery()
{
    FEnvQueryRequest Request(FindCoverQuery, this);
    Request.Execute(EEnvQueryRunMode::SingleResult,
        FQueryFinishedSignature::CreateUObject(this, &AMyAIController::OnCoverDone));
}

void AMyAIController::OnCoverDone(TSharedPtr<FEnvQueryResult> Result)
{
    if (Result.IsValid() && Result->IsSuccessful())
        GetBlackboardComponent()->SetValueAsVector(TEXT("CoverLocation"),
            Result->GetItemAsLocation(0));
}
运行模式
SingleResult
(性能开销最低)、
RandomBest5Pct
RandomBest25Pct
AllMatching
BTTask_RunEQSQuery:设置
EQSRequest.QueryTemplate
BlackboardKey
RunMode
。异步生命周期由
FBTEnvQueryTaskMemory.RequestID
管理。
自定义上下文(解析黑板中的Actor以供测试使用):
cpp
UCLASS()
class UEnvQueryContext_Enemy : public UEnvQueryContext
{
    GENERATED_BODY()
    virtual void ProvideContext(FEnvQueryInstance& QI, FEnvQueryContextData& CD) const override
    {
        AAIController* C = Cast<AAIController>(Cast<APawn>(QI.Owner.Get())->GetController());
        AActor* Enemy = Cast<AActor>(C->GetBlackboardComponent()->GetValueAsObject(TEXT("TargetActor")));
        if (IsValid(Enemy)) UEnvQueryItemType_Actor::SetContextHelper(CD, Enemy);
    }
};
生成器及测试配置请参考
references/eqs-reference.md

State Trees (UE 5.1+)

State Trees(状态树,UE 5.1+)

Use State Trees for simpler state machines, designer-friendly workflows, and Smart Object integration. Use Behavior Trees for complex reactive combat with priority-based interrupts.
cpp
// Build.cs: "GameplayStateTreeModule"
#include "Components/StateTreeComponent.h"

UCLASS()
class AMyNPC : public ACharacter
{
    GENERATED_BODY()
    UPROPERTY(VisibleAnywhere) TObjectPtr<UStateTreeComponent> StateTreeComp;
};
AMyNPC::AMyNPC() { StateTreeComp = CreateDefaultSubobject<UStateTreeComponent>(TEXT("StateTree")); }
void AMyNPC::BeginPlay() { Super::BeginPlay(); StateTreeComp->StartLogic(); }
State Tree tasks use
FStateTreeTaskBase
+
FInstanceDataType
. Override
EnterState
,
Tick
,
ExitState
.
State Tree evaluators (
FStateTreeEvaluatorBase
) run persistently across all active states — use them for shared context data (nearest enemy, threat level) that multiple states read, analogous to BT services.

State Trees适用于实现更简洁的状态机、设计师友好的工作流以及与Smart Objects的集成。Behavior Tree则更适合实现带有优先级中断的复杂反应式战斗逻辑。
cpp
// Build.cs: "GameplayStateTreeModule"
#include "Components/StateTreeComponent.h"

UCLASS()
class AMyNPC : public ACharacter
{
    GENERATED_BODY()
    UPROPERTY(VisibleAnywhere) TObjectPtr<UStateTreeComponent> StateTreeComp;
};
AMyNPC::AMyNPC() { StateTreeComp = CreateDefaultSubobject<UStateTreeComponent>(TEXT("StateTree")); }
void AMyNPC::BeginPlay() { Super::BeginPlay(); StateTreeComp->StartLogic(); }
State Tree任务使用
FStateTreeTaskBase
+
FInstanceDataType
。需重写
EnterState
Tick
ExitState
方法。
State Tree评估器
FStateTreeEvaluatorBase
)会在所有活动状态中持续运行——可用于存储多个状态都需要读取的共享上下文数据(如最近的敌人、威胁等级),功能类似于BT的服务节点。

Smart Objects

Smart Objects(智能对象)

Add
USmartObjectComponent
to world actors (set
SmartObjectDefinition
). AI interacts via
USmartObjectSubsystem
:
cpp
// Build.cs: "SmartObjectsModule"
#include "SmartObjectSubsystem.h"
USmartObjectSubsystem* SOS = USmartObjectSubsystem::GetCurrent(GetWorld());
FSmartObjectRequestFilter Filter;
FSmartObjectRequest Req(FBox(Origin, Origin).ExpandBy(500.f), Filter);
FSmartObjectRequestResult Res = SOS->FindSmartObject(Req);
if (Res.IsValid())
{
    FSmartObjectClaimHandle Handle = SOS->MarkSlotAsClaimed(Res.SlotHandle, ESmartObjectClaimPriority::Normal);
    // ... use slot, then:
    SOS->MarkSlotAsFree(Handle);
}

为世界中的Actor添加
USmartObjectComponent
(设置
SmartObjectDefinition
)。AI可通过
USmartObjectSubsystem
与之交互:
cpp
// Build.cs: "SmartObjectsModule"
#include "SmartObjectSubsystem.h"
USmartObjectSubsystem* SOS = USmartObjectSubsystem::GetCurrent(GetWorld());
FSmartObjectRequestFilter Filter;
FSmartObjectRequest Req(FBox(Origin, Origin).ExpandBy(500.f), Filter);
FSmartObjectRequestResult Res = SOS->FindSmartObject(Req);
if (Res.IsValid())
{
    FSmartObjectClaimHandle Handle = SOS->MarkSlotAsClaimed(Res.SlotHandle, ESmartObjectClaimPriority::Normal);
    // ... use slot, then:
    SOS->MarkSlotAsFree(Handle);
}

Common Mistakes

常见错误

Polling instead of event-driven: Do not check per-frame in
TickTask
if a
BTDecorator_Blackboard
observer abort or
WaitForMessage
achieves the same result.
Overcomplicated BTs: Flatten needless nesting. Use services for periodic knowledge updates at
Interval >= 0.5s
. Gate expensive EQS with
BTDecorator_Cooldown
.
NavMesh gaps: Always place
ANavMeshBoundsVolume
. For streaming/procedural levels, call
NavSys->Build()
after generation or use
UNavigationInvokerComponent
on AI pawns.
Server vs client:
AAIController
only exists on the server. Replicate AI state via Pawn replicated properties, not through the controller. BT Blackboard is not replicated.
Large worlds: Use hierarchical NavMesh (RecastNavMesh actor settings). Set EQS max parallel queries per frame in Project Settings > AI > EQS.

轮询而非事件驱动:如果通过
BTDecorator_Blackboard
观察者中断或
WaitForMessage
可实现相同效果,请勿在
TickTask
中逐帧检查。
过度复杂的BT结构:简化不必要的嵌套结构。使用服务节点进行周期性知识更新时,设置
Interval >= 0.5s
。通过
BTDecorator_Cooldown
来限制高开销EQS查询的执行频率。
NavMesh间隙:务必在关卡中放置
ANavMeshBoundsVolume
。若无此对象,将无法生成NavMesh瓦片。对于流加载/开放世界场景,请在AI Pawn上使用
UNavigationInvokerComponent
替代。
服务器与客户端差异
AAIController
仅存在于服务器端。请通过Pawn的复制属性来同步AI状态,而非通过Controller。BT的Blackboard数据不会被复制。
大型世界:使用分层NavMesh(RecastNavMesh Actor设置)。在项目设置>AI>EQS中设置每帧EQS最大并行查询数。

Edge Cases

边缘情况

  • Flying/swimming: Set
    FNavAgentProperties::bCanFly
    /
    bCanSwim
    ; assign a
    UNavArea
    subclass with matching
    SupportedAgents
    flags. Custom
    UNavArea
    subclasses define traversal cost and area flags — create one per movement domain (e.g.,
    UNavArea_Water
    with high cost for ground agents, zero for swimmers). Apply via NavModifierVolumes or NavMesh generation settings.
  • Dedicated server: Sight perception uses collision traces, not rendering — works fine. Guard non-server code with
    HasAuthority()
    .
  • AI with GAS: BT tasks request ability activation; abilities manage their own latent logic.

  • 飞行/水下导航:设置
    FNavAgentProperties::bCanFly
    /
    bCanSwim
    ;分配带有匹配
    SupportedAgents
    标记的
    UNavArea
    子类。自定义
    UNavArea
    子类可定义遍历成本和区域标记——可为每个移动域创建一个子类(例如,
    UNavArea_Water
    对地面Agent设置高成本,对水下Agent设置零成本)。可通过NavModifierVolumes或NavMesh生成设置应用。
  • 专用服务器:视觉感知使用碰撞检测而非渲染——可正常工作。使用
    HasAuthority()
    来保护非服务器端代码。
  • 集成GAS的AI:BT任务请求能力激活;能力自身管理其异步逻辑。

Related Skills

相关技能

  • ue-actor-component-architecture
    — component setup patterns for AI
  • ue-gameplay-framework
    — GameMode AI spawning, controller/pawn relationships
  • ue-cpp-foundations
    — delegates, subsystems, UObject patterns
  • ue-gameplay-abilities
    — GAS + AI integration
  • ue-actor-component-architecture
    — AI组件设置模式
  • ue-gameplay-framework
    — GameMode AI生成、Controller/Pawn关系
  • ue-cpp-foundations
    — 委托、子系统、UObject模式
  • ue-gameplay-abilities
    — GAS与AI的集成

Reference Files

参考文档

  • references/behavior-tree-patterns.md
    — patrol, combat, flee, investigate, squad BT patterns
  • references/eqs-reference.md
    — generator and test configurations for spatial queries
  • references/behavior-tree-patterns.md
    — 巡逻、战斗、逃跑、侦查、小队等行为树模式
  • references/eqs-reference.md
    — 空间查询的生成器及测试配置