Skip to content
Menu

Docs · Make it a game

Rengy Script

By the end

  • Scripts of your own on nodes, with fields you tune in the Inspector while the room plays
  • Hooks that run on the first step, on every step, and when something happens to a node
  • A script that waits, glides and pulses without keeping a clock of its own
  • Nodes that tell each other what happened
  • Lasers, coins and anything else you build once, copied into the room while the game runs
  • Nodes told apart, and told all at once, by the group they are in
  • A count the whole game keeps, until it closes
  • Where to look, and what to try, when a script is wrong
On this page

A script is a component

A script is a text file ending in .rgs that declares one component: some fields, and some hooks, the code the engine runs when something happens to the node carrying it. You put it on a node the way you put on a Shape or a Light. Many nodes can carry the same script, each with values of its own, and one node can carry several different scripts.

Make one

  1. In Assets, choose Create → New script, type swell and press Create. The editor writes swell.rgs into the project folder, already declaring a component called Swell.
  2. Click the Code tab above the Viewport. Its Scripts list shows every script in the project: click swell.rgs.
  3. Replace its text with the script below, and press Ctrl+S to save it.
  4. Select a node with a Shape, such as crate 1 from Getting started. If it still carries the Mover, remove it with the bin icon at the right of its header. Press Add component, type swell and choose Swell.
  5. Press F5. The crate swells to a fifth bigger, shrinks to a fifth smaller, and back, once every two seconds.
rgs swell.rgs
/// Swells and shrinks the node it is on, like something breathing.
component Swell {
    /// How much bigger it gets at its fullest.
    #[range(0.0..1.0)]
    amount: f32 = 0.2,
    /// Seconds for one swell and back.
    #[range(0.1..10.0)]
    #[suffix("s")]
    period: f32 = 2.0,
    /// Seconds since the room started. The script's own clock.
    #[hidden]
    clock: f32 = 0.0,

    fn loop(dt: f32) {
        clock += dt;
        let size = 1.0 + amount * (clock / period * tau()).sin();
        set_scale(vec2(size, size));
    }
}
component Swell
The component’s name. The file is named after it in snake case, so Swell lives in swell.rgs and SpikeTrap in spike_trap.rgs. A script whose file name disagrees is refused. Create → New script names both for you: type Spike Trap and it writes spike_trap.rgs, declaring SpikeTrap.
The fields
amount, period and clock: each a name, a type and a starting value, with a comma after it. The /// line above each is its tooltip. amount and period are rows in the Inspector. clock is marked #[hidden], so it is the script’s own memory and has no row. The next section has the rest.
fn loop(dt: f32)
A hook. loop runs on every step, and dt is the seconds since the last one. Hooks and bases lists the others.
The body
clock += dt; adds to a field. let size = …; names a value for the rest of the hook. .sin() is a method on a number. tau is one full turn in radians, and set_scale sets the node’s scale.
The Code panel open on mover.rgs in a new project called my-game, and beside it the Inspector for a node named crate 1, carrying a Mover section whose Speed slider reads 80
A project’s starter script, mover.rgs, on the left, and a node carrying it on the right. Each field without #[hidden] is a row.

The language

It is small on purpose. A hook’s body is made of let, assignments (=, +=, -=, *=, /=), if and else, calls, and await, and every line but an if ends in a semicolon. There are no loops and no functions of your own: loop is the loop, and the engine calls it. Keywords lists every word the language has.

A number written with a point, 2.0, is an f32; one without, 2, is an i32, a whole number. A whole number written where an f32 is wanted counts as one, so speed: f32 = 60 is fine. But an i32 field never mixes with an f32 in arithmetic, and there is no way to turn one into the other: keep counts in i32 and everything you measure in f32.

A script can live in any folder of the project: the editor and your game find every .rgs file in it. Click a folder in Assets and choose Create → New script to make one there, or right-click it and choose New script here, or drag a script onto a folder to move it. The file name is the component’s name, so two scripts cannot share one, whatever folders they are in.

Help while you type

The Code panel checks every script as you type. A mistake is underlined in red; see When a script is wrong. Ctrl+Space lists what you can type at the caret, and resting the mouse on a builtin’s name says what it does.

Change it while the room plays

