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

TargetWhere it livesExamples
RulesGS2-StateMachine’s StateMachineMaster (versioned)Spawning enemies, validating attacks, reviving
Room progress stateThe rules’ state record. Not delivered to clientsFloor number, enemy object IDs
Shared valuesShared objects. Delivered to all participantsPosition, HP, boss phase
DisplayUnity GameObjectsHP 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#

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

MethodWhen 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)

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

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).

APIDescription
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)
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)
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)
ManagementWho can modify valuesExample uses
Server-managedOnly Room.Set in the rulesEnemy HP, doors, boss phase, score
Control lendingOnly the one connection that has control. The rules cannot modify themA carried box, a paddle, an operator seat
SimulationOnly Room.Set in the rules. The owning player sends inputsPlayer characters, vehicles

Spawn Objects and Read and Write Values

APIDescription
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

APIDescription
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)

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.

APIDescription
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)
// 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)

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.

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);
}
APIDescription
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

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.

Room.DefineRelevance<Character>(300);   // after DefineTickRate
Distance from the receiving player’s characterUpdate rate
Within distanceEvery tick
Within twice itEvery 2 ticks
Within four times itEvery 4 ticks
FartherEvery 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)

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.

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

APIDescription
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

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.

[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,
}
ItemDescription
RoomSchema(prefab)Prefab ID. A positive int32 that must not duplicate other sync schemas
StandardFieldsStandard fields for position and rotation. RoomSync.Position (fields 0 to 2), RoomSync.Rotation (3 to 5), RoomSync.Transform (both)
PositionPrecision / RotationPrecisionStep size of the standard fields. Defaults are 0.01 and 360 / 4096 degrees
Enum valuesField 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
PrecisionStep 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

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

Rules resultBehavior
Step.NextProceeds to the next processing as part of the same input
Reaching a wait for input (WaitFor / Stay), Step.EndCommits changes to the state, shared objects, and timers, and delivers them to everyone
Step.IgnoreRolls back all changes made by this input. The reason reaches the client
Exception, exceeding the execution time or transition count limitRolls 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.

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

Generation and Registration

StepDescription
GenerateGenerate with RealtimeRoomV1 specified for hostProfile in project.json. For the settings, see C# definition generation settings
RegisterSpecify the contents of the generated main.gsl as payload of GS2-StateMachine’s UpdateStateMachineMaster. mainStateMachineName is the class name of the rules
Configure the roomIn 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.

FilePurpose
main.gslThe rules to register
package.zipA distribution file containing only main.gsl. When registering, read out the main.gsl inside and specify it as payload
unity/<schema name>.realtimeschemaThe sync schema to specify on Unity’s NetworkEntity
source-map.jsonA 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

TargetLimit
Participants per room64 connections
Shared objects per room4096
Object kindsUp to 64 defined during initialization. Up to 4096 for the whole room
Fields per object32 (numbers 0 to 31)
Control lease period50 milliseconds to 1 day (86,400,000 milliseconds)
After wait time50 milliseconds to 1 day (86,400,000 milliseconds)
After eventId1 or more
Scheduled timersOne per object, 4096 for the whole room
Changes per inputUp to 64 objects each for spawns and value changes. Up to 64 objects for scheduling and canceling timers
Processing per input100 milliseconds of execution time, up to 32 transitions via Next
Timers executed per processing cycleUp to 64. Any excess is carried over to the next cycle
Tick rate1 to 60 per second
Changes per tickValue changes, spawns and despawns: up to 1024 objects each. One player owns up to 64 objects
Hit test rewindUp to 500 milliseconds (positions are kept for 1 second)
Inputs sent aheadUp to 1 second of ticks
Size of the rules (GSL)1 MiB

Unsupported Features

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.

Writing GSL Directly

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.

    EventParameters
    Interactint revision, int objectId, int method
    Timerint revision, int objectId, int eventId
    Tickint 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.