Published on

AI_MoveTo分析和扩展

UK2Node_AIMoveTo

这是我们最熟悉的编辑器模式下的AI_MoveTo节点,也就是那个自带OnSuccess和OnFailed的异步节点,在蓝图中大多数时候使用起来都得心应手非常方便,但是它有个最大的缺点,异步节点意味着他有自己的生命周期,不能放到函数中使用,同样的,在代码中也没有此类节点,那么接下来我们尝试解决这一问题

翻看此节点的代码,没几行代码,主要看如下

UK2Node_AIMoveTo::UK2Node_AIMoveTo(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UAIBlueprintHelperLibrary, CreateMoveToProxyObject);
	ProxyFactoryClass = UAIBlueprintHelperLibrary::StaticClass();
	ProxyClass = UAIAsyncTaskBlueprintProxy::StaticClass();
}

我们发现一个关键函数CreateMoveToProxyObject,找到该函数的原型

UAIBlueprintHelperLibrary::CreateMoveToProxyObject

static UAIAsyncTaskBlueprintProxy * CreateMoveToProxyObject
(
    UObject * WorldContextObject,
    APawn * Pawn,
    FVector Destination,
    AActor * TargetActor,
    float AcceptanceRadius,
    bool bStopOnOverlap
)

熟悉的参数,就是我们调用AIMoveTo的参数,继续翻代码

UAIBlueprintHelperLibrary::CreateMoveToProxyObject

UAIBlueprintHelperLibrary函数库的一个方法,返回UAIAsyncTaskBlueprintProxy对象

看关键代码

UAIAsyncTaskBlueprintProxy* MyObj = NULL;

这里需要注意一点,UAIAsyncTaskBlueprintProxy构造以后会存到AISystem内,防止GC销毁

FAIMoveRequest MoveReq;
		MoveReq.SetUsePathfinding(true);
		MoveReq.SetAcceptanceRadius(AcceptanceRadius);
		MoveReq.SetReachTestIncludesAgentRadius(bStopOnOverlap);
		if (TargetActor)
		{
			MoveReq.SetGoalActor(TargetActor);
		}
		else
		{
			MoveReq.SetGoalLocation(Destination);
		}
		MoveReq.SetNavigationFilter(AIController->GetDefaultNavigationFilterClass());
		
		FPathFollowingRequestResult ResultData = AIController->MoveTo(MoveReq);

简单说就是,创建并设置了关键的结构体FAIMoveRequest,然后塞给AIController->MoveTo(MoveReq);至于AIController干了什么,我们后面再说

		FPathFollowingRequestResult ResultData = AIController->MoveTo(MoveReq);
		switch (ResultData.Code)
		{
		case EPathFollowingRequestResult::RequestSuccessful:
			MyObj->AIController = AIController;
			MyObj->AIController->ReceiveMoveCompleted.AddDynamic(MyObj, &UAIAsyncTaskBlueprintProxy::OnMoveCompleted);
			MyObj->MoveRequestId = ResultData.MoveId;
			break;

		case EPathFollowingRequestResult::AlreadyAtGoal:
			World->GetTimerManager().SetTimer(MyObj->TimerHandle_OnInstantFinish, MyObj, &UAIAsyncTaskBlueprintProxy::OnAtGoal, 0.1f, false);
			break;

		case EPathFollowingRequestResult::Failed:
		default:
			World->GetTimerManager().SetTimer(MyObj->TimerHandle_OnInstantFinish, MyObj, &UAIAsyncTaskBlueprintProxy::OnNoPath, 0.1f, false);
			break;
		}

Controller MoveTo方法返回一个结果结构体FPathFollowingRequestResult,这里用到结构体内的Code参数,看申明类型是TEnumAsByte<EPathFollowingRequestResult::Type> Code

看一下这个枚举

UENUM(BlueprintType)
namespace EPathFollowingRequestResult
{
	enum Type
	{
		Failed,
		AlreadyAtGoal,
		RequestSuccessful
	};
}

