状態同期(State Synchronization)は、Server から 各 Remote Client に対して行われます。ローカルクライアントはサーバーとの間でシーンを共有するため、それ用にシリアライズされたデータを持ちません。ローカルクライアント用にシリアライズされた一切のデータは不必要なものになります。ただし SyncVar フックはローカルクライアント上で呼び出されます。
データはリモートクライアントからサーバーへは同期されません。これはコマンドによって行われます。
SyncVars は、NetworkBehaviour コンポーネントのメンバー変数です。これはサーバーから各クライアントへ同期されます。オブジェクトが生成(Spawn)されたり新しいプレイヤーが進行中のゲームに参加したりすると、利用可能なネットワーク上のオブジェクトの全ての SyncVars の最新の状態が、その新しいオブジェクトやプレイヤーに送られます。メンバー変数は [SyncVar] カスタム属性の使用によって SyncVars になります。
class Player : NetworkBehaviour
{
[SyncVar]
int health;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
}
}
SyncVar の状態は、OnStartClient() が呼び出される前にクライアント上のオブジェクトに適用されます。したがって OnStartClient() 内でオブジェクトが最新の状態であることが保証されます。
SyncVar は int、string、float などのベーシックタイプで使用可能です。Vector3 やユーザー定義の構造体などの Unity タイプでも使用可能ですが、構造体内のフィールドが変更された場合、構造体の SyncVar の更新は、段階的な変更ではなくモノリシックの更新として送信されます。SyncLists に含まれる 1つの NetworkBehaviour には 32 個の SyncVar まで設定できます。
SycnVar の更新は、SyncVar の値が変更されたときにサーバーによって送信されます。SyncVar のために手動でフィールドに手を加える必要は一切ありません。
プロパティのセッター関数内で SyncVar メンバ変数を設定すると、ダーティになることはありません。これを実行しようとするとコンパイル時に警告が発生します。ダーティとしてそれらのメンバ変数自体をマークするため内部 SyncVar プロパティを使用するので、プロパティ関数内でそれらをダーティに設定すると再帰問題につながる可能性があります。
SyncList は SyncVar と似ていますが、SyncList は個々の値ではなく値のリストです。SyncList の内容は SyncVar の状態による最初の状態更新に含まれています。SyncList は SyncVar 属性を必要としない、固有のクラスです。ベーシックタイプ用のビルトイン SyncList タイプが存在します。
ユーザー定義構造体のリストに使用される SyncListStruct もあります。SyncListStruct 派生クラスに使われる構造体は、基本型、配列そして共通の Unity k型メンバ変数を含むことができます。それらは、複雑なクラスや一般的なコンテナを含めることはできません。
SyncLists は、リストのコンテンツが変更されたとき、クライアントにデータを送信できるようにするコールバックと呼ばれる SyncListChanged デリゲートを持っています。このデリゲートは、発生したときに行う操作のタイプや操作を行った項目のインデックスで呼ばれます。
public class MyScript : NetworkBehaviour
{
public struct Buf
{
public int id;
public string name;
public float timer;
};
public class TestBufs : SyncListStruct<Buf> {}
TestBufs m_bufs = new TestBufs();
void BufChanged(Operation op, int itemIndex)
{
Debug.Log("buf changed:" + op);
}
void Start()
{
m_bufs.Callback = BufChanged;
}
}
スクリプトがその状態をクライアント用にシリアライズする際、多くの場合は SyncVar の使用のみで事足りますが、より複雑なシリアライゼーション コードが必要とされる場合もあります。SyncVar シリアライゼーションに使用されるNetworkBehaviour の仮想関数は、開発者自身で実装してカスタムのシリアライゼーションを行うことも可能です。その関数とは下記の通りです:
public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
public virtual void OnDeSerialize(NetworkReader reader, bool initialState);
initialState flag は、オブジェクトが初めてシリアライズされた時と段階的な更新が送信できる時とを差別化するのに便利です。オブジェクトが初めてクライアントに送られる時は、状態の完全なスナップショットを含んでいる必要がありますが、その後の更新はその度毎の変更分のみを送れば良いので、情報処理量を節約できます。SyncVar フック関数は増分更新の為にだけ呼び出され、initialState が True のときには呼び出されませんのでご注意ください。
クラスに SyncVar がある場合、その関数の実装はそのクラスに自動的に追加されます。したがって SyncVar を持つクラスは同時にカスタムのシリアライゼーション関数を持つことはできません。
OnSerialize 関数は、更新が送られなければならないことを示すために True を返します。True が返されるとそのスクリプト用のダーティビットは 0 に設定され、False が返されるとダーティビットは変更されません。このおかげで、スクリプトに複数の変更が加えられた場合に、それを毎フレーム送信するのではなく、蓄積してシステムの準備が整ったときに送信するということが可能になります。
NetworkIdentity コンポーネントを持ったゲームオブジェクトは NetworkBehaviour から派生するスクリプトを複数持つことができます。オブジェクトのシリアライズの流れは以下の通りです。
サーバー上での流れ:
クライアント上での流れ:
したがって、このスクリプトの場合:
public class data : NetworkBehaviour
{
[SyncVar]
public int int1 = 66;
[SyncVar]
public int int2 = 23487;
[SyncVar]
public string MyString = "esfdsagsdfgsdgdsfg";
}
生成された OnSerialize 関数はおおむね下記のようになります:
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;
}
そして OnDeserialize 関数はおおむね下記のようになります:
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();
}
}
もし NetworkBehaviour にベースクラスがあり、それがシリアライゼーション関数も持っている場合、そのベースクラス関数も呼び出されます。
(注) オブジェクトの状態更新用に作成された UpdateVar パケットはクライアントに送られる前にバッファーに集結されるため、一つのトランスポート層パケットに複数のオブジェクトの更新が含まれることがあります。