DeveloperGuidePart03ForceSync

What force sync is, how to convert an AttributesSync into a Synchronizable so you control what it sends during force sync, and how to discard its force sync packet entirely.

What is force sync?

When a new user joins a room, they know nothing about what has already happened in it. Force sync is the event that fixes that: every client already in the room assembles a snapshot of every registered Synchronizable and sends it to the new user in a single force sync reply, so they start from the room's current state rather than from their scene's defaults.

During that snapshot, the framework calls Serialize on every Synchronizable as usual, except that SerializeInfo.ForceSync is true. On the joining user's side, DisassembleData is called with UnserializeInfo.ForceSync set to true as well, so both ends can tell a force sync packet apart from a regular update.

The related events live on MultiplayerManager:
  • OnForceSync - raised on the clients already in the room, right before they assemble their snapshot.
  • OnForceSynced - raised on the same clients once the snapshot has been sent.
  • OnForceSyncReply - raised on the joining user when a snapshot has been received and applied.

AttributesSync and force sync

AttributesSync handles force sync for you: every [SynchronizableField] is written into the snapshot, whether it has changed or not. On top of that, it has two settings you can change from code:
  • ForceSyncFields (on by default) - once the snapshot has been sent, the user with the lowest user ID sends every [SynchronizableField] again, to everyone in the room.
  • ForceSyncMethods (off by default) - at the same point, that user repeats the last call to every [SynchronizableMethod] it has invoked itself, with the same arguments. The method runs again for everyone in the room, not only for the new user.

These settings are enough for most simple components, but they only switch whole categories on or off. They can't leave out a single field, skip the object in the snapshot, or send something different to a joining user. They also send data again to users who already have it.

For full control and better performance, a Synchronizable is still recommended. You write the data yourself in AssembleData, which receives the SerializeInfo for the packet being built, so you can see when it is a force sync and decide exactly what to send. Nothing is sent twice, and you control the byte layout.

Converting an AttributesSync into a Synchronizable

Take this AttributesSync. It counts how many times something has been hit and plays an effect every time the count goes up:

C#
public class HitEffectSync : AttributesSync
{
    public ParticleSystem HitEffect;

    [SynchronizableField]
    public int HitCount;

    private int _lastPlayedHitCount;

    public void Hit()
    {
        HitCount++;
    }

    private void Update()
    {
        if (HitCount != _lastPlayedHitCount)
        {
            _lastPlayedHitCount = HitCount;
            HitEffect.Play();
        }
    }
}

Here is the same component as a Synchronizable. Three things change:
  • The field loses its [SynchronizableField] attribute. Instead, AssembleData writes it and DisassembleData reads it back, in the same order.
  • AttributesSync commits changed fields for you in its LateUpdate. A Synchronizable does not, so you check for changes yourself and call Commit().
  • You call base.SyncUpdate() every frame so the Synchronizable can send committed data.

C#
public class HitEffectSync : Synchronizable
{
    public ParticleSystem HitEffect;

    public int HitCount;

    private int _lastPlayedHitCount;
    private int _lastCommittedHitCount;

    public void Hit()
    {
        HitCount++;
    }

    public override void AssembleData(Writer writer, SerializeInfo info)
    {
        writer.Write(HitCount);
    }

    public override void DisassembleData(Reader reader, UnserializeInfo info)
    {
        HitCount = reader.ReadInt();

        // Don't commit the value we just received back to everyone else.
        _lastCommittedHitCount = HitCount;
    }

    private void Update()
    {
        if (HitCount != _lastCommittedHitCount)
        {
            _lastCommittedHitCount = HitCount;
            Commit();
        }

        if (HitCount != _lastPlayedHitCount)
        {
            _lastPlayedHitCount = HitCount;
            HitEffect.Play();
        }

        base.SyncUpdate();
    }
}

So far it behaves exactly like the AttributesSync version, force sync included. The difference is that AssembleData now has access to info.ForceSync.

Discarding the force sync packet

Both versions above have a problem when someone joins. The snapshot sends the joining user the current HitCount, their Update sees a new value, and the hit effect plays for a hit that happened long before they arrived. The count only exists to trigger the effect, so the joining user has no use for it.

To leave the object out of the snapshot, set info.Discard to true in AssembleData and return without writing anything:

C#
public override void AssembleData(Writer writer, SerializeInfo info)
{
    if (info.ForceSync)
    {
        // A joining user doesn't need to replay hits from before they joined.
        // Discard drops this Synchronizable from the force sync packet entirely.
        info.Discard = true;
        return;
    }

    writer.Write(HitCount);
}

When Discard is set, everything this Synchronizable wrote to the packet is removed, including any [SynchronizableField] members the base class wrote before calling AssembleData. The joining user never receives the object's data, so DisassembleData is never called for it and nothing needs to change on the receiving side.

Discarding only affects the force sync packet. Regular updates are sent as before, so the joining user sees the next hit once it happens.

Discard the whole packet only when none of the object's state matters to a joining user. If some of it does, write that part and skip the rest, then check info.ForceSync in DisassembleData so you read back exactly what was written:

C#
public override void AssembleData(Writer writer, SerializeInfo info)
{
    writer.Write(Health);

    // Only regular updates carry the hit count.
    if (!info.ForceSync)
    {
        writer.Write(HitCount);
    }
}

public override void DisassembleData(Reader reader, UnserializeInfo info)
{
    Health = reader.ReadFloat();

    if (!info.ForceSync)
    {
        HitCount = reader.ReadInt();
        _lastCommittedHitCount = HitCount;
    }
}

For a step-by-step example of upgrading an AttributesSync to a Synchronizable in a small game, see Example Game Guide Part 03.


← Back | Next →

See Also