Unity クライアント

共有オブジェクトを Unity で表示・操作するためのコンポーネントと設定

sync と stateMachine のルームでは、ルームにあるオブジェクトを Unity の GameObject として表示します。このページは、その Unity 側の設定と API を、やりたいことごとに説明します。

Photon(PUN2)から移行する場合は、Photon からの移行ガイドに対応表があります。

初めて使う場合は、先にチュートリアルで一通り動かすと、各設定の役割がつかみやすくなります。サーバー側のルールの書き方はルーム定義リファレンスを参照してください。

全体像

用語意味敵が 10 体いる場合の例
共有オブジェクトルームが作り、参加者全員が同じ状態を参照する対象10 体の敵
プレイヤーオブジェクトプレイヤーが生成し、所有するオブジェクト。所有者が値を書き換える各プレイヤーのキャラクター
オブジェクト IDルーム内の 1 つの実体を表す ID。サーバーが発行する敵ごとに異なる 10 個の ID
Prefab IDオブジェクトの種類を表す ID。Unity で表示する Prefab を選ぶのに使う10 体とも同じ ID
同期プロパティ参加者へ配信する値位置、HP

Unity では、次の 3 つのコンポーネントを使います。名前空間は Gs2.Realtime.Sync です。

コンポーネント置く場所役割
NetworkWorldシーンに 1 つ受信した状態から GameObject を生成・更新・削除し、変更をまとめて送信する
NetworkEntity同期する Prefab のルートGameObject と共有オブジェクトを対応付ける。値の読み書き、表示への反映、操作権を扱う
NetworkTransform位置・回転を同期する Prefab操作できるオブジェクトの移動を送信し、他のクライアントでは受信した位置へ補間する

サーバーが更新するオブジェクトへ操作を送るには、SharedObject も追加します。

データは次の順番で届きます。

  1. ルームに参加すると、その時点の全オブジェクトと値(スナップショット)が届き、NetworkWorld が GameObject を生成します。
  2. 以降は、変更された値だけが届き、表示が更新されます。

ルームに接続する

NetworkWorld は、接続やログインを行いません。GS2 SDK の RealtimeSession で接続し、次の順番で NetworkWorld に渡します。具体的なコードはチュートリアルの接続スクリプトを参照してください。

  1. RealtimeSession で接続し、MyConnectionId(自分の接続 ID)を得ます。
  2. world.Attach(connectionId, send) を呼びます。send は、送信データを受け取って送信キューに積む関数です。
  3. session.OnMessageWithMetadata で受け取ったデータを world.Receive(payload, metadata.LifeTimeMilliSeconds) に渡します。参加直後のスナップショットを取りこぼさないよう、session.Dispatch() を始める前に登録します。
  4. 切断したら world.Detach() を呼びます。表示中の GameObject はすべて削除されます。

LifeTimeMilliSeconds はルームのサーバーの時計です。これを渡すと、ルームの参加者に共通の時刻 world.ServerTime(秒)が使え、他のプレイヤーのオブジェクトが受信時刻に沿って滑らかに動きます。world.Receive(payload) だけでも動作します。

UDP で状態を送る方法はUDP で状態を送るを参照してください。

send は、データ配列と有効な長さを受け取り、送信キューに積めたら true を返します。配列は再利用されるので、有効な範囲をコピーしてから積みます。送信に失敗したら、変更を捨てずに切断し、接続し直してスナップショットから表示を作り直します。

RelayRealtimeSession はメッセージを別の形式で包むため、この用途には使えません。

Prefab を登録する

サーバーから届いたオブジェクトを表示する Prefab を、Prefab ID ごとに登録します。

  1. 表示用の Prefab を作り、ルートに NetworkEntity を追加します。位置・回転も同期するなら NetworkTransform も追加します。
  2. Project ウィンドウの Create > GS2 > Realtime > Prefab Catalog でカタログを作ります。
  3. カタログの Prefabs に、Prefab ID(Id)と表示用の Prefab を登録します。
  4. シーンの NetworkWorld > Prefab Catalog にカタログを指定します。

