关于 ue-ai-navigation
ue-ai-navigation 是面向 UE 5.8 开发的 AI 与导航技能,帮助开发者实现游戏 AI 系统。内容涵盖控制器、行为树、感知、寻路及相关模块的 API 用法。
主要功能
- 01UE 5.8 AI API 指引
- 02AI 控制器与移动请求
- 03黑板访问与观察器
- 04行为树节点实现
- 05感知与导航指引
- 06智能对象与人群系统
适用场景
- 01实现自定义行为树任务
- 02配置 AI 移动与路径查询
- 03通过黑板键管理 AI 状态
- 04为人群或可交互对象选择系统
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ quodsoler/ unreal-engine-skills --skill ue-ai-navigation安装后在 Claude Code 中输入/ue-ai-navigation确认已加载。
附带文件references/behavior-tree-patterns.mdreferences/eqs-reference.md
详细介绍
UE AI 和导航
目标引擎:UE 5.8。以下 API 已根据 5.8 头文件验证;较旧的形式列在“已弃用——请勿使用”一节中。
经典 UE AI 运行在仅限服务器的 AAIController 上,该控制器拥有一个大脑组件(UBehaviorTreeComponent)、一个 UBlackboardComponent、一个 UAIPerceptionComponent 和一个 UPathFollowingComponent。行为树、感知和 EQS 属于 AIModule;导航网格生成和查询属于 NavigationSystem;封装游戏任务的 BT 任务需要 GameplayTasks。Smart Objects(SmartObjectsModule,插件 SmartObjects)、ZoneGraph(ZoneGraph,在 5.8 中为实验性功能)、MassAI(在 5.8 中为实验性功能)和 NavCorridor(在 5.8 中为实验性功能)都是可选插件。将所用模块添加到 .Build.cs 中的 PublicDependencyModuleNames,例如 "AIModule", "NavigationSystem", "GameplayTasks"。
上下文
如果 .agents/ue-project-context.md 存在,请阅读它(模块名称、约定、已启用插件、GAS/网络设置)。如果该文件不存在,请继续。
根据请求和代码库识别对应领域。只有当两种合理的理解会产生不同代码时,才提出问题。
| 请求涉及…… | 请参阅 |
|---|---|
| 控制 Pawn、运行行为树、移动请求、焦点 | AI 控制器 |
| 读取/写入 AI 知识、键选择器、观察器 | 黑板 |
| 自定义任务、装饰器、服务、节点内存 | 行为树节点 |
| 视觉/听觉/伤害感知、刺激源 | AI 感知 |
| 路径查询、投影、随机点、调用器 | 导航与寻路 |
| 区域代价、查询过滤器、跳跃/离网格链接 | 导航区域、过滤器与离网格链接 |
| 掩体/侧翼/巡逻点选择、自定义上下文 | EQS 环境查询系统 |
| 使用 State Tree 而非行为树 | 面向 AI 的 State Tree |
| 可交互世界对象、占用槽位 | Smart Objects |
| 基于车道的人群、大量 Agent | ZoneGraph、MassAI 和 NavCorridor |
AI 控制器
// MyAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionTypes.h"
#include "MyAIController.generated.h"
class UBehaviorTree;
UCLASS()
class MYGAME_API AMyAIController : public AAIController
{
GENERATED_BODY()
public:
AMyAIController();
protected:
UPROPERTY(EditDefaultsOnly, Category = "AI")
TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
virtual void OnPossess(APawn* InPawn) override;
virtual void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) override;
UFUNCTION()
void HandleTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};
// MyAIController.cpp
#include "MyAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Navigation/PathFollowingComponent.h"
#include "Perception/AIPerceptionComponent.h"
AMyAIController::AMyAIController()
{
bStartAILogicOnPossess = true;
bStopAILogicOnUnposses = true;
}
void AMyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTreeAsset)
{
RunBehaviorTree(BehaviorTreeAsset); // calls UseBlackboard internally
}
if (UAIPerceptionComponent* Perception = GetAIPerceptionComponent())
{
Perception->OnTargetPerceptionUpdated.AddDynamic(this, &AMyAIController::HandleTargetPerceptionUpdated);
}
}
void AMyAIController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
Super::OnMoveCompleted(RequestID, Result);
if (UBlackboardComponent* BB = GetBlackboardComponent()) { BB->SetValueAsBool(TEXT("ReachedGoal"), Result.Code == EPathFollowingResult::Success); }
}
void AMyAIController::HandleTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
UBlackboardComponent* BlackboardComp = GetBlackboardComponent();
if (BlackboardComp && Stimulus.WasSuccessfullySensed())
{
BlackboardComp->SetValueAsObject(TEXT("TargetActor"), Actor);
BlackboardComp->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
}
}
// AIController.h — signatures you will call
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);
virtual FPathFollowingRequestResult MoveTo(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr* OutPath = nullptr);
virtual void SetFocus(AActor* NewFocus, EAIFocusPriority::Type InPriority = EAIFocusPriority::Gameplay);
virtual void SetFocalPoint(FVector NewFocus, EAIFocusPriority::Type InPriority = EAIFocusPriority::Gameplay);
virtual void ClearFocus(EAIFocusPriority::Type InPriority);
virtual bool RunBehaviorTree(UBehaviorTree* BTAsset);
bool UseBlackboard(UBlackboardData* BlackboardAsset, UBlackboardComponent*& BlackboardComponent);
EPathFollowingStatus::Type GetMoveStatus() const; // Idle, Waiting, Paused, Moving
bool HasPartialPath() const;
virtual void SetGenericTeamId(const FGenericTeamId& NewTeamID) override; // IGenericTeamAgentInterface
| 需求 | 使用 |
|---|---|
| 使用单次调用选项进行简单移动 | MoveToActor / MoveToLocation — 返回 EPathFollowingRequestResult::{Failed, AlreadyAtGoal, RequestSuccessful} |
| 完全控制(目标 Actor + 半径 + 过滤器 + 部分路径)并获取生成的路径 | MoveTo(FAIMoveRequest, FNavPathSharedPtr*) — 返回包含 MoveId 和 Code 的 FPathFollowingRequestResult |
| 移动没有 BT/黑板的控制器,例如点击移动测试 | UAIBlueprintHelperLibrary::SimpleMoveToLocation(AController*, const FVector&) |
| 响应抵达事件 | 重写 OnMoveCompleted,或绑定动态委托 ReceiveMoveCompleted(FAIRequestID、EPathFollowingResult::Type) |
使用其设置函数构建 FAIMoveRequest:SetGoalActor、SetGoalLocation、SetAcceptanceRadius、SetUsePathfinding、SetAllowPartialPath、SetNavigationFilter、SetProjectGoalLocation、SetCanStrafe、SetReachTestIncludesAgentRadius。
在 Pawn 上设置:AIControllerClass = AMyAIController::StaticClass(); AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
黑板
| 类型 | 获取 | 设置 |
|---|---|---|
| 对象 | GetValueAsObject | SetValueAsObject |
| 向量 | GetValueAsVector | SetValueAsVector |
| 布尔值 | GetValueAsBool | SetValueAsBool |
| 浮点数 | GetValueAsFloat | SetValueAsFloat |
| 整数 | GetValueAsInt | SetValueAsInt |
| 枚举 | GetValueAsEnum(返回 uint8) | SetValueAsEnum(接受 uint8) |
| 名称 / 字符串 / 类 / 旋转体 | GetValueAsName / …String / …Class / …Rotator | 对应的 SetValueAs… |
所有访问器都接受 const FName& KeyName。ClearValue(FName) 和 IsVectorValueSet(FName) 提供 FBlackboard::FKey 重载,可跳过名称查找。
#include "BehaviorTree/BlackboardComponent.h"
UBlackboardComponent* BlackboardComp = GetBlackboardComponent();
BlackboardComp->SetValueAsObject(TEXT("TargetActor"), TargetActor);
BlackboardComp->ClearValue(TEXT("TargetActor"));
// Observers: the delegate returns EBlackboardNotificationResult, it is not void.
const FBlackboard::FKey KeyID = BlackboardComp->GetKeyID(TEXT("TargetActor"));
const FDelegateHandle Handle = BlackboardComp->RegisterObserver(KeyID, this,
FOnBlackboardChangeNotification::CreateUObject(this, &AMyAIController::HandleTargetKeyChanged));
BlackboardComp->UnregisterObserver(KeyID, Handle);
// Declared in AMyAIController — returning void will not bind:
EBlackboardNotificationResult HandleTargetKeyChanged(const UBlackboardComponent& BlackboardComp, FBlackboard::FKey ChangedKeyID);
返回 EBlackboardNotificationResult::ContinueObserving 以保持注册状态,返回 RemoveObserver 以取消订阅。UnregisterObserversFrom(this) 会移除某个对象注册的所有观察者。
**在 BT 节点中,绝不要硬编码键名。**公开一个 FBlackboardKeySelector UPROPERTY 并读取 SelectedKeyName;行为树会通过 FBlackboardKeySelector::ResolveSelectedKey(const UBlackboardData&) 根据资产解析它,而 GetSelectedKeyID() 会提供缓存的 FBlackboard::FKey。将键标记为 实例同步(UBlackboardData 资产中的 FBlackboardEntry::bInstanceSynced),即可在使用该资产的所有 AI 之间共享其值——无需额外配置即可实现小队级警报;UBlackboardData::HasSynchronizedKeys() 会报告是否有任何键已同步。对于热点路径,可缓存键:由 (const UBlackboardComponent&, FBlackboard::FKey) 构造的 FBBKeyCachedAccessor<UBlackboardKeyType_Bool> 提供 Get() 和 SetValue(UBlackboardComponent&, Value),并跳过每次调用时的名称查找。
行为树节点
| 基类 | 重写函数 | 签名来源 |
|---|---|---|
UBTTaskNode | ExecuteTask、AbortTask、TickTask、OnTaskFinished、OnMessage | BehaviorTree/BTTaskNode.h |
UBTDecorator | CalculateRawConditionValue(const)、OnNodeActivation、OnNodeDeactivation、OnNodeProcessed | BehaviorTree/BTDecorator.h |
UBTService | TickNode、OnSearchStart、OnBecomeRelevant、OnCeaseRelevant | BehaviorTree/BTService.h(相关性钩子继承自 BTAuxiliaryNode.h) |
| 任意节点 | GetInstanceMemorySize、InitializeMemory、CleanupMemory | BehaviorTree/BTNode.h |
请原样复制这些内容(只更改类名):
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override;
virtual void OnMessage(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, FName Message, int32 RequestID, bool bSuccess) override;
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
virtual void OnNodeActivation(FBehaviorTreeSearchData& SearchData) override;
virtual void OnNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type NodeResult) override;
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
virtual void OnSearchStart(FBehaviorTreeSearchData& SearchData) override;
virtual uint16 GetInstanceMemorySize() const override;
virtual void InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const override;
virtual void CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryClear::Type CleanupType) const override;
EBTNodeResult::Type 恰好有四个值:Succeeded、Failed、Aborted、InProgress。没有 FBTNodeResult。
带节点内存的自定义任务
每个 AI 的运行时状态应放在普通结构体中(例如 FMyAttackTaskMemory { float ElapsedTime; TWeakObjectPtr<AActor> CachedTarget; }),绝不能放在节点的 UPROPERTY 成员中。任务通过 GetInstanceMemorySize() 返回该结构体的 sizeof。完整的 UMyBTTask_Attack 头文件和 .cpp(FBlackboardKeySelector 键、ExecuteTask / TickTask / AbortTask)见行为树模式。
节点内存规则。 NodeMemory 是由 GetInstanceMemorySize() 确定大小的原始字节块,并按 BT 实例布局,因此它是共享节点对象上存放每个 AI 运行时状态的唯一安全位置。使用 CastInstanceNodeMemory<T>() 访问,它会断言 sizeof(T) <= GetInstanceMemorySize()。对于非平凡成员,请重写 InitializeMemory/CleanupMemory,并调用辅助函数 InitializeNodeMemory<T>(NodeMemory, InitType) / CleanupNodeMemory<T>(NodeMemory, CleanupType);它们会正确地进行定位 new 和销毁。选择启用 Tick 间隔的任务会获得一个 FBTTaskMemory 头部(NextTickRemainingTime、AccumulatedDeltaTime),可通过 GetSpecialNodeMemory<FBTTaskMemory>() 访问;引擎会将它放在 NodeMemory 之前(BTNode.h:390),因此绝不会与您的结构体重叠。
在构造函数中调用 INIT_TASK_NODE_NOTIFY_FLAGS() / INIT_DECORATOR_NODE_NOTIFY_FLAGS() / INIT_SERVICE_NODE_NOTIFY_FLAGS():这些宏会根据您实际重写的虚函数设置 bNotifyTick、bNotifyTaskFinished、bNotifyActivation 等标志。如果不调用这些宏(或不手动设置 bNotify* 标志),任务的 TickTask/OnTaskFinished 以及装饰器/服务的激活和相关性重写都不会被调用(BTTaskNode.cpp:12-13);只有 UBTService 默认将 bNotifyTick 和 bNotifyOnSearch 设为 true(BTService.cpp:11-12)。返回 InProgress 表示任务处于潜伏状态——使用 FinishLatentTask(OwnerComp, Result) 完成任务(或在中止时使用 FinishLatentAbort(OwnerComp))。若要等待外部事件而不是持续 Tick,请调用 WaitForMessage(OwnerComp, FName),并在 OnMessage 中完成任务;发送方可使用 FAIMessage::Send(Pawn, FAIMessage(TEXT("MontageCompleted"), Sender, true)) 或 UAIBlueprintHelperLibrary::SendAIMessage。
若要从行为树外部强制重新评估,请使用 UBehaviorTreeComponent::RequestExecution(const UBTCompositeNode* RequestedOn, int32 InstanceIdx, const UBTNode* RequestedBy, int32 RequestedByChildIndex, EBTNodeResult::Type ContinueWithResult, bool bStoreForDebugger = true);在装饰器中,优先使用 ConditionalFlowAbort(OwnerComp, EBTDecoratorAbortRequest::ConditionResultChanged),并让 FlowAbortMode 决定作用范围。
编写自定义节点前,值得优先复用的内置节点:任务 UBTTask_MoveTo、UBTTask_MoveDirectlyToward、UBTTask_Wait、UBTTask_WaitBlackboardTime、UBTTask_RunEQSQuery、UBTTask_PlayAnimation、UBTTask_PlaySound、UBTTask_MakeNoise、UBTTask_RotateToFaceBBEntry、UBTTask_RunBehavior、UBTTask_RunBehaviorDynamic、UBTTask_SetKeyValueBool / UBTTask_SetKeyValueFloat / UBTTask_SetKeyValueObject(每种键类型对应一个节点)、UBTTask_FinishWithResult;装饰器 UBTDecorator_Blackboard、UBTDecorator_CompareBBEntries、UBTDecorator_Cooldown、UBTDecorator_TagCooldown、UBTDecorator_Loop、UBTDecorator_LoopUntil、UBTDecorator_TimeLimit、UBTDecorator_DoesPathExist、UBTDecorator_IsAtLocation、UBTDecorator_ReachedMoveGoal、UBTDecorator_CheckGameplayTagsOnActor、UBTDecorator_ConeCheck、UBTDecorator_ForceSuccess;服务 UBTService_DefaultFocus、UBTService_RunEQS。仅提供 UBTComposite_Selector、UBTComposite_Sequence 和 UBTComposite_SimpleParallel 这几种组合节点——没有通用的并行组合节点。
树拓扑、完整的装饰器和服务、中止模式以及消息驱动任务:行为树模式。
AI 感知
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Hearing.h"
#include "Perception/AISenseConfig_Sight.h"
// In AMyAIController::AMyAIController():
UAIPerceptionComponent* Perception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetPerceptionComponent(*Perception);
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;
Perception->ConfigureSense(*Sight);
Perception->SetDominantSense(Sight->GetSenseImplementation());
UAISenseConfig_Hearing* Hearing = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("Hearing"));
Hearing->HearingRange = 3000.f;
Hearing->DetectionByAffiliation.bDetectEnemies = true;
Perception->ConfigureSense(*Hearing);
运行时更改感知配置后,只有调用 RequestStimuliListenerUpdate() 才会生效。
| 委托 | 签名 | 触发时机 |
|---|---|---|
OnTargetPerceptionUpdated | (AActor* Actor, FAIStimulus Stimulus) | 每个改变状态的已处理刺激触发一次,即每个 Actor 针对每种感知 触发一次(AIPerceptionComponent.cpp:593) |
OnTargetPerceptionInfoUpdated | (const FActorPerceptionUpdateInfo& UpdateInfo) | 同上,但会携带 TargetId,因此即使 Actor 已销毁或为空也会触发 |
OnTargetPerceptionForgotten | (AActor* Actor) | 在 ForgetActor / ForgetAll 之后,或所有刺激过期时触发——只有启用 UAISystem::bForgetStaleActors 才会因过期触发(AISystem.h:84) |
OnPerceptionUpdated | (const TArray<AActor*>& UpdatedActors) | 每轮刺激处理过程中,只要有内容更新就触发一次,并批量传递 |
FAIStimulus 包含 Strength、StimulusLocation、ReceiverLocation、Tag、Type(一个 FAISenseID),以及 WasSuccessfullySensed()、IsActive() 和 GetAge()。可通过 Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>() 按感知类型分支。查询接口:GetCurrentlyPerceivedActors(TSubclassOf<UAISense> SenseToUse, TArray<AActor*>& OutActors)、GetPerceivedHostileActors(TArray<AActor*>&)、GetPerceivedHostileActorsBySense(TSubclassOf<UAISense>, TArray<AActor*>&)、HasActiveStimulus(const AActor&, FAISenseID)、ForgetActor(AActor*)、ForgetAll()。
事件采用推送方式,而非轮询:UAISense_Hearing::ReportNoiseEvent(WorldContextObject, NoiseLocation, Loudness, Instigator, MaxRange, Tag) 和 UAISense_Damage::ReportDamageEvent(WorldContextObject, DamagedActor, Instigator, DamageAmount, EventLocation, HitLocation, Tag)。Sight 没有报告函数——它由追踪驱动,且只能感知已注册的来源。UAISense_Prediction::RequestPawnPredictionEvent(APawn* Requestor, AActor* PredictedActor, float PredictionTime) 用于询问目标将出现的位置。
要让 Actor 可被感知,可添加 UAIPerceptionStimuliSourceComponent 并设置 bAutoRegisterAsSource = true(或调用 RegisterWithPerceptionSystem()),再针对每种感知调用 RegisterForSense(UAISense_Sight::StaticClass());也可以调用 UAIPerceptionSystem::GetCurrent(World)->RegisterSource(*Actor)(所有已实例化的感知类型)或 RegisterSourceForSenseClass(UAISense_Sight::StaticClass(), *Actor)——这两个接口都接受 AActor&(Perception/AIPerceptionSystem.h:139-141)。
其他感知配置:UAISenseConfig_Touch、UAISenseConfig_Team(通过 IGenericTeamAgentInterface 在队友之间传播感知信息)、UAISenseConfig_Prediction。
导航与寻路
#include "NavigationPath.h"
#include "NavigationSystem.h"
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
if (!NavSys) { return; }
FNavLocation RandomPoint;
const bool bFoundRandom = NavSys->GetRandomReachablePointInRadius(Origin, 1500.f, RandomPoint);
FNavLocation Projected;
const bool bOnNavMesh = NavSys->ProjectPointToNavigation(WorldLocation, Projected, FVector(500.f, 500.f, 500.f));
// Blueprint-facing helpers, equally usable from C++ (all static, all take a world context):
UNavigationPath* Path = UNavigationSystemV1::FindPathToLocationSynchronously(GetWorld(), Start, End, GetPawn());
const bool bReachable = Path && Path->IsValid() && !Path->IsPartial();
FVector HitLocation;
const bool bBlocked = UNavigationSystemV1::NavigationRaycast(GetWorld(), Start, End, HitLocation);
// Native path, with the full FNavPathPoint list:
const FPathFindingQuery Query(this, *NavSys->GetDefaultNavDataInstance(), Start, End);
const FPathFindingResult Result = NavSys->FindPathSync(Query);
const int32 NumPoints = Result.IsSuccessful() ? Result.Path->GetPathPoints().Num() : 0;
UNavigationPath 提供 PathPoints、GetPathLength()、IsValid() 和 IsPartial();FPathFindingResult 提供 Path、IsSuccessful() 和 IsPartial()。
异步调用:FindPathAsync(const FNavAgentProperties&, FPathFindingQuery, const FNavPathQueryDelegate&, EPathFindingMode::Type);委托的定义为 DECLARE_DELEGATE_ThreeParams(FNavPathQueryDelegate, uint32 QueryID, ENavigationQueryResult::Type, FNavPathSharedPtr),因此,签名不同的处理函数无法通过编译。
| 情况 | 请执行以下操作 |
|---|---|
| 完全没有导航网格 | 放置一个 ANavMeshBoundsVolume;没有它就不会生成任何瓦片 |
| 程序化/运行时几何体 | 将导航网格的 Runtime Generation 设为 Dynamic;注册或移动与导航相关的组件会标记受影响的瓦片并仅重建这些瓦片(AddDirtyArea,NavigationSystem.h:973)。NavSys->Build() 会重建所有导航数据——批量生成后使用一次,不要在每次更改时调用 |
| 开放世界/流式关卡 | 在 AI Pawn 上添加 UNavigationInvokerComponent,并设置 TileGenerationRadius / TileRemovalRadius(或调用 SetGenerationRadii) |
| 飞行或游泳的 Agent | 在移动组件的 NavAgentProps 上设置 FNavAgentProperties::bCanFly / bCanSwim;Agent 只会使用标志匹配的导航数据 |
| 多个 Agent 之间的群体避障 | 使用 UCrowdFollowingComponent 和 SetCrowdSimulationState、SetCrowdAvoidanceQuality、SetCrowdSeparationWeight、SetCrowdCollisionQueryRange |
Agent 的生成设置位于 ARecastNavMesh 上(Cast<ARecastNavMesh>(NavSys->GetDefaultNavDataInstance())):AgentRadius、AgentHeight、AgentMaxSlope、TileSizeUU,以及通过 GetAgentMaxStepHeight(ENavigationDataResolution) / SetAgentMaxStepHeight(ENavigationDataResolution, float) 设置的各分辨率步高。瓦片通过 FNavTileRef 标识,而不是原始索引——当前重载为 GetNavMeshTileBounds(FNavTileRef) 和 GetNavMeshTileXY(FNavTileRef, int32&, int32&, int32&)。
导航区域、过滤器与离网格链接
按遍历域派生 UNavArea,并设置 DefaultCost、FixedAreaEnteringCost 和 DrawColor。内置区域:UNavArea_Default、UNavArea_Null(不可行走)、UNavArea_Obstacle、UNavArea_LowHeight,以及用于按 Agent 替换的 UNavAreaMeta_SwitchByAgent。可在运行时使用 UNavModifierComponent(AreaClass、可选的 AreaClassToReplace、FailsafeExtent、bIncludeAgentHeight,以及设置器 SetAreaClass / SetAreaClassToReplace)绘制区域,也可以在关卡中使用 ANavModifierVolume 绘制。
每个网格的可行走区域。 UNavCollisionBase 有两种互斥模式:bIsDynamicObstacle 会在生成后绘制障碍物修改器,而 bUseSurfaceArea 会使 UNavCollision::AreaClass 参与栅格化,让网格自身的三角形成为该区域。当网格本身就是地形类型(泥地、水面、屋顶),而非其上的障碍物时,请使用表面模式。
查询过滤器是 UNavigationQueryFilter 子类——需要实际的类体,不能只是前置声明:
// MyNavFilter.h
#pragma once
#include "CoreMinimal.h"
#include "NavFilters/NavigationQueryFilter.h"
#include "MyNavFilter.generated.h"
UCLASS()
class MYGAME_API UMyNavFilter : public UNavigationQueryFilter
{
GENERATED_BODY()
public:
UMyNavFilter();
};
// MyNavFilter.cpp
#include "MyNavFilter.h"
#include "NavAreas/NavArea_Obstacle.h"
UMyNavFilter::UMyNavFilter()
{
FNavigationFilterArea& AvoidObstacles = Areas.AddDefaulted_GetRef();
AvoidObstacles.AreaClass = UNavArea_Obstacle::StaticClass();
AvoidObstacles.bIsExcluded = true;
}
将其作为 FilterClass 参数传入:MoveToActor(Target, -1.f, true, true, true, UMyNavFilter::StaticClass());。每个 FNavigationFilterArea 条目也可以通过 bOverrideTravelCost + TravelCostOverride 或 bOverrideEnteringCost + EnteringCostOverride 覆盖代价。UNavigationQueryFilter::GetQueryFilter(NavData, Querier, FilterClass) 会解析运行时的 FSharedConstNavQueryFilter;可重写 InitializeFilter(const ANavigationData&, const UObject*, FNavigationQueryFilter&) 来实现动态规则。
离网格链接。 在关卡中放置一个 ANavLinkProxy:其 PointLinks 数组存放简单链接,SegmentLinks 存放线段链接,而其 UNavLinkCustomComponent(通过 GetSmartLinkComp() 获取)提供智能链接路径——使用 SetLinkData(RelativeStart, RelativeEnd, ENavLinkDirection::Type)、SetEnabled(bool)、SetEnabledArea(TSubclassOf<UNavArea>) 和 SetMoveReachedLink(FOnMoveReachedLink const&),以便在 Agent 到达链接时执行代码(跳跃、爬梯等)。ANavLinkProxy::OnSmartLinkReached 是对应的 Blueprint 接口。Recast 还可以根据 ARecastNavMesh 上的 FNavLinkGenerationJumpConfig 数组 NavLinkJumpConfigs 自动生成跳跃链接。
EQS 环境查询系统
#include "EnvironmentQuery/EnvQueryManager.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
// AMyAIController declares: TObjectPtr<UEnvQuery> FindCoverQuery and
// void HandleCoverQueryFinished(TSharedPtr<FEnvQueryResult> Result).
void AMyAIController::RunCoverQuery()
{
if (!FindCoverQuery) { return; }
// Querier = pawn; a controller querier puts the Querier context at the controller (EnvQueryContext_Querier.cpp:19-21)
FEnvQueryRequest Request(FindCoverQuery, GetPawn());
Request.SetFloatParam(TEXT("SearchRadius"), 1200.f);
Request.Execute(EEnvQueryRunMode::SingleResult, this, &AMyAIController::HandleCoverQueryFinished);
}
void AMyAIController::HandleCoverQueryFinished(TSharedPtr<FEnvQueryResult> Result)
{
UBlackboardComponent* BlackboardComp = GetBlackboardComponent();
if (BlackboardComp && Result.IsValid() && Result->IsSuccessful())
{
BlackboardComp->SetValueAsVector(TEXT("CoverLocation"), Result->GetItemAsLocation(0));
}
}
FQueryFinishedSignature 是 DECLARE_DELEGATE_OneParam(FQueryFinishedSignature, TSharedPtr<FEnvQueryResult>)。运行模式:SingleResult、RandomBest5Pct、RandomBest25Pct、AllMatching。结果提供 Items、GetItemAsLocation(int32)、GetItemAsActor(int32)、GetItemScore(int32)、IsSuccessful()、IsAborted()。其他入口:UEnvQueryManager::GetCurrent(WorldContextObject)->RunQuery(Request, RunMode, FinishDelegate) 会返回用于取消的查询 ID;RunInstantQuery(Request, RunMode) 会阻塞并立即返回结果(谨慎使用);UEnvQueryManager::RunEQSQuery(WorldContextObject, QueryTemplate, Querier, RunMode, WrapperClass) 是 Blueprint 封装函数。
子类钩子(均为 const):UEnvQueryGenerator::GenerateItems(FEnvQueryInstance&)、UEnvQueryTest::RunTest(FEnvQueryInstance&)、UEnvQueryContext::ProvideContext(FEnvQueryInstance&, FEnvQueryContextData&)。Blueprint 上下文派生自 UEnvQueryContext_BlueprintBase,并实现 ProvideSingleActor、ProvideSingleLocation、ProvideActorsSet 或 ProvideLocationsSet。
在关卡中放置一个 AEQSTestingPawn(EnvironmentQuery/EQSTestingPawn.h),即可在编辑器中预览查询的评分条目,无需 PIE;设置其 QueryTemplate 和 QueryConfig。
生成器与测试属性表、自定义 C++ 上下文、评分方案及现成的查询配置:EQS 参考。
AI 状态树
行为树适用于带有优先级中断的响应式战斗;状态树适用于更扁平的状态机和 Smart Object 流程。对于 AI,请使用 UStateTreeAIComponent(来自 Components/StateTreeAIComponent.h),而不是普通的 UStateTreeComponent:它会返回 UStateTreeAIComponentSchema,确保上下文值中包含 AAIController。Build.cs 需要 StateTreeModule 和 GameplayStateTreeModule。
任务、条件、评估器和架构的编写归 ue-state-trees 负责。
Smart Objects
SmartObjects 是一个可选插件(SmartObjectsModule)。在世界中的 Actor 上添加 USmartObjectComponent,并为其指定一个 USmartObjectDefinition(GetDefinition() / SetDefinition());AI 通过 USmartObjectSubsystem 查找并占用槽位。
#include "SmartObjectSubsystem.h"
#include "SmartObjectRequestTypes.h" // FSmartObjectRequest, FSmartObjectRequestFilter, FSmartObjectRequestResult
USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(GetWorld());
if (!Subsystem) { return; }
FSmartObjectRequestFilter Filter;
const FSmartObjectRequest Request(FBox::BuildAABB(Origin, FVector(500.f)), Filter);
const FSmartObjectRequestResult Result = Subsystem->FindSmartObject(Request, GetPawn());
if (Result.IsValid())
{
const FSmartObjectClaimHandle Claim = Subsystem->MarkSlotAsClaimed(Result.SlotHandle, ESmartObjectClaimPriority::Normal);
FVector SlotLocation;
if (Subsystem->GetSlotLocation(Claim, SlotLocation))
{
MoveToLocation(SlotLocation);
}
Subsystem->MarkSlotAsFree(Claim); // in real code, only once the interaction ends
}
ESmartObjectClaimPriority 的取值为 Low、BelowNormal、Normal、AboveNormal、High。FindSmartObjects 会返回所有匹配项;FindSmartObjectsInList 则将搜索范围限制为你已有的 Actor。GameplayBehaviorSmartObjects 插件(在 5.8 中为实验性)添加了由 GameplayBehavior 驱动的槽位行为,而 UBlackboardKeyType_SOClaimHandle 可让 BT 在黑板键中保存占用句柄。
ZoneGraph、MassAI 和 NavCorridor
| 插件 | 5.8 中的成熟度 | 用途 | 入口 |
|---|---|---|---|
| ZoneGraph | 实验性 | 用于交通和行人流动的车道图,取代自由导航网格移动 | UZoneGraphSubsystem::FindNearestLane / FindOverlappingLanes、AZoneGraphData、UZoneShapeComponent、UE::ZoneGraph::Query 辅助函数 |
| MassAI | 实验性 | 管理数千个轻量级 Agent,无需为每个 Agent 创建 Actor | UMassStateTreeProcessor、FMassStateTreeExecutionContext、模块 MassAIBehavior、MassNavigation、MassZoneGraphNavigation、MassNavMeshNavigation |
| NavCorridor | 实验性 | 在导航网格路径周围的走廊内转向,而非沿其折线行进 | FNavCorridor、FNavCorridorParams、FNavCorridorPortal、FNavCorridorLocation |
Mass Agent 的编写、片段和处理器归 ue-mass-entity 负责。
已弃用 — 请勿使用
| 不要生成 | 在 5.8 中使用 | 来源 |
|---|---|---|
EPointOnCircleSpacingMethod | EEnvQueryPointSpacingMethod | EnvironmentQuery/Generators/EnvQueryGenerator_OnCircle.h:15 中的 UE_DEPRECATED(5.8) |
USmartObjectComponent::SetSmartObjectEnabled (BlueprintPure) | K2_SetSmartObjectEnabled | SmartObjectComponent.h:101 中的 UE_DEPRECATED(5.8) |
USmartObjectComponent::SetSmartObjectEnabledForReason (BlueprintPure) | K2_SetSmartObjectEnabledForReason | SmartObjectComponent.h:118 中的 UE_DEPRECATED(5.8) |
FNavLinkGenerationJumpDownConfig | FNavLinkGenerationJumpConfig | NavMesh/LinkGenerationConfig.h:159 中的 UE_DEPRECATED(5.7) |
UAIPerceptionComponent::RefreshStimulus | ConditionallyStoreSuccessfulStimulus(FAIStimulus&, const FAIStimulus&) | Perception/AIPerceptionComponent.h:448 中的 UE_DEPRECATED(5.6) |
UPathFollowingComponent::SetMovementComponent(UNavMovementComponent*) | SetNavMovementInterface(INavMovementInterface*)(弃用消息中的 SetNavMoveInterface 不存在) | Navigation/PathFollowingComponent.h:282 中的 UE_DEPRECATED(5.5);替代项位于 :286 |
接受 int32 TileIndex 的 ARecastNavMesh 瓦片函数 | 接受 FNavTileRef 的重载 | NavMesh/RecastNavMesh.h:1134,1141,1151,1161 中的 UE_DEPRECATED(5.5) |
ARecastNavMesh::AgentMaxStepHeight | GetAgentMaxStepHeight(ENavigationDataResolution) / NavMeshResolutionParams | NavMesh/RecastNavMesh.h:718 中的 UE_DEPRECATED(all) |
UBehaviorTreeComponent::RequestExecution(const UBTDecorator*) | RequestBranchEvaluation | BehaviorTree/BehaviorTreeComponent.h:153 中的“replaced by”注释 |
UNavigationSystemV1::K2_GetRandomPointInNavigableRadius | K2_GetRandomLocationInNavigableRadius | NavigationSystem.h:1484 中的 DeprecatedFunction |
常见错误
忘记通知标志宏: 除非构造函数调用 INIT_TASK_NODE_NOTIFY_FLAGS() / INIT_SERVICE_NODE_NOTIFY_FLAGS(),否则 TickTask 或 OnSearchStart 重写函数不会运行——决定树会调用什么的是标志,而不是重写函数。
将每个实例的状态作为成员变量: 所有运行该树的 AI 都共享 BT 节点对象,因此将 float ElapsedTime; 声明为 UPROPERTY 会造成跨 Agent 数据竞争。应将其放入由 GetInstanceMemorySize() 确定大小的结构体中,并通过 CastInstanceNodeMemory<T>() 访问。
返回 InProgress 却永远不结束: 任务会一直挂起。每条潜在执行路径都必须以 FinishLatentTask 或 FinishLatentAbort 结束。
黑板错误: FOnBlackboardChangeNotification 返回 EBlackboardNotificationResult,因此无法绑定返回 void 的观察函数;BT 节点还应公开 FBlackboardKeySelector,而不是将键名写死,以便设计人员重新绑定,并让 ResolveSelectedKey 根据资源进行验证。
没有 ANavMeshBoundsVolume: 没有边界体积就不会生成任何瓦片,所有 MoveTo 都会静默失败。对于程序化几何体,请使用 Dynamic 运行时生成(并在批量生成后调用一次 NavSys->Build()),或使用 UNavigationInvokerComponent。
假设客户端上存在 AI: AAIController 仅存在于服务器。通过 Pawn 的复制属性复制结果;黑板和行为树不会复制。
轮询而非限流: 优先使用带有 FlowAbortMode 的 UBTDecorator_Blackboard 或 WaitForMessage,而不是逐帧检查;将服务的 Interval 保持在 0.5 s 或以上,并设置 RandomDeviation;使用 UBTDecorator_Cooldown 控制 EQS,优先选择 SingleResult 而非 AllMatching,并将开销较低的筛选测试放在轨迹和寻路测试之前。
相关技能
ue-state-trees— State Tree 任务、条件、评估器、架构和 Mass 行为ue-mass-entity— 面向大量 Agent 的 Mass 片段、处理器和人群模拟ue-character-movement— 执行此技能请求路径的移动组件ue-gameplay-framework— GameMode 生成、控制器/Pawn 所有权、占有生命周期ue-physics-collision— 视线和 EQS 轨迹测试所用的轨迹通道和碰撞响应ue-gameplay-tags-messaging—UBTDecorator_CheckGameplayTagsOnActor和 EQS 标签测试所用的游戏玩法标签ue-actor-component-architecture— 组件创建、所有权和复制模式ue-mover— Mover 插件:移动模式、分层移动和回滚网络ue-testing-debugging— 自动化测试、日志记录、断言、性能分析和调试绘制