Play runs the scripts as they are saved, so press Ctrl+S before you press F5. Save a script while the room is playing and the editor swaps the new version in on the next step. Fields keep the values they have, a field you added starts at its starting value, and on_create does not run again, because the node already exists.

Run game and an exported game read the scripts once, when they start.

Fields and attributes

A field is a value the script keeps on each node that carries it, written name: Type = starting value,. Its Inspector row is labelled with its name in title case, so stay_open reads Stay Open.

TypeHoldsA starting value
f32A number with a fractional part.1.5, or a whole number: 60
i32A whole number.3
booltrue or false.true
StringText."door"
Vec2Two numbers, x and y: a place, a size or a direction.vec2(160.0, 0.0)
Vec4Four numbers. Usually a colour: red, green, blue and alpha, each 0 to 1.vec4(1.0, 0.9, 0.5, 1.0). Without one it starts at four zeroes, which as a colour is clear.
EasingThe curve an anneal follows..InOutSine
An enum you declareOne of a set of names.One of them: .Up

A field with no starting value starts at zero, empty text, false, or an enum’s first name.

This one bobs a node up and down, or side to side, and can tint its Shape. Make it the way you made swell.rgs, as bob, and put it on a crate:

rgs bob.rgs
/// Which way a Bob moves.
enum Way { Up, Across }

/// Bobs the node up and down, or side to side, and can tint its Shape.
component Bob {
    /// Which way it bobs.
    way: Way = .Up,
    /// How far it moves, in world units.
    #[name("Distance")]
    #[range(0.0..64.0)]
    #[suffix("u")]
    reach: f32 = 12.0,
    /// Seconds to get there, and the same again to come back.
    #[range(0.1..4.0)]
    #[step(0.1)]
    #[suffix("s")]
    seconds: f32 = 0.8,
    /// The curve it follows.
    curve: Easing = .InOutSine,
    /// The colour its Shape turns. Left clear, the Shape keeps its own.
    #[color]
    tint: Vec4,

    fn on_create() {
        if tint.w > 0.0 {
            set_colour("ShapeComponent", "color", tint);
        }
        if way == .Up {
            pulse_by(vec2(0.0, -reach), seconds, curve);
        } else {
            pulse_by(vec2(reach, 0.0), seconds, curve);
        }
    }
}
The Code panel open on bob.rgs, and beside it the Inspector for a crate carrying Bob: Way set to Up, a Distance slider reading 16 u, a Seconds slider reading 0.80 s, Curve set to InOutSine and an orange Tint swatch, with the crate’s Shape under it
Each field is a row: the enum and the curve are dropdowns, #[range] makes a slider, #[suffix] prints the unit, and #[color] draws a swatch.
enum Way { Up, Across }
A set of names of your own, declared in the same file. A field of that type is a dropdown, and way == .Up asks which one it holds.
#[name("Distance")]
The row’s label, in place of the field’s own name.
#[range(0.0..64.0)]
A slider from 0 to 64.
#[step(0.1)]
How far one notch of a drag moves the number.
#[suffix("u")]
The unit printed after the number.
#[color]
Draws a Vec4 as a colour swatch. A Vec4 field starts clear unless you write a starting value, as in tint: Vec4 = vec4(1.0, 0.9, 0.5, 1.0), and tint here is left clear, which is why the script only tints the Shape once you have picked a colour with some alpha in it.

Field attributes lists the rest. The ones you will want soonest are #[hidden], a field with no row, for the script’s own memory; #[readonly], a row you can read but not edit, for something the script counts; #[group("Name")], which folds a run of fields under a header; and #[node_path], in the next section but one.

Fields while the room plays

While the room plays, the Inspector shows each field as the script changes it, and a value you drag there is the one the script reads on its next step. Stop puts every field back as it was, like everything else a run changed.

Hooks and bases

A hook is a fn with a name the engine knows. You never call one: the engine does, on the step something happens. Every script can have these six:

on_create()
Once, on the first step the node exists. For everything in the room, that is the first step after you press Play. See on_create.
loop(dt)
On every step, after the hooks for everything else that happened on it. See loop.
on_signal()
On a step when a signal has reached the node. See Signals and on_signal.
on_position_change(old_position, new_position)
When the node has moved, with where it was and where it is, in world units. See on_position_change.
on_visibility_change(is_visible)
When the node was shown or hidden. See on_visibility_change.
on_destroy()
Once, as destroy or destroy_self removes the node. Nodes under it go with it without hearing it. See on_destroy.

