カスタムの Spawn 関数
リモートアクション

ステートの同期

状態同期(State Synchronization)は、Server から 各 Remote Client に対して行われます。ローカルクライアントはサーバーとの間でシーンを共有するため、それ用にシリアライズされたデータを持ちません。ローカルクライアント用にシリアライズされた一切のデータは不必要なものになります。ただし SyncVar フックはローカルクライアント上で呼び出されます。

データはリモートクライアントからサーバーへは同期されません。これはコマンドによって行われます。

SyncVars

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

SyncList は SyncVar と似ていますが、SyncList は個々の値ではなく値のリストです。SyncList の内容は SyncVar の状態による最初の状態更新に含まれています。SyncList は SyncVar 属性を必要としない、固有のクラスです。ベーシックタイプ用のビルトイン SyncList タイプが存在します。

  • SyncListString
  • SyncListFloat
  • SyncListInt
  • SyncListUInt
  • SyncListBool

ユーザー定義構造体のリストに使用される 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 から派生するスクリプトを複数持つことができます。オブジェクトのシリアライズの流れは以下の通りです。

サーバー上での流れ:

  • 各 NetworkBehaviour がダーティマスクを持つ。このマスクは OnSerialize の中で syncVarDirtyBits として使用可能。
  • NetworkBehaviour スクリプト内のそれぞれの SyncVar にダーティマスクの中のビットが一つアサインされる。
  • SyncVar の値が変更されると、その SyncVar 用のビットがダーティマスク内に設定される。
  • または、SetDirtyBit() の呼び出しによって直接ダーティマスクに記述される。
  • NetworkIdentity オブジェクトがサーバー上で、その更新ループとしてチェックされる。
  • NetworkIdentity の NetworkBehaviour にダーティなものがある場合、UpdateVars パケットがそのオブジェクト用に作成される。
  • UpdateVars パケットが OnSerialize の呼び出しによってオブジェクトの各 NetworkBehaviour に追加される。
  • ダーティでない NetworkBehaviour が、そのダーティビットのパケットに 0 を記述する。
  • ダーティな NetworkBehaviour がそのダーティマスクを記述し、その後、変更された SyncVars の値を記述する。
  • OnSerialize がある NetworkBehaviour に True を返すと、ダーティマスクはその NetworkBehaviour 用にはリセットされ、その値が変化するまでは再度送られることがなくなる。
  • UpdateVars パケットが、オブジェクトを観測しているクライアントで準備の整ったものに送信される。

クライアント上での流れ:

  • UpdateVars パケットがオブジェクト用に受け取られる。
  • OnDeserialize 関数がオブジェクトの各 NetworkBehaviour スクリプト用に呼び出される。
  • オブジェクトの各 NetworkBehaviour スクリプトがダーティマスクを一つ読む。
  • NetworkBehaviour のダーティマスクが 0 だと OnDeserialize 関数は、それ以上読み込むことなくリターンします。
  • ダーティマスクが 0 以外の値だと、OnDeserialize 関数は、設定されたダーティビットに対応する SyncVar 用の値を読み出します。
  • SyncVar フック関数がある場合、それはストリームから読み出された値によって実行されます。

したがって、このスクリプトの場合:

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)
    {
        // オブジェクトを初めてクライアントに送信。すべてのデータを送ります (ダーティビットは含みません)
        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)
        {
            //これが、初めて SyncVar を記述する場合であれば、ダーティビットを記述します
            writer.WritePackedUInt32(base.get_syncVarDirtyBits());
            wroteSyncVar = true;
        }
        writer.WritePackedUInt32((uint)this.int1);
    }
    if ((base.get_syncVarDirtyBits() & 2u) != 0u)
    {
        if (!wroteSyncVar)
        {
            // これが、初めて SyncVar を記述する場合であれば、ダーティビットを記述します
            writer.WritePackedUInt32(base.get_syncVarDirtyBits());
            wroteSyncVar = true;
        }
        writer.WritePackedUInt32((uint)this.int2);
    }
    if ((base.get_syncVarDirtyBits() & 4u) != 0u)
    {
        if (!wroteSyncVar)
        {
            // これが、初めて SyncVar を記述する場合であれば、ダーティビットを記述します
            writer.WritePackedUInt32(base.get_syncVarDirtyBits());
            wroteSyncVar = true;
        }
        writer.Write(this.MyString);
    }

    if (!wroteSyncVar)
    {
        // SyncVar が記述されていなければ、ダーティビットを記述しません
        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 パケットはクライアントに送られる前にバッファーに集結されるため、一つのトランスポート層パケットに複数のオブジェクトの更新が含まれることがあります。

カスタムの Spawn 関数
リモートアクション