Steering with Mass Framework

How to process steering with Mass Framework. Mass Framework is an ECS for managing crowds and traffic, available since Unreal Engine 5.0. Engine code excerpts below were taken from UE 5.3 and their signatures updated to the Unreal Engine 5.8 API - check the source of your engine version for implementation details.

Vladimír NeporAuthorVladimír Nepor
Published

Processing Steering with MassAI built-in resources

  • Steering Trait

    Built-in MassAI Trait located At UE5/Engine/Plugins/AI/MassAI/Source/MassNavigation

    void UMassSteeringTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
    {
    	FMassEntityManager& EntityManager = UE::Mass::Utils::GetEntityManagerChecked(World);
    
    	BuildContext.RequireFragment<FAgentRadiusFragment>();
    	BuildContext.RequireFragment<FTransformFragment>();
    	BuildContext.RequireFragment<FMassVelocityFragment>();
    	BuildContext.RequireFragment<FMassForceFragment>();
    
    	BuildContext.AddFragment<FMassMoveTargetFragment>();
    	BuildContext.AddFragment<FMassSteeringFragment>();
    	BuildContext.AddFragment<FMassStandingSteeringFragment>();
    	BuildContext.AddFragment<FMassGhostLocationFragment>();
    
    	const FConstSharedStruct MovingSteeringFragment = EntityManager.GetOrCreateConstSharedFragment(MovingSteering);
    	BuildContext.AddConstSharedFragment(MovingSteeringFragment);
    
    	const FConstSharedStruct StandingSteeringFragment = EntityManager.GetOrCreateConstSharedFragment(StandingSteering);
    	BuildContext.AddConstSharedFragment(StandingSteeringFragment);
    }
  • Fragments for Steering

    As you can see, the built-in Steering Trait of MassAI plugin works with the 8 following fragments:

    Required Fragments

    Required fragments are identical as fragments for Movement Trait

    Located at UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassMovement

    USTRUCT()
    struct MASSMOVEMENT_API FMassVelocityFragment : public FMassFragment
    {
    	GENERATED_BODY()
    
    	FVector Value = FVector::ZeroVector;};

    Added fragments

    Located At UE5/Engine/Plugins/AI/MassAI/Source/MassNavigation

    USTRUCT()
    struct MASSNAVIGATION_API FMassSteeringFragment : public FMassFragment
    {
    	GENERATED_BODY()
    
    	void Reset()
    	{
    		DesiredVelocity = FVector::ZeroVector;
    	}
    
    	/** Cached desired velocity from steering. Note: not used for moving the entity. */
    	FVector DesiredVelocity = FVector::ZeroVector;
    };
  • Steering Processor

    Built-in MassAI Processor located at UE5/Engine/Plugins/AI/MassAI/Source/MassNavigation

    UMassSteerToMoveTargetProcessor::UMassSteerToMoveTargetProcessor()
    	: EntityQuery(*this)
    {
    	ExecutionFlags = int32(EProcessorExecutionFlags::All);
    	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<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<FMassVelocityFragment>(EMassFragmentAccess::ReadWrite);
    	EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
    	EntityQuery.AddConstSharedRequirement<FMassMovingSteeringParameters>(EMassFragmentPresence::All);
    	EntityQuery.AddConstSharedRequirement<FMassStandingSteeringParameters>(EMassFragmentPresence::All);
    
    	// No need for Off LOD to do steering, applying move target directly
    	EntityQuery.AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::None);
    }
    
    void UMassSteerToMoveTargetProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
    {
    	EntityQuery.ForEachEntityChunk(Context, [this](FMassExecutionContext& Context)
    	{
    		const int32 NumEntities = Context.GetNumEntities();
    		const TConstArrayView<FTransformFragment> TransformList = Context.GetFragmentView<FTransformFragment>();
    		const TArrayView<FMassMoveTargetFragment> MoveTargetList = Context.GetMutableFragmentView<FMassMoveTargetFragment>();
    		const TArrayView<FMassVelocityFragment> VelocityList = Context.GetMutableFragmentView<FMassVelocityFragment>();
    		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>();
    
    		const FVector::FReal SteerK = 1. / MovingSteeringParams.ReactionTime;
    		const float DeltaTime = Context.GetDeltaTimeSeconds();
    
    		for (int32 EntityIndex = 0; EntityIndex < NumEntities; ++EntityIndex)
    		{
    			const FTransformFragment& TransformFragment = TransformList[EntityIndex];
    			FMassSteeringFragment& Steering = SteeringList[EntityIndex];
    			FMassStandingSteeringFragment& StandingSteering = StandingSteeringList[EntityIndex];
    			FMassGhostLocationFragment& Ghost = GhostList[EntityIndex];
    			FMassMoveTargetFragment& MoveTarget = MoveTargetList[EntityIndex];
    			FMassForceFragment& Force = ForceList[EntityIndex];
    			FMassVelocityFragment& Velocity = VelocityList[EntityIndex];
    			const FMassEntityHandle Entity = Context.GetEntity(EntityIndex);
    
    			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 (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;
    				}
    				
    				const FVector::FReal DirSpeedScale = UE::MassNavigation::CalcDirectionalSpeedScale(CurrentForward, SteerDirection);
    				FVector::FReal DesiredSpeed = MoveTarget.DesiredSpeed.Get() * 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));
    				}
    
    				MoveTarget.bSteeringFallingBehind = ForwardOffset < -LookAheadDistance * 0.8;
    
    				// @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 - Velocity.Value); // 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)
    				{
    					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 - Velocity.Value); // Goal force
    
    				}
    				else
    				{
    					// When reached destination, clamp small velocities to zero to avoid tiny drifting.
    					if (Velocity.Value.SquaredLength() < FMath::Square(StandingSteeringParams.LowSpeedThreshold))
    					{
    						Velocity.Value = FVector::ZeroVector;
    						Force.Value = FVector::ZeroVector;
    					}
    				}
    
    				MoveTarget.bSteeringFallingBehind = false;
    			}
    			else if (MoveTarget.GetCurrentAction() == EMassMovementAction::Animate)
    			{
    				// Stop all movement when animating.
    				Steering.Reset();
    				MoveTarget.bSteeringFallingBehind = false;
    				Force.Value = FVector::ZeroVector;
    				Velocity.Value = FVector::ZeroVector;
    			}			
    		}
    	});
    }

Anything unclear?

Let us know!

Contact Us