A hook may leave its parameters out. fn loop() can still read dt, and fn on_position_change() can still read new_position. Write them out only to rename them, and then write all of them, with their types.

Bases

A base, written after the name, gives a script the hooks of one kind of node as well as the six: component Coin : Area. There are four.

BaseIts own hooksThe node needs
Node, the default: write no baseOnly the six aboveNothing
Areaon_enter, on_exit and while_in, when another Area overlaps it; on_pointer_enter, on_pointer_exit and on_pointer_press, for the mouse or a fingerAn Area
Bodyon_land, on_leave_ground, on_hit_wall, on_hit_ceilingA Character Body
Screenon_screen_enter, on_screen_exit, as it comes into and leaves the camera’s viewA Screen Notifier

Write an Area hook in a script with no base and the script is refused, with a message that says which base to write. The other way round is quieter: a : Area script on a node with no Area still runs its six ordinary hooks, but its Area hooks never run, and the editor does not warn you.

A pickup, then. It needs an Area, and something with an Area of its own to touch it: the player from Characters and physics has one.

  1. Make a script called coin and replace its text with the one below.
  2. Select Room, add a node with + Add node → Area, and name it coin. Set its Position to -215 and 148, on the floor between the player and crate 1, and its Shape’s Size to 16 and 16.
  3. An Area draws nothing, so press Add component, choose ShapeComponent and give the Shape the same Size.
  4. Press Add component again and choose Coin.
  5. Press F5. The coin throbs until the player walks into it, and then it is gone.
rgs coin.rgs
/// A pickup. It throbs gently until an Area touches it, then it is gone.
component Coin : Area {
    fn on_create() {
        pulse_scale(vec2(1.2, 1.2), 0.5, .InOutSine);
    }

    fn on_enter() {
        destroy_self();
    }
}

destroy_self removes the node carrying the script, with everything under it in the Hierarchy. on_enter, on_exit and while_in are also handed the other Area’s node, as other. Pass it to destroy, set_visible or move_node to act on that node instead, or to in_group to ask what kind of node it is: see Groups.

Calling built-in functions

Everything a script does to the room, it does by calling a built-in function. Built-in functions lists every one; these are the ones you will reach for first.

This node
translate moves it by an offset, move_to puts it at a place in world units, set_scale scales it, hide and show hide and show it, and destroy_self removes it.
Another component on this node
set_number, set_flag, set_point and set_colour write one of its fields: a number, a true or false, a Vec2, a colour.
Another node
set_visible, move_node and destroy act on a node you name: other in an Area hook, or one picked in a #[node_path] field.
Making values
vec2 and vec4 build a Vec2 and a Vec4, and abs, min, max, clamp, tau and ease help with the arithmetic: min and max give the smaller and the larger of two numbers, and clamp(x, lo, hi) keeps x between lo and hi. A Vec2 has .x, .y, .length(), .distance(other) and .normalized(), which is the same direction at length one, and an f32 has .sin(), .cos() and .abs().
Asking
held, pressed and direction read the controls, in Input. heard reads a signal, in Signals.

A script cannot read another component’s fields, but it can ask position where its own node is. on_position_change tells it where the node has moved to; keep the answer in a field if you need it later.

Another component on the same node

The first two arguments of set_number and its kin are the component, by the name the reference gives it, and the field, by its name from the component’s From a script list, such as the Shape’s. This script outlines a Shape while the mouse rests on it. Put it on a node with a Shape and an Area the same size, press F5 and point at it:

rgs highlight.rgs
/// Outlines the Shape on this node while the pointer rests on it.
component Highlight : Area {
    /// How thick the outline is while the pointer is over it.
    #[range(0.0..8.0)]
    thickness: f32 = 3.0,

    fn on_pointer_enter() {
        set_number("ShapeComponent", "outline", thickness);
    }

    fn on_pointer_exit() {
        set_number("ShapeComponent", "outline", 0.0);
    }
}

Another node

A field marked #[node_path] is an i32 the Inspector draws as a node picker. node turns the pick into a node the other calls take:

