Processing Steering with MassAI
built-in resources
-
Steering Trait
Built-in
MassAI
Trait located AtUE5/Engine/Plugins/AI/MassAI/Source/MassNavigation
Image.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 Trait
ofMassAI
plugin works with the 8 following fragments:Required Fragments
Required fragments are identical as fragments for
Movement Trait
FMassVelocityFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassMovement
USTRUCT() struct MASSMOVEMENT_API FMassVelocityFragment : public FMassFragment { GENERATED_BODY() FVector Value = FVector::ZeroVector; };
FMassForceFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassMovement
USTRUCT() struct MASSMOVEMENT_API FMassForceFragment : public FMassFragment { GENERATED_BODY() FVector Value = FVector::ZeroVector; };
FAgentRadiusFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassCommon
USTRUCT() struct MASSCOMMON_API FAgentRadiusFragment : public FMassFragment { GENERATED_BODY() UPROPERTY(EditAnywhere, Category = "") float Radius = 40.f; };
FTransformFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassCommon
USTRUCT() 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/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; };
FMassStandingSteeringFragmentLocated At
UE5/Engine/Plugins/AI/MassAI/Source/MassNavigation
USTRUCT() 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/MassNavigation
USTRUCT() 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
MassAI
Processor 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() 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() { 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(EntityManager, 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
(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; } } }); }