Search Unity

Placeholder image Unity 2
Placeholder image Unity 2
Share

This post discusses state synchronization in networked Unity games. We begin with a summary of how this is done in the existing (legacy) Unity networking system, and move on to how it will function in the new UNET networking system and the decision making process that lead to that new design.

Background and Requirements

As a little bit of background. It is common for networked games to have a server that owns objects, and clients that need to be told when data in those objects change. For example in a combat game, the health of the players needs to visible to all players. This requires a member variable on a script class that is sent to all of the clients when it changes on the server. Below is a simple combat class:

class Combat : MonoBehaviour
{
    public int Health;
    public bool Alive;

    public void TakeDamage(int amount)
    {
        if (amount >= Health) {
            Alive = false;
            Health = 0;
        } else {
            Health -= amount;
        }
    }
}

When a player on the server takes damage, all of the players in the game need to be told about the new health value for that player.

This seems simple, but the challenge here is to make the system invisible to developers writing the code, efficient in terms of CPU, memory and bandwidth usage, and flexible enough to support all the types the developer wants to use. So some concrete goals for this system would be:

1.      Minimize memory usage by not keeping shadow copies of variables

2.      Minimize bandwidth usage by only sending states that have changed (incremental updates)

3.      Minimize CPU usage by not constantly checking to see if a state has changed

4.      Minimize protocol and serialization mismatch issues, by not requiring developers to hand-code serialization functions

5.      Don’t require developers to explicitly set variables as dirty

6.      Work with all supported Unity scripting languages

7.      Don’t disrupt developer workflow

8.      Don’t introduce manual steps that developers need to perform to use the system

9.      Allow the system to be driven by meta-data (custom attributes)

10.   Handle both simple and complex types

11.   Avoid reflection at runtime

This is an ambitious list of requirements!

Legacy Networking System

The existing Unity networking system has a “ReliableDeltaCompressed” type of synchronization that performs state synchronization by providing an OnSerializeNetworkView() hook function. This function is invoked on objects with the NetworkView component, and the serialization code written by the developer writes to (or reads from) the byte stream provided. The contents of this byte stream are then cached by the engine, and if the next time the function is called the result doesn’t match the cached version the object is considered dirty and the state is sent to clients. To take an example, a serialization function could look like this:

void OnSerializeNetworkView (Bitstream stream, NetworkMessageInfo info)
{
    float horizontalInput = 0.0f;
    if (stream.isWriting) {
        // Sending
        horizontalInput = Input.GetAxis ("Horizontal");
       	stream.Serialize (horizontalInput);
    } else {

        // Receiving
        stream.Serialize (horizontalInput);
        // ... do something meaningful with the received variable
    }
}

This approach meets some of the requirements listed above, but not all of them. It is automatic at runtime, since OnSerializeNetworkView() is invoked by the engine at the network send rate, and the developer doesn’t need to set variables as dirty. It also doesn’t add any extra steps to the build process or disrupt the developer workflow.

But, its performance is not great – especially when there are many networked objects. CPU time is spent on comparisons, and memory is used for caching copies of byte streams.  It is also susceptible to mismatch errors in the serialization function because it has to be updated by hand when new member variables are added that need to be synchronized. It is also not driven by metadata, so the editor and other tools cannot be aware of what variables are synchronized.

Code Generation for SyncVars

As the UNET team worked on the new state synchronization system, the solution we came up with was a code generator driven by custom attributes. In user code, this looks like:

using UnityEngine.UNetwork;
class Combat : UNetBehaviour
{
    [SyncVar]
    public int Health;

    [SyncVar]
    public bool Alive;
}

This new custom attribute tells the system that the Health  and Alive member variables need to be synchronized. Now, the developer doesn’t need to write a serialization function, since the code generator has the custom attribute data and it can generate perfect serialization and unserialization functions with the right ordering and types. This generated function looks something like this:

 public override void UNetSerializeVars(UWriter writer)
{
    writer.WriteInt(Health);
    writer.WriteBool(Alive);
}

Since this overrides a virtual function on the UNetBehaviour base class, when the game object is serialized, the script variables will also be automatically serialized. Then, they will be unpacked at the other end with a matching code-generated unserialization function. So there is no chance of mismatches, and the code updates automatically when a new [SyncVar] variable is added.

This data is now available to the editor, so the inspector window can show more detail like this:

But there are still some issues here. This function sends all the state all the time – it is not incremental; so if a single variable on an object changes, the entire object state would be sent. Also, how do we know when this serialization function should be called? It is not efficient to send states when nothing has changed.

