State Synchronization is done from the Server to Remote Clients. The local client does not have data serialized to it, since it shares the scene with the server. Any data serialized to a local client would be redundant.
Data is not synchronized from remote clients to the server. This is job of Commands.
SyncVars are member variables of NetworkBehaviour scripts that are synchronized from the server to clients. When an object is spawned, or a new player joins a game in progress, they are sent the latest state of all SyncVars on networked objects that are visible to them. Member variables are made into SyncVars by using the [SyncVar] custom attribute:
class Player : NetworkBehaviour
{
[SyncVar]
int health;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
}
}
The state of SyncVars is applied to objects on clients before OnStartClient() is called, so the state of the object is guaranteed to be up-to-date inside OnStartClient().
SyncVars can be basic types such as integers, strings and floats. They can also be Unity types such as Vector3 and user-defined structs, but updates for struct SyncVars are sent as monolithic updates, not incremental changes if fields within a struct change.
SycnVar updates are sent automatically by the server when the value of a SyncVar changes. There is no need to perform any manual dirtying of fields for SyncVars.
SyncVars can have hook functions that are invoked on the client when the value of a SyncVar changes. These functions are invoked with the new value, which allows the function access to the old and new value at the same time. It is up to hook functions to actually apply the new value to the local object.
class Player : NetworkBehaviour
{
[SyncVar(hook="OnDamage")]
int health;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
}
// will be called on clients when health changes on the server
void OnDamage(int newHealth)
{
PlayExplosion();
health = newHealth;
}
}
SyncVar hooks are called on local clients on the host, even though no serialization occurs there. This maintains consistent functionality for local and remote clients.
SyncVar hooks are not called for initial object state, only for incremental updates. The function OnStartClient() can be used for client-side initialization, as it is called after all SyncVar state has been applied to the local object.
SyncLists are like SyncVars but they are lists of values instead of individual values. SyncList contents are included in initial state updates with SyncVar state. SyncLists do not require the SyncVar attributes, they are specific classes. There are built-in SyncList types for basic types:
There is also SyncListStruct which can be used for lists of user-defined structs. You must derive a class from the generic SyncListStruct base class to use thi.
class Player : NetworkBehaviour
{
public struct Buf
{
public int id;
public string name;
public float timer;
};
public class TestBufs : SyncListStruct<Buf> {}
TestBufs m_bufs = new TestBufs();
}
Note that SyncList should not have the [SyncVar] attribute.
Often the use of SyncVars is enough for scripts to serialize their state to clients, but some cases require more complex serialization code. The virtual functions on NetworkBehaviour that are used for SyncVar serialization can be implmented by developers to perform their own custom serialization. These functions are:
public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
public virtual void OnDeSerialize(NetworkReader reader, bool initialState);
The initialState flag is useful to differentiate between the first time an object is serialized and when incremental updates can be sent. The first time an object is sent to a client, it must include a full state snapshot, but subsequent updates can save on bandwidth by including only incremental changes. Note that SyncVar hook fucntion are not called when initialState is true, only for incremental updates.
If a class has SyncVars, then implementations of these functions are added automatically to the class. So a class that has SyncVars cannot also have custom serialization functions.
The OnSerialize function should return true to indicate that an update should be sent. If it returns true, then the dirty bits for that script are set to zero, if it returns false then the dirty bits are not changed. This allows multiple changes to a script to be accumulated over time and sent when the system is ready, instead of every frame.
Game objects with the NetworkIdentity component can have multiple scripts derived from NetworkBehaviour. The flow for serializing these objects is:
On the server:
On the client:
So for this script:
public class data : NetworkBehaviour
{
[SyncVar]
public int int1 = 66;
[SyncVar]
public int int2 = 23487;
[SyncVar]
public string MyString = "esfdsagsdfgsdgdsfg";
}
The generated OnSerialize function is something like:
public override bool OnSerialize(NetworkWriter writer, bool forceAll)
{
if (forceAll)
{
// the first time an 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;
}
bool wroteSyncVar = false;
if ((base.get_syncVarDirtyBits() & 1u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.WritePackedUInt32((uint)this.int1);
}
if ((base.get_syncVarDirtyBits() & 2u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.WritePackedUInt32((uint)this.int2);
}
if ((base.get_syncVarDirtyBits() & 4u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.Write(this.MyString);
}
if (!wroteSyncVar)
{
// write zero dirty bits if no SyncVars were written
writer.WritePackedUInt32(0);
}
return wroteSyncVar;
}
And the OnDeserialize function is something like:
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
if (initialState)
{
this.int1 = (int)reader.ReadPackedUInt32();
this.int2 = (int)reader.ReadPackedUInt32();
this.MyString = reader.ReadString();
return;
}
int num = (int)reader.ReadPackedUInt32();
if ((num & 1) != 0)
{
this.int1 = (int)reader.ReadPackedUInt32();
}
if ((num & 2) != 0)
{
this.int2 = (int)reader.ReadPackedUInt32();
}
if ((num & 4) != 0)
{
this.MyString = reader.ReadString();
}
}
If a NetworkBehaviour has a base class that also has serialization functions, the base class functions should also be called.
Not that the UpdateVar packets created for object state updates may be aggregated in buffers before being sent to the client, so a single transport layer packet may contain updates for multiple objects.