Smooth Orientation with Mass Framework

Published on

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

Platforma

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

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

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

    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);
    				<mark>CurrentTransform.SetRotation(Rotation);</mark>
    			}
    		});
    	}
    
    	{
    		QUICK_SCOPE_CYCLE_COUNTER(LowRes);
    
    		LowResEntityQuery_Conditional.ForEachEntityChunk(EntityManager, Context, [this](FMassExecutionContext& Context)
    		{
    			const int32 NumEntities = Context.GetNumEntities();
    
    			const TArrayView&lt;FTransformFragment> LocationList = Context.GetMutableFragmentView&lt;FTransformFragment&gt;();
    			const TConstArrayView&lt;FMassMoveTargetFragment> MoveTargetList = Context.GetFragmentView&lt;FMassMoveTargetFragment&gt;();
    
    			for (int32 EntityIndex = 0; EntityIndex < NumEntities; ++EntityIndex)
    			{
    				FTransform& CurrentTransform = LocationList[EntityIndex].GetMutableTransform();
    				const FMassMoveTargetFragment& MoveTarget = MoveTargetList[EntityIndex];
    
    				// Snap position to move target directly
    				<mark>CurrentTransform.SetRotation(FQuat::FindBetweenNormals(FVector::ForwardVector, MoveTarget.Forward));</mark>
    			}
    		});
    	}
    }
Vrealmatic consulting

Anything unclear?

Let us know!

Contact Us