rgs reveal.rgs
/// Shows another node only while the action key is held.
component Reveal {
    /// The node to show.
    #[node_path]
    target: i32,

    fn loop() {
        set_visible(node(target), held("action"));
    }
}

Put it on any node, click its Target row and choose a node from the list. In Play, that node is hidden, and shows while you hold E. With nothing picked, target is 0, which names no node, and every call on it does nothing.

Moving by an offset

translate(vec2(speed * dt, 0.0)) on every step moves a node at speed units a second: that is how the starter script moves its crate. A Vec2 can be multiplied by a number, which scales both of its numbers, so when the speed and the direction are held together in one Vec2 you can write velocity * dt. This one slides a node the way it is pushed, and slows it to a stop:

rgs skid.rgs
/// Slides the way it is pushed, and slows to a stop.
component Skid {
    /// How far it moves each second, in world units, as x and y. A smaller y is higher up.
    velocity: Vec2 = vec2(240.0, -60.0),
    /// How fast it slows down. A higher number stops it sooner.
    #[range(0.0..10.0)]
    drag: f32 = 2.0,

    fn loop(dt: f32) {
        translate(velocity * dt);
        velocity *= 1.0 - drag * dt;
    }
}

Put it on a node and press F5. The node slides up and to the right and comes to rest about 120 units right and 30 up from where it began, and Velocity runs down to nothing in the Inspector as it goes. velocity *= … is the same multiplication on the field itself. Two Vec2s also add and subtract, x with x and y with y. Keep the number on the right: 2.0 * velocity is refused, and its message says to write the Vec2 first.

await, anneals and pulses

await stops a hook where it is and carries on from the same line later, with its lets as they were. Put it in front of something that takes time:

await wait(seconds)
Carries on after that many seconds. See wait.
await wait_until(condition)
Carries on once the condition is true, checked once a step: await wait_until(held("jump"));. See wait_until.
await signal(name)
Carries on when that signal reaches the node. See Signals.
await wait_frames(steps)
Carries on after that many steps. Play and an exported game both take steps at the project’s fixed rate, 60 a second unless you change it, so a count lasts the same time in each. Use wait for anything timed in seconds: it does not change with the rate.

Anneals

An anneal moves something from where it is to where you ask, over a number of seconds, along a curve. Start one and it runs on its own. Put await in front and the hook waits for it to finish.

anneal_to
Moves the node to a place, in world units.
anneal_by
Moves it by an offset.
anneal_turn and anneal_rotation
Turn it by an angle, or to one, in radians.
anneal_scale
Scales it.
anneal_number, anneal_point and anneal_colour
Change a field on another component on the node, as set_number and its kin do, over time.

The last argument is the curve, an Easing: .Linear keeps a steady pace, .InOutSine starts and ends gently, and .OutBack overshoots and settles. Its reference page draws all of them.

rgs shuttle.rgs
/// Glides between two places, resting at each end.
component Shuttle {
    /// Where it glides to, in world units.
    there: Vec2 = vec2(160.0, 0.0),
    /// Where it glides back to.
    back: Vec2 = vec2(-160.0, 0.0),
    /// Seconds each glide takes.
    #[range(0.1..10.0)]
    #[suffix("s")]
    seconds: f32 = 2.0,
    /// Seconds it rests at each end.
    #[suffix("s")]
    rest: f32 = 0.5,

    fn loop() {
        await anneal_to(there, seconds, .InOutSine);
        await wait(rest);
        await anneal_to(back, seconds, .InOutSine);
        await wait(rest);
    }
}

Put it on a node and press F5: the node glides to There, rests, glides to Back, rests, and goes round again, for as long as the room plays.

An `await` in `loop` makes a cycle. While one run of loop is waiting, the steps that follow do not start another. On the step after the run reaches its end, loop starts again from the top. Any other hook works the same way: while it is waiting, the engine calling it again does nothing, and the waiting run carries on. Save the script while the room plays, though, and every wait is given up: loop starts again from its top.

Pulses

A pulse goes there and back, forever: pulse_by, pulse_turn, pulse_scale, pulse_number, pulse_point and pulse_colour. Bob and Coin each start one in on_create. A pulse never finishes, so it cannot be awaited.

Start a new anneal or pulse on something already moving and it takes over from the old one. quench stops every anneal and pulse on the node where it stands, and quench_path stops just one: quench_path("transform.rotation").

