// 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& EntityManager) { EntityQuery.AddRequirement(EMassFragmentAccess::ReadOnly); EntityQuery.AddRequirement(EMassFragmentAccess::ReadOnly); EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirement(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(EMassFragmentPresence::All); EntityQuery.AddConstSharedRequirement(EMassFragmentPresence::All); EntityQuery.AddConstSharedRequirement(EMassFragmentPresence::All); #if WITH_MASSGAMEPLAY_DEBUG EntityQuery.DebugEnableEntityOwnerLogging(); #endif // No need for Off LOD to do steering, applying move target directly EntityQuery.AddTagRequirement(EMassFragmentPresence::None); EntityQuery.AddTagRequirement(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 TransformList = Context.GetFragmentView(); const TConstArrayView VelocityList = Context.GetFragmentView(); const TArrayView MoveTargetList = Context.GetMutableFragmentView(); const TArrayView MovementList = Context.GetMutableFragmentView(); const TArrayView ForceList = Context.GetMutableFragmentView(); const TArrayView SteeringList = Context.GetMutableFragmentView(); const TArrayView StandingSteeringList = Context.GetMutableFragmentView(); const TArrayView GhostList = Context.GetMutableFragmentView(); const FMassMovementParameters& MovementParams = Context.GetConstSharedFragment(); const FMassMovingSteeringParameters& MovingSteeringParams = Context.GetConstSharedFragment(); const FMassStandingSteeringParameters& StandingSteeringParams = Context.GetConstSharedFragment(); bool bIsCodeDriven = Context.DoesArchetypeHaveTag(); 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(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 } }); }