
蓝图逻辑的精准还原(1:1 映射)
核心节点还原:将 AI MoveTo 节点还原为 FAIMoveRequest + AIC->MoveTo()。
严格保留参数:Acceptance Radius = 20.0,SetTimer 的 Time = 0.2s 且 Looping = true。
Do Once 机制还原:确认了蓝图中 Do Once 有输入、输出且 Reset 有连线。在 C++ 中转化为 bLocationRetryStarted 变量,在 HandleMoveToLocationSuccess 中重置为 false。
对象池定时器:保留了蓝图中的 TimerHandle_ReturnToPool 清除逻辑。
二、 UE C++ 架构设计(职责分离)
摒弃了把逻辑全塞进一个函数的做法,拆分为清晰的四层架构:
入口层 (MoveToLocation):接收目标参数,更新缓存 CachedLocation。
核心执行层 (ExecuteMoveToLocation):构建 FAIMoveRequest,处理同步返回值,绑定异步回调。定时器也直接绑定此函数(天然无参,完美适配)。
回调层 (OnMoveToLocationCompleted):处理引擎底层的异步 PathFollowingComponent 结果,包含 RequestID 防串号过滤。
结果处理层 (HandleMoveToLocationSuccess / Fail):将同步(AlreadyAtGoal/Invalid)和异步(Success/Fail)的结果统一汇聚,避免代码重复。失败时触发 0.2 秒循环定时器。
三、 核心技术避坑指南
同步 vs 异步的区分:明确了 MoveTo() 的同步返回值(请求是否发出)和 OnMoveCompleted 的异步回调(是否到达/中途失败)是不同的阶段,解答了"为什么两处都可能成功失败却调用相同函数"的疑惑。
RequestID 过滤:确认了在高频切换目标时,必须靠 CurrentMoveRequestID 对比来拦截过期回调,防止旧请求的 Aborted 干扰新逻辑。
UE5.8 API 适配:修正了 FAIMoveRequest 中不存在 SetStopOnOverlap 的问题,改用 SetReachTestIncludesAgentRadius(false) 来对应蓝图中未勾选的 Stop on Overlap。
蓝图过渡期兼容:给 TimerHandle 变量加上 UPROPERTY(EditAnywhere, BlueprintReadWrite) 并移到 public 区域,解决了 not blueprint writable 的编译错误。
代码如下:
public:
//移动向某一位置
// ================= 蓝图过渡期需要的定时器变量 =================
// 【新增】加上 UPROPERTY 宏,允许蓝图读写
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI|Movement|Location")
FTimerHandle TimerHandle_RestartApproachingLocation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI|Movement|Location")
FTimerHandle TimerHandle_ReturnToPool;
// 缓存目标坐标,暴露给蓝图供过渡期手动赋值
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI|Movement|Location")
FVector CachedLocation;
// 核心执行者:定时器绑定 + 蓝图过渡期调用
UFUNCTION(BlueprintCallable, Category = "AI|Movement|Location")
void ExecuteMoveToLocation();
protected:
// ================= 回调层 =================
// 异步回调:由 PathFollowingComponent 触发
void OnMoveToLocationCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result);
// 成功处理(同步 AlreadyAtGoal 和异步 Success 都会调用它)
void HandleMoveToLocationSuccess();
// 失败处理(同步 Invalid 和异步 Fail 都会调用它)
void HandleMoveToLocationFail();
// 状态重置(对应蓝图右上角 Reset Status by Timer)
// UFUNCTION()
//void ResetStatus();
private:
// 对应蓝图中的 Do Once 节点
bool bLocationRetryStarted = false;
// 当前移动请求 ID,用于过滤过期回调(防串号)
FAIRequestID CurrentLocationMoveRequestID;
// 对应蓝图中 AI MoveTo 节点的 Acceptance Radius = 20.0
const float AcceptanceRadius = 20.0f;
#include "Enemy.h"
#include <Components/CapsuleComponent.h>
#include <GameFramework/CharacterMovementComponent.h>
#include <PaperFlipbookComponent.h>
#include <Interface/HitHandle_Player.h>
#include <Net/UnrealNetwork.h>
#include <Kismet/GameplayStatics.h>
#include <AIController.h>
#include <AITypes.h>
#include <Blueprint/AIAsyncTaskBlueprintProxy.h>
// ================= 核心执行者 =================
void AEnemy::ExecuteMoveToLocation()
{
AAIController* AIC = Cast(GetController());
if (!AIC) return;
// 1. 构建 FAIMoveRequest(UE5.8 API 适配)
FAIMoveRequest MoveReq;
MoveReq.SetGoalLocation(CachedLocation);
MoveReq.SetAcceptanceRadius(AcceptanceRadius); // 20.0f
MoveReq.SetReachTestIncludesAgentRadius(false); // 对应蓝图 Stop on Overlap 未勾选
MoveReq.SetUsePathfinding(true);
MoveReq.SetAllowPartialPath(true);
// 2. 执行移动
FPathFollowingRequestResult MoveResult = AIC->MoveTo(MoveReq);
// 3. 处理同步阶段结果
if (MoveResult.Code == EPathFollowingRequestResult::RequestSuccessful)
{
CurrentLocationMoveRequestID = MoveResult.MoveId;
if (AIC->GetPathFollowingComponent())
{
AIC->GetPathFollowingComponent()->OnRequestFinished.RemoveAll(this);
AIC->GetPathFollowingComponent()->OnRequestFinished.AddUObject(this, &AEnemy::OnMoveToLocationCompleted);
}
}
else if (MoveResult.Code == EPathFollowingRequestResult::AlreadyAtGoal)
{
HandleMoveToLocationSuccess();
}
else
{
HandleMoveToLocationFail();
}
}
// ================= 异步回调 =================
void AEnemy::OnMoveToLocationCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
// 防串号过滤
if (RequestID.IsValid() && CurrentLocationMoveRequestID.IsValid() && RequestID != CurrentLocationMoveRequestID)
{
return;
}
if (Result.IsSuccess())
{
HandleMoveToLocationSuccess();
}
else
{
HandleMoveToLocationFail();
}
}
// ================= 成功处理 =================
void AEnemy::HandleMoveToLocationSuccess()
{
UWorld* World = GetWorld();
if (!World) return;
FTimerManager& TimerMgr = World->GetTimerManager();
// 对应蓝图:Clear and Invalidate Timer by Handle (两个定时器)
TimerMgr.ClearTimer(TimerHandle_RestartApproachingLocation);
TimerMgr.ClearTimer(TimerHandle_ReturnToPool);
// 对应蓝图:Do Once 的 Reset 引脚连线
bLocationRetryStarted = false;
// 对应蓝图:Reset Status by Timer
// ResetStatus();
}
// ================= 失败处理 =================
void AEnemy::HandleMoveToLocationFail()
{
UWorld* World = GetWorld();
if (!World) return;
FTimerManager& TimerMgr = World->GetTimerManager();
// 对应蓝图:Do Once 节点 (Start Closed = false)
if (!bLocationRetryStarted)
{
bLocationRetryStarted = true;
// 对应蓝图:Set Timer by Event (Time: 0.2, Looping: true)
TimerMgr.SetTimer(
TimerHandle_RestartApproachingLocation,
this,
&AEnemy::ExecuteMoveToLocation,
0.2f,
true
);
}
}