也就3个成员,分别是失败/已经到达/请求成功

  • 对于3个结果分别绑定到本地3个函数
    • 请求成功:监听AIController内的ReceiveMoveCompleted,绑定到本地OnMoveCompleted,移动结果通过本地多播代理的OnFail 或者OnSuccess广播
    • 失败:0.1秒后执行本地OnNoPath事件,广播OnFail,然后从AISystem移除
    • 已经到达:0.1秒后执行本地事件OnAtGoal,广播OnSuccess,然后从AISystem移除

总结

创建一个代理类,通过AIController执行移动事件,绑定了AIController的移动结果回调事件,把结果通过此代理的2个代理广播

至于为什么用2个类型一样的代理,跟K2Node的使用有关系,理论上用1个代理可以实现一样的作用

封装蓝图方法

代理类

我们先来创建一个UObject类,作为一个移动代理,毕竟移动有生命周期,必须要有个类来管理和监听

//.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Navigation/PathFollowingComponent.h"
#include "AIMoveProxy.generated.h"
DECLARE_DYNAMIC_DELEGATE_OneParam(FMoveEnd, EPathFollowingResult::Type, MovementResult);

class UAIAsyncTaskBlueprintProxy;
class APawn;
UCLASS(BlueprintType,Blueprintable)
class INPUTSYS_API UAIMoveProxy : public UObject
{
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintReadWrite)
	UAIAsyncTaskBlueprintProxy* Proxy;

	UFUNCTION(BlueprintCallable)
		bool Proxy_AIMoveTo(APawn* Pawn,FVector Des,AActor* Target,const FMoveEnd& MoveResult,float AcceptanceRadius=5.0,bool StopOnOverlap=false);
	UFUNCTION()
		void MoveEnd(EPathFollowingResult::Type Result);

	UPROPERTY()
	FMoveEnd OnMoveEnd;
};

//.cpp


#include "AIMoveProxy.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "Blueprint/AIAsyncTaskBlueprintProxy.h"

bool UAIMoveProxy::Proxy_AIMoveTo(APawn* Pawn, FVector Des, AActor* Target, const FMoveEnd& MoveResult, float AcceptanceRadius/*=5.0*/, bool StopOnOverlap/*=false*/)
{
	if (!Pawn)
	{
		return false;
	}
	OnMoveEnd = MoveResult;
	Proxy = UAIBlueprintHelperLibrary::CreateMoveToProxyObject(Pawn, Pawn, Des, Target, AcceptanceRadius, StopOnOverlap);
	Proxy->OnFail.AddDynamic(this,&UAIMoveProxy::MoveEnd);
	Proxy->OnSuccess.AddDynamic(this, &UAIMoveProxy::MoveEnd);



	return true;
}

void UAIMoveProxy::MoveEnd(EPathFollowingResult::Type Result)
{
	OnMoveEnd.ExecuteIfBound(Result);
}

到蓝图实现如下图

函数库

但是这样还是不是很方便,每次都要手动创建一个类,于是就封装到了函数库里

//.h

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "AIMoveProxy.h"
#include "FLib_AI.generated.h"

/**
 * 
 */
UCLASS(Blueprintable,BlueprintType)
class INPUTSYS_API UFLib_AI : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable)
	static UAIMoveProxy* V_AI_MoveTo(APawn* Pawn, FVector Des, AActor* Target, const FMoveEnd& MoveResult, float AcceptanceRadius = 5.0, bool StopOnOverlap = false);
};

//.cpp
#include "FLib_AI.h"

UAIMoveProxy* UFLib_AI::V_AI_MoveTo(APawn* Pawn, FVector Des, AActor* Target, const FMoveEnd& MoveResult, float AcceptanceRadius, bool StopOnOverlap)
{
	if (!Pawn) { return nullptr; };
	UAIMoveProxy* p = `NewObject<UAIMoveProxy>`(Pawn);
	p->Proxy_AIMoveTo(Pawn, Des, Target, MoveResult, AcceptanceRadius, StopOnOverlap);

	return p;
}

