Files
UnrealEngine/Engine/Source/Runtime/AIModule/Private/Tasks/AITask_MoveTo.cpp
2025-05-18 13:04:45 +08:00

387 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Tasks/AITask_MoveTo.h"
#include "UObject/Package.h"
#include "TimerManager.h"
#include "AISystem.h"
#include "AIController.h"
#include "VisualLogger/VisualLogger.h"
#include "AIResources.h"
#include "GameplayTasksComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AITask_MoveTo)
UAITask_MoveTo::UAITask_MoveTo(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsPausable = true;
MoveRequestID = FAIRequestID::InvalidRequest;
MoveRequest.SetAcceptanceRadius(GET_AI_CONFIG_VAR(AcceptanceRadius));
MoveRequest.SetReachTestIncludesAgentRadius(GET_AI_CONFIG_VAR(bFinishMoveOnGoalOverlap));
MoveRequest.SetAllowPartialPath(GET_AI_CONFIG_VAR(bAcceptPartialPaths));
MoveRequest.SetUsePathfinding(true);
AddRequiredResource(UAIResource_Movement::StaticClass());
AddClaimedResource(UAIResource_Movement::StaticClass());
MoveResult = EPathFollowingResult::Invalid;
bUseContinuousTracking = false;
}
UAITask_MoveTo* UAITask_MoveTo::AIMoveTo(AAIController* Controller, FVector InGoalLocation, AActor* InGoalActor,
float AcceptanceRadius, EAIOptionFlag::Type StopOnOverlap, EAIOptionFlag::Type AcceptPartialPath,
bool bUsePathfinding, bool bLockAILogic, bool bUseContinuousGoalTracking, EAIOptionFlag::Type ProjectGoalOnNavigation,
EAIOptionFlag::Type RequireNavigableEndLocation)
{
UAITask_MoveTo* MyTask = Controller ? UAITask::NewAITask<UAITask_MoveTo>(*Controller, EAITaskPriority::High) : nullptr;
if (MyTask)
{
FAIMoveRequest MoveReq;
if (InGoalActor)
{
MoveReq.SetGoalActor(InGoalActor);
}
else
{
MoveReq.SetGoalLocation(InGoalLocation);
}
MoveReq.SetAcceptanceRadius(AcceptanceRadius);
MoveReq.SetReachTestIncludesAgentRadius(FAISystem::PickAIOption(StopOnOverlap, MoveReq.IsReachTestIncludingAgentRadius()));
MoveReq.SetAllowPartialPath(FAISystem::PickAIOption(AcceptPartialPath, MoveReq.IsUsingPartialPaths()));
MoveReq.SetUsePathfinding(bUsePathfinding);
MoveReq.SetProjectGoalLocation(FAISystem::PickAIOption(ProjectGoalOnNavigation, MoveReq.IsProjectingGoal()));
MoveReq.SetRequireNavigableEndLocation(FAISystem::PickAIOption(RequireNavigableEndLocation, MoveReq.IsNavigableEndLocationRequired()));
if (Controller)
{
MoveReq.SetNavigationFilter(Controller->GetDefaultNavigationFilterClass());
MoveReq.SetCanStrafe(Controller->bAllowStrafe);
}
MyTask->SetUp(Controller, MoveReq);
MyTask->SetContinuousGoalTracking(bUseContinuousGoalTracking);
if (bLockAILogic)
{
MyTask->RequestAILogicLocking();
}
}
return MyTask;
}
void UAITask_MoveTo::SetUp(AAIController* Controller, const FAIMoveRequest& InMoveRequest)
{
OwnerController = Controller;
MoveRequest = InMoveRequest;
}
void UAITask_MoveTo::SetContinuousGoalTracking(bool bEnable)
{
bUseContinuousTracking = bEnable;
}
void UAITask_MoveTo::FinishMoveTask(EPathFollowingResult::Type InResult)
{
if (MoveRequestID.IsValid())
{
UPathFollowingComponent* PFComp = OwnerController ? OwnerController->GetPathFollowingComponent() : nullptr;
if (PFComp && PFComp->GetStatus() != EPathFollowingStatus::Idle)
{
ResetObservers();
PFComp->AbortMove(*this, FPathFollowingResultFlags::OwnerFinished, MoveRequestID);
}
}
MoveResult = InResult;
EndTask();
if (InResult == EPathFollowingResult::Invalid)
{
OnRequestFailed.Broadcast();
}
else
{
OnMoveFinished.Broadcast(InResult, OwnerController);
}
if (OnMoveTaskFinished.IsBound())
{
OnMoveTaskFinished.Broadcast(InResult, OwnerController);
}
}
void UAITask_MoveTo::Activate()
{
Super::Activate();
UE_CVLOG(bUseContinuousTracking, GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("Continuous goal tracking requested, moving to: %s"),
MoveRequest.IsMoveToActorRequest() ? TEXT("actor => looping successful moves!") : TEXT("location => will NOT loop"));
MoveRequestID = FAIRequestID::InvalidRequest;
ConditionalPerformMove();
}
void UAITask_MoveTo::ConditionalPerformMove()
{
if (MoveRequest.IsUsingPathfinding() && OwnerController && OwnerController->ShouldPostponePathUpdates())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> can't path right now, waiting..."), *GetName());
OwnerController->GetWorldTimerManager().SetTimer(MoveRetryTimerHandle, this, &UAITask_MoveTo::ConditionalPerformMove, 0.2f, false);
}
else
{
MoveRetryTimerHandle.Invalidate();
PerformMove();
}
}
void UAITask_MoveTo::PerformMove()
{
UPathFollowingComponent* PFComp = OwnerController ? OwnerController->GetPathFollowingComponent() : nullptr;
if (PFComp == nullptr)
{
FinishMoveTask(EPathFollowingResult::Invalid);
return;
}
ResetObservers();
ResetTimers();
// start new move request
FNavPathSharedPtr FollowedPath;
const FPathFollowingRequestResult ResultData = OwnerController->MoveTo(MoveRequest, &FollowedPath);
switch (ResultData.Code)
{
case EPathFollowingRequestResult::Failed:
FinishMoveTask(EPathFollowingResult::Invalid);
break;
case EPathFollowingRequestResult::AlreadyAtGoal:
MoveRequestID = ResultData.MoveId;
OnRequestFinished(ResultData.MoveId, FPathFollowingResult(EPathFollowingResult::Success, FPathFollowingResultFlags::AlreadyAtGoal));
break;
case EPathFollowingRequestResult::RequestSuccessful:
MoveRequestID = ResultData.MoveId;
PathFinishDelegateHandle = PFComp->OnRequestFinished.AddUObject(this, &UAITask_MoveTo::OnRequestFinished);
SetObservedPath(FollowedPath);
if (IsFinished())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Error, TEXT("%s> re-Activating Finished task!"), *GetName());
}
break;
default:
checkNoEntry();
break;
}
}
void UAITask_MoveTo::Pause()
{
if (OwnerController && MoveRequestID.IsValid())
{
OwnerController->PauseMove(MoveRequestID);
}
ResetTimers();
Super::Pause();
}
void UAITask_MoveTo::Resume()
{
Super::Resume();
if (!MoveRequestID.IsValid() || (OwnerController && !OwnerController->ResumeMove(MoveRequestID)))
{
UE_CVLOG(MoveRequestID.IsValid(), GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> Resume move failed, starting new one."), *GetName());
ConditionalPerformMove();
}
}
void UAITask_MoveTo::SetObservedPath(FNavPathSharedPtr InPath)
{
if (PathUpdateDelegateHandle.IsValid() && Path.IsValid())
{
Path->RemoveObserver(PathUpdateDelegateHandle);
}
PathUpdateDelegateHandle.Reset();
Path = InPath;
if (Path.IsValid())
{
// disable auto repaths, it will be handled by move task to include ShouldPostponePathUpdates condition
Path->EnableRecalculationOnInvalidation(false);
PathUpdateDelegateHandle = Path->AddObserver(FNavigationPath::FPathObserverDelegate::FDelegate::CreateUObject(this, &UAITask_MoveTo::OnPathEvent));
}
}
void UAITask_MoveTo::ResetObservers()
{
if (Path.IsValid())
{
Path->DisableGoalActorObservation();
}
if (PathFinishDelegateHandle.IsValid())
{
UPathFollowingComponent* PFComp = OwnerController ? OwnerController->GetPathFollowingComponent() : nullptr;
if (PFComp)
{
PFComp->OnRequestFinished.Remove(PathFinishDelegateHandle);
}
PathFinishDelegateHandle.Reset();
}
if (PathUpdateDelegateHandle.IsValid())
{
if (Path.IsValid())
{
Path->RemoveObserver(PathUpdateDelegateHandle);
}
PathUpdateDelegateHandle.Reset();
}
}
void UAITask_MoveTo::ResetTimers()
{
if (OwnerController)
{
// Remove all timers including the ones that might have been set with SetTimerForNextTick
OwnerController->GetWorldTimerManager().ClearAllTimersForObject(this);
}
MoveRetryTimerHandle.Invalidate();
PathRetryTimerHandle.Invalidate();
}
void UAITask_MoveTo::OnDestroy(bool bInOwnerFinished)
{
Super::OnDestroy(bInOwnerFinished);
ResetObservers();
ResetTimers();
if (MoveRequestID.IsValid())
{
UPathFollowingComponent* PFComp = OwnerController ? OwnerController->GetPathFollowingComponent() : nullptr;
if (PFComp && PFComp->GetStatus() != EPathFollowingStatus::Idle)
{
PFComp->AbortMove(*this, FPathFollowingResultFlags::OwnerFinished, MoveRequestID);
}
}
// clear the shared pointer now to make sure other systems
// don't think this path is still being used
Path = nullptr;
}
void UAITask_MoveTo::OnRequestFinished(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
if (RequestID == MoveRequestID)
{
if (Result.HasFlag(FPathFollowingResultFlags::UserAbort) && Result.HasFlag(FPathFollowingResultFlags::NewRequest) && !Result.HasFlag(FPathFollowingResultFlags::ForcedScript))
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> ignoring OnRequestFinished, move was aborted by new request"), *GetName());
}
else
{
// reset request Id, FinishMoveTask doesn't need to update path following's state
MoveRequestID = FAIRequestID::InvalidRequest;
if (bUseContinuousTracking && MoveRequest.IsMoveToActorRequest() && Result.IsSuccess())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> received OnRequestFinished and goal tracking is active! Moving again in next tick"), *GetName());
GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UAITask_MoveTo::PerformMove);
}
else
{
FinishMoveTask(Result.Code);
}
}
}
else if (IsActive())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Warning, TEXT("%s> received OnRequestFinished with not matching RequestID!"), *GetName());
}
}
void UAITask_MoveTo::OnPathEvent(FNavigationPath* InPath, ENavPathEvent::Type Event)
{
const static UEnum* NavPathEventEnum = StaticEnum<ENavPathEvent::Type>();
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> Path event: %s"), *GetName(), *NavPathEventEnum->GetNameStringByValue(Event));
switch (Event)
{
case ENavPathEvent::NewPath:
case ENavPathEvent::UpdatedDueToGoalMoved:
case ENavPathEvent::UpdatedDueToNavigationChanged:
if (InPath && InPath->IsPartial() && !MoveRequest.IsUsingPartialPaths())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT(">> partial path is not allowed, aborting"));
UPathFollowingComponent::LogPathHelper(OwnerController, InPath, MoveRequest.GetGoalActor());
FinishMoveTask(EPathFollowingResult::Aborted);
}
#if ENABLE_VISUAL_LOG
else if (!IsActive())
{
UPathFollowingComponent::LogPathHelper(OwnerController, InPath, MoveRequest.GetGoalActor());
}
#endif // ENABLE_VISUAL_LOG
break;
case ENavPathEvent::Invalidated:
ConditionalUpdatePath();
break;
case ENavPathEvent::Cleared:
case ENavPathEvent::RePathFailed:
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT(">> no path, aborting!"));
FinishMoveTask(EPathFollowingResult::Aborted);
break;
case ENavPathEvent::MetaPathUpdate:
default:
break;
}
}
void UAITask_MoveTo::ConditionalUpdatePath()
{
// mark this path as waiting for repath so that PathFollowingComponent doesn't abort the move while we
// micro manage repathing moment
// note that this flag fill get cleared upon repathing end
if (Path.IsValid())
{
Path->SetManualRepathWaiting(true);
}
if (MoveRequest.IsUsingPathfinding() && OwnerController && OwnerController->ShouldPostponePathUpdates())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> can't path right now, waiting..."), *GetName());
OwnerController->GetWorldTimerManager().SetTimer(PathRetryTimerHandle, this, &UAITask_MoveTo::ConditionalUpdatePath, 0.2f, false);
}
else
{
PathRetryTimerHandle.Invalidate();
ANavigationData* NavData = Path.IsValid() ? Path->GetNavigationDataUsed() : nullptr;
if (NavData)
{
NavData->RequestRePath(Path, ENavPathUpdateType::NavigationChanged);
}
else
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> unable to repath, aborting!"), *GetName());
FinishMoveTask(EPathFollowingResult::Aborted);
}
}
}