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


# チュートリアル: ロールバック式同期でキャラクターを動かす

キャラクターの位置をサーバーが入力から計算し、自分のキャラクターは予測して遅延なく動かすまでの手順



このチュートリアルでは、各プレイヤーが入力でキャラクターを動かすルームを作ります。

- 接続すると、自分のキャラクターが 1 体生成されます。
- 矢印キー（WASD）で、上から見た 18 m × 10 m の範囲を 3 m/秒で移動します。
- 位置はルームが毎秒 30 回、全員の入力から計算します。クライアントは位置を書き込めません。
- 自分のキャラクターは、ルームの結果を待たずに、キーを押した瞬間に動きます。

自分のキャラクターは、ルームと同じ移動処理をクライアントで実行して先に表示します（予測）。ルームの結果が届くと、その時点まで巻き戻して計算し直し（ロールバック）、ずれていれば目立たないように補正します。仕組みは [GS2-Realtime の紹介](#rollback-sync)を参照してください。

サーバータイプは `stateMachine` を使います。ルールの登録の手順は[チュートリアル: サーバーで HP を管理する敵を作る]()と同じなので、先にそちらを読んでおくと進めやすくなります。

## 作業の流れ

| 手順 | 作業する場所 | 作るもの |
| --- | --- | --- |
| [1. ルールを書く](#rules) | テキストエディター / IDE | ティックで動くキャラクターと、予測する移動処理を書いた C# |
| [2. 生成する](#generate) | Unity Editor | `main.gsl`、同期スキーマ、入力スロットの enum、予測の C# |
| [3. 定義を登録する](#register) | GS2 | StateMachineMaster と Realtime のネームスペース設定 |
| [4. キャラクターの Prefab を作る](#prefab) | Unity Editor | 位置を表示する Prefab |
| [5. ルームに接続する](#connect) | Unity Editor | 予測を登録し、入力を送るシーン |
| [6. 動作を確かめる](#verify) | Unity（2 クライアント） | — |

GS2 SDK for Unity と、LocalStateMachineKit の C# 生成機能を含む版を導入した Unity プロジェクト（Unity 2021.3 以降）で作業します。

## 1. ルールを書く {#rules}

Unity プロジェクトの `Assets` の外に、次のフォルダーとファイルを作ります。ここに書く C# は Unity で実行するコードではなく、GSL を生成するための入力です。

```text
GameRules/
└── RunnerRoom/
    ├── project.json
    └── Rules/
        └── RunnerRoom.cs
```

`RunnerRoom.cs` に、ルームのルールを書きます。

```csharp
using Gs2.LocalStateMachine.Authoring;
using Step = Gs2.LocalStateMachine.Authoring.Step<
    GameRules.RunnerState, GameRules.RunnerResult, GameRules.RunnerError>;

namespace GameRules;

// プレイヤーのキャラクター。Prefab ID 10、位置（0.01 単位の整数）を配信する
[RoomSchema(10, StandardFields = RoomSync.Position)]
public enum Runner
{
    PositionX = 0,
    PositionY = 1,
}

// 入力スロット。ルームは範囲に丸めて読む
[RoomInput]
public enum RunInput
{
    [InputRange(-1, 1)] X = 0,
    [InputRange(-1, 1)] Y = 1,
}

public record RunnerState(int Ticks);
public record RunnerResult(bool Finished);
public enum RunnerError { InvalidOperation = RoomResults.InvalidOperation }

public static class RunnerRoom
{
    public static Step Start()
    {
        Room.DefineTickRate(30);               // 毎秒 30 回 Tick を実行する
        Room.DefineSimulatedPrefab<Runner>();  // プレイヤーが生成し、ルールが動かす
        return Step.WaitFor(nameof(Tick), new RunnerState(0));
    }

    public static Step Tick(RunnerState state, int tick)
    {
        int count = Room.ObjectCount<Runner>();
        for (int i = 0; i < count; i++)
            Move(Room.ObjectAt<Runner>(i));
        return Step.Stay(state with { Ticks = state.Ticks + 1 });
    }

    // 所有者のクライアントも同じ計算で自分のキャラクターを先に動かす
    [Predict(typeof(Runner))]
    static void Move(int id)
    {
        // 1 ティックに 10（0.1 m）。生成時の位置はクライアントが決めるので、先に範囲へ収める
        int x = RoomMath.Clamp(Room.Get(id, Runner.PositionX), -900, 900);
        int y = RoomMath.Clamp(Room.Get(id, Runner.PositionY), -500, 500);
        Room.Set(id, Runner.PositionX, RoomMath.Clamp(x + Room.Input(id, RunInput.X) * 10, -900, 900));
        Room.Set(id, Runner.PositionY, RoomMath.Clamp(y + Room.Input(id, RunInput.Y) * 10, -500, 500));
    }
}
```

ポイントは次の 5 つです。

- **ティックを定義します。** `DefineTickRate(30)` で、ルームは毎秒 30 回 `Tick` を実行します。`Tick` を受け付ける入力待ち（`Step.WaitFor(nameof(Tick), ...)`）の間、実行が続きます。
- **キャラクターは Simulated オブジェクトにします。** `DefineSimulatedPrefab<Runner>()` の種類は、プレイヤーが生成して所有しますが、値を書けるのはルールだけです。所有者は入力を送ります。
- **入力スロットに名前と範囲を付けます。** `[RoomInput]` の enum は Unity 用にも出力されるので、クライアントは同じ名前で入力を設定できます。入力はプレイヤーが自由に送れる値ですが、`[InputRange(-1, 1)]` を付けたスロットはルームが範囲に丸めて読むので、速度を改ざんされません。
- **移動処理に `[Predict]` を付けます。** コンパイラがこの処理を Unity 用の C# としても出力し、所有者のクライアントが自分のキャラクターの予測に使います。予測する処理では、int と bool の計算、`RoomMath`、`Room.Get`・`Set`・`Input` などが使えます（[使える範囲](#predict)）。
- **計算は整数で行います。** 位置は刻み幅 0.01 で割った整数です。`10` は 0.1 m、`900` は 9 m にあたります。整数の計算なので、ルームと Unity の予測で同じ結果になります。

## 2. 生成する {#generate}

`GameRules/RunnerRoom/project.json` に、生成設定を保存します。

```json
{
  "format": 1,
  "source": "Rules",
  "output": "../../Assets/Generated/RunnerRoom",
  "hostProfile": "RealtimeRoomV1"
}
```

Unity のメニューの **GS2 > Local State Machine > Generate from C#** で、この `project.json` を選んで **Generate** を押します（[手順の詳細](#generate)）。`Assets/Generated/RunnerRoom` に、次のファイルが生成されます。

| ファイル | 使い道 |
| --- | --- |
| `main.gsl` | 手順 3 で GS2 に登録するルール |
| `package.zip` | `main.gsl` だけを入れた配布用ファイル |
| `unity/Runner.realtimeschema` | 手順 4 でキャラクターの Prefab に指定する同期スキーマ |
| `unity/RunnerSchema.cs` | Prefab ID（`RunnerSchema.Prefab`）と、型付きで値を読む関数 |
| `unity/RunInput.cs` | 入力スロットの enum。`world.SetInput(RunInput.X, 1)` のように使う |
| `unity/RunnerRoomPrediction.cs` | `[Predict]` の処理から生成した予測。手順 5 で登録する |
| `source-map.json` | 生成された位置から元の C# を調べるための開発用ファイル |

`unity/` の C# は Unity でだけコンパイルされます。ルールを変えたら、ルール（`main.gsl`）と `unity/` のファイルを一緒に生成し直して配布してください。異なるルールから生成したものを組み合わせると、NetworkWorld が警告を出します。

## 3. 定義を登録する {#register}

[チュートリアル: サーバーで HP を管理する敵を作る](#register)と同じ手順で登録します。

| 登録先 | 設定項目 | 指定する値 |
| --- | --- | --- |
| GS2-StateMachine（UpdateStateMachineMaster） | `mainStateMachineName` | `RunnerRoom`（ルールのクラス名） |
| | `payload` | `main.gsl` の内容 |
| GS2-Realtime のネームスペース | `serverType` | `stateMachine` |
| | `stateMachineSetting.namespaceId` | ルールを登録した GS2-StateMachine のネームスペース |

## 4. キャラクターの Prefab を作る {#prefab}

キャラクターの見た目になる Prefab（Sprite や Quad など）を作り、ルートに次のコンポーネントを追加します。上から見たアリーナを XY 平面に置きます。

| コンポーネント | 設定 |
| --- | --- |
| **NetworkEntity** | **Schema** に `unity/Runner.realtimeschema` を指定 |
| **NetworkTransform** | **Position Axes** は `X` と `Y`、**Rotation Axes** は `None` |

移動のスクリプトは付けません。位置は、ルームの計算結果（ほかのプレイヤーのキャラクター）か、予測の結果（自分のキャラクター）から NetworkTransform が設定します。Simulated オブジェクトでは、所有者でも位置を書き込めません。

## 5. ルームに接続する {#connect}

### NetworkWorld を置く

1. **Create > GS2 > Realtime > Prefab Catalog** でカタログを作り、**Id** `10` と手順 4 の Prefab を登録します。
2. シーンに空の GameObject を作り、**NetworkWorld** を追加して、**Prefab Catalog** にカタログを指定します。
3. XY 平面を正面から映す Camera（Orthographic、Size 6 程度）を置きます。

NetworkWorld の **View Delay** は、NetworkTransform の **Interpolation Delay** と同じ値（どちらも既定は 0.1 秒）にしておきます。ルームは、この遅れを使って撃った側の画面どおりに当たり判定をします（[ラグ補償](#lag-compensation)）。

### 接続して入力を送る

NetworkWorld と同じ GameObject に **NetworkSession** と、次のスクリプトを追加します。ルームの接続情報とアクセストークンは、[ゲームサーバーへの接続情報を取得](#ゲームサーバーへの接続情報を取得)の方法で取得して渡します。

```csharp
using GameRules; // 生成された RunInput.cs・RunnerSchema.cs・RunnerRoomPrediction.cs の名前空間
using Gs2.Realtime.Sync;
using UnityEngine;

[RequireComponent(typeof(NetworkWorld), typeof(NetworkSession))]
public sealed class RunnerRoomClient : MonoBehaviour
{
    NetworkWorld world;
    NetworkSession session;

    void Awake()
    {
        world = GetComponent<NetworkWorld>();
        session = GetComponent<NetworkSession>();
        // ルールの [Predict] から生成された予測を登録する（接続の前後どちらでもよい）
        RunnerRoomPrediction.Register(world);
        // ルームの状態を反映したら、自分のキャラクターを生成する（再接続のたびに作り直す）
        session.Ready += () => world.Instantiate(
            RunnerSchema.Prefab,
            new Vector3(Random.Range(-5f, 5f), Random.Range(-3f, 3f), 0),
            Quaternion.identity);
        session.Disconnected += reason => Debug.Log("切断しました: " + reason);
    }

    public void Connect(string accessToken, string ipAddress, int port, string encryptionKey)
    {
        session.Connect(accessToken, ipAddress, port, encryptionKey);
    }

    // 位置は書き込まず、入力の現在の状態を設定する。NetworkWorld がティックごとに送る
    void Update()
    {
        world.SetInput(RunInput.X, Mathf.RoundToInt(Input.GetAxisRaw("Horizontal")));
        world.SetInput(RunInput.Y, Mathf.RoundToInt(Input.GetAxisRaw("Vertical")));
    }
}
```

ポイントは次の 3 つです。

- **予測を登録します。** `RunnerRoomPrediction.Register(world)` を呼ぶと、自分が所有するキャラクターは、入力したティックの結果がすぐに表示されます。登録しなくても動きますが、ルームの結果が届くまで（往復の通信時間ぶん）遅れて動きます。
- **入力は「現在の状態」を設定します。** `SetInput` はキーを押している間の状態を設定するだけで、送信は NetworkWorld がルームのティックに合わせて行います。1 回の送信に直近 3 ティック分の入力を入れるので、UDP で 1 つ落ちても補えます。
- **生成時の位置はクライアントが決めます。** ルールの `Move` が範囲に収めてから使うので、範囲外の位置を送られても問題ありません。

## 6. 動作を確かめる {#verify}

同じルームへ、2 つのクライアントから別々の userId で接続します。

1. 両方の画面に、2 体のキャラクターが表示されることを確かめます。
2. 片方でキーを押し、その画面では自分のキャラクターがすぐに動き出すことを確かめます。もう片方の画面では、少し遅れて滑らかに動きます。
3. 範囲の端まで動かし、両方の画面で同じ位置（横 ±9 m、縦 ±5 m）で止まることを確かめます。
4. ネットワークの遅延を大きくした環境でも、自分のキャラクターは遅れずに動くことを確かめます。

ルームから届いた値は `NetworkObject.Values`、予測した値は `PredictedValues` で読めます。予測の対象のオブジェクトは `IsPredicted` が `true` になります。

## 発展

SDK の `Assets/Scenes/Samples/Realtime/Arena` に、このチュートリアルを発展させたサンプルがあります。

| やりたいこと | 使う API | 説明 |
| --- | --- | --- |
| 撃った側の画面どおりに当てる | `DefineHitbox`・`Room.Raycast`・`Room.Overlap` | [ラグ補償](#lag-compensation) |
| 弾を撃った瞬間に表示する | 予測する処理の中の `Room.SpawnOwned` | [予測](#predict) |
| 人数が多いルームで通信量を抑える | `DefineRelevance` | [遠くのオブジェクトの更新を間引く](#relevance) |
| 出現位置をサーバーで決める | `Spawned` | [プレイヤーの生成に応じる](#spawned) |

## 次に読むもの

- [ルーム定義リファレンス](#tick)：ティック、入力、予測で使える処理、上限値。
- [Unity クライアント](#tick)：入力の送信、予測の表示、UDP。