on_destroy cannot await: the node is gone before anything could carry on.

Signals

A signal is a name one node sends and others hear. The sender calls emit, and a Signal component on the sender lists which nodes hear which name. The listener finds out in on_signal, where heard says which name arrived. Characters and physics wires a plate to a door this way, and the door hides.

A signal is heard on the step after the one that sent it, and only on that step. A node that does not look then misses it. heard works in any hook on that step, not only in on_signal.

This door slides up instead, and comes back down a few seconds later:

rgs sliding_door.rgs
/// Slides up out of the way when a plate wired to it is pressed,
/// and back down a few seconds later.
component SlidingDoor {
    /// How far it slides, in world units.
    #[suffix("u")]
    slide: f32 = 96.0,
    /// Seconds it stays open.
    #[suffix("s")]
    stay_open: f32 = 3.0,

    fn on_signal() {
        if heard("pressed") {
            await anneal_by(vec2(0.0, -slide), 0.4, .OutCubic);
            await wait(stay_open);
            await anneal_by(vec2(0.0, slide), 0.4, .InCubic);
        }
    }
}
  1. Make a script called sliding_door with the text above.
  2. Select the door from Characters and physics. Remove its Door with the bin icon at the right of its header, then add SlidingDoor.
  3. Press F5 and walk onto the plate. The door slides up, its Collider with it, and comes back down three seconds later. Stand clear of it.

Step on the plate again while the door is up and nothing more happens: the hook is still waiting, and a hook that is waiting ignores every call until it finishes. That second press is lost, not kept for later. The waiting run does not catch it either: when it carries on, heard() answers for the step it carries on in, not for the steps it waited through. If a second press matters, have on_signal only note it in a field, with no await, and do the waiting in loop.

To wait for a signal in the middle of a hook, await it:

rgs trapdoor.rgs
/// Waits for its signal, once, and vanishes.
component Trapdoor {
    fn on_create() {
        await signal("pressed");
        hide();
    }
}

A Signal wires one sender to the nodes you pick. To tell a whole group of nodes at once, with nothing wired, see Groups.

Making copies with spawn

Some things do not exist until the game needs them: a laser leaving a ship, a coin popping out of a crate, an enemy arriving at the edge. You build each one once, as an artifact, and a script asks for copies of it with spawn while the game runs.

spawn takes a name and a place: spawn("laser", at). The name is not a file. It is looked up on the Spawner component of the node whose script makes the call, which holds a list of entries, each a Name and an Artifact. The script says what it wants, and the Inspector says which artifact that is, the way the Triggers of an Audio Source name the sounds that play_sound plays: see Audio. Point a name at another artifact and no script changes.

A laser that flies up and removes itself a moment later, and a gun that fires one each time you press the action key:

rgs laser.rgs
/// A laser: it flies up, and removes itself a moment later.
component Laser {
    /// How fast it flies, in world units a second.
    #[suffix("u/s")]
    speed: f32 = 500.0,
    /// Seconds before it removes itself.
    #[suffix("s")]
    life: f32 = 0.6,

    fn on_create() {
        await wait(life);
        destroy_self();
    }

    fn loop(dt: f32) {
        translate(vec2(0.0, -speed * dt));
    }
}
rgs gun.rgs
/// Fires a laser from just above the node each time the action key goes down.
component Gun {
    /// How far above the node the laser starts, in world units.
    #[suffix("u")]
    height: f32 = 56.0,

    fn loop() {
        if pressed("action") {
            spawn("laser", position() + vec2(0.0, -height));
        }
    }
}
  1. Make the laser. Select Room, add a node with + Add node → Shape and name it laser. Set its Shape’s Size to 4 and 16, and pick a bright Color.
  2. Make a script called laser as A script is a component describes, with the first text above, and save it with Ctrl+S. Select the node, press Add component and choose Laser.
  3. Right-click laser in the Hierarchy and choose Save as artifact…. Name the file laser and save it in the project folder. Then delete the laser node from the room with Delete: the file stays, and the room has no use for a laser standing in it.
  4. Make a script called gun with the second text, and save it. Select player, press Add component and choose Gun.
  5. Press Add component again, type spawner and choose SpawnerComponent. Press + beside Entries and open the new [0] row. Type laser into Name. Click the Artifact slot and choose laser, or drag laser.artifact onto it from Assets.
  6. Press F5, then press E. A laser leaves from just above the player, flies up and is gone, and another follows each time you press.
