Smooth Orientation with Mass Framework

How to process Smooth Orientation with Mass Framework. Mass Framework is a ECS for managing crowds and traffic in Unreal 5.0 and above.

Processing Smooth Orientation with MassAI built-in resources

Smooth Orientation Trait is comes with functionality that take care about proper orientation of entities related to their movement. In other words, that they move in forward direction regardless of their possition to the World axes. It's processed by calculation the right FQuat angles for Transform.SetRotation(FQuat Angle) of each entity.

  • Smooth Orientation Trait

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

    UCLASS(meta = (DisplayName = "Smooth Orientation"))
    class MASSNAVIGATION_API UMassSmoothOrientationTrait : public UMassEntityTraitBase
    {
    	GENERATED_BODY()
    
    protected:
    	virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override;
    
    	UPROPERTY(EditAnywhere, Category="")
    	FMassSmoothOrientationParameters Orientation;
    };
    void UMassSmoothOrientationTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
    {
    	FMassEntityManager& EntityManager = UE::Mass::Utils::GetEntityManagerChecked(World);
    
    	BuildContext.RequireFragment<FMassMoveTargetFragment>();
    	BuildContext.RequireFragment<FMassVelocityFragment>();
    	BuildContext.RequireFragment<FTransformFragment>();
    
    	const FConstSharedStruct OrientationFragment = EntityManager.GetOrCreateConstSharedFragment(Orientation);
    	BuildContext.AddConstSharedFragment(OrientationFragment);
    }
  • Fragments for Smooth Orientation

    As you can see, the built-in Smooth Orientation Trait of MassAI plugin requires with the following fragments:

    Required Fragments

    Required fragments are identical as fragments for Movement Trait

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

    USTRUCT()
    struct MASSNAVIGATION_API FMassSmoothOrientationWeights : public FMassSharedFragment
    {
    	GENERATED_BODY()
    
    	FMassSmoothOrientationWeights() = default;
    
    	FMassSmoothOrientationWeights(const float InMoveTargetWeight, const float InVelocityWeight)
    		: MoveTargetWeight(InMoveTargetWeight)
    		, VelocityWeight(InVelocityWeight)
    	{
    	}
    	
    	UPROPERTY(EditAnywhere, Category = "Orientation", meta = (ClampMin = "0.0", ClampMax="1.0"))
    	float MoveTargetWeight = 0.0f;
    
    	UPROPERTY(EditAnywhere, Category = "Orientation", meta = (ClampMin = "0.0", ClampMax="1.0"))
    	float VelocityWeight = 0.0f;
    };
    
    USTRUCT()
    struct MASSNAVIGATION_API FMassSmoothOrientationParameters : public FMassSharedFragment
    {
    	GENERATED_BODY()
    
    	/** The time it takes the orientation to catchup to the requested orientation. */
    	UPROPERTY(EditAnywhere, Category = "Orientation", meta = (ClampMin = "0.0", ForceUnits="s"))
    	float EndOfPathDuration = 1.0f;
    	
    	/** The time it takes the orientation to catchup to the requested orientation. */
    	UPROPERTY(EditAnywhere, Category = "Orientation", meta = (ClampMin = "0.0", ForceUnits="s"))
    	float OrientationSmoothingTime = 0.3f;
    
    	/* Orientation blending weights while moving. */	
    	UPROPERTY(EditAnywhere, Category = "Orientation")
    	FMassSmoothOrientationWeights Moving = FMassSmoothOrientationWeights(/*MoveTarget*/0.4f, /*Velocity*/0.6f);
    
    	/* Orientation blending weights while standing. */	
    	UPROPERTY(EditAnywhere, Category = "Orientation")
    	FMassSmoothOrientationWeights Standing = FMassSmoothOrientationWeights(/*MoveTarget*/0.95f, /*Velocity*/0.05f);
    };

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

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

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

    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:
    	FString ToString() const;
    
    	EMassMovementAction GetPreviousAction() const { return PreviousAction; }
    	EMassMovementAction GetCurrentAction() const { return CurrentAction; }
    	double GetCurrentActionStartTime() const { return CurrentActionWorldStartTime; }
    	double GetCurrentActionServerStartTime() const { return CurrentActionServerStartTime; }
    	uint16 GetCurrentActionID() const { return CurrentActionID; }
    
    	/** Center of the move target. */
    	FVector Center = FVector::ZeroVector;
    
    	/** Forward direction of the movement target.  */
    	FVector Forward = FVector::ZeroVector;
    
    	/** Distance remaining to the movement goal. */
    	float DistanceToGoal = 0.0f;
    
    	/** Allowed deviation around the movement target. */
    	float SlackRadius = 0.0f;
    
    private:
    	/** World time in seconds when the action started. */
    	double CurrentActionWorldStartTime = 0.0;
    
    	/** Server time in seconds when the action started. */
    	double CurrentActionServerStartTime = 0.0;
    
    	/** Number incremented each time new action (i.e move, stand, animation) is started. */
    	uint16 CurrentActionID = 0;
    
    public:
    	/** Requested movement speed. */
    	FMassInt16Real DesiredSpeed = FMassInt16Real(0.0f);
    
    	/** Intended movement action at the target. */
    	EMassMovementAction IntentAtGoal = EMassMovementAction::Move;
    
    private:
    	/** Current movement action. */
    	EMassMovementAction CurrentAction = EMassMovementAction::Move;
    
    	/** Previous movement action. */
    	EMassMovementAction PreviousAction = EMassMovementAction::Move;
    
    	uint8 bNetDirty : 1;
    public:
    	/** True if the movement target is assumed to be outside navigation boundaries. */
    	uint8 bOffBoundaries : 1;
    
    	/** True if the movement target is assumed to be outside navigation boundaries. */
    	uint8 bSteeringFallingBehind : 1;
    };

    Located 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;
    };
  • Smooth Orientation Processor

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

    /**
     * Updates agent's orientation based on current movement.
     */
    UCLASS()
    class MASSNAVIGATION_API UMassSmoothOrientationProcessor : public UMassProcessor
    {
    	GENERATED_BODY()
    
    public:
    	UMassSmoothOrientationProcessor();
    
    protected:
    	virtual void ConfigureQueries() override;
    	virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
    
    private:
    	FMassEntityQuery HighResEntityQuery;
    	FMassEntityQuery LowResEntityQuery_Conditional;
    };
    UMassSmoothOrientationProcessor::UMassSmoothOrientationProcessor()
    	: HighResEntityQuery(*this)
    	, LowResEntityQuery_Conditional(*this)
    {
    	ExecutionFlags = (int32)EProcessorExecutionFlags::All;
    	ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Movement;
    }
    
    void UMassSmoothOrientationProcessor::ConfigureQueries()
    {
    	HighResEntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadOnly);
    	HighResEntityQuery.AddRequirement<FMassVelocityFragment>(EMassFragmentAccess::ReadWrite);
    	HighResEntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
    	HighResEntityQuery.AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::None);
    	HighResEntityQuery.AddConstSharedRequirement<FMassSmoothOrientationParameters>(EMassFragmentPresence::All);
    
    	LowResEntityQuery_Conditional.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
    	LowResEntityQuery_Conditional.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadOnly);
    	LowResEntityQuery_Conditional.AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::All);
    	LowResEntityQuery_Conditional.AddChunkRequirement<FMassSimulationVariableTickChunkFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::Optional);
    	LowResEntityQuery_Conditional.SetChunkFilter(&FMassSimulationVariableTickChunkFragment::ShouldTickChunkThisFrame);
    }
    
    void UMassSmoothOrientationProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
    {
    	// Clamp max delta time to avoid force explosion on large time steps (i.e. during initialization).
    	const float DeltaTime = FMath::Min(0.1f, Context.GetDeltaTimeSeconds());
    
    	{
    		QUICK_SCOPE_CYCLE_COUNTER(HighRes);
    
    		HighResEntityQuery.ForEachEntityChunk(EntityManager, Context, [this, DeltaTime](FMassExecutionContext& Context)
    		{
    			const int32 NumEntities = Context.GetNumEntities();
    
    			const FMassSmoothOrientationParameters& OrientationParams = Context.GetConstSharedFragment<FMassSmoothOrientationParameters>();
    
    			const TConstArrayView<FMassMoveTargetFragment> MoveTargetList = Context.GetFragmentView<FMassMoveTargetFragment>();
    			const TArrayView<FTransformFragment> LocationList = Context.GetMutableFragmentView<FTransformFragment>();
    			const TArrayView<FMassVelocityFragment> VelocityList = Context.GetMutableFragmentView<FMassVelocityFragment>();
    
    			for (int32 EntityIndex = 0; EntityIndex < NumEntities; ++EntityIndex)
    			{
    				const FMassMoveTargetFragment& MoveTarget = MoveTargetList[EntityIndex];
    
    				// Do not touch transform at all when animating
    				if (MoveTarget.GetCurrentAction() == EMassMovementAction::Animate)
    				{
    					continue;
    				}
    
    				const FMassVelocityFragment& CurrentVelocity = VelocityList[EntityIndex];
    				FTransform& CurrentTransform = LocationList[EntityIndex].GetMutableTransform();
    				const FVector CurrentForward = CurrentTransform.GetRotation().GetForwardVector();
    				const FVector::FReal CurrentHeading = UE::MassNavigation::GetYawFromDirection(CurrentForward);
    
    				const float EndOfPathAnticipationDistance = OrientationParams.EndOfPathDuration * MoveTarget.DesiredSpeed.Get();
    				
    				FVector::FReal MoveTargetWeight = 0.5;
    				FVector::FReal VelocityWeight = 0.5;
    				
    				if (MoveTarget.GetCurrentAction() == EMassMovementAction::Move)
    				{
    					if (MoveTarget.IntentAtGoal == EMassMovementAction::Stand && MoveTarget.DistanceToGoal < EndOfPathAnticipationDistance)
    					{
    						// Fade towards the movement target direction at the end of the path.
    						const float Fade = FMath::Square(FMath::Clamp(MoveTarget.DistanceToGoal / EndOfPathAnticipationDistance, 0.0f, 1.0f)); // zero at end of the path
    
    						MoveTargetWeight = FMath::Lerp(OrientationParams.Standing.MoveTargetWeight, OrientationParams.Moving.MoveTargetWeight, Fade);
    						VelocityWeight = FMath::Lerp(OrientationParams.Standing.VelocityWeight, OrientationParams.Moving.VelocityWeight, Fade);
    					}
    					else
    					{
    						MoveTargetWeight = OrientationParams.Moving.MoveTargetWeight;
    						VelocityWeight = OrientationParams.Moving.VelocityWeight;
    					}
    				}
    				else // Stand
    				{
    					MoveTargetWeight = OrientationParams.Standing.MoveTargetWeight;
    					VelocityWeight = OrientationParams.Standing.VelocityWeight;
    				}
    				
    				const FVector::FReal VelocityHeading = UE::MassNavigation::GetYawFromDirection(CurrentVelocity.Value);
    				const FVector::FReal MovementHeading = UE::MassNavigation::GetYawFromDirection(MoveTarget.Forward);
    
    				const FVector::FReal Ratio = MoveTargetWeight / (MoveTargetWeight + VelocityWeight);
    				const FVector::FReal DesiredHeading = UE::MassNavigation::LerpAngle(VelocityHeading, MovementHeading,Ratio);
    				
    				const FVector::FReal NewHeading = UE::MassNavigation::ExponentialSmoothingAngle(CurrentHeading, DesiredHeading, DeltaTime, OrientationParams.OrientationSmoothingTime);
    
    				FQuat Rotation(FVector::UpVector, NewHeading);
    				CurrentTransform.SetRotation(Rotation);
    			}
    		});
    	}
    
    	{
    		QUICK_SCOPE_CYCLE_COUNTER(LowRes);
    
    		LowResEntityQuery_Conditional.ForEachEntityChunk(EntityManager, Context, [this](FMassExecutionContext& Context)
    		{
    			const int32 NumEntities = Context.GetNumEntities();
    
    			const TArrayView<FTransformFragment> LocationList = Context.GetMutableFragmentView<FTransformFragment>();
    			const TConstArrayView<FMassMoveTargetFragment> MoveTargetList = Context.GetFragmentView<FMassMoveTargetFragment>();
    
    			for (int32 EntityIndex = 0; EntityIndex < NumEntities; ++EntityIndex)
    			{
    				FTransform& CurrentTransform = LocationList[EntityIndex].GetMutableTransform();
    				const FMassMoveTargetFragment& MoveTarget = MoveTargetList[EntityIndex];
    
    				// Snap position to move target directly
    				CurrentTransform.SetRotation(FQuat::FindBetweenNormals(FVector::ForwardVector, MoveTarget.Forward));
    			}
    		});
    	}
    }