Players Synchronization In Distributive Authority

Hey everyone, I’m using Distributive Authority for my multiplayer solution and building a 2D car game with continuous motion (no complex physics, just basic movement and acceleration). The issue I’m facing is synchronizing player movement over high latency. Locally, it’s smooth, but on the other side, it desyncs if the other player has good ping. How can I handle reconciliation with Distributive Authority in this case?

@mcanfield_unity

Hi @Freddy_08,

Not knowing your depth of experience in multiplayer, I’ll ask a few initial questions.

Can you describe the desync behavior you’re observing? Is this another computer elsewhere in the world and they are reporting to you some undesirable behavior? Or are you running 2 clients locally and just notice some delay on the receiving client e.g. ClientA and ClientB are connected to a session? If this is the case, are you observing ClientA is smooth and reactive but on ClientB there is quite a bit of delay from what ClientA is doing? My first question would be does ClientB still look fine if you don’t compare it to ClientA? Or is there noticeable performance issues you’re categorizing as desync?

@mcanfield_unity thanks for the reply

I’m connecting clients via WebGL to an available session using a distributed authority model “MultiplayerService.Instance.CreateOrJoinSessionAsync();”.
Each client processes collisions locally while exchanging positional updates via a network transform.

In my Unity game using distributed authority, cars can detect collisions locally. The logic is straightforward, if one car hits the back of another, the car in front crashes, if two cars collide head-on, they both crash. Each client handles crashes locally, Client A decides if its car crashes with Client B’s car, and Client B does the same.

Here’s the issue: Client A sends position updates to Client B via a network transform. If Client A has higher latency, these updates are delayed. So, if Client A makes a sharp turn just before a potential collision, Client B might still register a crash due to the outdated position data.

It gets more complicated in scenarios involving multiple players. For instance, if Clients A and B both have high latency, Client C might see them overlapping without crashing. In reality, Client A could be turning sharply while Client B moves forward, but Client C’s view of the situation doesn’t reflect this due to desync.

My questions are:

  1. Can this exact scenario occur with distributed authority, or does Unity’s network transform handle it differently?
  2. If not, what type of similar desync behavior might occur, and how is it usually mitigated?
  3. Is it possible to periodically reconcile and sync up player positions to ensure alignment across all connected clients? If so, what are the best practices for implementing reconciliation in such scenarios (Where DA involved, since its just using relay in backend)?

Hi @Freddy_08,

