Writing Rules in GDScript
You can write the rules for stateMachine rooms in GDScript as well as in C#. If you build your game with Godot only, you can write rules without setting up a C# environment.
GDScript rules do not run in Godot. GS2’s realtime rules compiler (gs2-realtime-compiler) converts them into the same GSL as C# rules, and into classes used in Godot. So what you can do and the execution rules are the same as for C# rules. For the meaning of the Room API, the execution rules, and the limits, see the Room Definition Reference. This page explains how to write rules in GDScript and the steps to generate and register them.
Rule scripts use the SDK’s stubs (Gs2RoomRules, Gs2Record, Gs2Step, Gs2Room, Gs2RoomSync). So editor completion and static type checking work.
Example
class_name TickRoom
extends Gs2RoomRules
const STATE := TickState
const RESULT := TickResult
const ERROR := TickError
class TickState extends Gs2Record:
var steps: int
class TickResult extends Gs2Record:
var finished: bool
enum TickError { INVALID_OPERATION = 5 }
## @room_schema(5)
enum Character { X = 0, Y = 1, LAST_TICK = 2 }
## @room_input
enum TickInput {
## @input_range(-1, 1)
MOVE_X = 0,
}
static func start() -> Gs2Step:
Gs2Room.define_tick_rate(30)
Gs2Room.define_simulated_prefab(Character)
return Gs2Step.wait_for(on_tick, TickState.new({steps = 0}))
static func on_tick(state: TickState, tick: int) -> Gs2Step:
for i in range(Gs2Room.object_count(Character)):
_move(Gs2Room.object_at(Character, i), tick)
return Gs2Step.stay(state.with({steps = state.steps + 1}))
## @predict(Character)
static func _move(id: int, step: int) -> void:
Gs2Room.set_field(id, Character.X, Gs2RoomMath.clamp(Gs2Room.get_field(id, Character.X) + Gs2Room.input(id, TickInput.MOVE_X) * 10, -1000, 1000))
Gs2Room.set_field(id, Character.LAST_TICK, step)Rule Structure
| Element | How to write it | C# equivalent |
|---|---|---|
| Rules | class_name <Name> and extends Gs2RoomRules. One rule per script | The rules class |
| State, result, error | const STATE := <record>, const RESULT := <record>, const ERROR := <enum> | The type arguments of Step |
| Records | Write only typed fields in class <Name> extends Gs2Record:. Create one with all fields specified using <Name>.new({field = value, ...}), and copy with record.with({field = value}) | record |
| Start | static func start() -> Gs2Step | Start |
| Handlers | on_interact(state, object_id: int, method: int), on_timer(state, object_id: int, event_id: int), on_tick(state, tick: int), on_spawned(state, object_id: int) | Interact, Timer, Tick, Spawned |
| Helpers | static func _name(...). Returns a value or -> void | private methods |
| Constants | const NAME := 3 | const int |
Make every function static, and give types to parameters and return values. Handler parameter names are the names the room passes, so you cannot change them (you can prefix an unused parameter with _, as in _object_id).
Supported types are int (computed as int32 in the room; going out of range fails that whole processing), bool, String, records, and enums. float can be used only for constants written to float fields (Gs2Room.set_float) and for tag arguments.
You cannot name the error enum Error (it collides with GDScript’s built-in Error). Give it a name specific to the rules, such as TickError.
Step
| GDScript | C# | Meaning |
|---|---|---|
Gs2Step.wait_for(on_tick, state) | Step.WaitFor(nameof(Tick), state) | Waits for the handler’s event in a new task |
.or_wait_for(on_timer) | .Or(nameof(Timer)) | Also waits for another handler at the same time |
Gs2Step.next(_claim, state) | Step.Next | Runs a function next within the same event |
Gs2Step.stay(state) | Step.Stay(state) | Updates the state while staying in the current task |
Gs2Step.end(result[, state]) | Step.End | Ends the rules |
Gs2Step.ignore(TickError.MEMBER) | Step.Ignore | Rejects the event (all changes made are rolled back) |
Specify handlers by the function itself (on_tick), not by a name string.
Tags
Instead of C# attributes, write tags in ## comments, one per line. Unknown tags and tags in the wrong place are errors.
| Tag | Where to write it | C# equivalent |
|---|---|---|
## @room_schema(7, standard=POSITION, position_precision=0.5, rotation_precision=...) | enum | [RoomSchema]. standard= is POSITION / ROTATION / TRANSFORM |
## @field(int|bool|float, precision=0.5, default=1.5) | Schema member | [RoomField] |
## @room_input | enum | [RoomInput] |
## @input_range(-1, 1) | Input member | [InputRange] |
## @room_action | enum | [RoomAction] |
## @room_timer | enum | [RoomTimer] |
## @predict(Character) | Function | [Predict]. static func _name(id: int[, tick: int]) -> void |
Gs2Room
You can use the same API as C#’s Room, with snake_case names. GDScript has no generics or overloads, so write them as follows.
| How to write it | Example | C# |
|---|---|---|
| Pass the schema as the first argument | Gs2Room.define_simulated_prefab(Character), Gs2Room.spawn(Crate, x, y) | Room.DefineSimulatedPrefab<Character>(), Room.Spawn<Crate>(x, y) |
Use _raw when passing a numeric Prefab ID | Gs2Room.define_server_prefab_raw(30, 1991), Gs2Room.spawn_raw(30, 0, 0, 0) | Room.DefineServerPrefab(30, ...) |
| Reading and writing fields | get_field / set_field; for bool, get_bool / set_bool; for float constants, set_float | Room.Get / Room.Set |
| Standard fields | get_standard / set_standard(id, Gs2RoomSync.StandardField.ROTATION_Y, v) | StandardField |
| 2D | spawn(S, x, y), raycast_2d, overlap_2d | The forms without z |
The other functions (input / input_clamped, object_count / object_at, is_prefab / prefab_of / owner_of / exists / find_owned, despawn, after / cancel_timer, sender_connection_id / sender_user_id, define_hitbox / define_relevance, and so on) mean the same as in C#. For integer math, you can use Gs2RoomMath (clamp, abs, sign, sqrt, length2, length3, mul_div, and the constants INT32_MIN / INT32_MAX) and mini / maxi.
Name Mapping
GDScript names are mapped to GSL names by fixed rules. Names that do not fit the rules are errors.
| GDScript | Name in GSL |
|---|---|
start, on_tick, _move | Start, Tick, Move |
Parameters and local variables object_id (_object_id) | objectId |
Record field box_id | BoxId |
Enum member LAST_TICK | LastTick |
| Classes and enums | Unchanged (PascalCase) |
Error messages are shown with GDScript syntax and names.
Supported Syntax
var x: int = .../var x := ...,constif/elif/else,while,for i in range(...)(the end value is evaluated only once)match(constant patterns and_)return,pass,break,continue- Arithmetic (
+ - * / %), comparisons,and/or/not,a if c else b @warning_ignore("...")
You cannot write arrays, dictionaries (except as arguments to a record’s new / with), subscripts, self, await, lambdas, signals, in / is / as, bitwise operations, **, or float arithmetic. If you do, you get an error with its position.
Integer division triggers GDScript’s integer_division warning. Disable this warning for the rules directory, or write @warning_ignore("integer_division") before the statement.
Generation and Registration
Set Up the Compiler
Get the gs2-realtime-compiler for your OS from the same release as the SDK (how to get it), and set its location in the Godot editor setting gs2/realtime/compiler_path.
Generate
Run Project > Tools > GS2 > Build Realtime Rules. With a single rule, the following project settings are used. If the default locations are fine, you do not need to set them.
| Setting | Meaning | Default |
|---|---|---|
gs2/realtime/rules_directory | Where the rule scripts are placed | res://rules |
gs2/realtime/generated_directory | Output location for the classes the game uses | res://generated/realtime |
gs2/realtime/package_directory | Output location for the package.zip to register | res://../build/realtime |
Each location can hold only one rule script. With several rules, put each rule in its own location, and list {"rules": ..., "generated": ..., "package": ...} sets in the project setting gs2/realtime/rule_sets. When rule_sets is not empty, the three settings above are not used.
You can also generate from the command line.
gs2-realtime-compiler --build path/to/rules --output path/to/buildThe following classes are output to generated_directory. Files that are no longer generated are removed, and the .uid of unchanged files is kept, so references from scenes do not break.
| File | Use |
|---|---|
<SchemaName>.realtimeschema | The sync schema to specify in Gs2NetworkEntity.schema |
<SchemaName>_schema.gd (class_name <SchemaName>Schema) | Field slots and typed accessors |
| A script for each input and action enum | Constants used as in world.set_input(TickInput.MOVE_X, 1) and door.interact(DoorAction.OPEN) |
| A script for the error enum | Constants for refusal result codes |
<RuleName>_prediction.gd | The prediction code. Enable it with <RuleName>Prediction.register(world) |
The generated classes have a class_name, and class_name is shared across the whole project. If a name collides with another rule’s output or another script in the project, that rule is not output and you get an error (names that differ only in case also count as collisions). For example, if two rules both have enum PlayerState, both generate PlayerStateSchema, so rename one of them.
Rule scripts are not used to run the game. Placing them in the project makes the editor’s type checking work, but exclude them with an export filter.
Register
Registration follows the same steps as for C# rules. Specify main.gsl in package.zip as the payload of GS2-StateMachine’s UpdateStateMachineMaster, and specify the rules’ class_name as mainStateMachineName. For the Realtime namespace settings, see Generation and Registration in the Room Definition Reference.
GSL generated from GDScript is identical to GSL generated from C# rules with the same content, except for the comments that show positions in the source file.