サーバーから届く可能性のある Prefab ID は、すべて登録してください。登録のない ID が届くとエラーになります。Prefab のルートには NetworkEntity が必要です。

共有オブジェクトはルームが作ります。プレイヤーが自分のオブジェクトを作るときは、Unity の Instantiate ではなく world.Instantiate を使います。Unity の Instantiate で作った GameObject は、ほかの参加者には共有されません。

ルームに生成や RPC を検査させたい場合は、オブジェクトカタログを設定します。

位置・回転を同期する

位置・回転は SDK に標準のフィールドがあり、同期スキーマのアセットがなくても同期できます。サーバー側で同期を許可した軸だけを、NetworkTransform で選びます。

サーバーの宣言Unity で同期できる軸刻み幅(標準)
RoomSync.PositionPosition X / Y / Z0.01(Unity の距離単位)
RoomSync.RotationRotation X / Y / Z360 / 4096 度
RoomSync.Transform上記の両方上記と同じ

NetworkTransform の設定は次のとおりです。

項目内容
Position Axes / Rotation Axes同期する軸。サーバーが許可していない軸を選ぶとエラーになる。位置だけ許可されているなら Rotation Axes は None
Target動かす Transform。空なら自身
Interpolationサーバー時刻を渡していないときの補間の速さ。0 なら受信した値へすぐ移動する
Teleport Distanceこの距離より離れた位置へは補間せずに移動する(リスポーンなど)。0 なら常に補間する
Interpolation Delayサーバー時刻を渡しているとき、何秒前の姿勢を表示するか(既定 0.1)。前後に受信した更新の間を補間する

位置はワールド座標、回転はオイラー角として扱います。3 軸とも回転を同期する場合は、回転を Quaternion で補間します。選ばなかった軸は、各クライアントのローカルの値のままです。Scale は同期しません。更新が止まると最後に受信した姿勢で止まり、先の位置を予測することはしません。

操作できるオブジェクトでは、移動スクリプトで普段どおり Transform を動かすと、NetworkTransform が値を刻み幅に丸めて送信します。刻み幅より小さな揺れは送信されません。

独自の値を表示する

HP や開閉状態などの独自の値は、サーバー側で RoomSchema として宣言し、生成された同期スキーマ(.realtimeschema)を Unity で使います。宣言の書き方は同期スキーマを参照してください。

  1. 生成された .realtimeschema を Assets 以下に置きます。同期スキーマのアセットとして取り込まれます。
  2. Prefab の NetworkEntity > Schema に、そのアセットを指定します。
  3. NetworkEntity > Property bindings > Add binding で項目を追加し、Field で値を選びます。
  4. Apply to に反映先を登録し、受信した値を引数に取る Dynamic のイベントを選びます。

イベントの引数の型は、同期スキーマの型(int / bool / float)に対応します。たとえば bool を GameObject の SetActive(bool) に、float を Slider の value に接続できます。参加直後と、値が変わるたびに呼ばれます。

スクリプトからは、NetworkEntity の次のメソッドで値を読み書きできます。

メソッド内容
GetInt / GetBool / GetFloat(name)受信した値を読む
SetInt / SetBool / SetFloat(name, value)値を送信する。そのオブジェクトの書き込み権限が必要

スクリプトで送受信をまとめて書く場合は、INetworkObservable を実装したコンポーネントを Prefab に置きます。Photon の OnPhotonSerializeView に当たります。

public sealed class Health : MonoBehaviour, INetworkObservable
{
    public float Value;
    // 所有者で、まとめて送信する直前に呼ばれる
    public void OnNetworkWrite(NetworkEntity entity) => entity.SetFloat("Health", Value);
    // ほかのクライアントで、値が届いたときに呼ばれる
    public void OnNetworkRead(NetworkEntity entity, uint changed)
    {
        if (entity.HasChanged("Health", changed))
            Value = entity.GetFloat("Health");
    }
}

