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.

Processing Steering with MassAI built-in resources
Steering Trait
Built-in
MassAITrait located AtUE5/Engine/Plugins/AI/MassAI/Source/MassNavigationImage
.hUCLASS(meta = (DisplayName = "Steering")) class MASSNAVIGATION_API UMassSteeringTrait : public UMassEntityTraitBase { GENERATED_BODY() protected: virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override; UPROPERTY(Category="Steering", EditAnywhere, meta=(EditInline)) FMassMovingSteeringParameters MovingSteering; UPROPERTY(Category="Steering", EditAnywhere, meta=(EditInline)) FMassStandingSteeringParameters StandingSteering; };.cppvoid 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 TraitofMassAIplugin works with the 8 following fragments:Required Fragments
Required fragments are identical as fragments for
Movement TraitFMassVelocityFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassMovementUSTRUCT() struct MASSMOVEMENT_API FMassVelocityFragment : public FMassFragment { GENERATED_BODY() FVector Value = FVector::ZeroVector;};FMassForceFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassMovementUSTRUCT() struct MASSMOVEMENT_API FMassForceFragment : public FMassFragment { GENERATED_BODY() FVector Value = FVector::ZeroVector;};FAgentRadiusFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassCommonUSTRUCT() struct MASSCOMMON_API FAgentRadiusFragment : public FMassFragment { GENERATED_BODY() UPROPERTY(EditAnywhere, Category = "") float Radius = 40.f;};FTransformFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassCommonUSTRUCT() struct MASSCOMMON_API FTransformFragment : public FMassFragment { GENERATED_BODY() const FTransform& GetTransform() const { return Transform; } void SetTransform(const FTransform& InTransform) { Transform = InTransform; } FTransform& GetMutableTransform() { return Transform; } protected: UPROPERTY(Transient) FTransform Transform;};Added fragments
FMassMoveTargetFragmentLocated At
UE5/Engine/Plugins/AI/MassAI/Source/MassNavigation/** Move target. */ USTRUCT() struct MASSNAVIGATION_API FMassMoveTargetFragment : public FMassFragment { GENERATED_BODY() FMassMoveTargetFragment() : bNetDirty(false), bOffBoundaries(false), bSteeringFallingBehind(false) {} /** To setup current action from the authoritative world */ void CreateNewAction(const EMassMovementAction InAction, const UWorld& InWorld); /** To setup current action from replicated data */ void CreateReplicatedAction(const EMassMovementAction InAction, const uint16 InActionID, const double InWorldStartTime, const double InServerStartTime); void MarkNetDirty() { bNetDirty = true; } bool GetNetDirty() const { return bNetDirty; } void ResetNetDirty() { bNetDirty = false; } public: ...FMassSteeringFragmentLocated At
UE5/Engine/Plugins/AI/MassAI/Source/MassNavigationUSTRUCT() 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; };FMassStandingSteeringFragmentLocated At
UE5/Engine/Plugins/AI/MassAI/Source/MassNavigationUSTRUCT() struct MASSNAVIGATION_API FMassStandingSteeringFragment : public FMassFragment { GENERATED_BODY() /** Selected steer target based on ghost, updates periodically. */ FVector TargetLocation = FVector::ZeroVector; /** Used during target update to see when the target movement stops */ float TrackedTargetSpeed = 0.0f; /** Cooldown between target updates */ float TargetSelectionCooldown = 0.0f; /** True if the target is being updated */ bool bIsUpdatingTarget = false; /** True if we just entered from move action */ bool bEnteredFromMoveAction = false; };FMassGhostLocationFragmentGhost location used for standing navigation.
Located At
UE5/Engine/Plugins/AI/MassAI/Source/MassNavigationUSTRUCT() struct MASSNAVIGATION_API FMassGhostLocationFragment : public FMassFragment { GENERATED_BODY() bool IsValid(const uint16 CurrentActionID) const { return LastSeenActionID == CurrentActionID; } /** The action ID the ghost was initialized for */ uint16 LastSeenActionID = 0; /** Location of the ghost */ FVector Location = FVector::ZeroVector; /** Velocity of the ghost */ FVector Velocity = FVector::ZeroVector; };Steering Processor
Built-in
MassAIProcessor located atUE5/Engine/Plugins/AI/MassAI/Source/MassNavigation.h/** * Processor for updating steering towards MoveTarget. */ UCLASS() class MASSNAVIGATION_API UMassSteerToMoveTargetProcessor : public UMassProcessor { GENERATED_BODY() protected: UMassSteerToMoveTargetProcessor(); virtual void ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager) override; virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override; FMassEntityQuery EntityQuery; };.cppUMassSteerToMoveTargetProcessor::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!