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

2018 lines
68 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Navigation/PathFollowingComponent.h"
#include "AbstractNavData.h"
#include "AIConfig.h"
#include "AIController.h"
#include "AISystem.h"
#include "BrainComponent.h"
#include "Engine/Canvas.h"
#include "GameFramework/Controller.h"
#include "GameFramework/Pawn.h"
#include "NavFilters/NavigationQueryFilter.h"
#include "Navigation/MetaNavMeshPath.h"
#include "NavigationSystem.h"
#include "NavLinkCustomInterface.h"
#include "NavMesh/RecastNavMesh.h"
#include "TimerManager.h"
#include "UObject/Package.h"
#include "VisualLogger/VisualLogger.h"
#include "VisualLogger/VisualLoggerTypes.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PathFollowingComponent)
#if UE_BUILD_TEST || UE_BUILD_SHIPPING
#define SHIPPING_STATIC static
#else
#define SHIPPING_STATIC
#endif // UE_BUILD_TEST || UE_BUILD_SHIPPING
DEFINE_LOG_CATEGORY(LogPathFollowing);
namespace UE::Navigation::Private
{
FORCEINLINE FVector FindGoalLocation(const UPathFollowingComponent& Component, const AActor& GoalActor, const INavAgentInterface* GoalNavAgent, float& GoalRadius, float& GoalHalfHeight)
{
if (GoalNavAgent)
{
const AActor* OwnerActor = Component.GetOwner();
FVector GoalOffset;
GoalNavAgent->GetMoveGoalReachTest(OwnerActor, Component.GetMoveGoalLocationOffset(), GoalOffset, GoalRadius, GoalHalfHeight);
return FQuatRotationTranslationMatrix(GoalActor.GetActorQuat(), GoalNavAgent->GetNavAgentLocation()).TransformPosition(GoalOffset);
}
else
{
return GoalActor.GetActorLocation();
}
}
}
FPathFollowingResult::FPathFollowingResult(uint16 InFlags) : Flags(InFlags)
{
Code =
HasFlag(FPathFollowingResultFlags::Success) && !HasFlag(FPathFollowingResultFlags::Blocked | FPathFollowingResultFlags::OffPath | FPathFollowingResultFlags::UserAbort) ? EPathFollowingResult::Success :
HasFlag(FPathFollowingResultFlags::Blocked) && !HasFlag(FPathFollowingResultFlags::Success | FPathFollowingResultFlags::OffPath | FPathFollowingResultFlags::UserAbort) ? EPathFollowingResult::Blocked :
HasFlag(FPathFollowingResultFlags::OffPath) && !HasFlag(FPathFollowingResultFlags::Success | FPathFollowingResultFlags::Blocked | FPathFollowingResultFlags::UserAbort) ? EPathFollowingResult::OffPath :
HasFlag(FPathFollowingResultFlags::UserAbort) && !HasFlag(FPathFollowingResultFlags::Success | FPathFollowingResultFlags::Blocked | FPathFollowingResultFlags::OffPath) ? EPathFollowingResult::Aborted :
EPathFollowingResult::Invalid;
}
FPathFollowingResult::FPathFollowingResult(EPathFollowingResult::Type ResultCode, uint16 ExtraFlags) : Code(ResultCode)
{
static uint16 CodeLookup[6] = {
FPathFollowingResultFlags::Success, // EPathFollowingResult::Success
FPathFollowingResultFlags::Blocked, // EPathFollowingResult::Blocked
FPathFollowingResultFlags::OffPath, // EPathFollowingResult::OffPath
FPathFollowingResultFlags::UserAbort, // EPathFollowingResult::Aborted
0, // EPathFollowingResult::Skipped_DEPRECATED
0, // EPathFollowingResult::Invalid
};
Flags = CodeLookup[ResultCode] | ExtraFlags;
}
FString FPathFollowingResult::ToString() const
{
static UEnum* ResultEnum = StaticEnum<EPathFollowingResult::Type>();
return FString::Printf(TEXT("%s[%s]"), ResultEnum ? *ResultEnum->GetNameStringByValue(Code) : TEXT("??"), *FPathFollowingResultFlags::ToString(Flags));
}
FString FPathFollowingResultFlags::ToString(uint16 Value)
{
static FString FlagDesc[] = {
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, Success),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, Blocked),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, OffPath),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, UserAbort),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, OwnerFinished),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, InvalidPath),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, MovementStop),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, NewRequest),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, ForcedScript),
GET_FUNCTION_NAME_STRING_CHECKED(FPathFollowingResultFlags, AlreadyAtGoal),
};
FString CombinedDesc;
for (int32 Idx = 0; Idx < FirstGameplayFlagShift; Idx++)
{
if (Value & (1 << Idx))
{
CombinedDesc += FlagDesc[Idx];
CombinedDesc += TEXT(' ');
}
}
Value = Value >> FirstGameplayFlagShift;
for (int32 Idx = 0; Value; Idx++, Value /= 2)
{
if (Value & 1)
{
CombinedDesc += TEXT("Game");
CombinedDesc += TTypeToString<int32>::ToString(Idx);
CombinedDesc += TEXT(' ');
}
}
return CombinedDesc.LeftChop(1);
}
//----------------------------------------------------------------------//
// Life cycle
//----------------------------------------------------------------------//
uint32 UPathFollowingComponent::NextRequestId = 1;
const float UPathFollowingComponent::DefaultAcceptanceRadius = -1.f;
UPathFollowingComponent::UPathFollowingComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
PrimaryComponentTick.bCanEverTick = true;
// Activation/Ticking is controlled by Status if this instance is of type UPathFollowingComponent (not a derived class)
bTickComponentOnlyWhenMoving = StaticClass() == GetClass();
PrimaryComponentTick.bStartWithTickEnabled = !bTickComponentOnlyWhenMoving;
bAutoActivate = !bTickComponentOnlyWhenMoving;
MinAgentRadiusPct = 1.1f;
MinAgentHalfHeightPct = 1.05f;
BlockDetectionDistance = 10.0f;
BlockDetectionInterval = 0.5f;
BlockDetectionSampleCount = 10;
WaitingTimeout = 1.0f;
LastSampleTime = 0.0f;
NextSampleIdx = 0;
bUseBlockDetection = true;
bLastMoveReachedGoal = false;
bStopMovementOnFinish = true;
bIsUsingMetaPath = false;
MoveSegmentStartIndex = 0;
MoveSegmentEndIndex = 1;
MoveSegmentStartRef = INVALID_NAVNODEREF;
MoveSegmentEndRef = INVALID_NAVNODEREF;
bMoveSegmentIsUsingCustomLinkReachCondition = false;
CachedBrakingDistance = 100.0f;
CachedBrakingMaxSpeed = 0.0f;
DecelerationSegmentIndex = INDEX_NONE;
bReachTestIncludesAgentRadius = true;
bReachTestIncludesGoalRadius = true;
bMoveToGoalOnLastSegment = true;
bMoveToGoalClampedToNavigation = false;
Status = EPathFollowingStatus::Idle;
CurrentMoveInput = FVector::ZeroVector;
#if !UE_BUILD_SHIPPING
DEBUG_bMovingDirectlyToGoal = false;
#endif // !UE_BUILD_SHIPPING
}
void UPathFollowingComponent::SetStatus(const EPathFollowingStatus::Type InStatus)
{
Status = InStatus;
if (bTickComponentOnlyWhenMoving)
{
if (Status == EPathFollowingStatus::Moving)
{
if (!IsActive())
{
Activate();
}
}
else
{
if (IsActive())
{
Deactivate();
}
}
}
}
void UPathFollowingComponent::LogPathHelper(const AActor* LogOwner, FNavigationPath* InLogPath, const AActor* LogGoalActor)
{
#if ENABLE_VISUAL_LOG
FVisualLogger& Vlog = FVisualLogger::Get();
if (Vlog.IsRecording() &&
InLogPath && InLogPath->IsValid() && InLogPath->GetPathPoints().Num())
{
const FVector PathEnd = *InLogPath->GetPathPointLocation(InLogPath->GetPathPoints().Num() - 1);
if (FVisualLogEntry* Entry = FVisualLogger::GetEntryToWrite(LogOwner, LogPathFollowing))
{
InLogPath->DescribeSelfToVisLog(Entry);
if (LogGoalActor)
{
const FVector GoalLoc = LogGoalActor->GetActorLocation();
if (FVector::DistSquared(GoalLoc, PathEnd) > 1.0f)
{
UE_VLOG_LOCATION(LogOwner, LogPathFollowing, Verbose, GoalLoc, 30, FColor::Green, TEXT("GoalActor"));
UE_VLOG_SEGMENT(LogOwner, LogPathFollowing, Verbose, GoalLoc, PathEnd, FColor::Green, TEXT_EMPTY);
}
}
}
UE_VLOG_BOX(LogOwner, LogPathFollowing, Verbose, FBox(PathEnd - FVector(30.0f), PathEnd + FVector(30.0f)), FColor::Green, TEXT("PathEnd"));
}
#endif // ENABLE_VISUAL_LOG
}
void UPathFollowingComponent::LogPathHelper(const AActor* LogOwner, FNavPathSharedPtr InLogPath, const AActor* LogGoalActor)
{
#if ENABLE_VISUAL_LOG
if (InLogPath.IsValid())
{
LogPathHelper(LogOwner, InLogPath.Get(), LogGoalActor);
}
#endif // ENABLE_VISUAL_LOG
}
void LogBlockHelper(AActor* LogOwner, INavMovementInterface* NavMoveInterface, float RadiusPct, float HeightPct, const FVector& SegmentStart, const FVector& SegmentEnd)
{
#if ENABLE_VISUAL_LOG
if (NavMoveInterface && LogOwner)
{
const FVector AgentLocation = NavMoveInterface->GetFeetLocation();
const FVector ToTarget = (SegmentEnd - AgentLocation);
const FVector::FReal SegmentDot = FVector::DotProduct(ToTarget.GetSafeNormal(), (SegmentEnd - SegmentStart).GetSafeNormal());
UE_VLOG(LogOwner, LogPathFollowing, Verbose, TEXT("[agent to segment end] dot [segment dir]: %f"), SegmentDot);
float AgentRadius = 0.0f;
float AgentHalfHeight = 0.0f;
NavMoveInterface->GetSimpleCollisionCylinder(AgentRadius, AgentHalfHeight);
const FVector::FReal Dist2D = ToTarget.Size2D();
UE_VLOG(LogOwner, LogPathFollowing, Verbose, TEXT("dist 2d: %f (agent radius: %f [%f])"), Dist2D, AgentRadius, AgentRadius * (1 + RadiusPct));
const FVector::FReal ZDiff = FMath::Abs(ToTarget.Z);
UE_VLOG(LogOwner, LogPathFollowing, Verbose, TEXT("Z diff: %f (agent halfZ: %f [%f])"), ZDiff, AgentHalfHeight, AgentHalfHeight * (1 + HeightPct));
}
#endif // ENABLE_VISUAL_LOG
}
FString GetPathDescHelper(FNavPathSharedPtr Path)
{
return !Path.IsValid() ? TEXT("missing") :
!Path->IsValid() ? TEXT("invalid") :
FString::Printf(TEXT("%s:%d"), Path->IsPartial() ? TEXT("partial") : TEXT("complete"), Path->GetPathPoints().Num());
}
void UPathFollowingComponent::OnPathEvent(FNavigationPath* InPath, ENavPathEvent::Type Event)
{
const static UEnum* NavPathEventEnum = StaticEnum<ENavPathEvent::Type>();
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("OnPathEvent: %s"), *NavPathEventEnum->GetNameStringByValue(Event));
if (InPath && Path.Get() == InPath)
{
switch (Event)
{
case ENavPathEvent::UpdatedDueToGoalMoved:
case ENavPathEvent::UpdatedDueToNavigationChanged:
case ENavPathEvent::MetaPathUpdate:
{
const bool bIsPathAccepted = HandlePathUpdateEvent();
if (!bIsPathAccepted)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT(">> updated path was not accepcted, aborting move"));
OnPathFinished(EPathFollowingResult::Aborted, FPathFollowingResultFlags::InvalidPath);
}
else if (InPath->IsPartial() && InPath->GetGoalActor())
{
float IgnoreGoalRadius, IgnoreGoalHalfHeight;
OriginalMoveRequestGoalLocation = UE::Navigation::Private::FindGoalLocation(*this, *InPath->GetGoalActor(), InPath->GetGoalActorAsNavAgent(), IgnoreGoalRadius, IgnoreGoalHalfHeight);
}
}
break;
default:
break;
}
}
}
bool UPathFollowingComponent::HandlePathUpdateEvent()
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("HandlePathUpdateEvent Path(%s) Status(%s)"), *GetPathDescHelper(Path), *GetStatusDesc());
LogPathHelper(GetOwner(), Path, DestinationActor.Get());
if (Status == EPathFollowingStatus::Waiting && Path.IsValid() && !Path->IsValid())
{
UE_VLOG(GetOwner(), LogPathFollowing, Error, TEXT("Received unusable path in Waiting state! (ready:%d upToDate:%d pathPoints:%d)"),
Path->IsReady() ? 1 : 0,
Path->IsUpToDate() ? 1 : 0,
Path->GetPathPoints().Num());
}
if (Status == EPathFollowingStatus::Idle || !Path.IsValid() || !Path->IsValid())
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT(">> invalid request"));
return false;
}
const AActor* PathGoalActor = Path->GetGoalActor();
if (PathGoalActor)
{
float IgnoreGoalRadius, IgnoreGoalHalfHeight;
OriginalMoveRequestGoalLocation = UE::Navigation::Private::FindGoalLocation(*this, *PathGoalActor, Path->GetGoalActorAsNavAgent(), IgnoreGoalRadius, IgnoreGoalHalfHeight);
}
if (FAISystem::IsValidLocation(OriginalMoveRequestGoalLocation))
{
PreciseAcceptanceRadiusCheckStartNodeIndex = FindPreciseAcceptanceRadiusTestsStartNodeIndex(*Path, OriginalMoveRequestGoalLocation);
}
OnPathUpdated();
GetWorld()->GetTimerManager().ClearTimer(WaitingForPathTimer);
UpdateDecelerationData();
if (Status == EPathFollowingStatus::Waiting || Status == EPathFollowingStatus::Moving)
{
SetStatus(EPathFollowingStatus::Moving);
const int32 CurrentSegment = DetermineStartingPathPoint(Path.Get());
SetMoveSegment(CurrentSegment);
}
return true;
}
void UPathFollowingComponent::SetAcceptanceRadius(const float InAcceptanceRadius)
{
AcceptanceRadius = InAcceptanceRadius;
if (Path.IsValid() && Path->IsValid() && FAISystem::IsValidLocation(OriginalMoveRequestGoalLocation))
{
// figure out when we need to start doing precise end-condition tests
PreciseAcceptanceRadiusCheckStartNodeIndex = FindPreciseAcceptanceRadiusTestsStartNodeIndex(*Path, OriginalMoveRequestGoalLocation);
}
}
FAIRequestID UPathFollowingComponent::RequestMove(const FAIMoveRequest& RequestData, FNavPathSharedPtr InPath)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("RequestMove: Path(%s) %s"), *GetPathDescHelper(InPath), *RequestData.ToString());
LogPathHelper(GetOwner(), InPath, RequestData.GetGoalActor());
if (ResourceLock.IsLocked())
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Rejecting move request due to resource lock by %s"), *ResourceLock.GetLockPriorityName());
return FAIRequestID::InvalidRequest;
}
const float UseAcceptanceRadius = (RequestData.GetAcceptanceRadius() == UPathFollowingComponent::DefaultAcceptanceRadius) ? MyDefaultAcceptanceRadius : RequestData.GetAcceptanceRadius();
if (!InPath.IsValid() || UseAcceptanceRadius < 0.0f)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("RequestMove: invalid request"));
return FAIRequestID::InvalidRequest;
}
// try to grab movement component
if (!UpdateMovementComponent())
{
UE_VLOG(GetOwner(), LogPathFollowing, Warning, TEXT("RequestMove: missing movement component"));
return FAIRequestID::InvalidRequest;
}
FAIRequestID MoveId = CurrentRequestId;
// abort previous movement
if (Status == EPathFollowingStatus::Paused && Path.IsValid() && InPath.Get() == Path.Get() && DestinationActor == RequestData.GetGoalActor())
{
ResumeMove();
}
else
{
if (Status != EPathFollowingStatus::Idle)
{
// setting to false to make sure OnPathFinished won't result in stoping
bStopMovementOnFinish = false;
OnPathFinished(EPathFollowingResult::Aborted, FPathFollowingResultFlags::NewRequest);
}
// force-setting this to true since this is the default state, and gets modified only
// when aborting movement.
bStopMovementOnFinish = true;
Reset();
StoreRequestId();
// store new Id, using CurrentRequestId directly at the end of function is not safe with chained moves
MoveId = CurrentRequestId;
// store new data
Path = InPath;
Path->AddObserver(FNavigationPath::FPathObserverDelegate::FDelegate::CreateUObject(this, &UPathFollowingComponent::OnPathEvent));
if (NavMovementInterface.IsValid() && NavMovementInterface->GetOwnerAsObject())
{
if (AActor* OwnerAsActor = Cast<AActor>(NavMovementInterface->GetOwnerAsObject()))
{
Path->SetSourceActor(*(OwnerAsActor));
}
}
OriginalMoveRequestGoalLocation = RequestData.GetGoalActor() ? RequestData.GetGoalActor()->GetActorLocation() : RequestData.GetGoalLocation();
// update meta path data
FMetaNavMeshPath* MetaNavPath = Path->CastPath<FMetaNavMeshPath>();
if (MetaNavPath)
{
bIsUsingMetaPath = true;
const FVector CurrentLocation = NavMovementInterface.IsValid() ? NavMovementInterface->GetFeetLocation() : FAISystem::InvalidLocation;
MetaNavPath->Initialize(CurrentLocation);
}
NavigationFilter = ExtractNavigationFilter(RequestData, Path);
PathTimeWhenPaused = 0.;
OnPathUpdated();
// make sure that OnPathUpdated didn't change current move request
// otherwise there's no point in starting it
if (CurrentRequestId == MoveId)
{
AcceptanceRadius = UseAcceptanceRadius;
bReachTestIncludesAgentRadius = RequestData.IsReachTestIncludingAgentRadius();
bReachTestIncludesGoalRadius = RequestData.IsReachTestIncludingGoalRadius();
GameData = RequestData.GetUserData();
SetDestinationActor(RequestData.GetGoalActor());
SetAcceptanceRadius(UseAcceptanceRadius);
UpdateDecelerationData();
#if ENABLE_VISUAL_LOG
const FVector CurrentLocation = NavMovementInterface.IsValid() ? NavMovementInterface->GetFeetLocation() : FVector::ZeroVector;
const FVector DestLocation = InPath->GetDestinationLocation();
const FVector ToDest = DestLocation - CurrentLocation;
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("RequestMove: accepted, ID(%u) dist2D(%.0f) distZ(%.0f)"), MoveId.GetID(), ToDest.Size2D(), FMath::Abs(ToDest.Z));
#endif // ENABLE_VISUAL_LOG
// with async pathfinding paths can be incomplete, movement will start after receiving path event
if (!bIsUsingMetaPath && Path.IsValid() && Path->IsValid())
{
SetStatus(EPathFollowingStatus::Moving);
// determine with path segment should be followed
const uint32 CurrentSegment = DetermineStartingPathPoint(InPath.Get());
SetMoveSegment(CurrentSegment);
}
else
{
SetStatus(EPathFollowingStatus::Waiting);
GetWorld()->GetTimerManager().SetTimer(WaitingForPathTimer, this, &UPathFollowingComponent::OnWaitingPathTimeout, WaitingTimeout);
}
}
}
return MoveId;
}
void UPathFollowingComponent::AbortMove(const UObject& Instigator, FPathFollowingResultFlags::Type AbortFlags, FAIRequestID RequestID, EPathFollowingVelocityMode VelocityMode)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("AbortMove: RequestID(%u) Instigator(%s)"), RequestID.GetID(), *Instigator.GetName());
if ((Status != EPathFollowingStatus::Idle) && RequestID.IsEquivalent(GetCurrentRequestId()))
{
bStopMovementOnFinish = (VelocityMode == EPathFollowingVelocityMode::Reset);
OnPathFinished(FPathFollowingResult(EPathFollowingResult::Aborted, AbortFlags & FPathFollowingResultFlags::UserAbortFlagMask));
}
}
FAIRequestID UPathFollowingComponent::RequestMoveWithImmediateFinish(EPathFollowingResult::Type Result, EPathFollowingVelocityMode VelocityMode)
{
// make sure we stop (or not) depending on VelocityMode's value
bStopMovementOnFinish = (VelocityMode == EPathFollowingVelocityMode::Reset);
if (Status != EPathFollowingStatus::Idle)
{
OnPathFinished(EPathFollowingResult::Aborted, FPathFollowingResultFlags::NewRequest);
}
StoreRequestId();
const FAIRequestID MoveId = CurrentRequestId;
OnPathFinished(Result, FPathFollowingResultFlags::None);
return MoveId;
}
void UPathFollowingComponent::PauseMove(FAIRequestID RequestID, EPathFollowingVelocityMode VelocityMode)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("PauseMove: RequestID(%u)"), RequestID.GetID());
if (Status == EPathFollowingStatus::Paused)
{
return;
}
if (RequestID.IsEquivalent(GetCurrentRequestId()))
{
if ((VelocityMode == EPathFollowingVelocityMode::Reset) && NavMovementInterface.IsValid() && HasMovementAuthority())
{
NavMovementInterface->StopMovementKeepPathing();
}
LocationWhenPaused = NavMovementInterface.IsValid() ? NavMovementInterface->GetFeetLocation() : FVector::ZeroVector;
PathTimeWhenPaused = Path.IsValid() ? Path->GetTimeStamp() : 0.;
Status = EPathFollowingStatus::Paused;
UpdateMoveFocus();
}
}
void UPathFollowingComponent::ResumeMove(FAIRequestID RequestID)
{
if (RequestID.IsEquivalent(CurrentRequestId) && RequestID.IsValid())
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("ResumeMove: RequestID(%u)"), RequestID.GetID());
const bool bMovedDuringPause = ShouldCheckPathOnResume();
const bool bIsOnPath = IsOnPath();
if (bIsOnPath)
{
SetStatus(EPathFollowingStatus::Moving);
const bool bWasPathUpdatedRecently = Path.IsValid() ? (Path->GetTimeStamp() > PathTimeWhenPaused) : false;
if (bMovedDuringPause || bWasPathUpdatedRecently)
{
const int32 CurrentSegment = DetermineStartingPathPoint(Path.Get());
SetMoveSegment(CurrentSegment);
}
else
{
UpdateMoveFocus();
}
}
else if (Path.IsValid() && Path->IsValid() && Path->GetNavigationDataUsed() == NULL)
{
// this means it's a scripted path, just resume
SetStatus(EPathFollowingStatus::Moving);
UpdateMoveFocus();
}
else
{
OnPathFinished(EPathFollowingResult::OffPath, FPathFollowingResultFlags::None);
}
}
else
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("ResumeMove: RequestID(%u) is neither \'AnyRequest\' not CurrentRequestId(%u). Ignoring."), RequestID.GetID(), CurrentRequestId.GetID());
}
}
bool UPathFollowingComponent::ShouldCheckPathOnResume() const
{
bool bCheckPath = true;
if (NavMovementInterface != NULL)
{
float AgentRadius = 0.0f, AgentHalfHeight = 0.0f;
NavMovementInterface->GetSimpleCollisionCylinder(AgentRadius, AgentHalfHeight);
const FVector CurrentLocation = NavMovementInterface->GetFeetLocation();
const FVector::FReal DeltaMove2DSq = (CurrentLocation - LocationWhenPaused).SizeSquared2D();
const FVector::FReal DeltaZ = FMath::Abs(CurrentLocation.Z - LocationWhenPaused.Z);
if (DeltaMove2DSq < FMath::Square(AgentRadius) && DeltaZ < (AgentHalfHeight * 0.5))
{
bCheckPath = false;
}
}
return bCheckPath;
}
void UPathFollowingComponent::OnPathFinished(const FPathFollowingResult& Result)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("OnPathFinished: %s"), *Result.ToString());
INavLinkCustomInterface* CustomNavLink = Cast<INavLinkCustomInterface>(CurrentCustomLinkOb.Get());
if (CustomNavLink)
{
CustomNavLink->OnLinkMoveFinished(this);
CurrentCustomLinkOb.Reset();
}
// update meta path if needed
if (bIsUsingMetaPath && Result.IsSuccess() && NavMovementInterface.IsValid())
{
FMetaNavMeshPath* MetaNavPath = Path->CastPath<FMetaNavMeshPath>();
const bool bNeedPathUpdate = MetaNavPath && MetaNavPath->ConditionalMoveToNextSection(NavMovementInterface->GetFeetLocation(), EMetaPathUpdateReason::PathFinished);
if (bNeedPathUpdate)
{
// keep move request active
return;
}
}
// save move status
bLastMoveReachedGoal = Result.IsSuccess() && !HasPartialPath();
// save data required for observers before reseting temporary variables
const FAIRequestID FinishedMoveId = CurrentRequestId;
Reset();
UpdateMoveFocus();
if (ShouldStopMovementOnPathFinished())
{
NavMovementInterface->StopMovementKeepPathing();
}
// notify observers after state was reset (they can request another move)
OnRequestFinished.Broadcast(FinishedMoveId, Result);
FAIMessage Msg(UBrainComponent::AIMessage_MoveFinished, this, FinishedMoveId, Result.IsSuccess());
Msg.SetFlag(Result.Flags & 0xff);
FAIMessage::Send(Cast<AController>(GetOwner()), Msg);
}
bool UPathFollowingComponent::ShouldStopMovementOnPathFinished() const
{
return bStopMovementOnFinish && NavMovementInterface.IsValid() && HasMovementAuthority() && !NavMovementInterface->UseAccelerationForPathFollowing();
}
void UPathFollowingComponent::OnSegmentFinished()
{
UE_VLOG(GetOwner(), LogPathFollowing, Verbose, TEXT("OnSegmentFinished"));
}
void UPathFollowingComponent::OnPathUpdated()
{
}
void UPathFollowingComponent::SetMovementComponent(UNavMovementComponent* MoveComp)
{
SetNavMovementInterface(Cast<INavMovementInterface>(MoveComp));
}
void UPathFollowingComponent::Initialize()
{
UpdateCachedComponents();
LocationSamples.Reserve(BlockDetectionSampleCount);
}
void UPathFollowingComponent::Cleanup()
{
SetNavMovementInterface(nullptr);
Reset();
}
void UPathFollowingComponent::UpdateCachedComponents()
{
UpdateMovementComponent(/*bForce=*/true);
}
void UPathFollowingComponent::OnNewPawn(APawn* NewPawn)
{
UpdateCachedComponents();
}
void UPathFollowingComponent::OnRegister()
{
Super::OnRegister();
AController* ControllerOwner = Cast<AController>(GetOwner());
if (ControllerOwner)
{
ControllerOwner->GetOnNewPawnNotifier().AddUObject(this, &UPathFollowingComponent::OnNewPawn);
}
}
void UPathFollowingComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (Status == EPathFollowingStatus::Moving)
{
// check finish conditions, update current segment if needed
UpdatePathSegment();
}
if (Status == EPathFollowingStatus::Moving)
{
// follow current path segment
FollowPathSegment(DeltaTime);
}
};
void UPathFollowingComponent::SetNavMovementInterface(INavMovementInterface* NavMoveInterface)
{
if (!NavMoveInterface && NavMovementInterface.IsValid() && NavMovementInterface->GetPathFollowingAgent() == this)
{
NavMovementInterface->SetPathFollowingAgent(nullptr);
}
NavMovementInterface = NavMoveInterface;
MyNavData = nullptr;
if (NavMovementInterface.IsValid())
{
const FNavAgentProperties& NavAgentProps = NavMovementInterface->GetNavAgentPropertiesRef();
MyDefaultAcceptanceRadius = NavAgentProps.AgentRadius * 0.1f;
NavMovementInterface->SetPathFollowingAgent(Cast<IPathFollowingAgentInterface>(this));
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (NavSys)
{
MyNavData = NavSys->GetNavDataForProps(NavAgentProps, NavMovementInterface->GetLocation());
if (MyNavData == nullptr)
{
if (NavSys->IsInitialized() == false)
{
NavSys->OnNavigationInitDone.AddUObject(this, &UPathFollowingComponent::OnNavigationInitDone);
}
else
{
NavSys->OnNavDataRegisteredEvent.AddUniqueDynamic(this, &UPathFollowingComponent::OnNavDataRegistered);
}
}
}
}
}
void UPathFollowingComponent::OnNavigationInitDone()
{
UWorld* MyWorld = GetWorld();
if (NavMovementInterface.IsValid() && MyWorld)
{
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(MyWorld);
check(NavSys);
const FNavAgentProperties& NavAgentProps = NavMovementInterface->GetNavAgentPropertiesRef();
MyNavData = NavSys->GetNavDataForProps(NavAgentProps, NavMovementInterface->GetLocation());
NavSys->OnNavigationInitDone.RemoveAll(this);
}
}
void UPathFollowingComponent::OnNavDataRegistered(ANavigationData* NavData)
{
if (NavData && NavMovementInterface.IsValid())
{
const FNavAgentProperties& NavAgentProps = NavMovementInterface->GetNavAgentPropertiesRef();
if (NavData->DoesSupportAgent(NavAgentProps))
{
MyNavData = NavData;
UWorld* MyWorld = GetWorld();
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(MyWorld);
check(NavSys);
NavSys->OnNavDataRegisteredEvent.RemoveAll(this);
}
}
}
FVector UPathFollowingComponent::GetMoveFocus(bool bAllowStrafe) const
{
FVector MoveFocus = FVector::ZeroVector;
if (bAllowStrafe && DestinationActor.IsValid())
{
MoveFocus = DestinationActor->GetActorLocation();
}
else
{
const FVector CurrentMoveDirection = GetCurrentDirection();
MoveFocus = *CurrentDestination + (CurrentMoveDirection * FAIConfig::Navigation::FocalPointDistance);
}
return MoveFocus;
}
FSharedConstNavQueryFilter UPathFollowingComponent::ExtractNavigationFilter(const FAIMoveRequest& RequestData, FNavPathSharedPtr InPath) const
{
if (InPath.IsValid() && !InPath->CastPath<FAbstractNavigationPath>() && InPath->GetFilter() != nullptr)
{
return InPath->GetFilter();
}
if (MyNavData)
{
if (RequestData.GetNavigationFilter() != nullptr)
{
return UNavigationQueryFilter::GetQueryFilter(*MyNavData, GetOwner(), RequestData.GetNavigationFilter());
}
if (const AAIController* AIOwner = Cast<AAIController>(GetOwner()))
{
if (AIOwner->GetDefaultNavigationFilterClass() != nullptr)
{
return UNavigationQueryFilter::GetQueryFilter(*MyNavData, AIOwner->GetDefaultNavigationFilterClass());
}
}
return MyNavData->GetDefaultQueryFilter();
}
return FSharedConstNavQueryFilter();
}
void UPathFollowingComponent::Reset()
{
MoveSegmentStartIndex = 0;
MoveSegmentStartRef = INVALID_NAVNODEREF;
MoveSegmentEndRef = INVALID_NAVNODEREF;
DecelerationSegmentIndex = INDEX_NONE;
ResetMoveSegmentCustomLinkCache();
LocationSamples.Reset();
LastSampleTime = 0.0f;
NextSampleIdx = 0;
Path.Reset();
GameData.Reset();
DestinationActor.Reset();
CurrentDestination.Clear();
AcceptanceRadius = MyDefaultAcceptanceRadius;
bReachTestIncludesAgentRadius = true;
bReachTestIncludesGoalRadius = true;
bCollidedWithGoal = false;
bIsUsingMetaPath = false;
bWalkingNavLinkStart = false;
PreciseAcceptanceRadiusCheckStartNodeIndex = INDEX_NONE;
OriginalMoveRequestGoalLocation = FAISystem::InvalidLocation;
CurrentRequestId = FAIRequestID::InvalidRequest;
SetStatus(EPathFollowingStatus::Idle);
if (WaitingForPathTimer.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(WaitingForPathTimer);
WaitingForPathTimer.Invalidate();
}
}
int32 UPathFollowingComponent::DetermineStartingPathPoint(const FNavigationPath* ConsideredPath) const
{
int32 PickedPathPoint = INDEX_NONE;
if (ConsideredPath && ConsideredPath->IsValid())
{
// if we already have some info on where we were on previous path
// we can find out if there's a segment on new path we're currently at
if (MoveSegmentStartRef != INVALID_NAVNODEREF &&
MoveSegmentEndRef != INVALID_NAVNODEREF &&
ConsideredPath->GetNavigationDataUsed() != NULL)
{
// iterate every new path node and see if segment match
for (int32 PathPoint = 0; PathPoint < ConsideredPath->GetPathPoints().Num() - 1; ++PathPoint)
{
if (ConsideredPath->GetPathPoints()[PathPoint].NodeRef == MoveSegmentStartRef &&
ConsideredPath->GetPathPoints()[PathPoint + 1].NodeRef == MoveSegmentEndRef)
{
PickedPathPoint = PathPoint;
break;
}
}
}
if (NavMovementInterface.IsValid() && PickedPathPoint == INDEX_NONE)
{
if (ConsideredPath->GetPathPoints().Num() > 2)
{
// check if is closer to first or second path point (don't assume AI's standing)
const FVector CurrentLocation = NavMovementInterface->GetFeetLocation();
const FVector PathPt0 = *ConsideredPath->GetPathPointLocation(0);
const FVector PathPt1 = *ConsideredPath->GetPathPointLocation(1);
// making this test in 2d to avoid situation where agent's Z location not being in "navmesh plane"
// would influence the result
const FVector::FReal SqDistToFirstPoint = (CurrentLocation - PathPt0).SizeSquared2D();
const FVector::FReal SqDistToSecondPoint = (CurrentLocation - PathPt1).SizeSquared2D();
PickedPathPoint = FMath::IsNearlyEqual(SqDistToFirstPoint, SqDistToSecondPoint) ?
((FMath::Abs(CurrentLocation.Z - PathPt0.Z) < FMath::Abs(CurrentLocation.Z - PathPt1.Z)) ? 0 : 1) :
((SqDistToFirstPoint < SqDistToSecondPoint) ? 0 : 1);
}
else
{
// If there are only two point we probably should start from the beginning
PickedPathPoint = 0;
}
}
}
return PickedPathPoint;
}
void UPathFollowingComponent::SetDestinationActor(const AActor* InDestinationActor)
{
DestinationActor = MakeWeakObjectPtr(const_cast<AActor*>(InDestinationActor));
DestinationAgent = Cast<const INavAgentInterface>(InDestinationActor);
const AActor* OwnerActor = GetOwner();
MoveOffset = DestinationAgent ? DestinationAgent->GetMoveGoalOffset(OwnerActor) : FVector::ZeroVector;
}
void UPathFollowingComponent::SetMoveSegment(int32 SegmentStartIndex)
{
SHIPPING_STATIC const float PathPointAcceptanceRadius = GET_AI_CONFIG_VAR(PathfollowingRegularPathPointAcceptanceRadius);
SHIPPING_STATIC const float NavLinkAcceptanceRadius = GET_AI_CONFIG_VAR(PathfollowingNavLinkAcceptanceRadius);
int32 EndSegmentIndex = SegmentStartIndex + 1;
const FNavigationPath* PathInstance = Path.Get();
if (PathInstance != nullptr && PathInstance->GetPathPoints().IsValidIndex(SegmentStartIndex) && PathInstance->GetPathPoints().IsValidIndex(EndSegmentIndex))
{
EndSegmentIndex = DetermineCurrentTargetPathPoint(SegmentStartIndex);
MoveSegmentStartIndex = SegmentStartIndex;
MoveSegmentEndIndex = EndSegmentIndex;
const FNavPathPoint& PathPt0 = PathInstance->GetPathPoints()[MoveSegmentStartIndex];
const FNavPathPoint& PathPt1 = PathInstance->GetPathPoints()[MoveSegmentEndIndex];
MoveSegmentStartRef = PathPt0.NodeRef;
MoveSegmentEndRef = PathPt1.NodeRef;
CurrentDestination = PathInstance->GetPathPointLocation(MoveSegmentEndIndex);
const FVector SegmentStart = *PathInstance->GetPathPointLocation(MoveSegmentStartIndex);
FVector SegmentEnd = *CurrentDestination;
// make sure we have a non-zero direction if still following a valid path
if (SegmentStart.Equals(SegmentEnd) && PathInstance->GetPathPoints().IsValidIndex(MoveSegmentEndIndex + 1))
{
MoveSegmentEndIndex++;
CurrentDestination = PathInstance->GetPathPointLocation(MoveSegmentEndIndex);
SegmentEnd = *CurrentDestination;
}
// Check if the next segment is a custom link and has the need to use its own custom reach conditions
if (PathPt1.CustomNavLinkId != FNavLinkId::Invalid)
{
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (const INavLinkCustomInterface* MoveSegmentCustomLink = NavSys->GetCustomLink(PathPt1.CustomNavLinkId))
{
// Cache the CustomLinkOb for faster access during the update
MoveSegmentCustomLinkOb = Cast<const UObject>(MoveSegmentCustomLink);
bMoveSegmentIsUsingCustomLinkReachCondition = MoveSegmentCustomLink->IsLinkUsingCustomReachCondition(this);
}
else
{
ResetMoveSegmentCustomLinkCache();
}
}
else
{
ResetMoveSegmentCustomLinkCache();
}
CurrentAcceptanceRadius = (PathInstance->GetPathPoints().Num() == (MoveSegmentEndIndex + 1))
? GetFinalAcceptanceRadius(*PathInstance, OriginalMoveRequestGoalLocation)
// pick appropriate value base on whether we're going to nav link or not
: (FNavMeshNodeFlags(PathPt1.Flags).IsNavLink() == false ? PathPointAcceptanceRadius : NavLinkAcceptanceRadius);
MoveSegmentDirection = (SegmentEnd - SegmentStart).GetSafeNormal();
bWalkingNavLinkStart = FNavMeshNodeFlags(PathPt0.Flags).IsNavLink();
// handle moving through custom nav links
if (PathPt0.CustomNavLinkId != FNavLinkId::Invalid)
{
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
INavLinkCustomInterface* CustomNavLink = NavSys->GetCustomLink(PathPt0.CustomNavLinkId);
StartUsingCustomLink(CustomNavLink, SegmentEnd);
}
// update move focus in owning AI
UpdateMoveFocus();
UpdateDecelerationData();
}
}
int32 UPathFollowingComponent::DetermineCurrentTargetPathPoint(int32 StartIndex)
{
return StartIndex + 1;
}
void UPathFollowingComponent::UpdatePathSegment()
{
#if !UE_BUILD_SHIPPING
DEBUG_bMovingDirectlyToGoal = false;
#endif // !UE_BUILD_SHIPPING
if ((Path.IsValid() == false) || (NavMovementInterface == nullptr))
{
UE_CVLOG(Path.IsValid() == false, this, LogPathFollowing, Log, TEXT("Aborting move due to not having a valid path object"));
OnPathFinished(EPathFollowingResult::Aborted, FPathFollowingResultFlags::InvalidPath);
return;
}
const FVector CurrentLocation = NavMovementInterface->GetFeetLocation();
const bool bCanUpdateState = HasMovementAuthority();
if (!Path->IsValid())
{
if (!Path->IsWaitingForRepath())
{
UE_VLOG(this, LogPathFollowing, Log, TEXT("Aborting move due to path being invalid and not waiting for repath"));
OnPathFinished(EPathFollowingResult::Aborted, FPathFollowingResultFlags::InvalidPath);
return;
}
else if (HasStartedNavLinkMove() && bCanUpdateState && Status == EPathFollowingStatus::Moving)
{
// pawn needs to get off navlink to unlock path updates (AAIController::ShouldPostponePathUpdates)
if (HasReachedCurrentTarget(CurrentLocation))
{
OnSegmentFinished();
SetNextMoveSegment();
}
}
else
{
// continue with execution, if navigation is being rebuild constantly AI will get stuck with current waypoint
// path updates should be still coming in, even though they get invalidated right away
UE_VLOG(this, LogPathFollowing, Log, TEXT("Updating path points in invalid & pending path!"));
}
}
FMetaNavMeshPath* MetaNavPath = bIsUsingMetaPath ? Path->CastPath<FMetaNavMeshPath>() : nullptr;
/** it's possible that finishing this move request will result in another request
* which won't be easily detectable from this function. This simple local
* variable gives us this knowledge. */
const FAIRequestID MoveRequestId = GetCurrentRequestId();
// if agent has control over its movement, check finish conditions
if (bCanUpdateState && Status == EPathFollowingStatus::Moving)
{
const int32 LastSegmentEndIndex = Path->GetPathPoints().Num() - 1;
const bool bFollowingLastSegment = (MoveSegmentEndIndex >= LastSegmentEndIndex);
const bool bLastPathChunk = (MetaNavPath == nullptr || MetaNavPath->IsLastSection());
if (bCollidedWithGoal)
{
// check if collided with goal actor
OnSegmentFinished();
OnPathFinished(EPathFollowingResult::Success, FPathFollowingResultFlags::None);
}
else if (MoveSegmentEndIndex > PreciseAcceptanceRadiusCheckStartNodeIndex && HasReachedDestination(CurrentLocation))
{
OnSegmentFinished();
OnPathFinished(EPathFollowingResult::Success, FPathFollowingResultFlags::None);
}
else if (bFollowingLastSegment && bMoveToGoalOnLastSegment && bLastPathChunk)
{
// use goal actor for end of last path segment
// UNLESS it's partial path (can't reach goal)
if (DestinationActor.IsValid() && Path->IsPartial() == false)
{
const FVector AgentLocation = DestinationAgent ? DestinationAgent->GetNavAgentLocation() : DestinationActor->GetActorLocation();
FVector GoalLocation = FQuatRotationTranslationMatrix(DestinationActor->GetActorQuat(), AgentLocation).TransformPosition(MoveOffset);
if (bMoveToGoalClampedToNavigation && NavigationFilter)
{
FVector HitLocation;
if (MyNavData->Raycast(CurrentLocation, GoalLocation, HitLocation, NavigationFilter))
{
GoalLocation = HitLocation;
}
}
CurrentDestination.Set(NULL, GoalLocation);
UE_VLOG(this, LogPathFollowing, Log, TEXT("Moving directly to move goal rather than following last path segment"));
UE_VLOG_LOCATION(this, LogPathFollowing, VeryVerbose, GoalLocation, 30, FColor::Green, TEXT("Last-segment-to-actor"));
UE_VLOG_SEGMENT(this, LogPathFollowing, VeryVerbose, CurrentLocation, GoalLocation, FColor::Green, TEXT_EMPTY);
}
UpdateMoveFocus();
#if !UE_BUILD_SHIPPING
DEBUG_bMovingDirectlyToGoal = true;
#endif // !UE_BUILD_SHIPPING
}
// check if current move segment is finished
else if (HasReachedCurrentTarget(CurrentLocation))
{
OnSegmentFinished();
SetNextMoveSegment();
}
}
if (bCanUpdateState
&& Status == EPathFollowingStatus::Moving
// still the same move request
&& MoveRequestId == GetCurrentRequestId())
{
// check waypoint switch condition in meta paths
if (MetaNavPath)
{
MetaNavPath->ConditionalMoveToNextSection(CurrentLocation, EMetaPathUpdateReason::MoveTick);
}
// gather location samples to detect if moving agent is blocked
const bool bHasNewSample = UpdateBlockDetection();
if (bHasNewSample && IsBlocked())
{
if (Path->GetPathPoints().IsValidIndex(MoveSegmentEndIndex) && Path->GetPathPoints().IsValidIndex(MoveSegmentStartIndex))
{
LogBlockHelper(GetOwner(), NavMovementInterface.Get(), MinAgentRadiusPct, MinAgentHalfHeightPct,
*Path->GetPathPointLocation(MoveSegmentStartIndex),
*Path->GetPathPointLocation(MoveSegmentEndIndex));
}
else
{
if ((GetOwner() != NULL) && (NavMovementInterface != NULL))
{
UE_VLOG(GetOwner(), LogPathFollowing, Verbose, TEXT("Path blocked, but move segment indices are not valid: start %d, end %d of %d"), MoveSegmentStartIndex, MoveSegmentEndIndex, Path->GetPathPoints().Num());
}
}
OnPathFinished(EPathFollowingResult::Blocked, FPathFollowingResultFlags::None);
}
}
}
void UPathFollowingComponent::FollowPathSegment(float DeltaTime)
{
if (!Path.IsValid() || NavMovementInterface == nullptr)
{
return;
}
const FVector CurrentLocation = NavMovementInterface->GetFeetLocation();
const FVector CurrentTarget = GetCurrentTargetLocation();
// set to false by default, we will set set this back to true if appropriate
bIsDecelerating = false;
const bool bAccelerationBased = NavMovementInterface->UseAccelerationForPathFollowing();
if (bAccelerationBased)
{
CurrentMoveInput = (CurrentTarget - CurrentLocation).GetSafeNormal();
if (bStopMovementOnFinish && (MoveSegmentStartIndex >= DecelerationSegmentIndex))
{
const FVector PathEnd = Path->GetEndLocation();
const FVector::FReal DistToEndSq = FVector::DistSquared(CurrentLocation, PathEnd);
const bool bShouldDecelerate = DistToEndSq < FMath::Square(CachedBrakingDistance);
if (bShouldDecelerate)
{
bIsDecelerating = true;
const FVector::FReal SpeedPct = FMath::Clamp(FMath::Sqrt(DistToEndSq) / CachedBrakingDistance, 0., 1.);
CurrentMoveInput *= SpeedPct;
}
}
PostProcessMove.ExecuteIfBound(this, CurrentMoveInput);
NavMovementInterface->RequestPathMove(CurrentMoveInput);
}
else
{
FVector MoveVelocity = (CurrentTarget - CurrentLocation) / DeltaTime;
const int32 LastSegmentStartIndex = Path->GetPathPoints().Num() - 2;
const bool bNotFollowingLastSegment = (MoveSegmentStartIndex < LastSegmentStartIndex);
PostProcessMove.ExecuteIfBound(this, MoveVelocity);
NavMovementInterface->RequestDirectMove(MoveVelocity, bNotFollowingLastSegment);
}
}
bool UPathFollowingComponent::HasReached(const FVector& TestPoint, EPathFollowingReachMode ReachMode, float InAcceptanceRadius) const
{
// simple test for stationary agent, used as early finish condition
const FVector CurrentLocation = NavMovementInterface.IsValid() ? NavMovementInterface->GetFeetLocation() : FVector::ZeroVector;
const float GoalRadius = 0.0f;
const float GoalHalfHeight = 0.0f;
if (InAcceptanceRadius == UPathFollowingComponent::DefaultAcceptanceRadius)
{
InAcceptanceRadius = MyDefaultAcceptanceRadius;
}
const float AgentRadiusMod = (ReachMode == EPathFollowingReachMode::ExactLocation) || (ReachMode == EPathFollowingReachMode::OverlapGoal) ? 0.0f : MinAgentRadiusPct;
return HasReachedInternal(TestPoint, GoalRadius, GoalHalfHeight, CurrentLocation, InAcceptanceRadius, AgentRadiusMod);
}
bool UPathFollowingComponent::HasReached(const AActor& TestGoal, EPathFollowingReachMode ReachMode, float InAcceptanceRadius, bool bUseNavAgentGoalLocation) const
{
// simple test for stationary agent, used as early finish condition
float GoalRadius = 0.0f;
float GoalHalfHeight = 0.0f;
FVector GoalOffset = FVector::ZeroVector;
FVector TestPoint = TestGoal.GetActorLocation();
if (InAcceptanceRadius == UPathFollowingComponent::DefaultAcceptanceRadius)
{
InAcceptanceRadius = MyDefaultAcceptanceRadius;
}
const INavAgentInterface* NavAgent = Cast<const INavAgentInterface>(&TestGoal);
if (NavAgent)
{
const AActor* OwnerActor = GetOwner();
const FVector GoalMoveOffset = NavAgent->GetMoveGoalOffset(OwnerActor);
NavAgent->GetMoveGoalReachTest(OwnerActor, GoalMoveOffset, GoalOffset, GoalRadius, GoalHalfHeight);
if (bUseNavAgentGoalLocation)
{
TestPoint = FQuatRotationTranslationMatrix(TestGoal.GetActorQuat(), NavAgent->GetNavAgentLocation()).TransformPosition(GoalOffset);
if ((ReachMode == EPathFollowingReachMode::ExactLocation) || (ReachMode == EPathFollowingReachMode::OverlapAgent))
{
GoalRadius = 0.0f;
}
}
if ((ReachMode == EPathFollowingReachMode::ExactLocation) || (ReachMode == EPathFollowingReachMode::OverlapAgent))
{
GoalRadius = 0.0f;
}
}
const FVector CurrentLocation = NavMovementInterface.IsValid() ? NavMovementInterface->GetFeetLocation() : FVector::ZeroVector;
const float AgentRadiusMod = (ReachMode == EPathFollowingReachMode::ExactLocation) || (ReachMode == EPathFollowingReachMode::OverlapGoal) ? 0.0f : MinAgentRadiusPct;
return HasReachedInternal(TestPoint, GoalRadius, GoalHalfHeight, CurrentLocation, InAcceptanceRadius, AgentRadiusMod);
}
bool UPathFollowingComponent::HasReached(const FAIMoveRequest& MoveRequest) const
{
const EPathFollowingReachMode ReachMode = MoveRequest.IsReachTestIncludingAgentRadius() ?
(MoveRequest.IsReachTestIncludingGoalRadius() ? EPathFollowingReachMode::OverlapAgentAndGoal : EPathFollowingReachMode::OverlapAgent) :
(MoveRequest.IsReachTestIncludingGoalRadius() ? EPathFollowingReachMode::OverlapGoal : EPathFollowingReachMode::ExactLocation);
return MoveRequest.IsMoveToActorRequest() ?
(MoveRequest.GetGoalActor() ? HasReached(*MoveRequest.GetGoalActor(), ReachMode, MoveRequest.GetAcceptanceRadius()) : false) :
HasReached(MoveRequest.GetGoalLocation(), ReachMode, MoveRequest.GetAcceptanceRadius());
}
bool UPathFollowingComponent::HasReachedDestination(const FVector& CurrentLocation) const
{
// get cylinder at goal location
FVector GoalLocation = *Path->GetPathPointLocation(Path->GetPathPoints().Num() - 1);
float GoalRadius = 0.0f;
float GoalHalfHeight = 0.0f;
// take goal's current location, unless path is partial or last segment doesn't reach goal actor (used by tethered AI)
if (!Path->IsPartial() && bMoveToGoalOnLastSegment)
{
GoalLocation = *CurrentDestination;
// Testing IsNearlyZero is not optimal as (0,0,0) could be a valid world position. Tracking the initialization state inside FBasedPosition would be better.
if (GoalLocation.IsNearlyZero() && DestinationActor.IsValid() && !bMoveToGoalClampedToNavigation)
{
// In case CurrentDestination is not set because we didn't update MoveSegment yet, let's use the goal Actor's location directly
if (DestinationAgent)
{
const AActor* OwnerActor = GetOwner();
FVector GoalOffset;
DestinationAgent->GetMoveGoalReachTest(OwnerActor, MoveOffset, GoalOffset, GoalRadius, GoalHalfHeight);
GoalLocation = FQuatRotationTranslationMatrix(DestinationActor->GetActorQuat(), DestinationAgent->GetNavAgentLocation()).TransformPosition(GoalOffset);
}
else
{
GoalLocation = DestinationActor->GetActorLocation();
}
}
}
const float AcceptanceRangeToUse = GetFinalAcceptanceRadius(*Path, OriginalMoveRequestGoalLocation);
return HasReachedInternal(GoalLocation, bReachTestIncludesGoalRadius ? GoalRadius : 0.0f, GoalHalfHeight, CurrentLocation
, AcceptanceRangeToUse, bReachTestIncludesAgentRadius ? MinAgentRadiusPct : 0.0f);
}
bool UPathFollowingComponent::HasReachedCurrentTarget(const FVector& CurrentLocation) const
{
if (NavMovementInterface == NULL)
{
return false;
}
// If the next segment is a link with a custom reach condition, we need to call the HasReachedLinkStart on the link interface.
if (bMoveSegmentIsUsingCustomLinkReachCondition)
{
if (const INavLinkCustomInterface* MoveSegmentCustomLink = Cast<const INavLinkCustomInterface>(MoveSegmentCustomLinkOb.Get()))
{
if (ensureMsgf(Path.IsValid(), TEXT("%hs: Path should be valid when we get here. Owner [%s]."), __FUNCTION__, *GetNameSafe(GetOwner())))
{
const FNavPathPoint& LinkStart = Path->GetPathPoints()[MoveSegmentEndIndex];
if (Path->GetPathPoints().IsValidIndex(MoveSegmentEndIndex + 1))
{
const FNavPathPoint& LinkEnd = Path->GetPathPoints()[MoveSegmentEndIndex + 1];
return MoveSegmentCustomLink->HasReachedLinkStart(this, CurrentLocation, LinkStart, LinkEnd);
}
else
{
UE_LOG(LogPathFollowing, Error, TEXT("%hs: NavLink has a start, but no end. Custom reach condition won't be called. NavLinkID [%llu] - LinkStartPos [%s] - Owner [%s]"), __FUNCTION__, LinkStart.CustomNavLinkId.GetId(), *LinkStart.Location.ToString(), *GetNameSafe(GetOwner()));
}
}
}
}
const FVector CurrentTarget = GetCurrentTargetLocation();
const FVector CurrentDirection = GetCurrentDirection();
// check if moved too far
const FVector ToTarget = (CurrentTarget - NavMovementInterface->GetFeetLocation());
const FVector::FReal SegmentDot = FVector::DotProduct(ToTarget, CurrentDirection);
if (SegmentDot < 0.)
{
return true;
}
// or standing at target position
// don't use acceptance radius here, it has to be exact for moving near corners (2D test < 5% of agent radius)
const float GoalRadius = 0.0f;
const float GoalHalfHeight = 0.0f;
return HasReachedInternal(CurrentTarget, GoalRadius, GoalHalfHeight, CurrentLocation, CurrentAcceptanceRadius, 0.05f);
}
bool UPathFollowingComponent::HasReachedInternal(const FVector& GoalLocation, float GoalRadius, float GoalHalfHeight, const FVector& AgentLocation, float RadiusThreshold, float AgentRadiusMultiplier) const
{
if (NavMovementInterface == NULL)
{
return false;
}
// get cylinder of moving agent
float AgentRadius = 0.0f;
float AgentHalfHeight = 0.0f;
NavMovementInterface->GetSimpleCollisionCylinder(AgentRadius, AgentHalfHeight);
// check if they overlap (with added AcceptanceRadius)
const FVector ToGoal = GoalLocation - AgentLocation;
const FVector::FReal Dist2DSq = ToGoal.SizeSquared2D();
const FVector::FReal UseRadius = RadiusThreshold + GoalRadius + (AgentRadius * AgentRadiusMultiplier);
if (Dist2DSq > FMath::Square(UseRadius))
{
return false;
}
const FVector::FReal ZDiff = FMath::Abs(ToGoal.Z);
const FVector::FReal UseHeight = GoalHalfHeight + (AgentHalfHeight * MinAgentHalfHeightPct);
if (ZDiff > UseHeight)
{
return false;
}
return true;
}
int32 UPathFollowingComponent::FindPreciseAcceptanceRadiusTestsStartNodeIndex(const FNavigationPath& PathInstance, const FVector& GoalLocation) const
{
const float DistanceFromEndOfPath = GetFinalAcceptanceRadius(PathInstance, GoalLocation);
// @todo support based paths
int32 TestStartIndex = MAX_int32;
const TArray<FNavPathPoint>& PathPoints = PathInstance.GetPathPoints();
if (PathPoints.Num() > 1)
{
FVector::FReal DistanceSum = 0.f;
int32 NodeIndex = PathPoints.Num() - 1;
FVector PrevLocation = PathPoints[NodeIndex].Location;
--NodeIndex;
for (; NodeIndex >= 0; --NodeIndex)
{
const FVector CurrentLocation = PathPoints[NodeIndex].Location;
DistanceSum += FVector::Dist(CurrentLocation, PrevLocation);
if (DistanceSum > DistanceFromEndOfPath)
{
TestStartIndex = NodeIndex;
break;
}
PrevLocation = CurrentLocation;
TestStartIndex = NodeIndex;
}
}
return TestStartIndex;
}
float UPathFollowingComponent::GetFinalAcceptanceRadius(const FNavigationPath& PathInstance, const FVector OriginalGoalLocation, const FVector* PathEndOverride) const
{
if (PathInstance.IsPartial())
{
ensure(FAISystem::IsValidLocation(OriginalGoalLocation));
const FVector::FReal PathEndToGoalDistance = FVector::Dist(PathEndOverride ? *PathEndOverride : PathInstance.GetEndLocation(), OriginalGoalLocation);
const FVector::FReal Remaining = AcceptanceRadius - PathEndToGoalDistance;
// if goal is more than AcceptanceRadius from the end of the path
// we just need to do regular path following, with default radius value
return FloatCastChecked<float>(FMath::Max(Remaining, (FVector::FReal)MyDefaultAcceptanceRadius), UE::LWC::DefaultFloatPrecision);
}
return AcceptanceRadius;
}
void UPathFollowingComponent::DebugReachTest(float& CurrentDot, float& CurrentDistance, float& CurrentHeight, uint8& bDotFailed, uint8& bDistanceFailed, uint8& bHeightFailed) const
{
if (!Path.IsValid() || NavMovementInterface == nullptr || NavMovementInterface->GetUpdatedObject() == nullptr)
{
return;
}
const int32 LastSegmentEndIndex = Path->GetPathPoints().Num() - 1;
const bool bFollowingLastSegment = (MoveSegmentEndIndex >= LastSegmentEndIndex);
float GoalRadius = 0.0f;
float GoalHalfHeight = 0.0f;
float RadiusThreshold = 0.0f;
float AgentRadiusPct = 0.05f;
FVector AgentLocation = NavMovementInterface->GetFeetLocation();
FVector GoalLocation = GetCurrentTargetLocation();
RadiusThreshold = CurrentAcceptanceRadius;
if (bFollowingLastSegment)
{
GoalLocation = *Path->GetPathPointLocation(Path->GetPathPoints().Num() - 1);
AgentRadiusPct = MinAgentRadiusPct;
// take goal's current location, unless path is partial
if (DestinationActor.IsValid() && !Path->IsPartial())
{
if (DestinationAgent)
{
const AActor* OwnerActor = GetOwner();
FVector GoalOffset;
DestinationAgent->GetMoveGoalReachTest(OwnerActor, MoveOffset, GoalOffset, GoalRadius, GoalHalfHeight);
GoalLocation = FQuatRotationTranslationMatrix(DestinationActor->GetActorQuat(), DestinationAgent->GetNavAgentLocation()).TransformPosition(GoalOffset);
}
else
{
GoalLocation = DestinationActor->GetActorLocation();
}
}
}
const FVector ToGoal = (GoalLocation - AgentLocation);
const FVector CurrentDirection = GetCurrentDirection();
CurrentDot = FloatCastChecked<float>(FVector::DotProduct(ToGoal.GetSafeNormal(), CurrentDirection), /* Precision */ 1./128.);
bDotFailed = (CurrentDot < 0.0f) ? 1 : 0;
// get cylinder of moving agent
float AgentRadius = 0.0f;
float AgentHalfHeight = 0.0f;
NavMovementInterface->GetSimpleCollisionCylinder(AgentRadius, AgentHalfHeight);
CurrentDistance = FloatCastChecked<float>(ToGoal.Size2D(), UE::LWC::DefaultFloatPrecision);
const float UseRadius = FMath::Max(RadiusThreshold, GoalRadius + (AgentRadius * AgentRadiusPct));
bDistanceFailed = (CurrentDistance > UseRadius) ? 1 : 0;
CurrentHeight = FloatCastChecked<float>(FMath::Abs(ToGoal.Z), UE::LWC::DefaultFloatPrecision);
const float UseHeight = GoalHalfHeight + (AgentHalfHeight * MinAgentHalfHeightPct);
bHeightFailed = (CurrentHeight > UseHeight) ? 1 : 0;
}
void UPathFollowingComponent::StartUsingCustomLink(INavLinkCustomInterface* CustomNavLink, const FVector& DestPoint)
{
INavLinkCustomInterface* PrevNavLink = Cast<INavLinkCustomInterface>(CurrentCustomLinkOb.Get());
if (PrevNavLink)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Force finish custom move using navlink: %s"), *GetPathNameSafe(CurrentCustomLinkOb.Get()));
PrevNavLink->OnLinkMoveFinished(this);
CurrentCustomLinkOb.Reset();
}
UObject* NewNavLinkOb = Cast<UObject>(CustomNavLink);
if (NewNavLinkOb)
{
CurrentCustomLinkOb = NewNavLinkOb;
const bool bCustomMove = CustomNavLink->OnLinkMoveStarted(this, DestPoint);
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("%s navlink: %s"),
bCustomMove ? TEXT("Custom move using") : TEXT("Notify"), *GetPathNameSafe(NewNavLinkOb));
if (!bCustomMove)
{
CurrentCustomLinkOb = NULL;
}
}
}
void UPathFollowingComponent::FinishUsingCustomLink(INavLinkCustomInterface* CustomNavLink)
{
if (CustomNavLink)
{
UObject* NavLinkOb = Cast<UObject>(CustomNavLink);
if (CurrentCustomLinkOb == NavLinkOb)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Finish custom move using navlink: %s"), *GetPathNameSafe(NavLinkOb));
CustomNavLink->OnLinkMoveFinished(this);
CurrentCustomLinkOb.Reset();
}
}
}
bool UPathFollowingComponent::UpdateMovementComponent(bool bForce)
{
if (NavMovementInterface == NULL || bForce == true)
{
APawn* MyPawn = Cast<APawn>(GetOwner());
if (MyPawn == NULL)
{
AController* MyController = Cast<AController>(GetOwner());
if (MyController)
{
MyPawn = MyController->GetPawn();
}
}
if (MyPawn)
{
MyPawn->OnActorHit.AddUniqueDynamic(this, &UPathFollowingComponent::OnActorBump);
SetNavMovementInterface(MyPawn->FindComponentByInterface<INavMovementInterface>());
}
}
return (NavMovementInterface != nullptr);
}
void UPathFollowingComponent::OnActorBump(AActor* SelfActor, AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit)
{
if (Path.IsValid() && DestinationActor.IsValid() && DestinationActor.Get() == OtherActor)
{
UE_VLOG(GetOwner(), LogPathFollowing, Verbose, TEXT("Collided with goal actor"));
bCollidedWithGoal = true;
}
}
void UPathFollowingComponent::ResetMoveSegmentCustomLinkCache()
{
MoveSegmentCustomLinkOb.Reset();
bMoveSegmentIsUsingCustomLinkReachCondition = false;
}
bool UPathFollowingComponent::IsOnPath() const
{
bool bOnPath = false;
if (Path.IsValid() && Path->IsValid() && Path->GetNavigationDataUsed() != NULL)
{
const bool bHasNavigationCorridor = (Path->CastPath<FNavMeshPath>() != NULL);
if (bHasNavigationCorridor)
{
FNavLocation NavLoc = GetCurrentNavLocation();
bOnPath = Path->ContainsNode(NavLoc.NodeRef);
}
else
{
bOnPath = true;
}
}
return bOnPath;
}
bool UPathFollowingComponent::IsBlocked() const
{
bool bBlocked = false;
if (LocationSamples.Num() == BlockDetectionSampleCount && BlockDetectionSampleCount > 0)
{
const FVector::FReal BlockDetectionDistanceSq = FMath::Square(BlockDetectionDistance);
FVector Center = FVector::ZeroVector;
for (int32 SampleIndex = 0; SampleIndex < LocationSamples.Num(); SampleIndex++)
{
Center += *LocationSamples[SampleIndex];
}
Center /= LocationSamples.Num();
bBlocked = true;
for (int32 SampleIndex = 0; SampleIndex < LocationSamples.Num(); SampleIndex++)
{
const FVector::FReal TestDistanceSq = FVector::DistSquared(*LocationSamples[SampleIndex], Center);
if (TestDistanceSq > BlockDetectionDistanceSq)
{
bBlocked = false;
break;
}
}
}
return bBlocked;
}
bool UPathFollowingComponent::UpdateBlockDetection()
{
const double GameTime = GetWorld()->GetTimeSeconds();
if (bUseBlockDetection &&
NavMovementInterface.IsValid() &&
GameTime > (LastSampleTime + BlockDetectionInterval) &&
BlockDetectionSampleCount > 0)
{
LastSampleTime = GameTime;
if (LocationSamples.Num() == NextSampleIdx)
{
LocationSamples.AddZeroed(1);
}
LocationSamples[NextSampleIdx] = NavMovementInterface->GetFeetLocationBased();
NextSampleIdx = (NextSampleIdx + 1) % BlockDetectionSampleCount;
return true;
}
return false;
}
void UPathFollowingComponent::SetBlockDetection(float DistanceThreshold, float Interval, int32 NumSamples)
{
BlockDetectionDistance = DistanceThreshold;
BlockDetectionInterval = Interval;
BlockDetectionSampleCount = NumSamples;
ResetBlockDetectionData();
}
void UPathFollowingComponent::SetBlockDetectionState(bool bEnable)
{
bUseBlockDetection = bEnable;
ResetBlockDetectionData();
}
void UPathFollowingComponent::ResetBlockDetectionData()
{
LastSampleTime = 0.0f;
NextSampleIdx = 0;
LocationSamples.Reset();
}
void UPathFollowingComponent::ForceBlockDetectionUpdate()
{
LastSampleTime = 0.0f;
}
void UPathFollowingComponent::UpdateDecelerationData()
{
// no point in updating if we don't have a MovementComp
if (NavMovementInterface == nullptr)
{
return;
}
if (!bStopMovementOnFinish)
{
return;
}
const float CurrentMaxSpeed = NavMovementInterface->GetMaxSpeedForNavMovement();
bool bUpdatePathSegment = (DecelerationSegmentIndex == INDEX_NONE);
if (CurrentMaxSpeed != CachedBrakingMaxSpeed)
{
const float PrevBrakingDistance = CachedBrakingDistance;
CachedBrakingDistance = NavMovementInterface->GetPathFollowingBrakingDistance(CurrentMaxSpeed);
CachedBrakingMaxSpeed = CurrentMaxSpeed;
bUpdatePathSegment = bUpdatePathSegment || (PrevBrakingDistance != CachedBrakingDistance);
}
if (bUpdatePathSegment && Path.IsValid())
{
DecelerationSegmentIndex = 0;
const TArray<FNavPathPoint>& PathPoints = Path->GetPathPoints();
FVector::FReal PathLengthFromEnd = 0.;
for (int32 Idx = PathPoints.Num() - 1; Idx > 0; Idx--)
{
const FVector::FReal PathSegmentLength = FVector::Dist(PathPoints[Idx].Location, PathPoints[Idx - 1].Location);
PathLengthFromEnd += PathSegmentLength;
if (PathLengthFromEnd > CachedBrakingDistance)
{
DecelerationSegmentIndex = Idx - 1;
break;
}
}
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Updated deceleration segment: %d (MaxSpeed:%.2f, BrakingDistance:%.2f"), DecelerationSegmentIndex, CachedBrakingMaxSpeed, CachedBrakingDistance);
}
}
void UPathFollowingComponent::UpdateMoveFocus()
{
AAIController* AIOwner = Cast<AAIController>(GetOwner());
if (AIOwner != NULL)
{
if (Status == EPathFollowingStatus::Moving)
{
const FVector MoveFocus = GetMoveFocus(AIOwner->bAllowStrafe);
AIOwner->SetFocalPoint(MoveFocus, EAIFocusPriority::Move);
}
else if (Status == EPathFollowingStatus::Idle)
{
AIOwner->ClearFocus(EAIFocusPriority::Move);
}
}
}
void UPathFollowingComponent::SetPreciseReachThreshold(float AgentRadiusMultiplier, float AgentHalfHeightMultiplier)
{
MinAgentRadiusPct = AgentRadiusMultiplier;
MinAgentHalfHeightPct = AgentHalfHeightMultiplier;
}
FVector::FReal UPathFollowingComponent::GetRemainingPathCost() const
{
FVector::FReal Cost = 0.;
if (Path.IsValid() && Path->IsValid() && Status == EPathFollowingStatus::Moving)
{
const FNavLocation& NavLocation = GetCurrentNavLocation();
Cost = Path->GetCostFromNode(NavLocation.NodeRef);
}
return Cost;
}
FNavLocation UPathFollowingComponent::GetCurrentNavLocation() const
{
// get navigation location of moved actor
if (NavMovementInterface == nullptr)
{
return FNavLocation();
}
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (NavSys == nullptr)
{
return FNavLocation();
}
const FVector OwnerLoc = NavMovementInterface->GetNavLocation();
const float Tolerance = 1.0f;
// Using !Equals rather than != because exact equivalence isn't required. (Equals allows a small tolerance.)
if (!OwnerLoc.Equals(CurrentNavLocation.Location, Tolerance))
{
const FVector OwnerExtent = NavMovementInterface.IsValid() ? NavMovementInterface->GetSimpleCollisionCylinderExtent() : FVector::ZeroVector;
NavSys->ProjectPointToNavigation(OwnerLoc, CurrentNavLocation, OwnerExtent, MyNavData);
}
return CurrentNavLocation;
}
bool UPathFollowingComponent::IsCurrentSegmentNavigationLink() const
{
return Path.IsValid() && Path->GetPathPoints().IsValidIndex(MoveSegmentStartIndex) && FNavMeshNodeFlags(Path->GetPathPoints()[MoveSegmentStartIndex].Flags).IsNavLink();
}
FVector UPathFollowingComponent::GetCurrentDirection() const
{
if (CurrentDestination.Base)
{
// calculate direction to based destination
const FVector SegmentStartLocation = *Path->GetPathPointLocation(MoveSegmentStartIndex);
const FVector SegmentEndLocation = *CurrentDestination;
return (SegmentEndLocation - SegmentStartLocation).GetSafeNormal();
}
// use cached direction of current path segment
return MoveSegmentDirection;
}
bool UPathFollowingComponent::HasDirectPath() const
{
return Path.IsValid() ? (Path->CastPath<FNavMeshPath>() == NULL) : false;
}
FString UPathFollowingComponent::GetStatusDesc() const
{
const static UEnum* StatusEnum = StaticEnum<EPathFollowingStatus::Type>();
if (StatusEnum)
{
return StatusEnum->GetNameStringByValue(Status);
}
return TEXT("Unknown");
}
FString UPathFollowingComponent::GetResultDesc(EPathFollowingResult::Type Result) const
{
const static UEnum* ResultEnum = StaticEnum<EPathFollowingResult::Type>();
if (ResultEnum)
{
return ResultEnum->GetNameStringByValue(Result);
}
return TEXT("Unknown");
}
void UPathFollowingComponent::DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos) const
{
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
DisplayDebugManager.SetDrawColor(FColor::Blue);
DisplayDebugManager.DrawString(FString::Printf(TEXT(" Move status: %s"), *GetStatusDesc()));
if (Status == EPathFollowingStatus::Moving)
{
const int32 NumMoveSegments = (Path.IsValid() && Path->IsValid()) ? Path->GetPathPoints().Num() : -1;
FString TargetDesc = FString::Printf(TEXT(" Move target [%d/%d]: %s (%s)"),
MoveSegmentEndIndex, NumMoveSegments, *GetCurrentTargetLocation().ToString(), *GetNameSafe(DestinationActor.Get()));
DisplayDebugManager.DrawString(FString::Printf(TEXT(" Move status: %s"), *GetStatusDesc()));
}
}
#if ENABLE_VISUAL_LOG
void UPathFollowingComponent::DescribeSelfToVisLog(FVisualLogEntry* Snapshot) const
{
FVisualLogStatusCategory Category;
Category.Category = TEXT("Path following");
if (DestinationActor.IsValid())
{
Category.Add(TEXT("Goal"), GetNameSafe(DestinationActor.Get()));
}
FString StatusDesc = GetStatusDesc();
if (Status == EPathFollowingStatus::Moving && Path.IsValid())
{
StatusDesc += FString::Printf(TEXT(" [%d..%d/%d]%s%s"), MoveSegmentStartIndex + 1, MoveSegmentEndIndex + 1, Path->GetPathPoints().Num()
, bWalkingNavLinkStart ? TEXT("navlink") : TEXT(""), Path->IsValid() ? TEXT("") : TEXT(" Invalidated"));
}
Category.Add(TEXT("Status"), StatusDesc);
Category.Add(TEXT("MoveID"), GetCurrentRequestId().ToString());
Category.Add(TEXT("Path"), !Path.IsValid() ? TEXT("none") :
(Path->CastPath<FNavMeshPath>() != NULL) ? TEXT("navmesh") :
(Path->CastPath<FAbstractNavigationPath>() != NULL) ? TEXT("direct") :
TEXT("unknown"));
Category.Add(TEXT("Block detection"), bUseBlockDetection ? FString::Printf(TEXT("last sample time %.2f"), LastSampleTime) : TEXT("Disabled"));
UObject* CustomNavLinkOb = CurrentCustomLinkOb.Get();
if (CustomNavLinkOb)
{
Category.Add(TEXT("Custom NavLink"), CustomNavLinkOb->GetName());
}
Snapshot->Status.Add(Category);
}
#endif // ENABLE_VISUAL_LOG
void UPathFollowingComponent::GetDebugStringTokens(TArray<FString>& Tokens, TArray<EPathFollowingDebugTokens::Type>& Flags) const
{
Tokens.Add(GetStatusDesc());
Flags.Add(EPathFollowingDebugTokens::Description);
if (Status != EPathFollowingStatus::Moving)
{
return;
}
FString& StatusDesc = Tokens[0];
if (Path.IsValid())
{
const int32 NumMoveSegments = (Path.IsValid() && Path->IsValid()) ? Path->GetPathPoints().Num() : -1;
const bool bIsDirect = (Path->CastPath<FAbstractNavigationPath>() != NULL);
const bool bIsCustomLink = CurrentCustomLinkOb.IsValid();
if (!bIsDirect)
{
StatusDesc += FString::Printf(TEXT(" (%d..%d/%d)%s"), MoveSegmentStartIndex + 1, MoveSegmentEndIndex + 1, NumMoveSegments,
bIsCustomLink ? TEXT(" (custom NavLink)") : TEXT(""));
}
else
{
StatusDesc += TEXT(" (direct)");
}
}
else
{
StatusDesc += TEXT(" (invalid path)");
}
// add debug params
float CurrentDot = 0.0f, CurrentDistance = 0.0f, CurrentHeight = 0.0f;
uint8 bFailedDot = 0, bFailedDistance = 0, bFailedHeight = 0;
DebugReachTest(CurrentDot, CurrentDistance, CurrentHeight, bFailedHeight, bFailedDistance, bFailedHeight);
Tokens.Add(TEXT("dot"));
Flags.Add(EPathFollowingDebugTokens::ParamName);
Tokens.Add(FString::Printf(TEXT("%.2f"), CurrentDot));
Flags.Add(bFailedDot ? EPathFollowingDebugTokens::FailedValue : EPathFollowingDebugTokens::PassedValue);
Tokens.Add(TEXT("dist2D"));
Flags.Add(EPathFollowingDebugTokens::ParamName);
Tokens.Add(FString::Printf(TEXT("%.0f"), CurrentDistance));
Flags.Add(bFailedDistance ? EPathFollowingDebugTokens::FailedValue : EPathFollowingDebugTokens::PassedValue);
Tokens.Add(TEXT("distZ"));
Flags.Add(EPathFollowingDebugTokens::ParamName);
Tokens.Add(FString::Printf(TEXT("%.0f"), CurrentHeight));
Flags.Add(bFailedHeight ? EPathFollowingDebugTokens::FailedValue : EPathFollowingDebugTokens::PassedValue);
}
FString UPathFollowingComponent::GetDebugString() const
{
TArray<FString> Tokens;
TArray<EPathFollowingDebugTokens::Type> Flags;
GetDebugStringTokens(Tokens, Flags);
FString Desc;
for (int32 TokenIndex = 0; TokenIndex < Tokens.Num(); TokenIndex++)
{
Desc += Tokens[TokenIndex];
Desc += (Flags.IsValidIndex(TokenIndex) && Flags[TokenIndex] == EPathFollowingDebugTokens::ParamName) ? TEXT(": ") : TEXT(", ");
}
return Desc;
}
bool UPathFollowingComponent::IsPathFollowingAllowed() const
{
return NavMovementInterface.IsValid() && NavMovementInterface->CanStartPathFollowing();
}
void UPathFollowingComponent::OnUnableToMove(const UObject& Instigator)
{
AbortMove(Instigator, FPathFollowingResultFlags::MovementStop);
}
void UPathFollowingComponent::OnStartedFalling()
{
bWalkingNavLinkStart = false;
}
void UPathFollowingComponent::LockResource(EAIRequestPriority::Type LockSource)
{
const static UEnum* SourceEnum = StaticEnum<EAILockSource::Type>();
const bool bWasLocked = ResourceLock.IsLocked();
ResourceLock.SetLock(LockSource);
if (bWasLocked == false)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Locking Move by source %s"), SourceEnum ? *SourceEnum->GetNameStringByValue(Status) : TEXT("invalid"));
PauseMove(CurrentRequestId, EPathFollowingVelocityMode::Reset);
}
}
void UPathFollowingComponent::ClearResourceLock(EAIRequestPriority::Type LockSource)
{
const bool bWasLocked = ResourceLock.IsLocked();
ResourceLock.ClearLock(LockSource);
if (bWasLocked && !ResourceLock.IsLocked())
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Unlocking Move"));
ResumeMove();
}
}
void UPathFollowingComponent::ForceUnlockResource()
{
const bool bWasLocked = ResourceLock.IsLocked();
ResourceLock.ForceClearAllLocks();
if (bWasLocked)
{
UE_VLOG(GetOwner(), LogPathFollowing, Log, TEXT("Unlocking Move - forced"));
ResumeMove();
}
}
bool UPathFollowingComponent::IsResourceLocked() const
{
return ResourceLock.IsLocked();
}
void UPathFollowingComponent::SetLastMoveAtGoal(bool bFinishedAtGoal)
{
if (Status == EPathFollowingStatus::Idle)
{
bLastMoveReachedGoal = bFinishedAtGoal;
}
}
void UPathFollowingComponent::OnWaitingPathTimeout()
{
if (Status == EPathFollowingStatus::Waiting)
{
UE_VLOG(GetOwner(), LogPathFollowing, Warning, TEXT("Waiting for path timeout! Aborting current move"));
OnPathFinished(EPathFollowingResult::Invalid, FPathFollowingResultFlags::None);
}
}
// deprecated functions
EPathFollowingAction::Type UPathFollowingComponent::GetPathActionType() const
{
switch (Status)
{
case EPathFollowingStatus::Idle:
return EPathFollowingAction::NoMove;
case EPathFollowingStatus::Waiting:
case EPathFollowingStatus::Paused:
case EPathFollowingStatus::Moving:
return Path.IsValid() == false ? EPathFollowingAction::Error :
Path->CastPath<FAbstractNavigationPath>() ? EPathFollowingAction::DirectMove :
Path->IsPartial() ? EPathFollowingAction::PartialPath : EPathFollowingAction::PathToGoal;
default:
break;
}
return EPathFollowingAction::Error;
}
FVector UPathFollowingComponent::GetPathDestination() const
{
return Path.IsValid() ? Path->GetDestinationLocation() : FVector::ZeroVector;
}
#undef SHIPPING_STATIC