Property bindings は、受信した値を表示に渡すだけです。Slider をローカルで動かしても、サーバーの値は変わりません。サーバーが管理する値は、サーバーに操作を送ることで変更します。

操作権を使って値を送る

サーバーが「操作権を貸し出す」と宣言したオブジェクトは、操作権を得たプレイヤーだけが値を送信できます。箱を持ち運ぶ、パドルを動かす、といった用途に使います。

メンバー内容
RequestControl() / ReleaseControl()操作権の取得を要求する / 返却する。Button の On Click にも登録できる
Acquire On Enable有効になったときに自動で取得を要求する
Auto Renew操作権の期限が切れる前に自動で更新する(既定で有効)
Controlled Behaviours操作権がある間だけ有効にするスクリプト(移動や入力の処理)。ネットワーク用のコンポーネントは登録しない
HasControl / Control Changed今操作できるか / 操作できる状態が変わったときのイベント
LastResult / Command Completed取得・返却の要求の結果

取得の要求は非同期で処理されるので、要求した直後に操作できるとは限りません。ほかのプレイヤーが操作権を持っているときは、LeaseBusy で拒否されます。拒否された場合は自動で再要求しないので、必要なら RequestControl() を呼び直します。

GameObject が無効になると、操作権は自動で返却されます。

サーバーに操作を送る

サーバーが管理するオブジェクト(敵の HP など)は、クライアントから値を書き換えられません。代わりに、SharedObject.Interact(method) で「このオブジェクトに method の操作をする」という要求を送ります。要求はサーバーのルールの Interact に届き、ルールが検証して値を更新します。

var target = GetComponent<SharedObject>();
var request = target.Interact(1); // 送信できなければ 0

method は、ルールで操作の種類を見分けるための番号です。追加の引数は送れません。

要求の結果は NetworkWorld.Client.CommandCompleted で受け取ります。引数は、オブジェクト ID、Interact の戻り値の要求 ID、結果です。

world.Client.CommandCompleted += (objectId, request, result) =>
{
    Debug.Log($"#{objectId} の要求 {request}: {result}");
};
結果値意味
Success0受理され、変更が全員に配信された
MissingObject1対象のオブジェクトがない
Forbidden2このオブジェクトには許可されていない操作
LeaseBusy3ほかのプレイヤーが操作権を持っている
StaleEpoch4操作権の状態が変わった後の古い要求
InvalidOperation5今の状態では受け付けない操作、またはルールの実行エラー
AlreadyApplied6ルールがすでに適用済みとして返した
StaleRequest7同じ要求 ID がすでに処理済み
ルールの拒否理由1 以上の任意の値ルールが Step.Ignore で返した値

ルールが Step.Ignore で拒否した場合は、その理由の数値がそのまま届きます。上の結果と区別できるよう、ルールの拒否理由には 1000 以上の値を使ってください。

プレイヤーオブジェクトを作る

プレイヤーが操作するキャラクターなど、プレイヤーが所有するオブジェクトは world.Instantiate で作ります。Photon の PhotonNetwork.Instantiate に当たります。

var entity = world.Instantiate(prefabId, position, rotation);
entity.Rpc(nameof(Weapon.Equip), RpcTarget.AllBuffered, 3); // 生成直後から使える
  • 戻り値の NetworkEntity は、その場で自分の所有(IsMine)として使えます。移動や RPC は、ルームが生成を確定するまで保留され、確定後に送られます。確定までに動かした位置は保たれます。IsSpawned は確定すると true になります。
  • 位置と回転は、Prefab の NetworkTransform で同期する軸に設定されます。
  • ルームが生成を拒否すると(所有数の上限超過や、オブジェクトカタログを設定したルームでの未登録の Prefab)、world.SpawnFailed が呼ばれ、インスタンスは削除されます。
  • 生成の確定を待ってから GameObject を作る場合は、world.Spawn(prefabId, position, rotation, entity => ...) を使います。
