Files
UnrealEngine/Engine/Plugins/AI/MassAI/Source/MassNavigation/Private/MassSteeringProcessors.cpp
2025-05-18 13:04:45 +08:00

408 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Steering/MassSteeringProcessors.h"
#include "MassCommonUtils.h"
#include "MassCommandBuffer.h"
#include "MassDebugLogging.h"
#include "MassCommonFragments.h"
#include "MassDebugger.h"
#include "MassExecutionContext.h"
#include "MassMovementFragments.h"
#include "MassNavigationFragments.h"
#include "Steering/MassSteeringFragments.h"
#include "Math/UnrealMathUtility.h"
#include "MassSimulationLOD.h"
#if WITH_MASSGAMEPLAY_DEBUG
#include "MassNavigationDebug.h"
#endif // WITH_MASSGAMEPLAY_DEBUG
namespace UE::MassNavigation
{
/*
* Calculates speed scale based on agent's forward direction and desired steering direction.
*/
static FVector::FReal CalcDirectionalSpeedScale(const FVector ForwardDirection, const FVector SteerDirection)
{
// @todo: make these configurable
constexpr FVector::FReal ForwardSpeedScale = 1.;
constexpr FVector::FReal BackwardSpeedScale = 0.25;
constexpr FVector::FReal SideSpeedScale = 0.5;
const FVector LeftDirection = FVector::CrossProduct(ForwardDirection, FVector::UpVector);
const FVector::FReal DirX = FVector::DotProduct(LeftDirection, SteerDirection);
const FVector::FReal DirY = FVector::DotProduct(ForwardDirection, SteerDirection);
// Calculate intersection between a direction vector and ellipse, where A & B are the size of the ellipse.
// The direction vector is starting from the center of the ellipse.
constexpr FVector::FReal SideA = SideSpeedScale;
const FVector::FReal SideB = DirY > 0. ? ForwardSpeedScale : BackwardSpeedScale;
const FVector::FReal Disc = FMath::Square(SideA) * FMath::Square(DirY) + FMath::Square(SideB) * FMath::Square(DirX);
const FVector::FReal Speed = (Disc > SMALL_NUMBER) ? (SideA * SideB / FMath::Sqrt(Disc)) : 0.;
return Speed;
}
/** Speed envelope when approaching a point. NormalizedDistance in range [0..1] */
static FVector::FReal ArrivalSpeedEnvelope(const FVector::FReal NormalizedDistance)
{
return FMath::Sqrt(NormalizedDistance);
}
} // UE::MassNavigation
//----------------------------------------------------------------------//
// UMassSteerToMoveTargetProcessor
//----------------------------------------------------------------------//
UMassSteerToMoveTargetProcessor::UMassSteerToMoveTargetProcessor()
: EntityQuery(*this)
{
ExecutionFlags = int32(EProcessorExecutionFlags::AllNetModes);
ExecutionOrder.ExecuteAfter.Add(UE::Mass::ProcessorGroupNames::Tasks);
ExecutionOrder.ExecuteBefore.Add(UE::Mass::ProcessorGroupNames::Avoidance);
}
void UMassSteerToMoveTargetProcessor::ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager)
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FMassVelocityFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassSteeringFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassStandingSteeringFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassGhostLocationFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassForceFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassDesiredMovementFragment>(EMassFragmentAccess::ReadWrite); // Note this is read-write because this processor will sometimes zero the desired velocity, but normally it affects it via the FForceFragment
EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
EntityQuery.AddConstSharedRequirement<FMassMovingSteeringParameters>(EMassFragmentPresence::All);
EntityQuery.AddConstSharedRequirement<FMassStandingSteeringParameters>(EMassFragmentPresence::All);
#if WITH_MASSGAMEPLAY_DEBUG
EntityQuery.DebugEnableEntityOwnerLogging();
#endif
// No need for Off LOD to do steering, applying move target directly
EntityQuery.AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::None);
EntityQuery.AddTagRequirement<FMassCodeDrivenMovementTag>(EMassFragmentPresence::Optional);
}
void UMassSteerToMoveTargetProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
const UWorld* World = EntityManager.GetWorld();
ensure(World);
EntityQuery.ForEachEntityChunk(Context, [this, World](FMassExecutionContext& Context)
{
const TConstArrayView<FTransformFragment> TransformList = Context.GetFragmentView<FTransformFragment>();
const TConstArrayView<FMassVelocityFragment> VelocityList = Context.GetFragmentView<FMassVelocityFragment>();
const TArrayView<FMassMoveTargetFragment> MoveTargetList = Context.GetMutableFragmentView<FMassMoveTargetFragment>();
const TArrayView<FMassDesiredMovementFragment> MovementList = Context.GetMutableFragmentView<FMassDesiredMovementFragment>();
const TArrayView<FMassForceFragment> ForceList = Context.GetMutableFragmentView<FMassForceFragment>();
const TArrayView<FMassSteeringFragment> SteeringList = Context.GetMutableFragmentView<FMassSteeringFragment>();
const TArrayView<FMassStandingSteeringFragment> StandingSteeringList = Context.GetMutableFragmentView<FMassStandingSteeringFragment>();
const TArrayView<FMassGhostLocationFragment> GhostList = Context.GetMutableFragmentView<FMassGhostLocationFragment>();
const FMassMovementParameters& MovementParams = Context.GetConstSharedFragment<FMassMovementParameters>();
const FMassMovingSteeringParameters& MovingSteeringParams = Context.GetConstSharedFragment<FMassMovingSteeringParameters>();
const FMassStandingSteeringParameters& StandingSteeringParams = Context.GetConstSharedFragment<FMassStandingSteeringParameters>();
bool bIsCodeDriven = Context.DoesArchetypeHaveTag<FMassCodeDrivenMovementTag>();
const FVector::FReal SteerK = 1. / MovingSteeringParams.ReactionTime;
const float DeltaTime = Context.GetDeltaTimeSeconds();
for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt)
{
const FTransformFragment& TransformFragment = TransformList[EntityIt];
const FMassVelocityFragment& VelocityFragment = VelocityList[EntityIt];
FMassSteeringFragment& Steering = SteeringList[EntityIt];
FMassStandingSteeringFragment& StandingSteering = StandingSteeringList[EntityIt];
FMassGhostLocationFragment& Ghost = GhostList[EntityIt];
FMassMoveTargetFragment& MoveTarget = MoveTargetList[EntityIt];
FMassForceFragment& Force = ForceList[EntityIt];
FMassDesiredMovementFragment& DesiredMovement = MovementList[EntityIt];
const FMassEntityHandle Entity = Context.GetEntity(EntityIt);
const FTransform& Transform = TransformFragment.GetTransform();;
// Calculate velocity for steering.
const FVector CurrentLocation = Transform.GetLocation();
const FVector CurrentForward = Transform.GetRotation().GetForwardVector();
const FVector::FReal LookAheadDistance = FMath::Max(1.0f, MovingSteeringParams.LookAheadTime * MoveTarget.DesiredSpeed.Get());
#if WITH_MASSGAMEPLAY_DEBUG
UE::MassNavigation::Debug::FDebugContext NavigationDebugContext(Context, this, LogMassNavigation, World, Entity, EntityIt);
const bool bDisplayDebug = NavigationDebugContext.ShouldLogEntity();
const UObject* LogOwner = NavigationDebugContext.GetLogOwner();
#endif
if (MoveTarget.GetCurrentAction() == EMassMovementAction::Move)
{
// Tune down avoidance and speed when arriving at goal.
FVector::FReal ArrivalFade = 1.;
if (MoveTarget.IntentAtGoal == EMassMovementAction::Stand)
{
ArrivalFade = FMath::Clamp(MoveTarget.DistanceToGoal / LookAheadDistance, 0., 1.);
}
const FVector::FReal SteeringPredictionDistance = LookAheadDistance * ArrivalFade;
// Steer towards and along the move target.
const FVector TargetSide = FVector::CrossProduct(MoveTarget.Forward, FVector::UpVector);
const FVector Delta = CurrentLocation - MoveTarget.Center;
const FVector::FReal ForwardOffset = FVector::DotProduct(MoveTarget.Forward, Delta);
// Calculate steering direction. When far away from the line defined by TargetPosition and TargetTangent,
// the steering direction is towards the line, the close we get, the more it aligns with the line.
const FVector::FReal SidewaysOffset = FVector::DotProduct(TargetSide, Delta);
const FVector::FReal SteerForward = FMath::Sqrt(FMath::Max(0., FMath::Square(SteeringPredictionDistance) - FMath::Square(SidewaysOffset)));
// The Max() here makes the steering directions behind the TargetPosition to steer towards it directly.
FVector SteerTarget = MoveTarget.Center + MoveTarget.Forward * FMath::Clamp(ForwardOffset + SteerForward, 0., SteeringPredictionDistance);
FVector SteerDirection = SteerTarget - CurrentLocation;
SteerDirection.Z = 0.;
const FVector::FReal DistanceToSteerTarget = SteerDirection.Length();
if (DistanceToSteerTarget > KINDA_SMALL_NUMBER)
{
SteerDirection *= 1. / DistanceToSteerTarget;
}
#if WITH_MASSGAMEPLAY_DEBUG
if (bDisplayDebug)
{
// Display SteerDirection
const FVector ZOffset(0,0,25);
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, CurrentLocation + ZOffset, CurrentLocation + ZOffset + 100.*SteerDirection,
FColor::Red, /*Thickness*/2, TEXT("SteerDirection"));
}
#endif //WITH_MASSGAMEPLAY_DEBUG
FVector::FReal DesiredSpeed = MoveTarget.DesiredSpeed.Get();
// When being animation driven, animation has authority over the movement, so it might be useful to disable
// this catchup mechanic to avoid subtle speed variations affecting animation.
if (MovingSteeringParams.bAllowSpeedVariance)
{
const FVector::FReal DirSpeedScale = UE::MassNavigation::CalcDirectionalSpeedScale(CurrentForward, SteerDirection);
DesiredSpeed *= DirSpeedScale;
// Control speed based relation to the forward axis of the move target.
FVector::FReal CatchupDesiredSpeed = DesiredSpeed;
if (ForwardOffset < 0.)
{
// Falling behind, catch up
const FVector::FReal T = FMath::Min(-ForwardOffset / LookAheadDistance, 1.);
CatchupDesiredSpeed = FMath::Lerp(DesiredSpeed, MovementParams.MaxSpeed, T);
}
else if (ForwardOffset > 0.)
{
// Ahead, slow down.
const FVector::FReal T = FMath::Min(ForwardOffset / LookAheadDistance, 1.);
CatchupDesiredSpeed = FMath::Lerp(DesiredSpeed, DesiredSpeed * 0., 1. - FMath::Square(1. - T));
}
// Control speed based on distance to move target. This allows to catch up even if speed above reaches zero.
const FVector::FReal DeviantSpeed = FMath::Min(FMath::Abs(SidewaysOffset) / LookAheadDistance, 1.) * DesiredSpeed;
DesiredSpeed = FMath::Max(CatchupDesiredSpeed, DeviantSpeed);
}
// Slow down towards the end of path.
if (MoveTarget.IntentAtGoal == EMassMovementAction::Stand)
{
const FVector::FReal NormalizedDistanceToSteerTarget = FMath::Clamp(DistanceToSteerTarget / LookAheadDistance, 0., 1.);
DesiredSpeed *= UE::MassNavigation::ArrivalSpeedEnvelope(FMath::Max(ArrivalFade, NormalizedDistanceToSteerTarget));
}
constexpr FVector::FReal FallingBehindScale = 0.8;
if (MoveTarget.EntityDistanceToGoal != FMassMoveTargetFragment::UnsetDistance)
{
MoveTarget.bSteeringFallingBehind = (MoveTarget.EntityDistanceToGoal - MoveTarget.DistanceToGoal) > LookAheadDistance * FallingBehindScale;
}
else
{
// If EntityDistanceToGoal is not available, use ForwardOffset.
MoveTarget.bSteeringFallingBehind = ForwardOffset < -LookAheadDistance * FallingBehindScale;
}
// @todo: This current completely overrides steering, we probably should have one processor that resets the steering at the beginning of the frame.
Steering.DesiredVelocity = SteerDirection * DesiredSpeed;
// Important: we want steering force to be stable against noisy velocity, so we use
// the difference between current desired velocity and target desired velocity.
// We dont want to read the actual agent velocity directly since this can create a feedback loop for animated characters
Force.Value = SteerK * (Steering.DesiredVelocity - DesiredMovement.DesiredVelocity); // Goal force
}
else if (MoveTarget.GetCurrentAction() == EMassMovementAction::Stand)
{
// Calculate unique target move threshold so that different agents react a bit differently.
const FVector::FReal PerEntityScale = UE::RandomSequence::FRand(Entity.Index);
const FVector::FReal TargetMoveThreshold = StandingSteeringParams.TargetMoveThreshold * (1. - StandingSteeringParams.TargetMoveThresholdVariance + PerEntityScale * StandingSteeringParams.TargetMoveThresholdVariance * 2.);
if (Ghost.LastSeenActionID != MoveTarget.GetCurrentActionID())
{
// Reset when action changes. @todo: should reset only when move->stand?
Ghost.Location = MoveTarget.Center;
Ghost.Velocity = FVector::ZeroVector;
Ghost.LastSeenActionID = MoveTarget.GetCurrentActionID();
StandingSteering.TargetLocation = MoveTarget.Center;
StandingSteering.TrackedTargetSpeed = 0.0f;
StandingSteering.bIsUpdatingTarget = false;
StandingSteering.TargetSelectionCooldown = StandingSteeringParams.TargetSelectionCooldown * FMath::RandRange(1.f - StandingSteeringParams.TargetSelectionCooldownVariance, 1.f + StandingSteeringParams.TargetSelectionCooldownVariance);
StandingSteering.bEnteredFromMoveAction = MoveTarget.GetPreviousAction() == EMassMovementAction::Move;
}
StandingSteering.TargetSelectionCooldown = FMath::Max(0.0f, StandingSteering.TargetSelectionCooldown - DeltaTime);
if (!StandingSteering.bIsUpdatingTarget)
{
// Update the move target if enough time has passed and the target has moved.
if (StandingSteering.TargetSelectionCooldown <= 0.0f
&& FVector::DistSquared(StandingSteering.TargetLocation, Ghost.Location) > FMath::Square(TargetMoveThreshold))
{
StandingSteering.TargetLocation = Ghost.Location;
StandingSteering.TrackedTargetSpeed = 0.0f;
StandingSteering.bIsUpdatingTarget = true;
StandingSteering.bEnteredFromMoveAction = false;
}
}
else
{
// Updating target
StandingSteering.TargetLocation = Ghost.Location;
const FVector::FReal GhostSpeed = Ghost.Velocity.Length();
if (GhostSpeed > (StandingSteering.TrackedTargetSpeed * StandingSteeringParams.TargetSpeedHysteresisScale))
{
const FVector::FReal TrackedTargetSpeed = FMath::Max(StandingSteering.TrackedTargetSpeed, GhostSpeed);
StandingSteering.TrackedTargetSpeed = static_cast<float>(TrackedTargetSpeed);
}
else
{
// Speed is dropping, we have found the peak change, stop updating the target and start cooldown.
StandingSteering.TargetSelectionCooldown = StandingSteeringParams.TargetSelectionCooldown * FMath::RandRange(1.0f - StandingSteeringParams.TargetSelectionCooldownVariance, 1.0f + StandingSteeringParams.TargetSelectionCooldownVariance);
StandingSteering.bIsUpdatingTarget = false;
}
}
// Move directly towards the move target when standing.
FVector SteerDirection = FVector::ZeroVector;
FVector::FReal DesiredSpeed = 0.;
FVector Delta = StandingSteering.TargetLocation - CurrentLocation;
Delta.Z = 0.;
const FVector::FReal Distance = Delta.Size();
if (Distance > StandingSteeringParams.DeadZoneRadius)
{
#if WITH_MASSGAMEPLAY_DEBUG
if (bDisplayDebug)
{
UE_VLOG_UELOG(LogOwner, LogMassNavigation, Verbose, TEXT("Standing steering: out of deadzone (Distance: %.2f)"), Distance);
}
#endif
SteerDirection = Delta / Distance;
if (StandingSteering.bEnteredFromMoveAction)
{
// If the current steering target is from approaching a move target, use the same speed logic as movement to ensure smooth transition.
const FVector::FReal Range = FMath::Max(1., LookAheadDistance - StandingSteeringParams.DeadZoneRadius);
const FVector::FReal SpeedFade = FMath::Clamp((Distance - StandingSteeringParams.DeadZoneRadius) / Range, 0., 1.);
DesiredSpeed = MoveTarget.DesiredSpeed.Get() * UE::MassNavigation::CalcDirectionalSpeedScale(CurrentForward, SteerDirection) * UE::MassNavigation::ArrivalSpeedEnvelope(SpeedFade);
}
else
{
const FVector::FReal Range = FMath::Max(1., LookAheadDistance - StandingSteeringParams.DeadZoneRadius);
const FVector::FReal SpeedFade = FMath::Clamp((Distance - StandingSteeringParams.DeadZoneRadius) / Range, 0., 1.);
// Not using the directional scaling so that the steps we take to avoid are done quickly, and the behavior is reactive.
DesiredSpeed = MoveTarget.DesiredSpeed.Get() * UE::MassNavigation::ArrivalSpeedEnvelope(SpeedFade);
}
// @todo: This current completely overrides steering, we probably should have one processor that resets the steering at the beginning of the frame.
Steering.DesiredVelocity = SteerDirection * DesiredSpeed;
Force.Value = SteerK * (Steering.DesiredVelocity - DesiredMovement.DesiredVelocity); // Goal force
Force.Value = Force.Value.GetClampedToMaxSize(MovementParams.MaxAcceleration);
}
else
{
// When reached destination, clamp small desired velocities to zero to avoid tiny drifting.
if (DesiredMovement.DesiredVelocity.SquaredLength() < FMath::Square(StandingSteeringParams.LowSpeedThreshold))
{
DesiredMovement.DesiredVelocity = FVector::ZeroVector;
Force.Value = FVector::ZeroVector;
}
}
MoveTarget.bSteeringFallingBehind = false;
}
else if (MoveTarget.GetCurrentAction() == EMassMovementAction::Animate)
{
Steering.Reset();
MoveTarget.bSteeringFallingBehind = false;
Force.Value = FVector::ZeroVector;
if (bIsCodeDriven)
{
// Stop all movement when animating.
DesiredMovement.DesiredVelocity = FVector::ZeroVector;
}
else
{
// Animation driven, sync desired movement to animation
Steering.DesiredVelocity = VelocityFragment.Value;
DesiredMovement.DesiredVelocity = VelocityFragment.Value;
if (!DesiredMovement.DesiredVelocity.IsNearlyZero())
{
DesiredMovement.DesiredFacing = FQuat(DesiredMovement.DesiredVelocity.Rotation());
}
}
}
#if WITH_MASSGAMEPLAY_DEBUG
FColor EntityColor = FColor::White;
if (bDisplayDebug)
{
const FVector ZOffset(0,0,25);
const FColor LightEntityColor = UE::MassNavigation::Debug::MixColors(EntityColor, FColor::White);
const FVector MoveTargetCenter = MoveTarget.Center + ZOffset;
// Display MoveTarget location
UE_VLOG_CIRCLE_THICK(LogOwner, LogMassNavigation, Log, MoveTargetCenter, FVector::UpVector, 5.f, EntityColor, /*Thickness*/2,
TEXT("MoveTarget\ngoal: %s"), *UEnum::GetDisplayValueAsText(MoveTarget.IntentAtGoal).ToString());
// Display ghost location
UE_VLOG_CIRCLE_THICK(LogOwner, LogMassNavigation, Log, Ghost.Location, FVector::UpVector, 5.f, FColor::Silver, /*Thickness*/2, TEXT("Ghost"));
// Display MoveTarget orientation
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, MoveTargetCenter, MoveTargetCenter + MoveTarget.Forward * 100, EntityColor, /*Thickness*/1,
TEXT("MoveTarget\nforward"));
// Display MoveTarget - current location relation
if (MoveTarget.DesiredSpeed.Get() > 0.f && FVector::Dist2D(CurrentLocation, MoveTarget.Center) > LookAheadDistance * 1.5f)
{
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, MoveTargetCenter, CurrentLocation + ZOffset, FColor::Red, /*Thickness*/1, TEXT("LOST"));
}
else
{
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, MoveTargetCenter, CurrentLocation + ZOffset, LightEntityColor, /*Thickness*/1, TEXT(""));
}
// Display DesiredMovement DesiredVelocity
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, CurrentLocation + ZOffset, CurrentLocation + ZOffset + DesiredMovement.DesiredVelocity, FColor::Yellow, /*Thickness*/4,
TEXT("Mvt DesiredVelocity %.1f"), DesiredMovement.DesiredVelocity.Length());
// Display Steering internal DesiredVelocity
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, CurrentLocation + ZOffset + FVector(0,0,2),
CurrentLocation + Steering.DesiredVelocity + ZOffset + FVector(0,0,2), FColor::Orange, /*Thickness*/4,
TEXT("Steering DesiredVelocity %.1f"), Steering.DesiredVelocity.Length());
// Display Force
UE_VLOG_SEGMENT_THICK(LogOwner, LogMassNavigation, Log, CurrentLocation + ZOffset, CurrentLocation + Force.Value + ZOffset, FColor::Emerald, /*Thickness*/4,
TEXT("Steering Force %.1f"), Force.Value.Length());
}
#endif // WITH_MASSGAMEPLAY_DEBUG
}
});
}