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


# Room Definition Reference

How to write rules that run in stateMachine rooms, the Room API, execution rules, and limits



In `stateMachine` rooms, the rules (GSL) registered with GS2-StateMachine run inside the room to create and update shared objects, validate operations, and handle the passage of time. This page summarizes the specifications for writing those rules.

The basic approach is to write rules in C# and generate GSL. The C# definition format (state records, `Step`, and so on) is shared with StateMachine, so also see the [C# Definition Reference](). To try the whole procedure, see the [Tutorial]().

## Rules Overview {#overview}

| Target | Where it lives | Examples |
| --- | --- | --- |
| Rules | GS2-StateMachine's StateMachineMaster (versioned) | Spawning enemies, validating attacks, reviving |
| Room progress state | The rules' state record. Not delivered to clients | Floor number, enemy object IDs |
| Shared values | Shared objects. Delivered to all participants | Position, HP, boss phase |
| Display | Unity GameObjects | HP bar, position interpolation |

The room state and shared objects are kept only while the room is running. They are lost when the room ends and are not persisted.

## Writing in C# {#definition}

In the rules class, write the following three kinds of methods.

| Method | When it is called |
| --- | --- |
| `public static Step Start()` | Once, when the room starts |
| `public static Step Interact(TState state, int objectId, int method)` | When a player sends `SharedObject.Interact(method)` |
| `public static Step Timer(TState state, int objectId, int eventId)` | When the time scheduled with `Room.After` arrives |
| `public static Step Tick(TState state, int tick)` | At the rate set with `DefineTickRate` ([ticks](#tick)) |

In `Start`, define the kinds of shared objects, spawn the initial objects, and wait for input with `Step.WaitFor(nameof(Interact), state).Or(nameof(Timer))`. After processing, `Interact` and `Timer` keep waiting for the same input with `Step.Stay(state)`.

`Interact` is the only operation players can send. Distinguish kinds of operations by the value of `method`. Adding public methods in C# does not make them callable from clients.

In the generation settings, specify `RealtimeRoomV1` for `hostProfile`. With this profile, `GameRewards` (rewards) and `GameRandom` (random numbers) are not available.

## Room API {#room-api}

From C#, call `Room.*`; from Lua in GSL, call `room.*` with the same names. All values are int32.

### Define Object Kinds

These can be called only during room initialization (from `Start` until the first wait for input).

| API | Description |
| --- | --- |
| `DefineServerPrefab<TSchema>()` | Defines the kind for sync schema `TSchema` as a server-managed object |
| `DefineLeasedPrefab<TSchema>(leaseMilliseconds)` | Defines the kind for sync schema `TSchema` as an object whose control is lent to players |
| `DefineServerPrefab(prefab, fields)` | Defines it by directly specifying the Prefab ID and a bitmask of synced fields |
| `DefineLeasedPrefab(prefab, fields, leaseMilliseconds)` | Same as above. Specifies the control lease period in milliseconds |
| `DefineSimulatedPrefab<TSchema>()` / `DefineSimulatedPrefab(prefab, fields)` | Defines the kind as objects that players spawn and the rules move every tick ([ticks](#tick)) |
| `DefineRuleSpawnedPrefab<TSchema>()` / `DefineRuleSpawnedPrefab(prefab, fields)` | Like `DefineSimulatedPrefab`, but players cannot spawn it; only the rules' `SpawnOwned` does (projectiles, for example) |
| `DefineTickRate(ticksPerSecond)` | Sets how many times per second `Tick` runs (1 to 60) |
| `DefineHitbox<TSchema>(radius)` / `DefineHitbox(prefab, radius)` | Gives objects of the kind a hitbox (a sphere of `radius`; a circle in 2D). Call after `DefineTickRate` ([lag compensation](#lag-compensation)) |
| `DefineRelevance<TSchema>(distance)` / `DefineRelevance(prefab, distance)` | Sends updates of objects of the kind less often the farther they are from the receiving player. Call after `DefineTickRate` ([update rate](#relevance)) |

| Management | Who can modify values | Example uses |
| --- | --- | --- |
| Server-managed | Only `Room.Set` in the rules | Enemy HP, doors, boss phase, score |
| Control lending | Only the one connection that has control. The rules cannot modify them | A carried box, a paddle, an operator seat |
| Simulation | Only `Room.Set` in the rules. The owning player sends inputs | Player characters, vehicles |

### Spawn Objects and Read and Write Values

| API | Description |
| --- | --- |
| `Spawn<TSchema>(x, y, z)` / `Spawn(prefab, x, y, z)` | Spawns an object and returns its object ID. `x` `y` `z` are the initial values of the position fields; other fields start at 0 |
| `SpawnOwned<TSchema>(ownerObjectId, x, y, z)` / `SpawnOwned(prefab, ownerObjectId, x, y, z)` | Spawns a simulated object owned by the player who owns the `ownerObjectId` object; it moves by that player's input (for projectiles and respawns). Returns 0 without spawning when that player already owns 64 objects |
| `Despawn(objectId)` | Removes a server-managed, leased or simulated object and cancels its timer. Player objects cannot be removed |
| `Get(objectId, field)` | Reads a field value |
| `Set(objectId, field, value)` | Writes a field value. Server-managed and simulated objects only |

For `field`, specify a sync schema enum value (such as `EnemyFields.Health`) or a standard field such as `RoomFields.PositionX`. Values read and written in the rules are reflected in `Get` immediately within the processing of the same input.

Position values are integers divided by the step size. With the default step size (0.01), `50` corresponds to `0.5` in Unity.

### Handle the Passage of Time

| API | Description |
| --- | --- |
| `After(objectId, milliseconds, eventId)` | Schedules `Timer(state, objectId, eventId)` to be called once after `milliseconds` |
| `CancelTimer(objectId)` | Cancels the object's scheduled timer |

Each object has one scheduled timer; calling `After` on the same object replaces it. For processing repeated at a fixed interval, schedule the next `After` inside `Timer`. Use it for enemy action intervals, the time until a door closes, the wait before reviving, and so on.

Timers are processed in the room about every 50 milliseconds. They are not synchronized with Unity's rendering frames and are not guaranteed to run at an exact time. A fired timer is consumed even if the rules reject or fail.

### Simulate at a Fixed Rate (Ticks) {#tick}

When the initialization calls `DefineTickRate`, the room runs `Tick(state, int tick)` a fixed number of times per second. Use it when the server computes things such as player character movement from inputs. Clients cannot write positions, which prevents tampering with movement speed or walking through walls.

| API | Description |
| --- | --- |
| `Input(objectId, field)` | Reads the input the owner of a simulated object sent for the current tick (slots 0 to 31). Passing a non-simulated object is an error |
| `ObjectCount<TSchema>()` / `ObjectCount(prefab)` | The number of objects of the kind (including those spawned in this processing) |
| `ObjectAt<TSchema>(index)` / `ObjectAt(prefab, index)` | The ID of the kind's `index`-th object (in ascending ID order) |

```csharp
// Position is the standard XYZ (fields 0..2, 0.01 units); custom fields start at 6.
[RoomSchema(5, StandardFields = RoomSync.Position)]
public enum Character { Hits = 6 }

public static Step Start() {
    Room.DefineTickRate(30);
    Room.DefineSimulatedPrefab<Character>();
    return Step.WaitFor(nameof(Tick), new State(0));
}
public static Step Tick(State state, int tick) {
    int count = Room.ObjectCount<Character>();
    for (int i = 0; i < count; i++) {
        int id = Room.ObjectAt<Character>(i);
        int dx = System.Math.Max(-1, System.Math.Min(1, Room.Input(id, 0)));
        int dy = System.Math.Max(-1, System.Math.Min(1, Room.Input(id, 1)));
        Room.Set(id, RoomFields.PositionX, Room.Get(id, RoomFields.PositionX) + dx * 10);
        Room.Set(id, RoomFields.PositionY, Room.Get(id, RoomFields.PositionY) + dy * 10);
    }
    return Step.Stay(state);
}
```

- The parameter of `Tick` must be named `tick` (the tick number, starting at 1). A definition that uses `DefineTickRate` without `Tick` fails to compile.
- `Tick` runs only while the current input wait accepts it. You can wait for it together with `Interact` and `Timer` (`.Or(nameof(Interact))`). Include `Tick` in every wait the simulation should continue in; elsewhere the tick number still advances.
- A tick whose `Tick` fails changes nothing, and the next tick runs normally.
- Objects of a `DefineSimulatedPrefab` kind are spawned by clients like player objects and owned by that player. A client may spawn one object of each such kind, and only its position and rotation are taken from the request (other fields start at 0). Only the rules can write or remove them; the owner's writes and removals are rejected. They are deleted when the owner leaves.
- A definition that uses `Input`, `DefineHitbox`, `DefineRelevance`, `Raycast` or `Overlap` without `DefineTickRate` fails to compile.
- Players send inputs tagged with the tick they are meant for. When a tick's input has not arrived, the previous input is used.
- Values changed by ticks reach clients with the periodic updates (over UDP when available).
- When processing falls behind, the room runs at most 3 ticks at once and skips the rest.
- The rules compute with integers only. Positions are integers divided by the step size (0.01 by default).

#### Hit What the Shooter Saw (Lag Compensation) {#lag-compensation}

Other characters on a player's screen are slightly in the past, by the network delay and interpolation. Testing hits against their current positions misses shots that hit on screen. The client sends with every input how far behind its screen was, so the room can test hits at the positions the shooting player saw.

```csharp
public static Step Start() {
    Room.DefineTickRate(30);
    Room.DefineSimulatedPrefab<Character>();
    Room.DefineHitbox<Character>(50);   // after DefineTickRate
    return Step.WaitFor(nameof(Tick), new State(0));
}
static void Fire(int id) {
    if (Room.Input(id, 2) == 0) return;
    int target = Room.Raycast<Character>(id,
        Room.Get(id, RoomFields.PositionX), Room.Get(id, RoomFields.PositionY), 0,
        Room.Input(id, 3), Room.Input(id, 4), 0, 2000);
    if (target != 0) Room.Set(target, Character.Hits, Room.Get(target, Character.Hits) + 1);
}
```

| API | Description |
| --- | --- |
| `Raycast<TSchema>(shooterId, x, y, z, dx, dy, dz, distance)` | The ID of the first object hit by the line from `(x, y, z)` in the direction `(dx, dy, dz)` up to `distance`, or 0 |
| `Overlap<TSchema>(shooterId, x, y, z, radius)` | The ID of the nearest object overlapping the sphere of `radius`, or 0 |

- A hitbox is a sphere centered on the position fields (0 to 2, the standard Position). In a 2D game, keep Z at 0 to treat it as a circle.
- When `shooterId` is a simulated object, other objects are moved back to where its owner saw them when sending the current tick's input, interpolating between ticks, by at most 500 milliseconds. `shooterId` itself is never hit.
- `shooterId` must be an existing object (0 is not allowed).
- The origin and direction are used as given. Pass the shooter's current position (its owner's client predicts it, so it matches the screen).
- These cannot be called from `[Predict]` code. On the client, show only effects immediately and apply damage after the room's result arrives.

#### Send Distant Objects Less Often {#relevance}

In a room with many players, sending everyone's position to everyone every tick takes a lot of bandwidth. With `DefineRelevance`, updates of objects of that kind reach each player less often the farther they are from that player's character.

```csharp
Room.DefineRelevance<Character>(300);   // after DefineTickRate
```

| Distance from the receiving player's character | Update rate |
| --- | --- |
| Within `distance` | Every tick |
| Within twice it | Every 2 ticks |
| Within four times it | Every 4 ticks |
| Farther | Every 8 ticks |

- The distance is measured on the position fields (0 to 2) from the nearest simulated object the player owns. Your own character arrives every tick.
- Values changed in between arrive with their latest values the next time the object is sent. Display is interpolated, so distant characters still move smoothly.
- Players owning no simulated object (spectators, for example) receive every tick.
- The resend every second and values sent right before RPCs and similar commands are not thinned out.
- For example, with 64 players spread over 20 m square and all moving (`distance` 300), update traffic drops to about a third.

#### Move Objects on the Client First (Prediction) {#predict}

Put the code that moves one object in a private helper and mark it with `[Predict(typeof(Schema))]`. The owner's client then runs it too, so your own character moves the moment you press the input.

```csharp
public static Step Tick(State state, int tick) {
    int count = Room.ObjectCount<Character>();
    for (int i = 0; i < count; i++)
        Move(Room.ObjectAt<Character>(i), tick);
    return Step.Stay(state);
}

[Predict(typeof(Character))]
static void Move(int id, int tick) {
    int dx = System.Math.Max(-1, System.Math.Min(1, Room.Input(id, 0)));
    Room.Set(id, RoomFields.PositionX, Room.Get(id, RoomFields.PositionX) + dx * 10);
}
```

- The helper must be `private static void Name(int objectId)` or `(int objectId, int tick)`, one `[Predict]` per schema. In the room, `Tick` calls it as usual.
- The compiler also emits this helper and the helpers it calls as C# for Unity (`unity/<RuleClass>Prediction.cs`). Values are computed with the same integer arithmetic as the room.
- Predicted code can use int and bool values, `System.Math.Min` and `Max`, helpers of the rule class, and `Room.Get`, `Set`, `Input`, `ObjectCount`, `ObjectAt`, `SpawnOwned` and `Despawn`. All computation uses int or bool (schema fields by name; casts to the enum are not allowed). Anything else (float, static fields, reading the `state` and so on) is a compile error.
- Calling `SpawnOwned` in predicted code (launching a projectile, for example) shows the projectile on the shooter's screen at once (predicted spawn). When the projectile spawned by the room arrives, it takes over the same object on screen; if the room did not spawn it, it disappears. To match projectiles correctly, call `SpawnOwned` only in predicted code and keep hit tests in code that only the room runs.
- On the client, other players' inputs read as 0 and other objects have the latest values received from the room. Predictions can miss, for example when characters collide, and are corrected by the room's results.

### Find the User Who Operated

| API | Description |
| --- | --- |
| `SenderUserId()` | The userId of the player who operated. Taken from the access token; clients cannot rewrite it |
| `SenderConnectionId()` | The ID of the connection that operated. Changes on reconnection |

`Start` and `Timer` have no operating player, so `SenderUserId()` returns an empty string and `SenderConnectionId()` returns 0. If timer processing needs to know "who started the operation", save it in the state record while processing `Interact`.

When managing per-player ownership or turn order, use `SenderUserId()`, which does not change on reconnection.

## Sync Schema {#schema}

Declare the synced values of shared objects with an enum that has the `RoomSchema` attribute. Both the rules definition and the Unity sync schema (`.realtimeschema`) are generated from this declaration.

```csharp
[RoomSchema(7, StandardFields = RoomSync.Position)]
public enum EnemyFields
{
    Health = 6,
}

[RoomSchema(8)]
public enum SwitchFields
{
    [RoomField(typeof(bool))] IsOpen = 6,
    [RoomField(typeof(float), Precision = 0.01f)] Progress = 7,
}
```

| Item | Description |
| --- | --- |
| `RoomSchema(prefab)` | Prefab ID. A positive int32 that must not duplicate other sync schemas |
| `StandardFields` | Standard fields for position and rotation. `RoomSync.Position` (fields 0 to 2), `RoomSync.Rotation` (3 to 5), `RoomSync.Transform` (both) |
| `PositionPrecision` / `RotationPrecision` | Step size of the standard fields. Defaults are 0.01 and 360 / 4096 degrees |
| Enum values | Field numbers on the wire (0 to 31). Use 6 or higher so they don't overlap with the standard field numbers |
| `RoomField(typeof(...))` | Type in Unity. `int` (default), `bool`, `float` |
| `Precision` | Step size of a `float`. The rules handle it as an integer, and Unity receives it as "integer × step size" |

In the example above, if you `Set` `125` to `Progress`, Unity receives `1.25f`. Set 0 or 1 for a `bool`.

Field numbers are identifiers on the wire. After release, do not change them or let them shift by reordering.

## Processing and Rollback per Input {#transactions}

Changes made by a single input (`Interact` or `Timer`) are committed together or rolled back together.

| Rules result | Behavior |
| --- | --- |
| `Step.Next` | Proceeds to the next processing as part of the same input |
| Reaching a wait for input (`WaitFor` / `Stay`), `Step.End` | Commits changes to the state, shared objects, and timers, and delivers them to everyone |
| `Step.Ignore` | Rolls back all changes made by this input. The reason reaches the client |
| Exception, exceeding the execution time or transition count limit | Rolls back all changes made by this input. The client receives `InvalidOperation` |

The reason value of `Step.Ignore` reaches the operating client's `CommandCompleted` as is. Values 1 to 7 are the same numbers as system result codes (`MissingObject`, etc.), so use values of 1000 or more for reasons. For the list of results the client receives, see [Unity Client](#interaction).

`Step.End` only ends execution of the rules; it does not destroy shared objects or end the room.

## Generation and Registration {#deployment}

| Step | Description |
| --- | --- |
| Generate | Generate with `RealtimeRoomV1` specified for `hostProfile` in `project.json`. For the settings, see [C# definition generation settings](#project) |
| Register | Specify the contents of the generated `main.gsl` as `payload` of GS2-StateMachine's [UpdateStateMachineMaster](#updatestatemachinemaster). `mainStateMachineName` is the class name of the rules |
| Configure the room | In the Realtime namespace, specify the server type `stateMachine` and the namespace where the rules are stored (`stateMachineSetting.namespaceId`). If you omit the version (`stateMachineSetting.version`), the latest is used; specify it only when you want to pin one |

The generated files are as follows.

| File | Purpose |
| --- | --- |
| `main.gsl` | The rules to register |
| `package.zip` | A distribution file containing only `main.gsl`. When registering, read out the `main.gsl` inside and specify it as `payload` |
| `unity/<schema name>.realtimeschema` | The sync schema to specify on Unity's NetworkEntity |
| `source-map.json` | A development file for tracing generated locations back to the original C#. Not used for registration |

A room loads the rules once when it is allocated. If the version is omitted, it loads the latest rules at that time; if specified, it loads that version. If no rules are registered in the storage namespace, or if loading or initialization fails, the room is not used. A running room uses the same version until it ends, and new rules are used by rooms allocated afterwards.

Distribute the rules (GSL) and the sync schema as a pair generated together from the same C# definition.

## Limits {#limits}

| Target | Limit |
| --- | --- |
| Participants per room | 64 connections |
| Shared objects per room | 4096 |
| Object kinds | Up to 64 defined during initialization. Up to 4096 for the whole room |
| Fields per object | 32 (numbers 0 to 31) |
| Control lease period | 50 milliseconds to 1 day (86,400,000 milliseconds) |
| `After` wait time | 50 milliseconds to 1 day (86,400,000 milliseconds) |
| `After` `eventId` | 1 or more |
| Scheduled timers | One per object, 4096 for the whole room |
| Changes per input | Up to 64 objects each for spawns and value changes. Up to 64 objects for scheduling and canceling timers |
| Processing per input | 100 milliseconds of execution time, up to 32 transitions via `Next` |
| Timers executed per processing cycle | Up to 64. Any excess is carried over to the next cycle |
| Tick rate | 1 to 60 per second |
| Changes per tick | Value changes, spawns and despawns: up to 1024 objects each. One player owns up to 64 objects |
| Hit test rewind | Up to 500 milliseconds (positions are kept for 1 second) |
| Inputs sent ahead | Up to 1 second of ticks |
| Size of the rules (GSL) | 1 MiB |

## Unsupported Features {#unsupported}

Room rules run only processing that is self-contained within the room. The following features are not available.

- `GameRewards` (granting rewards), `GameRandom` (random numbers)
- GSL `Script` (calling GS2-Script)
- Access to external APIs, files, or the network
- Unity APIs or arbitrary .NET libraries

For the C# syntax you can use in rules, see the [C# Definition Reference](#language).

## Writing GSL Directly {#gsl}

You can also write GSL directly without C#. For the basic GSL syntax, see [GSL Format](). GSL that runs in a room follows these rules.

- The state variables are `map run` and `map execution`.
- Events received by a `WaitTask` that waits for input use the following signatures.

  | Event | Parameters |
  | --- | --- |
  | `Interact` | `int revision, int objectId, int method` |
  | `Timer` | `int revision, int objectId, int eventId` |
  | `Tick` | `int revision, int tick` |

- Call Room APIs such as `room.Spawn` from the Lua written in a Task's Payload.
- Calling GS2-Script via `Script` is not available.