メンバー内容
IsMine自分が書き換えられるか(プレイヤーオブジェクトの所有者、またはリースの操作権の保持者)
OwnerId所有者の接続 ID。共有オブジェクトでは操作権の保持者(いなければ 0)
Destroy()自分のプレイヤーオブジェクトを削除する
TransferOwnership(connectionId)所有権を別の参加者へ渡す(Photon の TransferOwnership)
RequestOwnership()所有権を取得する(Photon の RequestOwnership)。オブジェクトカタログを設定したルームでは、"ownership": "takeover" のオブジェクトだけ
OwnershipChanged所有者が変わったときのイベント

所有者が退出すると、そのプレイヤーオブジェクトは削除されます。オブジェクトカタログで "keepOnLeave": true にしたオブジェクトは、マスタークライアントへ引き継がれます。

RPC

Prefab のコンポーネントのメソッドに [NetworkRpc] を付けると、ほかのクライアントから呼び出せます。Photon の [PunRPC] と photonView.RPC に当たります。

public sealed class Weapon : MonoBehaviour
{
    [NetworkRpc]
    void Fire(Vector3 direction, int damage, NetworkRpcInfo info)
    {
        // info.Sender は呼び出したプレイヤーの接続 ID(サーバーが設定する)
    }
}

entity.Rpc(nameof(Weapon.Fire), RpcTarget.Others, direction, 10);
entity.RpcTo(nameof(Weapon.Fire), connectionId, direction, 10); // 1 人だけに送る
宛先届く相手
RpcTarget.All呼び出したプレイヤーを含む全員
RpcTarget.Others呼び出したプレイヤー以外
RpcTarget.Ownerオブジェクトの所有者
RpcTarget.AllBuffered / OthersBuffered上記に加え、あとから参加したプレイヤーにも届く。オブジェクトの削除か、呼び出したプレイヤーの退出で破棄される
RpcTo(method, connectionId, ...)指定した 1 人
  • RPC の ID は、メソッド名から決まります([NetworkRpc(id)] で固定もできる)。NetworkEntity の Inspector に、Prefab にある RPC の ID が表示されます。
  • 誰がどの RPC を呼べるかを制限したい場合は、オブジェクトカタログを設定します。設定すると、methods に登録した ID を所有者だけが呼べるようになり、ほかのプレイヤーのオブジェクトにも呼ぶ RPC は publicMethods に登録します([NetworkRpc(AnyCaller = true)] を付けると、カタログの生成でこちらに入る)。
  • 引数に使える型は、bool、整数型、char、float、double、string、byte[]、enum、Vector2/3/4、Vector2Int/3Int、Quaternion、Color です。合計 4096 バイトまでです。最後の引数を NetworkRpcInfo にすると、呼び出し元を受け取れます。IEnumerator を返すとコルーチンとして実行されます。
  • 受信した RPC は、届いた順に、受信処理を終えてから呼ばれます。RPC の中から RPC を送ったり値を書き換えたりできます。
  • 共有オブジェクトには RPC を送れません。サーバーに操作を送るを使います。

ルームイベント

オブジェクトに紐づかない通知は、ルームイベントで送ります。Photon の RaiseEvent と OnEvent に当たります。

public sealed class Scoreboard : MonoBehaviour
{
    void OnEnable() => world.AddEventTarget(this);
    void OnDisable() => world.RemoveEventTarget(this);

    [NetworkEvent]
    void Goal(int team, NetworkRpcInfo info) { /* ... */ }
}

world.RaiseEvent("Goal", RpcTarget.All, 1);
world.RaiseEventTo("Goal", connectionId, 1);

参加者なら誰でも送れます。宛先は All / Others と、それぞれの Buffered、または 1 人です。Buffered のイベントは、送ったプレイヤーが退出すると破棄されます。引数のエンコードは、登録したハンドラーのメソッドの宣言に従います。ハンドラーを使わずにバイト列を扱う場合は、RaiseRawEvent と EventReceived を使います。

カスタムプロパティ

