In my previous post, I talked about how to replicate variables with Mass Entity. The example explained how to replicate the position of an entity from the server to the client. One thing that is very clear on the client view is that the movement is not fluid. This is because the entity teleports to the server position when a network package is received.
In this post, we are going to tackle this issue. I will start with the previous repository as a base to add smoothing to the client position.
In our previous post, if we simulate our example with high ping, the movement of the entity will look something like this:
It can be seen how the entity moves by skipping frames.
Introduction
We will smooth the entity’s position by offsetting the entity mesh position. The client position will be set to the server position when replicated, but we will add an offset to the mesh so it retains its previous position. Then, we will correct the mesh position to the actual entity location by gradually reducing the offset smoothly.
We can visualize this implementation in the following video:
The bad network simulation is still on. The red debug square represents the actual position of the entity. We can see that the mesh does not snap to the new entity position but instead moves smoothly to the new position.
We are extending the Mass Replication Base plugin discussed in the post, which explains how to replicate a variable by implementing a custom client bubble.
AMRSMassClientBubbleSmoothInfo: An actor class facilitating actual replication. There is one instance for each client, containing a FMRSMassClientBubbleSerializer.
FMRSMassClientBubbleSerializer: This class replicates the fast array between the server and the client. Each client has one instance, which contains an array of FMassFastArrayItemBase and a FMassClientBubbleHandler.
FMRSMassClientBubbleHandler: This class inserts server-replicated data into the client fragments. It initializes the FMRSMeshTranslationOffset fragment when receiving the server position.
FMRSMeshTranslationOffset: A fragment that contains the mesh offset from the entity position.
UMRSSmoothMeshOffsetProcessor: This class iterates over the FMRSMeshTranslationOffset fragments and gradually reduces the offset to zero.
UMRSMassUpdateISMProcessor: Overrides UMassUpdateISMProcessor to implement dynamic offset for static meshes rendering.
Implementation
Fragments
First, we are going to implement the fragment containing the mesh offset from the entity position. It only contains a vector and inherits from FMassFragment.
FMRSMeshTranslationOffset
/** Used to offset the static mesh representation of an entity */USTRUCT() struct FMRSMeshTranslationOffset : public FMassFragment{ GENERATED_BODY() UPROPERTY(Transient) FVector TranslationOffset = FVector::ZeroVector; };
Next, we will implement a shared fragment containing the parameters to smooth the offset.
FMRSMeshOffsetParams
/** Shared params to offset the entity mesh */USTRUCT() struct FMRSMeshOffsetParams : public FMassSharedFragment{ GENERATED_BODY() /** Maximum time the smoothing can take. If it takes more than this values it will snap to the actual position */ UPROPERTY(EditAnywhere, meta = (UIMin = 0.0f, ClampMin = 0.0f)) float MaxTimeToSmooth = 1.0f; /** How much time the smooth can take */ UPROPERTY(EditAnywhere, meta = (UIMin = 0.0f, ClampMin = 0.0f)) float SmoothTime = 0.2f; /** The tolerated distance to smooth. If the distance is higher the mesh will snap to the actual position. */ UPROPERTY(EditAnywhere, meta = (UIMin = 0.0f, ClampMin = 0.0f)) float MaxSmoothNetUpdateDistance = 50.0f; float MaxSmoothNetUpdateDistanceSqr = 0.0f; public: /** Returns a copy of this instance with the parameters validated */ FMRSMeshOffsetParams GetValidated() const { FMRSMeshOffsetParams Params = *this; Params.MaxTimeToSmooth = FMath::Max(0.0f, Params.MaxTimeToSmooth); Params.SmoothTime = FMath::Max(0.0f, Params.SmoothTime); Params.MaxSmoothNetUpdateDistance = FMath::Max(0.0f, Params.MaxSmoothNetUpdateDistance); Params.MaxSmoothNetUpdateDistanceSqr = Params.MaxSmoothNetUpdateDistance * Params.MaxSmoothNetUpdateDistance; return Params; }};
Processors
Next, we are going to implement two processors.
UMRSSmoothMeshOffsetProcessor will be the processor responsible for smoothly reducing the mesh offset to zero. It will only run on clients and will use FMRSMeshTranslationOffset, FTransformFragment, and the shared fragment FMRSMeshOffsetParams.
For the last processor, we will override UMassUpdateISMProcessor to add a dynamic offset to the visualization of static meshes. The requirement for FMRSMeshTranslationOffset is optional so the processor can still render meshes that do not have this fragment.
Lastly, we are going to implement a custom client bubble to initialize FMRSMeshTranslationOffset when receiving the server position.
First, we implement FMRSMassClientBubbleHandler, which inherits from FMRBMassClientBubbleHandler created in our previous post. This class will be responsible for inserting the server replicated data into the client fragments.
FMRSMassClientBubbleHandler
/** Inserts the data that the server replicated into the fragments */class FMRSMassClientBubbleHandler : public FMRBMassClientBubbleHandler{ protected:#if UE_REPLICATION_COMPILE_CLIENT_CODE virtual void AddQueryRequirements(FMassEntityQuery& InQuery) const override { FMRBMassClientBubbleHandler::AddQueryRequirements(InQuery); InQuery.AddRequirement<FMRSMeshTranslationOffset>(EMassFragmentAccess::ReadWrite); InQuery.AddConstSharedRequirement<FMRSMeshOffsetParams>(); } virtual void PostReplicatedChangeEntity(const FMassEntityView& EntityView, const FMRBReplicatedAgent& Item) const override { FTransformFragment& TransformFragment = EntityView.GetFragmentData<FTransformFragment>(); const FVector PreviousLocation = TransformFragment.GetTransform().GetLocation(); FMRBMassClientBubbleHandler::PostReplicatedChangeEntity(EntityView, Item); const FVector NewLocation = TransformFragment.GetTransform().GetLocation(); // Initializes mesh offset FMRSMeshTranslationOffset& TranslationOffset = EntityView.GetFragmentData<FMRSMeshTranslationOffset>(); const FMRSMeshOffsetParams& OffsetParams = EntityView.GetConstSharedFragmentData<FMRSMeshOffsetParams>(); if (OffsetParams.MaxSmoothNetUpdateDistanceSqr > FVector::DistSquared(PreviousLocation, NewLocation)) { // Offsetting the mesh to sync with the sever locations smoothly TranslationOffset.TranslationOffset += PreviousLocation - NewLocation; } }#endif //UE_REPLICATION_COMPILE_CLIENT_CODE };
The next classes will override AMassClientBubbleInfoBase and FMassClientBubbleSerializerBase, and they will be very similar to the previous post where we replicated a variable using Mass.
The first class, FMRSMassClientBubbleSerializer, will inherit from FMassClientBubbleSerializerBase. It will handle replicating the fast array of agents between the server and clients.
FMRSMassClientBubbleSerializer
USTRUCT()struct FMRSMassClientBubbleSerializer : public FMassClientBubbleSerializerBase{ GENERATED_BODY()public: FMRSMassClientBubbleSerializer() { Bubble.Initialize(Entities, *this); } /** Define a custom replication for this struct */ bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams) { return FFastArraySerializer::FastArrayDeltaSerialize<FMRBMassFastArrayItem, FMRSMassClientBubbleSerializer>(Entities, DeltaParams, *this); } /** The one responsible for storing the server data in the client fragments */ FMRSMassClientBubbleHandler Bubble;protected: /** Fast Array of Agents for efficient replication. Maintained as a freelist on the server, to keep index consistency as indexes are used as Handles into the Array * Note array order is not guaranteed between server and client so handles will not be consistent between them, FMassNetworkID will be.*/ UPROPERTY(Transient) TArray<FMRBMassFastArrayItem> Entities;};
It will need a custom replication so we need to implement the following struct:
TStructOpsTypeTraits
template<>struct TStructOpsTypeTraits<FMRSMassClientBubbleSerializer> : public TStructOpsTypeTraitsBase2<FMRBMassClientBubbleSerializer>{ enum { // We need to use the NetDeltaSerialize function for this struct to define a custom replication WithNetDeltaSerializer = true, // Copy is not allowed for this struct WithCopy = false, };};
Finally, we need to implement the Unreal actor that will provide us the actual replication
AMRSMassClientBubbleSmoothInfo
/** The info actor base class that provides the actual replication */UCLASS()class MASSREPLICATIONSMOOTH_API AMRSMassClientBubbleSmoothInfo : public AMassClientBubbleInfoBase{ GENERATED_BODY()public: AMRSMassClientBubbleSmoothInfo(const FObjectInitializer& ObjectInitializer) { Serializers.Add(&BubbleSerializer); } FMRSMassClientBubbleSerializer& GetBubbleSerializer() { return BubbleSerializer; }protected: virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override { Super::GetLifetimeReplicatedProps(OutLifetimeProps); FDoRepLifetimeParams SharedParams; SharedParams.bIsPushBased = true; // Technically, this doesn't need to be PushModel based because it's a FastArray and they ignore it. DOREPLIFETIME_WITH_PARAMS_FAST(AMRSMassClientBubbleSmoothInfo, BubbleSerializer, SharedParams); }private: /** Contains the entities fast array */ UPROPERTY(Replicated, Transient) FMRSMassClientBubbleSerializer BubbleSerializer;};
Configuration files
We need to disable the MassUpdateISMProcessor and enable our MRSMassUpdateISMProcessor. To do this, add the following lines to the DefaultMass.ini file in the Config folder of your Unreal Project.
This configuration ensures that the MassUpdateISMProcessor is disabled, and the MRSMassUpdateISMProcessor is enabled for automatic registration with processing phases.
Conclusion
With the implementation finished, we can now see how the entity moves smoothly even under high latency conditions. The values in the shared fragment worked well for my case, but you may need to tweak them based on your specific use case. Additionally, I haven’t tested this system in a production environment, so its performance in such settings is uncertain.
Some potential improvements include adding LOD tag requirements to ensure we only smooth entities relevant to our player, which could enhance performance and efficiency.