Skip to main content

Code Generation

To synchronize variables, Mirage uses property-based [SyncVar] definitions. During the build process, the Weaver intercepts each auto-property, completely clearing its compiled getter and setter method bodies. It then injects custom IL directly into these accessors:

  • Getter: Directly returns the backing field (e.g. ldfld).
  • Setter: Injects an equality check (SyncVarEqual), a dirty flag update (SetDirtyBit), hook execution checks, and updates the backing field.

The compiler also generates SerializeSyncVars and DeserializeSyncVars methods to serialize/deserialize these values.

So for this script:

public class Data : NetworkBehaviour
{
[SyncVar(hook = nameof(OnInt1Changed))]
public int int1 = 66;

[SyncVar]
public int int2 = 23487;

[SyncVar]
public string MyString = "Example string";

void OnInt1Changed(int oldValue, int newValue)
{
// do something here
}
}

The following sample shows the code that is generated by Mirage for the SerializeSyncVars function which is called inside NetworkBehaviour.OnSerialize:

public override bool SerializeSyncVars(NetworkWriter writer, bool initialState)
{
// Write any SyncVars in base class
bool written = base.SerializeSyncVars(writer, initialState);

if (initialState)
{
// The first time a game object is sent to a client, send all the data (and no dirty bits)
writer.WritePackedUInt32((uint)this.int1);
writer.WritePackedUInt32((uint)this.int2);
writer.Write(this.MyString);
return true;
}
else
{
// Writes which SyncVars have changed
writer.Write(base.SyncVarDirtyBits, 3);

if ((base.SyncVarDirtyBits & 1uL) != 0uL)
{
writer.WritePackedUInt32((uint)this.int1);
written = true;
}

if ((base.SyncVarDirtyBits & 2uL) != 0uL)
{
writer.WritePackedUInt32((uint)this.int2);
written = true;
}

if ((base.SyncVarDirtyBits & 4uL) != 0uL)
{
writer.Write(this.MyString);
written = true;
}

return written;
}
}

The following sample shows the code that is generated by Mirage for the DeserializeSyncVars function which is called inside NetworkBehaviour.OnDeserialize:

public override void DeserializeSyncVars(NetworkReader reader, bool initialState)
{
// Read any SyncVars in base class
base.DeserializeSyncVars(reader, initialState);

if (initialState)
{
// The first time a game object is sent to a client, read all the data (and no dirty bits)
int oldInt1 = this.int1;
this.int1 = (int)reader.ReadPackedUInt32();
// if old and new values are not equal, call hook
if (!base.SyncVarEqual<int>(oldInt1, this.int1))
this.OnInt1Changed(oldInt1, this.int1);

this.int2 = (int)reader.ReadPackedUInt32();
this.MyString = reader.ReadString();
return;
}

ulong dirtySyncVars = reader.Read(3);
base.SetDeserializeMask(dirtySyncVars, 0);

// is 1st SyncVar dirty
if ((dirtySyncVars & 1uL) != 0uL)
{
int oldInt1 = this.int1;
this.int1 = (int)reader.ReadPackedUInt32();
// if old and new values are not equal, call hook
if (!base.SyncVarEqual<int>(oldInt1, this.int1))
this.OnInt1Changed(oldInt1, this.int1);
}

// is 2nd SyncVar dirty
if ((dirtySyncVars & 2uL) != 0uL)
this.int2 = (int)reader.ReadPackedUInt32();

// is 3rd SyncVar dirty
if ((dirtySyncVars & 4uL) != 0uL)
this.MyString = reader.ReadString();
}

If a NetworkBehaviour has a base class that also has serialization functions, the base class functions should also be called.