Writing Rules in GDScript

How to write the rules for stateMachine rooms in GDScript, and generate and register them from Godot

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

ElementHow to write itC# equivalent
Rulesclass_name <Name> and extends Gs2RoomRules. One rule per scriptThe rules class
State, result, errorconst STATE := <record>, const RESULT := <record>, const ERROR := <enum>The type arguments of Step
RecordsWrite 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
Startstatic func start() -> Gs2StepStart
Handlerson_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
Helpersstatic func _name(...). Returns a value or -> voidprivate methods
Constantsconst NAME := 3const 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

GDScriptC#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.NextRuns 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.EndEnds the rules
Gs2Step.ignore(TickError.MEMBER)Step.IgnoreRejects 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.

TagWhere to write itC# 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_inputenum[RoomInput]
## @input_range(-1, 1)Input member[InputRange]
## @room_actionenum[RoomAction]
## @room_timerenum[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 itExampleC#
Pass the schema as the first argumentGs2Room.define_simulated_prefab(Character), Gs2Room.spawn(Crate, x, y)Room.DefineSimulatedPrefab<Character>(), Room.Spawn<Crate>(x, y)
Use _raw when passing a numeric Prefab IDGs2Room.define_server_prefab_raw(30, 1991), Gs2Room.spawn_raw(30, 0, 0, 0)Room.DefineServerPrefab(30, ...)
Reading and writing fieldsget_field / set_field; for bool, get_bool / set_bool; for float constants, set_floatRoom.Get / Room.Set
Standard fieldsget_standard / set_standard(id, Gs2RoomSync.StandardField.ROTATION_Y, v)StandardField
2Dspawn(S, x, y), raycast_2d, overlap_2dThe 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.

GDScriptName in GSL
start, on_tick, _moveStart, Tick, Move
Parameters and local variables object_id (_object_id)objectId
Record field box_idBoxId
Enum member LAST_TICKLastTick
Classes and enumsUnchanged (PascalCase)

Error messages are shown with GDScript syntax and names.

Supported Syntax

  • var x: int = ... / var x := ..., const
  • if / 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.

SettingMeaningDefault
gs2/realtime/rules_directoryWhere the rule scripts are placedres://rules
gs2/realtime/generated_directoryOutput location for the classes the game usesres://generated/realtime
gs2/realtime/package_directoryOutput location for the package.zip to registerres://../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/build

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

FileUse
<SchemaName>.realtimeschemaThe 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 enumConstants used as in world.set_input(TickInput.MOVE_X, 1) and door.interact(DoorAction.OPEN)
A script for the error enumConstants for refusal result codes
<RuleName>_prediction.gdThe 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.