ルームやプレイヤーに付ける値です。あとから参加したプレイヤーにも届きます。Photon のルーム・プレイヤーのカスタムプロパティに当たります。

world.SetRoomProperty("stage", "forest");
world.SetPlayerProperty("team", 2);   // 自分のプロパティだけ書ける
world.RoomPropertyChanged += key => { /* ... */ };
if (world.TryGetPlayerProperty<int>(connectionId, "team", out var team)) { /* ... */ }
  • 値に使える型は RPC の引数と同じです(enum は整数で保存する)。null を書くと削除します。
  • 書き込みは、ルームを経由して全員に届いた時点で反映されます。書いたプレイヤーの手元でも同じです。
  • プレイヤーが退出すると、そのプレイヤーのプロパティは削除されます。
  • キーは 64 バイトまで、ルーム全体で 1024 件までです。

マスタークライアント

ルームは参加者のうち 1 人をマスタークライアントにします。最初に参加したプレイヤーがマスターになり、マスターが退出すると、残りのうち最も早く参加したプレイヤーへ移ります。

メンバー内容
world.MasterClientId / IsMasterClientマスターの接続 ID / 自分がマスターか
world.SetMasterClient(connectionId)マスターを変更する。マスターだけが呼べる
world.MasterClientChangedマスターが変わったときのイベント

ゲームの進行をサーバーで決めたい場合は、マスタークライアントではなく stateMachine のルールを使うと、クライアントの改造の影響を受けません。

Interest Group

オブジェクトをグループに分け、購読しているプレイヤーにだけ更新を届けます。Photon の PhotonView.Group と SetInterestGroups に当たります。

entity.SetGroup(5);          // 所有者がオブジェクトのグループを決める(0 は全員に届く)
world.SubscribeGroup(5);     // グループ 5 の更新を受け取る
world.UnsubscribeGroup(5);
  • グループで絞り込まれるのは、値の更新と、そのオブジェクトへの All / Others 宛ての RPC です。生成・削除・所有者やグループの変更は、全員に届きます。
  • 購読すると、そのグループのオブジェクトの現在値がすぐに届きます。
  • 購読は再接続しても引き継がれます。1 接続あたり 256 グループまでです。

ティックで動くオブジェクトを操作する

ルームのルールが DefineTickRate と DefineSimulatedPrefab を使う場合、その種類のオブジェクトはサーバーが入力から動かします(ティック)。クライアントは位置を書き込まず、入力を送ります。

// 自分のキャラクターを生成する(Simulated の Prefab)
var me = world.Instantiate(5, Vector3.zero, Quaternion.identity);

// 毎フレーム、現在の入力を設定する
void Update()
{
    world.SetInput(0, Mathf.RoundToInt(Input.GetAxisRaw("Horizontal")));
    world.SetInput(1, Mathf.RoundToInt(Input.GetAxisRaw("Vertical")));
    world.SetInput(2, Input.GetButton("Fire1"));
    // Aim direction for Room.Raycast (x 1000)
    world.SetInput(3, Mathf.RoundToInt(aim.x * 1000));
    world.SetInput(4, Mathf.RoundToInt(aim.y * 1000));
}
  • 入力は 32 個の整数スロットの「現在の状態」です。SetInput(slot, int)・SetInput(slot, bool)・SetInput(slot, float, precision) で設定し、ルームのティックごとに送られます。ティック単位で設定したいときは InputSampling イベントを使います。
  • 自分が所有する Simulated オブジェクトがある間だけ送ります。1 回の送信に直近 3 ティック分の入力を入れるので、UDP で 1 つ落ちても補えます。UDP を使えない環境(WebGL など)では WebSocket で送ります(UDP)。
  • 動作するサンプルが SDK の Assets/Scenes/Samples/Realtime/Arena にあります(移動の予測、ラグ補償付きの射撃、予測スポーンのロケット)。
  • NetworkWorld は、入力がルームの処理より InputLeadTicks(既定 2 ティック)早く届くように、入力に付けるティック番号(InputTick)をルームより少し先に進めます。ルームから届く Clock を見て、進み方を ±10% 以内で自動調整します。
  • オブジェクトはルームの計算結果を表示します。IsMine は所有者で true になりますが、値の書き込みも Destroy もできません。削除はルールの Despawn か、所有者の退出で行われます。

