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 AtUE5/Engine/Plugins/AI/MassAI/Source/MassNavigation
Image.hUCLASS(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; };
.cppvoid 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
ofMassAI
plugin requires with the following fragments:Required Fragments
Required fragments are identical as fragments for
Movement Trait
FMassVelocityFragmentLocated 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); };
FMassVelocityFragmentLocated at
UE5/Engine/Plugins/Runtime/MassGameplay/Source/MassMovement
USTRUCT() struct MASSMOVEMENT_API FMassVelocityFragment : public FMassFragment { GENERATED_BODY() FVector Value = FVector::ZeroVector; };
FMassMoveTargetFragmentLocated 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; };
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; };
-
Smooth Orientation Processor
Built-in
MassAI
Processor located atUE5/Engine/Plugins/AI/MassAI/Source/MassNavigation
.h/** * 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; };
.cppUMassSmoothOrientationProcessor::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)); } }); } }