We wrestled with using properties and dirty flags to do this. It seemed natural that a property could wrap each [SyncVar] variable and set dirty flags when something changes. This approach was partially successful. Having a bitmask of dirty flags lets the code generator make code to do incremental updates. That generated code would look something like this:

public override void UNetSerializeVars(UWriter writer)
{
    Writer.Write(m_DirtyFlags)
    if (m_DirtyFlags & 0x01) { writer.WriteInt(Health); }
    if (m_DirtyFlags & 0x02) { writer.WriteBool(Alive); }
    m_DirtyFlags = 0;
}

In this way, the unserialization function can read the dirty flags mask and only unserialize variables written to the stream. This makes for efficient bandwidth usage, and lets us know when the object is dirty. Plus, it's still all automatic for the user. But how do these properties work?

Say we try wrapping the [SyncVar] member variable:

 using UnityEngine.UNetwork;
class Combat : UNetBehaviour
{
    [SyncVar]
    public int Health;

    // generated property
    public int HealthSync {
        get { return Health; }
        set { m_dirtyFlags |= 0x01;  Health = value; }
    }
}

This does the job but it has the wrong name. The TakeDamage() function from above uses Health not HealthSync, so it bypasses the property.  The user can’t even use the HealthSync property directly since it doesn’t even exist until code generation happens.  It could be made into a two-phase process where the code generation step happens, then the user updates their code – but this is fragile. It is prone to compilation errors that can’t be fixed without undoing large chunks of code.

Another approach would be to require developers to write the above property code for each [SyncVar] variable.  But that is work for developers, and potentially error prone.  The bitmasks in user-written and generated code would have to match up exactly for this to work, so adding and removing [SyncVar] variables would be delicate.

Enter Mono Cecil

So we need to be able to generate wrapper properties and make existing code use them even if that code isn’t even aware of their existence. Well, fortunately there is a tool for Mono called Cecil which does exactly this. Cecil is able to load Mono assemblies in the ECMA CIL format, modify them and write them back out.

This is where is gets a little crazy. The UNET code generator creates the wrapper properties, then it finds all of the code sites where the original member variables were accessed. It then replaces the references to the member variables with references to the wrapper properties and Voila! Now the user code is calling through the newly created properties without any work from the user.

Since Cecil operates at the CIL level, it has the added advantage of working with all languages since they all compile down to the same instruction format.

The generated CIL for a final serialization function that gets injected into the script assembly now looks like this:

IL_0000: ldarg.2
IL_0001: brfalse IL_000d

IL_0006: ldarg.0
IL_0007: ldc.i4.m1
IL_0008: stfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits

IL_000d: nop
IL_000e: ldarg.1
IL_000f: ldarg.0
IL_0010: ldfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_0015: callvirt instance void [UnityEngine]UnityEngine.UNetwork.UWriter::UWriteUInt32(uint32)
IL_001a: ldarg.0
IL_001b: ldfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_0020: ldc.i4 1
IL_0025: and
IL_0026: brfalse IL_0037

IL_002b: ldarg.1
IL_002c: ldarg.0
IL_002d: ldfld valuetype Buf/BufType Powerup::mbuf
IL_0032: callvirt instance void [mscorlib]System.IO.BinaryWriter::Write(int32)

IL_0037: nop
IL_0038: ldarg.0
IL_0039: ldc.i4.0
IL_003a: stfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_003f: ret

Luckily ILSpy can convert between CIL and C# in both directions, so in this case it allows us view the generated CIL code as C#. ILSpy is a great tool for working with Mono/.Net assemblies. The C# looks like:

public override void UNetSerializeVars(UWriter writer, bool forceAll)
{
    if (forceAll)
    {
        this.m_DirtyBits = 4294967295u;
    }
    writer.UWriteUInt32(this.m_DirtyBits);
    if ((this.m_DirtyBits & 1u) != 0u)
    {
        writer.Write((int)this.mbuf);
    }
    this.m_DirtyBits = 0u;
}

So let’s see how this meets our requirements:

1.      No shadow copies of variables

2.      Incremental updates

3.      No comparison checks for state changes

4.      No hand-coded serialization functions

5.      No explicit dirty calls

6.      Works with all supported Unity scripting languages

7.      No workflow changes for the developer

8.      No manual steps for the developer to perform

9.      Driven by meta-data

10.   Handles all types (with new UWriter/UReader serializers)

11.   No reflection at runtime

Looks like we have them all covered.  This system will be efficient and friendly to developers. Hopefully it will help make developing multiplayer games with Unity easier for everyone.

We also use Cecil for RPC call implementations to avoid looking up functions by name with reflection. More on that in a later blog post.

May 29, 2014 in Technology | 9 min. read