So, when you have high latency getting things to synchronize does become tricky. When dealing with scenarios like this you are almost always going to need to approach things a bit differently. With that said, a “traditional” approach is out the window. By traditional, I mean just using what is already available (i.e. basic NetworkTransform, basic NetworkRigidbody2D, etc.

The first thing you want to do is make sure all clients know what the linear and angular velocity of the cars are (within reason) at any given point in (tick-based) time. The best way to do this is by synchronizing both linear and angular velocities using NetworkVariables. You will want to treat these much like NetworkTransform treats changes to axial values where you define a threshold (amount of change) that determines if you need to update a cars velocity (linear or angular).

The next thing you are going to want to determine is what each client’s ping time is to the other clients in the session. A good example of how to do this you can find in this Ping Tool example. You should examine how the “ping” time is really a “one way” probe to determine the time it takes to send a message from say Client-A to Client-B with the addition of the time that it takes to process the message. The delta returned is based on “network server” time (provided by the DA service) which gives you an averaged “network synchronized time delta” to send a message and have it processed (client to client). You would want to modify how the example does this by saving each client’s ping time in a local table.

  • Client-A sends RPC ping to Client-B
    • Upon receiving the RPC, Client-B updates a local table of how long it takes to receive a message from Client-A (Client-B now knows the time it takes for Client-A to send state updates to it).

At this point you would have:

  • Velocities of the other cars
  • The client-to-client message delivery and processing time for each client

From the visual perspective, you know that any position update a non-owner gets from an owner client is going to be “a position from the past” (physics collisions is tricky even with a full blown prediction system which is why a lot of prediction based systems, like N4E, also inlcude rolling back). So, you need to make sure non-owner clients are seeing (as best as possible) where the other clients’ cars are currently (local client relative) without having all of the position points that are most likely “in-flight”. To do this you will most likely want to make your car prefab look something like:

  • Root Car (NetworkObject, CarNetworkTransform)
    • Car Visual ( Model/Sprite, Rigidbody2D (if you use this), Collider, NetworkRigidbody2D, and Trigger)

An optional addition:
Then you can also place a trigger on the CarVisual that would expand as the car is moving faster and contract as it slows down. This would be something the owner of the car would want to use to help determine a collision…just keep that in mind.

On The Non-Owner Instances
The Root Car is “where the car was”, but the Car Visual is where the “predicted car is”. You can adjust the child Car Visual (local space) based on the linear velocity and the time it takes to get a message from the owner client to the non-owner client (ping times above). You also know the velocity of the owner’s car. You can now offset (local space) the non-owner instance of the owner’s car by calculating the estimated “predicted position” and offset the CarVisual by that amount in local space.

The below (pseudo) code gives the general idea:

public class CarNetworkTransform : NetworkTransform
{
    // The child holding the visual, collider, trigger, etc.
    public GameObject CarVisual;

    /// <summary>
    /// Owner Authority Specific:
    /// What velocity threshold do we want to consider sending an update
    /// </summary>
    [Range(0.001f, 2.0f)]
    public float VelocityThreshold = 0.1f;

    /// <summary>
    /// Updated from the received ping time of the owner.
    /// i.e. How long does it take for the owner to send me its
    /// next position?
    /// </summary>
    public float OwnerTimeDelta;

    private NetworkVariable<Vector2> m_CarVelocity =  new NetworkVariable<Vector2>();

    private NetworkRigidbody2D m_NetworkRigidbody2D;

    protected override void Awake()
    {
        base.Awake();
        m_NetworkRigidbody2D = GetComponent<NetworkRigidbody2D>();
    }

    protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState)
    {
        var carVelocity = m_NetworkRigidbody2D.GetLinearVelocity();
        // Authority checks for velocity updates when it is sending transform state updates (i.e. if the transform is moving, then it most likely has velocity)
        if (Mathf.Abs(m_CarVelocity.Value.x - carVelocity.x) >= VelocityThreshold || Mathf.Abs(m_CarVelocity.Value.y - carVelocity.y) >= VelocityThreshold)
        {
            m_CarVelocity.Value = m_NetworkRigidbody2D.GetLinearVelocity();
        }
        base.OnAuthorityPushTransformState(ref networkTransformState);
    }

    /// <summary>
    /// Non-authority will invoke this, authority (owner) does not.
    /// This happens automatically as the non-authority handles interpolation here.
    /// </summary>
    public override void OnFixedUpdate()
    {
        // What direction is the car moving towards?
        var carVelocityDirection = m_CarVelocity.Value.normalized;

        // Calculate an "estimated" position of the owner car by calculating a "future/predicted" amount of velocity.
        var carVelocityPredictedMagnitude = (m_CarVelocity.Value.magnitude * OwnerTimeDelta);

        // Then offset the visual by the direction x the predicted amount of velocity applied
        // This is a local space offset, so you just want to offset the car's visuals, colliders, and triggers by enough
        // to make the non-owner version "closer to accurate".
        CarVisual.transform.localPosition = carVelocityDirection * carVelocityPredictedMagnitude;

        // You could also use TickWithPartial to provide more accuracy in the calculations to determine "how far" you
        // are into a given tick.
        // NetworkManager.ServerTime.TickWithPartial

        base.OnFixedUpdate();
    }

}

On The Owner Instances
You want the Car Visual always at 0.0, 0.0 in local space.

There is obviously more to this general line of thinking. If you need more info or details feel free to respond…but with a little bit of “thinking outside of the traditional” approach you can create a “closer to realistic” implementation without having to do too much heavy lifting.

Thank you so much @NoelStephens_Unity

It really helped alot

1 Like