自分のキャラクターを予測して動かす

ルールに [Predict] を付けた処理があると(予測)、コンパイラが <ルールクラス名>Prediction.cs を出力します。これを Unity プロジェクトに入れて、登録します。

TickRoomPrediction.Register(world);  // ルールクラスが TickRoom の場合。Attach の前後どちらでもよい
  • 登録すると、自分が所有する Simulated オブジェクトは、入力したティックの結果をすぐに表示します。往復の通信時間を待ちません。
  • ルームの計算結果が届くたびに、その結果から現在のティックまで計算し直します。予測が外れていた場合は、NetworkTransform がずれを 約 CorrectionTime(既定 0.1 秒)で目立たないように詰めます。
  • NetworkObject.Values はルームから届いた値のままです。予測した値は PredictedValues で読めます。IsPredicted が true のオブジェクトが予測の対象です。
  • ほかのプレイヤーのキャラクターは、これまでどおりルームの結果を補間して表示します。
  • 予測の中で生成されたオブジェクト(予測スポーン)は NetworkWorld.PredictedSpawned で通知されます。ルームのオブジェクトが届くと、同じインスタンスが NetworkEntity.Object を持つようになります。予測で削除されたオブジェクトは、ルームが削除するまで描画が止まります(NetworkObject.PredictedRemoved)。
  • 入力には、そのときほかのオブジェクトが画面上でどれだけ遅れて表示されていたかが付きます。ルームはこれを使って、撃った側の画面どおりに当たり判定をします(ラグ補償)。NetworkWorld.ViewDelay は NetworkTransform.InterpolationDelay と同じ値(既定 0.1 秒)にしてください。ViewDelay は Inspector でも設定できます。正確に計算するには、world.Receive(bytes, milliseconds) でサーバー時刻を渡す必要があります(ルームに接続する)。

UDP で状態を送る

位置などの定期的な更新を UDP で送ると、通信が詰まったときの遅延を抑えられます。Photon の unreliable な送信に当たります。生成・RPC・イベント・プロパティなど、届かないと困るものは WebSocket のままです。

var udp = new RealtimeUdpChannel(session);  // 接続後に作る
// 毎フレーム
udp.Poll((bytes, milliseconds) => world.Receive(bytes, milliseconds));
world.Unreliable = udp.Active ? udp.TrySend : (TrySend)null;
  • Poll が、ルームへの登録と登録の維持をします。ルームから応答があると Active が true になり、10 秒応答がないと false に戻ります。false の間は WebSocket で送ります。
  • UDP で送った値は、取りこぼしに備えて 1 秒ごとに送り直されます。そのため、値が失われても最長で約 1 秒後には正しい値になります。
  • RPC などの直前に送る値は WebSocket で送るので、RPC より先に届きます。
  • WebGL では UDP を使えません。WebSocket だけで動作します。

オブジェクトカタログ

ルームに、プレイヤーオブジェクトの種類・フィールド・RPC・所有権の要求を検査させたい場合は、Realtime のネームスペース設定の syncSetting.objectCatalog に、検査の基準を JSON で設定します。キーは Prefab ID です。

設定しない場合、ルームはこれらを検査しません。登録のない Prefab も生成時に宣言したフィールドを持つプレイヤーオブジェクトとして生成でき、どの RPC も誰でも呼べ、誰でも所有権を取得できます。設定した場合は、登録した Prefab・フィールド・RPC だけが通ります。