spawn("laser", position() + vec2(0.0, -height))
The name, then the place to put the copy: a point in world units, here height above the node, since a smaller y is higher up. position is where the node stands.
await wait(life), then destroy_self()
The laser’s own timer. Nothing removes a copy for you: it stays in the scene until a script removes it, so a gun that fires forever needs lasers that leave.

A copy is a new node, not a child of the node that spawned it. It goes in at the top level of the scene, so a laser does not follow the player that fired it. Its position is the point you gave, and its rotation and scale stay as the artifact has them.

It appears at the end of the step, and starts on the next. Like everything a hook asks for, the copy is made once every hook of the step has run, and its on_create and loop first run on the next step. spawn gives nothing back, so the node that asked never holds the copy: the copy runs its own scripts, as Laser does.

A name that finds nothing makes nothing. Misspell laser, leave the entry’s Artifact empty, or call spawn on a node with no Spawner, and no copy appears. The Console says which node asked and what to add, once for each name. Once every script parses, Problems says it before you press Play, whenever the name is written out in quotes as it is here: as a warning on each node that carries the script, or, for a script inside an artifact the room can spawn, on the node in the room whose Spawner starts the chain: the one that spawns the artifact, or the one that spawns the copy that spawns it.

Copies come from the saved artifact. Play, Run game and an exported game all make them from the artifact’s file as it was saved when the game started, so save an artifact you have changed before you press F5 again. Scenes and nodes covers changing one.

One Spawner holds as many entries as you like, so one node can spawn a laser and a bomb: give each entry its own name. spawn works in any hook, on_destroy included, so a node can leave something behind as it goes. The Spawner page lists its rows.

Groups

A group is a name you give to nodes, so that a script can ask whether a node is one of them, or speak to all of them at once, without knowing any of them by name. It matters most for nodes nobody placed by hand. A laser made by spawn cannot be picked in a #[node_path] field or wired to with a Signal, because it did not exist when you built the room, but it can be in a group.

Put nodes in a group

Select a node and find the Groups row in the Node section of the Inspector. Type a name into Add group and press Enter, or press the + beside it. The name appears in the row as a tag, and the small × on the tag takes the node out again. A node can be in as many groups as you like, a group exists for as long as some node is in it, and with several nodes selected the row changes all of them. The Hierarchy does the same from a node’s right-click menu: Groups ticks and unticks the groups that exist, and adds a new one.

A group set on an artifact’s node comes with every copy of it, placed or spawned. Only you set groups, in the editor: a script can ask about them and cannot change them. A group is also only a name. Nothing moves, hides or scales with it, as things do with a parent: to move nodes together, make them children of one node, as Scenes and nodes shows.

Asking: in_group

in_group asks whether a node is in a group. In an Area hook the node to ask about is other, the one that walked in: in_group(other, "player") is true when it is in the group player. It answers for the node as the step began, and false for a node that has gone.

Telling: emit_to

emit_to sends a signal to every node in a group. The group is the address, so there is no Signal component and no wiring to draw. Each node hears it on the next step, in on_signal with heard, just as Signals describes, and the node that sends does not hear itself, even if it is in the group.

This button on the floor sets off an alarm, but only when the player steps on it. Every node in the group lamps hears it and turns red:

rgs alarm.rgs
/// A button on the floor. When the player steps on it, every node in the
/// group "lamps" is told.
component Alarm : Area {
    fn on_enter() {
        if in_group(other, "player") {
            emit_to("lamps", "alarm");
        }
    }
}
rgs alarm_lamp.rgs
/// Turns red when the group it is in raises the alarm.
component AlarmLamp {
    fn on_signal() {
        if heard("alarm") {
            set_colour("ShapeComponent", "color", vec4(1.0, 0.1, 0.1, 1.0));
        }
    }
}
  1. Make scripts called alarm and alarm_lamp with the texts above, and save them.
  2. Select player. In the Node section of the Inspector, type player into Add group and press Enter.
  3. Select wall left and put it in a group called lamps the same way. Press Add component and choose AlarmLamp. Do the same for wall right.
  4. Select Room, add a node with + Add node → Area and name it button. Set its Position to -120 and 150, on the floor to the right of crate 1, and its Shape’s Size to 48 and 12.
  5. An Area draws nothing, so press Add component, choose ShapeComponent and give the Shape the same Size. Press Add component again and choose Alarm.
  6. Press F5, jump crate 1 and walk onto the button. Both walls turn red. Anything else with an Area that touches it is ignored, because it is not in the group player.

