> For the complete documentation index, see [llms.txt](/ja/llms.txt)


# Unity クライアント

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



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

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

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

## 全体像 {#overview}

| 用語 | 意味 | 敵が 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. 以降は、変更された値だけが届き、表示が更新されます。

## ルームに接続する {#connection}

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

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 で状態を送る](#udp)を参照してください。

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

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

## Prefab を登録する {#prefabs}

サーバーから届いたオブジェクトを表示する 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`](#player-objects) を使います。Unity の `Instantiate` で作った GameObject は、ほかの参加者には共有されません。

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

## 位置・回転を同期する {#transform}

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

| サーバーの宣言 | Unity で同期できる軸 | 刻み幅（標準） |
| --- | --- | --- |
| `RoomSync.Position` | Position X / Y / Z | 0.01（Unity の距離単位） |
| `RoomSync.Rotation` | Rotation X / Y / Z | 360 / 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 が値を刻み幅に丸めて送信します。刻み幅より小さな揺れは送信されません。

## 独自の値を表示する {#bindings}

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

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` に当たります。

```csharp
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 をローカルで動かしても、サーバーの値は変わりません。サーバーが管理する値は、[サーバーに操作を送る](#interaction)ことで変更します。

## 操作権を使って値を送る {#authority}

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

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

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

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

## サーバーに操作を送る {#interaction}

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

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

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

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

```csharp
world.Client.CommandCompleted += (objectId, request, result) =>
{
    Debug.Log($"#{objectId} の要求 {request}: {result}");
};
```

| 結果 | 値 | 意味 |
| --- | --- | --- |
| `Success` | 0 | 受理され、変更が全員に配信された |
| `MissingObject` | 1 | 対象のオブジェクトがない |
| `Forbidden` | 2 | このオブジェクトには許可されていない操作 |
| `LeaseBusy` | 3 | ほかのプレイヤーが操作権を持っている |
| `StaleEpoch` | 4 | 操作権の状態が変わった後の古い要求 |
| `InvalidOperation` | 5 | 今の状態では受け付けない操作、またはルールの実行エラー |
| `AlreadyApplied` | 6 | ルールがすでに適用済みとして返した |
| `StaleRequest` | 7 | 同じ要求 ID がすでに処理済み |
| ルールの拒否理由 | 1 以上の任意の値 | ルールが `Step.Ignore` で返した値 |

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

## プレイヤーオブジェクトを作る {#player-objects}

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

```csharp
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` にしたオブジェクトは、[マスタークライアント](#master)へ引き継がれます。

## RPC {#rpc}

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

```csharp
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 を呼べるかを制限したい場合は、[オブジェクトカタログ](#catalog)を設定します。設定すると、`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 を送れません。[サーバーに操作を送る](#interaction)を使います。

## ルームイベント {#events}

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

```csharp
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` を使います。

## カスタムプロパティ {#properties}

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

```csharp
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 件までです。

## マスタークライアント {#master}

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

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

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

## Interest Group {#interest-groups}

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

```csharp
entity.SetGroup(5);          // 所有者がオブジェクトのグループを決める（0 は全員に届く）
world.SubscribeGroup(5);     // グループ 5 の更新を受け取る
world.UnsubscribeGroup(5);
```

- グループで絞り込まれるのは、値の更新と、そのオブジェクトへの All / Others 宛ての RPC です。生成・削除・所有者やグループの変更は、全員に届きます。
- 購読すると、そのグループのオブジェクトの現在値がすぐに届きます。
- 購読は再接続しても引き継がれます。1 接続あたり 256 グループまでです。

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

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

```csharp
// 自分のキャラクターを生成する（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](#udp)）。
- 動作するサンプルが SDK の `Assets/Scenes/Samples/Realtime/Arena` にあります（移動の予測、ラグ補償付きの射撃、予測スポーンのロケット）。
- `NetworkWorld` は、入力がルームの処理より `InputLeadTicks`（既定 2 ティック）早く届くように、入力に付けるティック番号（`InputTick`）をルームより少し先に進めます。ルームから届く `Clock` を見て、進み方を ±10% 以内で自動調整します。
- オブジェクトはルームの計算結果を表示します。`IsMine` は所有者で `true` になりますが、値の書き込みも `Destroy` もできません。削除はルールの `Despawn` か、所有者の退出で行われます。

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

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

```csharp
TickRoomPrediction.Register(world);  // ルールクラスが TickRoom の場合。Attach の前後どちらでもよい
```

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

## UDP で状態を送る {#udp}

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

```csharp
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 だけで動作します。

## オブジェクトカタログ {#catalog}

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

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

```json
{
  "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"`（誰でも所有権を取得できる） |
| `keepOnLeave` | `true` なら、所有者の退出時に削除せずマスタークライアントへ引き継ぐ。この動作はカタログに登録した Prefab でだけ使える |

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

## オブジェクトの寿命と再接続 {#lifetime}

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

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

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

## ルームの終了条件との関係 {#idle}

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

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

## 送信量の目安 {#size}

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

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

## うまく動かないとき {#troubleshooting}

| 症状 | 確認すること |
| --- | --- |
| オブジェクトが表示されない | 接続後にスナップショットを受信しているか。届く 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` の結果を確認する。[結果の一覧](#interaction)を参照 |
| RPC が届かない | オブジェクトカタログを設定している場合、RPC の ID がカタログの `methods`（ほかのプレイヤーから呼ぶなら `publicMethods`）にあるか。メソッド名を変えると ID も変わる |
| `world.Instantiate` したオブジェクトが消える | `SpawnFailed` の結果を確認する。所有数の上限（64）を超えていないか。オブジェクトカタログを設定している場合、Prefab が `"mode": "player"` で登録されているか |
| UDP が `Active` にならない | ルームのポートが UDP で開いているか。WebGL では UDP を使えない |

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