{
  "1": {
    "fields": 63,
    "mode": "player",
    "methods": [1],
    "publicMethods": [2],
    "ownership": "takeover",
    "keepOnLeave": true
  }
}
項目内容
fields同期するフィールドのビットマスク。位置 X/Y/Z が 1/2/4、回転 X/Y/Z が 8/16/32
modeプレイヤーオブジェクトは "player"
methods所有者が呼べる RPC の ID(上の例は [NetworkRpc(1)] のように ID を固定した場合。名前から決まる ID は Inspector か Copy Object Catalog JSON で確認する)
publicMethods参加者の誰でも、ほかのプレイヤーのオブジェクトに対して呼べる RPC の ID
ownership"fixed"(既定)か "takeover"(誰でも所有権を取得できる)
keepOnLeavetrue なら、所有者の退出時に削除せずマスタークライアントへ引き継ぐ。この動作はカタログに登録した Prefab でだけ使える

手で書く代わりに、カタログアセットの右クリックメニュー Copy Object Catalog JSON で生成できます。stateMachine で共有オブジェクトを使う場合は、ルールが定義する Prefab とあわせて登録します。

オブジェクトの寿命と再接続

対象切断したとき再接続したとき
サーバーが作った共有オブジェクトルームに残るスナップショットで、その時点の値のまま再表示される
自分のプレイヤーオブジェクト削除される(keepOnLeave ならマスターへ引き継がれる)必要なら作り直す
自分のプレイヤープロパティ・Buffered の RPC とイベント削除される必要なら設定し直す
ルームのプロパティルームに残るスナップショットで届く
持っていた操作権返却される必要なら取得し直す
自分の接続 ID無効になる新しい ID が発行される

再接続のたびに、届いたスナップショットから表示を作り直します。クライアントでルールや操作履歴をやり直す必要はありません。

同じ userId で接続し直すと、古い接続は退出したものとして扱われます。

ルームの終了条件との関係

ルームは、参加者全員から 1 分間通信がないと終了します。sync と stateMachine のルームでは、接続を保つための Ping は、この通信に数えません。

NetworkWorld は値が変わったときだけ送信します。UDP の登録の維持も、この通信には数えません。そのため、全員が操作せずに待つ場面があるゲームでは、その間もルームが続くよう、定期的に操作を送るなどの設計が必要です。

送信量の目安

同期データは、フィールド名を送らず、変更されたフィールドの番号と整数値だけを送ります。NetworkWorld は、Send Rate(既定 20 回/秒、world.SendRate でも変更可)の間隔で変更をまとめて送信します。同じ値を何度書いても、前回送った値に戻っても、送信量は増えません。

位置の刻み幅と同期する軸を必要な範囲に絞ると、小さな揺れによる送信を減らせます。

うまく動かないとき

症状確認すること
オブジェクトが表示されない接続後にスナップショットを受信しているか。届く Prefab ID がカタログに登録されているか
Unknown prefab ID のエラーになるサーバーのルールの Prefab ID と、カタログの Id が一致しているか
Select a field from the schema と表示される独自の値を使う Prefab の Schema に、生成した同期スキーマを指定したか
位置だけなのにフィールドのエラーになるNetworkTransform の Rotation Axes が None になっているか
動かしても送信されない操作権を取得しているか。移動スクリプトを Controlled Behaviours に登録しているか
値の大きさや位置がずれるルール(GSL)と同期スキーマを、同じ C# 定義から同時に生成したものに揃えているか
Inspector のイベントで値が変わらない固定値を渡すイベントではなく、Dynamic のイベントを選んだか
攻撃などの操作が反映されないCommandCompleted の結果を確認する。結果の一覧を参照
RPC が届かないオブジェクトカタログを設定している場合、RPC の ID がカタログの methods(ほかのプレイヤーから呼ぶなら publicMethods)にあるか。メソッド名を変えると ID も変わる
world.Instantiate したオブジェクトが消えるSpawnFailed の結果を確認する。所有数の上限(64)を超えていないか。オブジェクトカタログを設定している場合、Prefab が "mode": "player" で登録されているか
UDP が Active にならないルームのポートが UDP で開いているか。WebGL では UDP を使えない

ルール(GSL)と同期スキーマは、同じ C# 定義から同時に生成したものを組み合わせて配布してください。