Group names are text, so spell them the same everywhere. A name nobody is in is not an error: in_group says false for it, and emit_to reaches nobody.

Keeping values for the rest of the game

A field belongs to one node, and goes when the node does. A score, the coins the player has, and where they last stood belong to the whole game. Rengy Script can keep a number or a place under a name, for the rest of the game, and every script on every node can read it back. The name is a key: any text you like, such as "coins". Every script that spells it the same way reaches the same value, and capitals count.

keep_number
Keeps a number under a key, in place of whatever was there: keep_number("coins", 0.0).
add_number
Adds to the number under a key: add_number("coins", 1.0). A key nothing has kept counts as 0, so the first add needs no setting up, and a negative amount takes away.
kept_number
Reads the number under a key. The second argument is the answer when nothing has been kept there: kept_number("coins", 0.0).
keep_point and kept_point
The same for a place, a Vec2: keep_point("checkpoint", position()) remembers where a node stands, and kept_point("checkpoint", vec2(0.0, 0.0)) reads it back.

Kept values start empty when Play starts, or when an exported game opens, and last until Stop or the game closes: nothing is written to a file. The Inspector does not list them, because they belong to the game and not to a node. A key holds a number or a place, never both. Ask for the other kind and you get your default, so keep numbers and places under different keys.

When a change is seen

Every hook in a step reads the kept values as they were when the step began, and what the hooks keep is written once all of them have run. So a value kept in one step is what every script reads from the next step on, and two scripts that ask in the same step get the same answer.

That is why add_number exists. Suppose two coins are taken on the same step. If each read the count and kept “the count plus 1”, both would keep the same number and one coin would be lost. Each add_number adds to what the one before it left, so both count.

Coins that count themselves, and a door that opens once the game has counted three, and takes them:

rgs counted_coin.rgs
/// A coin that counts itself. Only the player takes it.
component CountedCoin : Area {
    fn on_enter() {
        if in_group(other, "player") {
            add_number("coins", 1.0);
            destroy_self();
        }
    }
}
rgs toll_door.rgs
/// Stays shut until the game has counted enough coins, then takes them all.
component TollDoor {
    /// Coins it takes to open.
    price: f32 = 3.0,

    fn loop() {
        if kept_number("coins", 0.0) >= price {
            keep_number("coins", 0.0);
            hide();
        }
    }
}
  1. Make scripts called counted_coin and toll_door with the texts above, and save them.
  2. Select coin, the one from Hooks and bases. Remove its Coin with the bin icon at the right of its header, and add CountedCoin. It counts only the player, who has been in the group player since the last section. Press Ctrl+D twice: three coins now stand on one spot, so the player takes all three on the same step.
  3. Select door, remove the script it carries the same way, and add TollDoor.
  4. Press F5 and walk right into the coins. All three go at once, and the door disappears, because the game has counted three.

The door reads the count as it was when the step began, so it opens one step after the third coin was taken. A key that was never kept reads as its default, which is why kept_number("coins", 0.0) needs no setting up before the first coin.

When things happen in a step

A running room moves in steps, taken at a fixed rate: Simulation rate in File → Project settings…, 60 a second unless you change it. Play and an exported game take them the same way. After a slow frame the game takes up to Max catch-up steps in a row to catch up, and past that it runs slow rather than falling further behind.

dt is the same on every step: one second divided by the rate, a sixtieth of a second unless you change it. A slow frame runs more than one step, and a quick one can run none. A script that multiplies by dt keeps its speed if you change the rate.

