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.