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


# Tutorial: Move Characters with Rollback Sync

Steps to have the server calculate character positions from inputs while your own character moves without delay through prediction



In this tutorial, you build a room in which each player moves a character with inputs.

- When you connect, one character of your own is spawned.
- With the arrow keys (WASD), it moves at 3 m/s within an 18 m × 10 m area viewed from above.
- The room calculates positions from everyone's inputs 30 times per second. Clients cannot write positions.
- Your own character moves the instant you press a key, without waiting for the room's result.

Your own character is displayed first by running the same movement logic as the room on the client (prediction). When the room's result arrives, the client rewinds to that point and recalculates (rollback), and if there is a discrepancy, it corrects it unobtrusively. For how this works, see the [GS2-Realtime introduction](#rollback-sync).

You use the `stateMachine` server type. Registering the rules follows the same steps as [Tutorial: Build an Enemy Whose HP Is Managed by the Server](), so reading that first makes this easier to follow.

## Workflow

| Step | Where you work | What you build |
| --- | --- | --- |
| [1. Write the rules](#rules) | Text editor / IDE | C# with characters moved by ticks and the movement logic to predict |
| [2. Generate](#generate) | Unity Editor | `main.gsl`, the sync schema, the input slot enum, and the prediction C# |
| [3. Register the definition](#register) | GS2 | StateMachineMaster and the Realtime namespace settings |
| [4. Create the character Prefab](#prefab) | Unity Editor | A Prefab that displays the position |
| [5. Connect to the room](#connect) | Unity Editor | A scene that registers the prediction and sends inputs |
| [6. Verify the behavior](#verify) | Unity (2 clients) | — |

You work in a Unity project (Unity 2021.3 or later) with the GS2 SDK for Unity and a version of LocalStateMachineKit that includes the C# generation feature.

## 1. Write the Rules {#rules}

Outside your Unity project's `Assets`, create the following folders and files. The C# you write here is not code that runs in Unity; it is input for generating GSL.

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

Write the room rules in `RunnerRoom.cs`.

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

namespace GameRules;

// A player's character. Prefab ID 10; delivers position (integers in units of 0.01)
[RoomSchema(10, StandardFields = RoomSync.Position)]
public enum Runner
{
    PositionX = 0,
    PositionY = 1,
}

// Input slots. The room clamps them to the range when reading
[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);               // Run Tick 30 times per second
        Room.DefineSimulatedPrefab<Runner>();  // Players spawn it; the rules move it
        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 });
    }

    // The owner's client also moves its own character first with the same calculation
    [Predict(typeof(Runner))]
    static void Move(int id)
    {
        // 10 (0.1 m) per tick. The client decides the spawn position, so clamp it to the range first
        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));
    }
}
```

There are five key points.

- **Define ticks.** With `DefineTickRate(30)`, the room runs `Tick` 30 times per second. It keeps running while the rules wait for input that accepts `Tick` (`Step.WaitFor(nameof(Tick), ...)`).
- **Make the characters simulated objects.** Kinds registered with `DefineSimulatedPrefab<Runner>()` are spawned and owned by players, but only the rules can write their values. The owner sends inputs.
- **Give input slots names and ranges.** The `[RoomInput]` enum is also emitted for Unity, so the client can set inputs by the same names. Players can send any value as input, but the room clamps slots marked with `[InputRange(-1, 1)]` to the range when reading them, so speed cannot be tampered with.
- **Add `[Predict]` to the movement logic.** The compiler also emits this logic as C# for Unity, and the owner's client uses it to predict its own character. In predicted logic you can use int and bool arithmetic, `RoomMath`, `Room.Get`, `Set`, `Input`, and so on ([what you can use](#predict)).
- **Calculate with integers.** Positions are integers divided by a step of 0.01. `10` is 0.1 m and `900` is 9 m. Because the calculation uses integers, the room and the Unity prediction produce the same results.

## 2. Generate {#generate}

Save the generation settings in `GameRules/RunnerRoom/project.json`.

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

From the Unity menu, select **GS2 > Local State Machine > Generate from C#**, choose this `project.json`, and press **Generate** ([detailed steps](#generate)). The following files are generated in `Assets/Generated/RunnerRoom`.

| File | Purpose |
| --- | --- |
| `main.gsl` | The rules you register with GS2 in step 3 |
| `package.zip` | A distribution file containing only `main.gsl` |
| `unity/Runner.realtimeschema` | The sync schema you specify on the character Prefab in step 4 |
| `unity/RunnerSchema.cs` | The Prefab ID (`RunnerSchema.Prefab`) and functions that read values with types |
| `unity/RunInput.cs` | The input slot enum. Use it like `world.SetInput(RunInput.X, 1)` |
| `unity/RunnerRoomPrediction.cs` | The prediction generated from the `[Predict]` logic. Registered in step 5 |
| `source-map.json` | A development file for tracing generated locations back to the original C# |

The C# in `unity/` compiles only in Unity. When you change the rules, regenerate the rules (`main.gsl`) and the files in `unity/` together and distribute them. If you combine outputs generated from different rules, NetworkWorld logs a warning.

## 3. Register the Definition {#register}

Register using the same steps as in [Tutorial: Build an Enemy Whose HP Is Managed by the Server](#register).

| Where to register | Field | Value |
| --- | --- | --- |
| GS2-StateMachine (UpdateStateMachineMaster) | `mainStateMachineName` | `RunnerRoom` (the class name of the rules) |
| | `payload` | The contents of `main.gsl` |
| GS2-Realtime namespace | `serverType` | `stateMachine` |
| | `stateMachineSetting.namespaceId` | The GS2-StateMachine namespace where you registered the rules |

## 4. Create the Character Prefab {#prefab}

Create a Prefab for the character's appearance (a Sprite or Quad, for example), and add the following components to its root. The arena, viewed from above, lies on the XY plane.

| Component | Setting |
| --- | --- |
| **NetworkEntity** | Specify `unity/Runner.realtimeschema` for **Schema** |
| **NetworkTransform** | **Position Axes** is `X` and `Y`, **Rotation Axes** is `None` |

Do not add a movement script. NetworkTransform sets the position from the room's calculation results (other players' characters) or from the prediction results (your own character). For simulated objects, even the owner cannot write the position.

## 5. Connect to the Room {#connect}

### Place a NetworkWorld

1. Create a catalog with **Create > GS2 > Realtime > Prefab Catalog**, and register **Id** `10` and the Prefab from step 4.
2. Create an empty GameObject in the scene, add **NetworkWorld**, and specify the catalog for **Prefab Catalog**.
3. Place a Camera (Orthographic, Size about 6) that faces the XY plane head-on.

Set NetworkWorld's **View Delay** to the same value as NetworkTransform's **Interpolation Delay** (both default to 0.1 seconds). The room uses this delay to perform hit detection as the shooter saw it ([lag compensation](#lag-compensation)).

### Connect and Send Inputs

Add **NetworkSession** and the following script to the same GameObject as the NetworkWorld. Obtain the room's connection information and the access token as described in [Obtaining connection information for the game server](#obtaining-connection-information-for-the-game-server), and pass them in.

```csharp
using GameRules; // The namespace of the generated RunInput.cs, RunnerSchema.cs, and 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>();
        // Register the prediction generated from the rules' [Predict] (before or after connecting)
        RunnerRoomPrediction.Register(world);
        // Once the room state is applied, spawn your character (create it again on each reconnect)
        session.Ready += () => world.Instantiate(
            RunnerSchema.Prefab,
            new Vector3(Random.Range(-5f, 5f), Random.Range(-3f, 3f), 0),
            Quaternion.identity);
        session.Disconnected += reason => Debug.Log("Disconnected: " + reason);
    }

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

    // Do not write positions; set the current state of the inputs. NetworkWorld sends them every tick
    void Update()
    {
        world.SetInput(RunInput.X, Mathf.RoundToInt(Input.GetAxisRaw("Horizontal")));
        world.SetInput(RunInput.Y, Mathf.RoundToInt(Input.GetAxisRaw("Vertical")));
    }
}
```

There are three key points.

- **Register the prediction.** When you call `RunnerRoomPrediction.Register(world)`, characters you own show the result of each input tick immediately. It works without registering, but movement is delayed until the room's result arrives (by one round trip).
- **Inputs set the "current state".** `SetInput` only sets the state while a key is held; NetworkWorld does the sending, in step with the room's ticks. Each send carries the inputs of the last 3 ticks, so a lost UDP datagram is covered.
- **The client decides the spawn position.** The rules' `Move` clamps it to the range before using it, so sending a position outside the range causes no problem.

## 6. Verify the Behavior {#verify}

Connect to the same room from two clients with different userIds.

1. Confirm that both screens show two characters.
2. Press a key on one client, and confirm that your own character starts moving immediately on that screen. On the other screen, it moves smoothly after a short delay.
3. Move it to the edge of the area, and confirm that it stops at the same position (±9 m horizontally, ±5 m vertically) on both screens.
4. Confirm that your own character moves without delay even in an environment with increased network latency.

You can read the values received from the room with `NetworkObject.Values`, and the predicted values with `PredictedValues`. Objects subject to prediction have `IsPredicted` set to `true`.

## Going Further

The SDK's `Assets/Scenes/Samples/Realtime/Arena` contains a sample that builds on this tutorial.

| What you want to do | API to use | Details |
| --- | --- | --- |
| Hit what the shooter saw | `DefineHitbox`, `Room.Raycast`, `Room.Overlap` | [Lag compensation](#lag-compensation) |
| Display bullets the instant they are fired | `Room.SpawnOwned` in predicted logic | [Prediction](#predict) |
| Reduce traffic in rooms with many players | `DefineRelevance` | [Send distant objects less often](#relevance) |
| Decide spawn positions on the server | `Spawned` | [Respond to spawns](#spawned) |

## What to Read Next

- [Room Definition Reference](#tick): ticks, inputs, the logic you can use in predictions, and limits.
- [Unity Client](#tick): sending inputs, displaying predictions, and UDP.