One step, in order

  1. Every Character Body moves: its keys, read through your controls, gravity, and what it runs into.
  2. Anneals and pulses move what they are moving.
  3. The hooks run, one kind at a time: on_create, on_position_change, on_visibility_change, a body’s landing and wall hooks, the Screen hooks, an Area’s on_exit, on_enter and while_in, the pointer hooks, on_signal, and last, loop. Within one kind, nodes go in Hierarchy order.
  4. What the hooks asked for happens: moves, hides and shows, set_number and its kin, new anneals and pulses. Then each node a hook destroyed runs its on_destroy, and goes, with the nodes under it.
  5. The signals sent during the step are posted, to be heard on the next one, and the values the hooks asked to keep are written.
  6. The copies spawn asked for are made. Each one runs its first hooks on the next step.

What that means for a script

  • on_create and loop both run on the first step, on_create first.
  • A call that moves or changes something takes effect once every hook of the step has run, so on_position_change hears about a move a hook made on the next step.
  • A body has already moved when the hooks run, so a Body script hears a landing on the step it happens. A value a script writes onto a body, such as its Speed, reaches the body on the next step.
  • The line after an await never runs on the step that stopped at it.
  • A signal takes one step to arrive.

To watch it happen, press Pause while the room plays, then Step to run exactly one step at a time, and read the Inspector between steps. See The editor.

When a script is wrong

Mistakes the Code panel finds

The Code panel checks every script as you type. A mistake is underlined in red, with a red dot beside its line number, and the line under the text counts them: 1 error in the scripts. Rest the mouse on the line to read what is wrong. Something that works but is probably not what you meant, such as a field no hook uses, is underlined as a warning.

The Code panel open on swell.rgs, the word sise in the last line of its loop hook underlined in red with a red dot beside line 17, the panel’s status line reading 1 error in the scripts, and the editor’s status bar reading 6 of 7 scripts loaded, swell.rgs line 17: no field or local named sise; this hook can see amount, clock, period, dt, size
size misspelt. The message names the names the hook can see, so a typo is a one-word fix.

Once you save, Play runs the scripts that check and leaves out the ones that do not. A node carrying a broken script keeps its other components, and the script does nothing until you fix it and save. The status bar says how many scripts loaded, and quotes the first mistake. A broken script is also missing from Add component, which shows the same line.

Mistakes nothing finds

  • A : Area, : Body or : Screen script on a node without that component: its own hooks never run.
  • A signal name spelt one way where it is sent and another where it is heard: it is never heard.
  • A group name spelt one way on the node and another in in_group or emit_to: in_group says false, and the signal reaches nobody.
  • A kept key spelt one way where it is kept and another where it is read: the read answers its default.

The Problems panel catches some mistakes that need the room to see: a Signal connection with no name or no node, one that points at a node that has been deleted, an Area that can never notice anything, a call like set_number or anneal_number that names a component or field that is not there, and a spawn of a name the node’s Spawner cannot answer. A call that names a component that does not exist, or a field its component does not have, is an error. One that names a component the node does not carry is a warning, and so is a spawn of a name the Spawner cannot answer. These read names written out in quotes, once every script parses, in the scripts on the room’s nodes and in those inside the artifacts it can spawn; a row about one of those sits on the node in the room whose Spawner starts the chain, the one that spawns the artifact or the copy that spawns it, and names the artifact. A name that reaches the call some other way is caught when the call runs, by a line in the Console.

Stop the room when it goes wrong

When something goes wrong only now and then, have the script stop the room at the moment it does. break_here pauses Play at the end of the step it is called in, and says why:

rgs watchdog.rgs
/// Pauses the room if the node it is on falls out of it.
component Watchdog {
    /// How far down counts as out of the room, in world units.
    #[suffix("u")]
    floor: f32 = 400.0,

    fn on_position_change() {
        if new_position.y > floor {
            break_here("fell out of the room");
        }
    }
}
The editor paused during a run, the toolbar showing a warning that reads fell out of the room, with Resume, Step and Select node beside it, and a crate carrying Watchdog selected low in the room, at y 150 with its Floor set to 100 u
The toolbar says why the room stopped. Select node selects the node whose script asked.

The banner stays until you press Resume or Stop, and Step runs one more step without clearing it. The Console keeps a line for it too. The rest of the step still runs: break_here does not stop the hook it is in.

An exported game does not pause. It writes a line to its log instead, with the reason in it, and Run game shows that line in the Console.

Next

Input makes the controls your scripts read. The script reference has every hook, builtin, type and attribute, with its exact signature. Characters and physics has scripts that read the controls, a character landing, and a plate wired to a door.