---
title: "Client-side prediction"
url: https://recursiveai.org/onegame/cookbook/client-side-prediction/
description: "Show a player’s command on their own screen at once while the server decides, with predicted commands, a predicted world beside the replica, and rollback…"
version: "1.2.0"
---

# Client-side prediction

Show a player’s command on their own screen at once while the server decides, with predicted commands, a predicted world beside the replica, and rollback when the server disagrees.

A client’s replica is exactly the server’s state, so a command’s effect shows only after a round trip. Prediction adds a second, writable world beside the replica. The client runs predicted commands on it at once and draws it instead. When confirmed state arrives, the predicted world is rebuilt from it and the commands still in flight are run again. If the server refused one, its effect disappears: that is the rollback.

This builds on the game from [Client/server sync with a dedicated server](https://recursiveai.org/onegame/cookbook/client-server-sync/).

## Mark the command

In `Sim.cs`:

```csharp
using OneGame.Core;

namespace Harbor;

/// <summary>The state of the game: one synced number and a command that changes it.</summary>
[Entity]
public partial class Counter : Entity
{
    [Synced] public partial int Count { get; private set; }

    // Predicted: a client runs it on its predicted world at once; the server still decides.
    [Command(Predicted = true)]
    public void Increment(int by) => Count += by;
}
```

## Turn prediction on and draw the predicted world

In `Game.cs`:

```csharp
using OneGame.Core;
using OneGame.Host;
using OneGame.Sync;
using OneGame.UI;

namespace Harbor;

public sealed class HarborGame : GameDefinition
{
    private Prediction? _prediction;

    public override string Name => "harbor";

    public override void RegisterEntities(EntityRegistry registry) => OneGame.Generated.Harbor.EntityTypes.Register(registry);

    public override void Setup(World world) => world.Add(new Counter());

    // Runs every frame of a connected game, after the network was pumped: turn prediction on once the replica exists.
    public override void OnFrame(GameInstance game)
    {
        if (_prediction is null && game.Replicas.Count > 0)
        {
            var registry = new EntityRegistry();
            RegisterEntities(registry);
            var predicted = new World(game.Seed, registry, WorldRole.Predicted);
            _prediction = game.Replicas[0].EnablePrediction(predicted, new PredictionOptions());
        }
    }

    public override void DefineScreens(ScreenSet screens) =>
        screens.Add("main", game => Ui.Column(
            Ui.Label(() => Shown(game).Entities.Count > 0 ? "Count: " + CounterIn(Shown(game)).Count : "connecting").Named("count"),
            Ui.Button("Add one", () => CounterIn(Shown(game)).Send.Increment(1)).Named("add").Padding(Length.Px(8)).Background(Color.Rgb(0x3060A0)))
            .Padding(Length.Px(24)).Gap(Length.Px(12)).Background(Color.Rgb(0x203040)).TextColor(Color.White));

    // Draw the predicted world when there is one, else the game's own (single player, or the confirmed replica).
    private World Shown(GameInstance game) => _prediction?.World ?? game.World;

    private static Counter CounterIn(World world) => (Counter)world.Entities[0];
}
```

- `OnFrame` runs once per frame of a game connected to a server. The predicted world needs the same entity types as the replica, so it gets its own registry filled by `RegisterEntities`.
- `EnablePrediction` returns a `Prediction`. Reading `Prediction.World` is what rebuilds it when confirmed state has changed, so read it from the frame, as the view does here.
- `Send.Increment` works from an entity of either world. The command goes to the server exactly as before, and a predicted command also runs on the predicted world at once.
- Played alone (no server), `game.Replicas` is empty, so the game draws its own world and nothing changes.

## See it

To make the gap easy to see, give `p1` a 1500 ms link in `cluster-lag.yaml`:

```yaml
name: harbor
nodes:
  - { name: dock, role: server, exe: srv/harbor-server-1.0.0-linux-x64/HarborServer, listen: udp+ws, scopes: [{ id: 1, seed: 7 }] }
  - { name: p1, role: client, exe: bin/Debug/net10.0/Harbor, connect: dock, transport: udp, net: { latencyMs: 1500 } }
  - { name: p2, role: client, exe: bin/Debug/net10.0/Harbor, connect: dock, transport: ws }
```

Rebuild the game and the server, start the cluster, click on `p1`, then read what it shows and what it has confirmed:

```shell
onegame build
onegame export --server --project server --out ./srv
onegame cluster up --spec cluster-lag.yaml
onegame cluster call p1 ui_click '{"id":"add"}'
onegame cluster call p1 ui_query '{"selector":"#count"}'
onegame cluster call p1 state_query '{"entity":"Counter"}'
```

```text
{"focused":"Button#add","hovered":"Label","tick":527}
[{
  "kind": "Label",
  "id": "count",
  "text": "Count: 1",
  ...
}]
{"id":1,"type":"Counter","Count":0}
```

The label already says 1, drawn from the predicted world, while the confirmed replica still holds 0. Three seconds later the server has run the command and both clients and the server agree:

```text
{"id":1,"type":"Counter","Count":1}
{"id":1,"type":"Counter","Count":1}
p1           client scope 1  tick 727  hash 3047118b10da5c0c
p2           client scope 1  tick 727  hash 3047118b10da5c0c
dock         server scope 1  tick 727  hash 3047118b10da5c0c
ok   p1 vs dock at tick 727: 3047118b10da5c0c / 3047118b10da5c0c
ok   p2 vs dock at tick 727: 3047118b10da5c0c / 3047118b10da5c0c
```

The first two lines are `state_query` on `p1` and on `p2`; the rest is `onegame cluster hashes`.

## Good to know

- The server stays the authority. The command log, recordings, replays and drift hashes come from the server and never see the predicted world.
- `PredictionOptions.LeadTicks` sets how far the predicted clock runs ahead of the confirmed one, and `AutoLead` (on by default) measures it from the round trip. The `Prediction` counts `CommandsPredicted`, `Reconciliations`, `Rollbacks` and `ResimulatedTicks`.
- A predicted command that needs values this client doesn’t receive (hidden properties) is a compile error. One that draws random numbers is predicted only when the scope sends its random streams.
- Sim state is never blended. To smooth a correction, a view can use the `Reconciling` and `Reconciled` events to ease what it draws.
- This is client-side prediction against a server, with reconciliation. It isn’t peer-to-peer input rollback of the kind fighting games use.

A longer guide: [Client-side prediction in C#](https://recursiveai.org/onegame/articles/client-side-prediction-csharp/).

How this compares with LiteNetLib, LiteEntitySystem and Photon Fusion: [C# multiplayer networking](https://recursiveai.org/onegame/articles/csharp-multiplayer-networking/).