使用如下图

简单剖析一下

AIController

构造

PathFollowingComponent->OnRequestFinished.AddUObject(this, &AAIController::OnMoveCompleted);

绑定了UPathFollowingComponent的完成事件,我们上面代理监听的事件是从这里来的

MoveTo

先贴代码配注释

FPathFollowingRequestResult AAIController::MoveTo(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr* OutPath)
{
	// MoveToActor和MoveToLocation都可以从蓝图/脚本中调用,并且应仅同时保留单个移动请求。
	// 此功能是所有运动机制的切入点-请勿在此处中止操作,因为运动可能由支持堆叠的AITasks处理

	SCOPE_CYCLE_COUNTER(STAT_MoveTo);
	UE_VLOG(this, LogAINavigation, Log, TEXT("MoveTo: %s"), *MoveRequest.ToString());

	FPathFollowingRequestResult ResultData;
	ResultData.Code = EPathFollowingRequestResult::Failed;

	if (MoveRequest.IsValid() == false)//判断请求是否有效,依据是有目标actor类或者不是移动到actor即坐标
	{
		UE_VLOG(this, LogAINavigation, Error, TEXT("MoveTo request failed due MoveRequest not being valid. Most probably desireg Goal Actor not longer exists"), *MoveRequest.ToString());
		return ResultData;
	}

	if (PathFollowingComponent == nullptr)//判断Path组件,默认就有挂载
	{
		UE_VLOG(this, LogAINavigation, Error, TEXT("MoveTo request failed due missing PathFollowingComponent"));
		return ResultData;
	}
//确保有寻路过滤器
	ensure(MoveRequest.GetNavigationFilter() || !DefaultNavigationFilterClass);

	bool bCanRequestMove = true;
	bool bAlreadyAtGoal = false;
    if (!MoveRequest.IsMoveToActorRequest())//移动到坐标点
	{
        //确保没有垃圾值,以及点在AI系统的最大边界值之内
		if (MoveRequest.GetGoalLocation().ContainsNaN() || FAISystem::IsValidLocation(MoveRequest.GetGoalLocation()) == false)
		{
			UE_VLOG(this, LogAINavigation, Error, TEXT("AAIController::MoveTo: Destination is not valid! Goal(%s)"), TEXT_AI_LOCATION(MoveRequest.GetGoalLocation()));
			bCanRequestMove = false;
		}

		//确保开启投射,这个参数在请求数据内可以通过set方法设置,默认开启
		if (bCanRequestMove && MoveRequest.IsProjectingGoal())
		{
            //得到寻路系统
			UNavigationSystemV1* NavSys = FNavigationSystem::`GetCurrent<UNavigationSystemV1>`(GetWorld());
            //得到寻路数据,保存于NavMovementComponent内
			const FNavAgentProperties& AgentProps = GetNavAgentPropertiesRef();
			FNavLocation ProjectedLocation;//寻路位置,存一个位置和一个数据元素(uint64)
			//确保投射成功,这里给了一个无效extent,在内部有做判断,使用默认寻路数据的extent
			if (NavSys && !NavSys->ProjectPointToNavigation(MoveRequest.GetGoalLocation(), ProjectedLocation, INVALID_NAVEXTENT, &AgentProps))
			{
				UE_VLOG_LOCATION(this, LogAINavigation, Error, MoveRequest.GetGoalLocation(), 30.f, FColor::Red, TEXT("AAIController::MoveTo failed to project destination location to navmesh"));
				bCanRequestMove = false;
			}
//刷新目标点为寻路点
			MoveRequest.UpdateGoalLocation(ProjectedLocation.Location);
		}
//判断是否到达,具体逻辑在UPathFollowingComponent内,稍复杂
		bAlreadyAtGoal = bCanRequestMove && PathFollowingComponent->HasReached(MoveRequest);
	}
    else 
	{
		bAlreadyAtGoal = bCanRequestMove && PathFollowingComponent->HasReached(MoveRequest);
	}
//如果已经到达就设置结果参数
	if (bAlreadyAtGoal)
	{
		UE_VLOG(this, LogAINavigation, Log, TEXT("MoveTo: already at goal!"));
		ResultData.MoveId = PathFollowingComponent->RequestMoveWithImmediateFinish(EPathFollowingResult::Success);
		ResultData.Code = EPathFollowingRequestResult::AlreadyAtGoal;
	}
    //前面判断都过了,就得到建立寻路查询
	else if (bCanRequestMove)
	{
		FPathFindingQuery PFQuery;

		const bool bValidQuery = BuildPathfindingQuery(MoveRequest, PFQuery);
		if (bValidQuery)
		{
			FNavPathSharedPtr Path;
			FindPathForMoveRequest(MoveRequest, PFQuery, Path);
//RequestMove方法来提交移动的请求
			const FAIRequestID RequestID = Path.IsValid() ? RequestMove(MoveRequest, Path) : FAIRequestID::InvalidRequest;
			if (RequestID.IsValid())
			{
				bAllowStrafe = MoveRequest.CanStrafe();
				ResultData.MoveId = RequestID;
				ResultData.Code = EPathFollowingRequestResult::RequestSuccessful;

				if (OutPath)
				{
					*OutPath = Path;
				}
			}
		}
	}

	if (ResultData.Code == EPathFollowingRequestResult::Failed)
	{
		ResultData.MoveId = PathFollowingComponent->RequestMoveWithImmediateFinish(EPathFollowingResult::Invalid);
	}

	return ResultData;

上面的MoveTo完成了移动的请求即RequestMove,但是具体哪里在执行角色的移动呢,继续翻代码

RequestMove

FAIRequestID AAIController::RequestMove(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr Path)
{
	uint32 RequestID = FAIRequestID::InvalidRequest;
	if (PathFollowingComponent)
	{
		RequestID = PathFollowingComponent->RequestMove(MoveRequest, Path);
	}

	return RequestID;
}

PathFollowingComponent

一顿操作以后调用UPathFollowingComponent::RequestMove

代码不贴全,茫茫多的判断以后来到关键的地方


			if (!bIsUsingMetaPath && Path.IsValid() && Path->IsValid())
			{
				Status = EPathFollowingStatus::Moving;

				// determine with path segment should be followed
				const uint32 CurrentSegment = DetermineStartingPathPoint(InPath.Get());
				SetMoveSegment(CurrentSegment);
			}
			else
			{
				Status = EPathFollowingStatus::Waiting;
				GetWorld()->GetTimerManager().SetTimer(WaitingForPathTimer, this, &UPathFollowingComponent::OnWaitingPathTimeout, WaitingTimeout);//等待以后结束寻路移动
			}

艰难通过前面几道判断(什么Idle,Paused等等我们先不管)以后,把状态参数Status改为Moving

然后呢?然后这个函数就结束了,继续翻,发现内容就在Tick里面

TickComponent

if (Status == EPathFollowingStatus::Moving)
	{
		// 判断是否结束以及刷新分段
		UpdatePathSegment();
	}

	if (Status == EPathFollowingStatus::Moving)
	{
		//这个是执行移动的地方
		FollowPathSegment(DeltaTime);
	}

很好奇,为什么同样一个if判断用2次?

继续翻FolowPathSegment

FolowPathSegment

完整代码

void UPathFollowingComponent::FollowPathSegment(float DeltaTime)
{
	if (!Path.IsValid() || MovementComp == nullptr)
	{
		return;
	}

	const FVector CurrentLocation = MovementComp->GetActorFeetLocation();
	const FVector CurrentTarget = GetCurrentTargetLocation();
	
	// set to false by default, we will set set this back to true if appropriate
	bIsDecelerating = false;

	const bool bAccelerationBased = MovementComp->UseAccelerationForPathFollowing();
	if (bAccelerationBased)
	{
		CurrentMoveInput = (CurrentTarget - CurrentLocation).GetSafeNormal();

		if (MoveSegmentStartIndex >= DecelerationSegmentIndex)
		{
			const FVector PathEnd = Path->GetEndLocation();
			const float DistToEndSq = FVector::DistSquared(CurrentLocation, PathEnd);
			const bool bShouldDecelerate = DistToEndSq < FMath::Square(CachedBrakingDistance);
			if (bShouldDecelerate)
			{
				bIsDecelerating = true;

				const float SpeedPct = FMath::Clamp(FMath::Sqrt(DistToEndSq) / CachedBrakingDistance, 0.0f, 1.0f);
				CurrentMoveInput *= SpeedPct;
			}
		}

		PostProcessMove.ExecuteIfBound(this, CurrentMoveInput);
		MovementComp->RequestPathMove(CurrentMoveInput);
	}
	else
	{
		FVector MoveVelocity = (CurrentTarget - CurrentLocation) / DeltaTime;

		const int32 LastSegmentStartIndex = Path->GetPathPoints().Num() - 2;
		const bool bNotFollowingLastSegment = (MoveSegmentStartIndex < LastSegmentStartIndex);

		PostProcessMove.ExecuteIfBound(this, MoveVelocity);
		MovementComp->RequestDirectMove(MoveVelocity, bNotFollowingLastSegment);
	}
}

主要就看2个函数MovementComp->RequestPathMove(CurrentMoveInput);MovementComp->RequestDirectMove(MoveVelocity, bNotFollowingLastSegment);

  • 前者点进去以后啥也没有,原因是基类不做实现,提示至少要从PawnMovementComponent开始才有实现

看一下PawnMovementComponent 的实现

  • 后者简单设置了一下Velocity变量

OnPathFinished

计算寻路的时候各种判断,最后通过OnRequestFinished广播给监听者

小结

AI_MoveTo通过AIController调用UPathFollowingComponent的移动请求方法

AIControllerUPathFollowingComponent得到移动请求后,调用了Pawn内的移动组件的移动方法

PawnMovementComponent

void UPawnMovementComponent::RequestPathMove(const FVector& MoveInput)
{
	if (PawnOwner)
	{
		PawnOwner->Internal_AddMovementInput(MoveInput);
	}
}

绕了一大圈结果Pawn就是通过AddMovement执行路径移动,关于DirectMoveUPawnMovementComponent 中没有实现

继续看看UCharacterMovementComponent

UCharacterMovementComponent

主要执行移动的函数在PerformMovement里,此函数在TickComponent中调用

另外无意中发现了跟其他内容有关系的代码

if (MovementMode == MOVE_None || UpdatedComponent->Mobility != EComponentMobility::Movable || UpdatedComponent->IsSimulatingPhysics())

如果MovementMode设置了None 或者开启物理模拟了就不能移动

if (bHasRootMotionSources && !CharacterOwner->bClientUpdating && !CharacterOwner->bServerMoveIgnoreRootMotion)
		{
			if( CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh() )
			{

RootMotion动画优先于移动

if (bHasAuthority && UNetDriver::IsAdaptiveNetUpdateFrequencyEnabled() && UpdatedComponent)

只对权威角色开启移动效果

主要通过MaybeUpdateBasedMovement或者MoveUpdatedComponent方法(在rootmotion情况下)等方法最后调用到方法MoveUpdatedComponentImpl,然后调用 UpdatedComponent->MoveComponent

另外

设置了当前的Velocity以及位置和旋转,也存储了Last和New2份

这个Velocity其实就是2帧渲染的位置插值

Pawn

AddMovement

//Pawn.h
void Internal_AddMovementInput(FVector WorldAccel, bool bForce = false);

//******************//
ControlInputVector += WorldAccel;

如果有UCharacterMovementComponent就调用组件里的方法(组件也是直接调用Pawn的内部方法)否则调用自身的内部方法Internal_AddMovementInput

同理用于Get方法

改变的这个参数用于派生类计算速度值Velocity用于,例如UFloatingPawnMovement::ApplyControlInputToVelocity

速度计算要通过USceneComponent

刷新位置顺序

用上面的速度变量来刷新位置信息

先看调用顺序,从移动组件的Tick开始

  1. PerformMovement(DeltaTime)
  2. MovementComponent::MoveUpdatedComponentImpl(const FVector& Delta,...)
  3. UpdatedComponent(MovementComponent的parent的rootcomp)->MoveComponent()
  4. UpdatedComponent::MoveComponentImpl()
  5. UpdateComponentToWorld()
  6. UpdateComponentToWorldWithParent()
  7. PropagateTransformUpdate()

到这开始刷新根组件即parent的root的位置、渲染、体积、寻路等信息,同时也刷新子组件的信息,之后引擎的处理可以暂时参考由SetActorLocation分析渲染流程

其他移动方法

  1. 诸如SimpleMoveToLocationAIController内的方法最后都是调用了PathFollowingComponent::RequestMove

  2. AI行为的Task节点,最终调用的也是AIControllerMoveTo方法

总结

2020.12.31补充

玩家使用SimpleMoveTo的回调事件

AIMoveTo不能作用于玩家, 关键在于CreateMoveToProxyObject函数中的

AAIController* AIController = `Cast<AAIController>`(Pawn->GetController());

Simple版本可以作用给玩家, 例如Topdown模板内就用此来移动玩家, 但是Simple版本的2个函数没有回调事件, 如果想要得到回调事件如何做?

查看SimpleMoveToActor()

UPathFollowingComponent* PFollowComp = InitNavigationControl(*Controller);
UPathFollowingComponent* InitNavigationControl(AController& Controller)
	{
		AAIController* AsAIController = `Cast<AAIController>`(&Controller);
		UPathFollowingComponent* PathFollowingComp = nullptr;

		if (AsAIController)
		{
			PathFollowingComp = AsAIController->GetPathFollowingComponent();
		}
		else
		{
			PathFollowingComp = Controller.`FindComponentByClass<UPathFollowingComponent>`();
			if (PathFollowingComp == nullptr)
			{
				PathFollowingComp = `NewObject<UPathFollowingComponent>`(&Controller);
				PathFollowingComp->RegisterComponentWithWorld(Controller.GetWorld());
				PathFollowingComp->Initialize();
			}
		}

		return PathFollowingComp;
	}

上面代码看到如果不是AIController也可以, 所以我们玩家才可以使用Simple版本的移动

那么回调事件其实也在该组件内, 即我们需要一个PathFollowingComp

在我们的PlayerController

ASRPlayerController::ASRPlayerController(const FObjectInitializer& ObjectInitializer) :Super(ObjectInitializer)
{
	PathFollowingComponent = `CreateDefaultSubobject<UPathFollowingComponent>`(TEXT("PathFollowingComponent"));
}

PathFollowingComponent->OnRequestFinished.AddUObject(this, &ASRPlayerController::MoveComplete);
void ASRPlayerController::MoveComplete(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
	if (Result.IsSuccess())
	{
		ReceiveMoveComplete(true,CurrentMoveType);
	}
	else
	{
		ReceiveMoveComplete(false, CurrentMoveType);
	}
	CurrentMoveType = ESRMoveToType::None;
}

void ASRPlayerController::ReceiveMoveComplete_Implementation(bool bIsSuccess, ESRMoveToType MoveType)
{
	CurrentMoveType = ESRMoveToType::None;
}

然后蓝图就可以通过事件ReceiveMoveComplete_Implementation得到移动结束的消息, 当然有很多扩展方式, 你完全可以自己继承一个PathFollowingComp来做动态代理广播的方式


停止距离

问题来了, SimpleMoveToActor没有类似AIMoveTo的停止距离, 也不方便

扩展

	UFUNCTION(BlueprintCallable, Category = "SR|Lib|Movement")
		static void SimpleMoveToForAllController(AController* Controller, FVector Destination, AActor* Target, float AcceptanceRadius = 5.0f, bool StopOnOverlap= false);
void UFlib_SRUtilities::SimpleMoveToForAllController(AController* Controller, FVector Destination, AActor* Target, float AcceptanceRadius /*= 5.0f*/, bool StopOnOverlap/*= false*/)
{
	UNavigationSystemV1* NavSys = Controller ? FNavigationSystem::`GetCurrent<UNavigationSystemV1>`(Controller->GetWorld()) : nullptr;
	if (NavSys == nullptr || Controller == nullptr || Controller->GetPawn() == nullptr)
	{
		SRWARNING(TEXT("SimpleMoveToForAllController [no NavSys or Controller or pawn]"));
		return;
	}

	UPathFollowingComponent* PFollowComp = Controller->`FindComponentByClass<UPathFollowingComponent>`();

	if (PFollowComp == nullptr)
	{
		SRWARNING(TEXT("SimpleMoveToForAllController [no UPathFollowingComponent]"));
		return;
	}

	if (!PFollowComp->IsPathFollowingAllowed())
	{
		SRWARNING(TEXT("SimpleMoveToForAllController [not IsPathFollowingAllowed()]"));
		return;
	}

	bool bAlreadyAtGoal = false;
	if (Target)
	{
		bAlreadyAtGoal = PFollowComp->HasReached(*Target, EPathFollowingReachMode::OverlapAgentAndGoal);
	}
	else
	{
		bAlreadyAtGoal = PFollowComp->HasReached(Destination, EPathFollowingReachMode::OverlapAgentAndGoal);
	}



	if (PFollowComp->GetStatus() != EPathFollowingStatus::Idle)
	{
		PFollowComp->AbortMove(*NavSys, FPathFollowingResultFlags::ForcedScript | FPathFollowingResultFlags::NewRequest
			, FAIRequestID::AnyRequest, bAlreadyAtGoal ? EPathFollowingVelocityMode::Reset : EPathFollowingVelocityMode::Keep);
	}

	if (bAlreadyAtGoal)
	{
		PFollowComp->RequestMoveWithImmediateFinish(EPathFollowingResult::Success);
	}
	else
	{
		const FVector AgentNavLocation = Controller->GetNavAgentLocation();
		const ANavigationData* NavData = NavSys->GetNavDataForProps(Controller->GetNavAgentPropertiesRef(), AgentNavLocation);
		if (NavData)
		{

			FPathFindingQuery Query(Controller, *NavData, AgentNavLocation, Target?Target->GetActorLocation():Destination);
			FPathFindingResult Result = NavSys->FindPathSync(Query);

			if (Result.IsSuccessful())
			{
				Result.Path->SetGoalActorObservation(*Target, AcceptanceRadius);
				//这里不同于Simple版本的方式
				FAIMoveRequest MoveReq;
				MoveReq.SetUsePathfinding(true);
				MoveReq.SetAcceptanceRadius(AcceptanceRadius);
				MoveReq.SetReachTestIncludesAgentRadius(StopOnOverlap);
				if (Target)
				{
					MoveReq.SetGoalActor(Target);
				}
				else
				{
					MoveReq.SetGoalLocation(Destination);
				}

				PFollowComp->RequestMove(MoveReq, Result.Path);
			}
			else if (PFollowComp->GetStatus() != EPathFollowingStatus::Idle)
			{
				PFollowComp->RequestMoveWithImmediateFinish(EPathFollowingResult::Invalid);
			}

		}
	}
}