Skip to content

Tutorials · Animation and Cutscenes · Chapter 5

Orders from a script

30 minutes Uses: StateMachine, PropertyAnimation, AudioSource, Area, Sprite, Signal, emit, heard, heard_value, in_group, pressed, play_property_clip, play_sound, set_flag, set_number, state, trigger

By the end

  • An attack action on J and X, and two triggers, hurt and slash, that the knight’s graph takes from any state
  • knight.rgs: J slashes, the spikes hurt him once a hurt has played out, and the hurt’s recoil lifts him
  • spikes, a row of spikes at 720, 288, and a group each for the knight and the spikes
  • lever.rgs and trapdoor.rgs: the blade pulls the lever once, it clicks and tells the trapdoor, which bounces open on its own clip and drops what stood on it
On this page

An attack key

Until now the knight’s clip has been chosen by what his body does: his speed, and whether he stands on something. From here a script gives him orders. Each script is a short file of Rengy Script, which the editor runs as it stands; Rengy Script covers the language, and each page of this part prints its scripts whole and says what each line is for.

The first order is a slash on a key. A script never asks whether J is down. It asks about an action, a name for what the player wants, and the project’s input map says which keys give it. The knight’s body reads A, D, the arrow keys and Space by itself, so the show’s input map starts with one action.

  1. Choose View → Input map…. With no input map saved yet, the window starts on six actions of its own, left, right, up, down, jump and action, which nothing in this show reads. Select each of the six in turn and press Remove.
  2. Type attack into New action and press Add. With attack selected, press Record a control and press J. Press Record a control again and press X.
  3. Press Save, and close the window. It writes input.ron into the project.
The Input window with one action, attack, selected; under Bindings for attack are two controls, J and X, each set to Press
Press
What a control does with the key. A Press counts on the step the key goes down, which is what pressed asks about, and not again until it has come up and gone down again. Holding J does not slash twice.

Triggers in the graph

A script does not play a clip: the graph chooses the clip, as chapter 2 said. What a script can do is raise a trigger, a named flag that lives for one step, and the graph can have an edge that waits for it. The knight gets two: slash, for a swing, and hurt, for a hit. Each gets a state that plays the clip of its name.

  1. Select knight and click the Graph tab. It shows knight.asm.
  2. In the Parameters list, type hurt into the name box, choose trigger in the list of kinds beside it and press Add. Do the same for slash. Both rows read trigger, lowered.
  3. In the row over the canvas, type hurt into State and hurt into Clip, and press Add state. Do the same with slash and slash. Drag the two new boxes to the right of the others, where no arrow crosses them.
  4. Click the Any State box, press Link and click hurt. In the Transitions list, the new row, Any State to hurt, goes at the end: its number, at the row’s left, reads 7. While Any State is selected the list shows only its own arrows, and the number is the arrow’s place in the whole list. Choose Trigger in its row and type hurt in the name box under it. Press the up arrow at the row’s left until the number at its left reads 0.
  5. Click Any State again, press Link and click slash. Choose Trigger and type slash. Press its up arrow until its number reads 1.
  6. Click slash, press Link and click idle. Choose Finished and set its Blend, the box with an s beside the names, to 0.05. Click hurt, press Link and click idle. Choose Finished; its blend stays 0.
  7. Press Save.
The Graph tab on the knight’s graph: boxes for idle, run, air, land, hurt and slash, and the dashed Any State box at the top left with an arrow to hurt labelled trigger hurt and another to slash labelled trigger slash; the Parameters list at the right names grounded, hurt, slash, speed, velocity_x and velocity_y, hurt and slash marked trigger and lowered
Trigger
A kind of parameter, and a kind of condition. A trigger is raised from a script with trigger, and it stays raised for one step: whichever edge reads it takes it, and after that step it is gone, whether an edge took it or not. An edge whose condition is Trigger holds on the step the named trigger is up.
Any State
The dashed box. An edge from it can fire in every state but the one it enters, so hurt can be raised from idle, run, air or land without an edge from each.
Parameters
The list at the right of the graph. A parameter is a value a condition reads. grounded, speed, velocity_x and velocity_y are the body’s, marked the body, and hurt and slash are yours: declared, they read lowered, which is what a trigger is until it is raised.

The order of the list matters, and the two new rows are the first two for a reason. Edges are tried in order, and the first whose condition holds wins. A trigger lives one step. If idle → run came first and also held on the step hurt was raised, it would take the step, and the trigger would be gone before the Any State edge was tried.

The Transitions list, headed First match wins: row 0, Any State to hurt, 0.00 s, when Trigger hurt; row 1, Any State to slash, when Trigger slash; then idle to air and run to air, each when grounded is false; idle to run at 0.10 s when speed is above 10; run to idle at 0.15 s when speed is below 10; air to land when Finished; land to run at 0.10 s; land to idle at 0.10 s when Finished; and the top of the next row, slash to idle at 0.05 s
The list after the chapter, the first nine rows of eleven. The two Any State rows are first.

The graph, as the chapter leaves it

The graph holds the states idle, run, air (with rise and fall inside), land, and now hurt and slash. The entry is idle. These are the parameters and the new states, then the whole list of edges in order. Chapter 2’s edges moved down two places, and their conditions are the same.

KindNameValue
Parameterhurttrigger
Parameterslashtrigger
Statehurtclip hurt
Stateslashclip slash
RowFromToConditionBlend
0Any Statehurttrigger hurt0
1Any Stateslashtrigger slash0
2idleairgrounded is false0
3runairgrounded is false0
4idlerunspeed > 100.1
5runidlespeed < 100.15
6airlandfinished0
7landrunspeed > 100.1
8landidlefinished0.1
9slashidlefinished0.05
10hurtidlefinished0

The knight’s orders

The knight’s script does three things. When J or X goes down it raises slash, unless he is already hurt. When his own Area meets the spikes it raises hurt, once. And when the hurt clip reaches its recoil frame, the number that frame carries, the 220 you put on it in chapter 4, lifts him.

A script on an Area’s node hears what the Area meets, so he needs one first, about the size of his body. Then the script, and then the spikes.

  1. Select knight, press Add component and choose AreaComponent. Set the Shape’s Size to 12 and 30, and its Offset to 0 and -15: the same box as his Character Body, standing on his feet. Leave Solid unticked: an Area is felt, never bumped into.
  2. Under Groups, type knight and press Enter.
  3. In Assets, press Create, choose New script, type knight, and press Enter. In the Code tab, replace everything in knight.rgs with the script below, and press Ctrl+S.
rgs knight.rgs
/// The knight's orders: J or X slashes, unless he is hurt. The spikes hurt
/// him, and not again until his hurt clip has played out; its recoil frame
/// lifts him by the number it carries.
component Knight : Area {
    /// Whether his hurt clip is still playing.
    #[hidden]
    hurting: bool = false,

    fn while_in() {
        if in_group(other, "spikes") && !hurting {
            hurting = true;
            trigger("hurt");
        }
    }

    fn on_signal() {
        if heard("frame:recoil") {
            set_number("CharacterBodyComponent", "velocity.y", -heard_value("frame:recoil"));
        }
        if heard("finished:hurt") {
            hurting = false;
        }
    }

    fn loop() {
        if pressed("attack") && state() != "hurt" {
            trigger("slash");
        }
    }
}
  1. Select knight, press Add component and choose Knight. Its one field, hurting, is hidden: the script keeps it, and it starts false.
component Knight : Area
A script with an Area base: it goes on a node that has an Area, and it hears what that Area meets. while_in runs on every step that another Area is still overlapping it, with that Area’s NodeId as other.
in_group
Whether the node it is given was in the group as the step began. in_group(other, "spikes") is true while the Area that is overlapping the knight’s belongs to a node in the group spikes. It is how a script tells what it has met.
trigger
Raises the named trigger for this node’s graph on the next step, and it is gone after that step. trigger("hurt") is what the Any State edge to hurt waits for.
on_signal and heard
on_signal runs on a step when a signal has reached this node. heard("frame:recoil") is true on that step if the signal was that one. Several signals can reach a node on one step, and the script asks about each.
frame:recoil
The signal a frame’s Trigger track raises: frame: and the track’s name. It arrives the step the clip shows that frame. hurt’s recoil track is on its frame 1, carrying 220, the number you gave it in chapter 4.
heard_value
The number the signal carried: 220 for frame:recoil, and 0 if the signal carried none or was not heard.
set_number
Writes a number onto another component on this node. "velocity.y" reaches the y part of the Character Body’s velocity. Down counts as positive, so -220 is up, at 220 units a second: a hop of about a dozen units, which is what the hurt’s lift is.
finished:hurt
The signal a clip raises the step it plays out: finished: and the clip’s name. The script clears hurting on it, which is how it knows the hurt is over.
loop
Runs on every step while the node exists. The knight’s asks there whether attack went down, since pressed is true for one step only.
state
The name of the state the graph was in as the step began: hurt, idle, or air/rise inside a sub-graph. It is empty if the node has no graph.
pressed
Whether the action went down on this step. pressed("attack") is true on the one step J or X goes down.

The spikes

The spikes are a picture with an Area on it, in the group spikes. spikes.png came with the starter, in art.

  1. Select theatre, press + Add node and choose Sprite. Rename it spikes, set its Position to 720 and 288, and under Groups type spikes and press Enter.
  2. In its Sprite, drag art/spikes.png from Assets onto Texture, and set Size to 32 and 12, Anchor to 0.5 and 1 and Sort Layer to 4.
  3. Press Add component and choose AreaComponent. Set the Shape’s Size to 28 and 6, and its Offset to 0 and -3: a box over the lower half of the picture, a little in from each side, so a jump that grazes the tips does not hurt.
  4. In the Hierarchy, drag spikes up until it sits just under platform.
spikes selected and framed: the Viewport shows four grey-purple spikes much enlarged, a yellow rectangle, the Area, over their lower half, with the move arrows at its foot; the Inspector shows Name spikes, the group spikes, Position x 720.0, y 288.0, and the AreaComponent’s Shape: Kind Rectangle, Size x 28.0, y 6.0, Offset x 0.0, y -3.0, Solid and One Way unticked, Layer 1; the Hierarchy lists spikes between platform and lever
Groups
Names on a node, shown as tags by its name. A node can be in several. A group is how a script knows what it has met: knight, spikes and, from chapter 4, blade.
Size and Offset
An Area’s Shape: the box it covers, in world units, Size wide and tall, with its centre Offset from the node. The knight’s 12 by 30 at 0, -15 stands on his feet. The spikes’ 28 by 6 at 0, -3 covers the lower half of their 32 by 12 picture, a little in from each side.

A lever that opens the trapdoor

The lever stands in the wings, and the trapdoor lies in the boards beyond it, shut over a pit. The blade pulls the lever, the lever tells the trapdoor, and the trapdoor drops. Each is a node with a short script, and the telling is a Signal component, wired in the Inspector rather than in code.

The lever

  1. Copy the two sounds above into audio, and press Refresh in Assets.
  2. Select lever, press Add component and choose AreaComponent. Set the Shape’s Size to 16 and 28, and its Offset to 0 and -14.
  3. Press Add component and choose SignalComponent. Press the + beside Connections, open the new row, set Signal to pulled and To to trapdoor. The row remembers the trapdoor itself, so renaming it later does not break the row.
  4. Press Add component and choose AudioSourceComponent. Leave Bus on Sfx and Autoplay off. Press the + beside Triggers, open the new row, set Name to click and Clip to metalClick.ogg.
  5. In Assets, press Create, choose New script, type lever, and press Enter. In the Code tab, replace everything in lever.rgs with the script below, and press Ctrl+S. Then select lever, press Add component and choose Lever.
rgs lever.rgs
/// The lever. The knight's blade pulls it, once: it clicks, and tells
/// whoever the scene connected to it that it was pulled.
component Lever : Area {
    /// Whether it has been pulled.
    #[hidden]
    pulled: bool = false,

    fn on_enter(other: NodeId) {
        if in_group(other, "blade") && !pulled {
            pulled = true;
            play_sound("click");
            emit("pulled");
        }
    }
}
The lever selected, its Inspector narrowed with the search box, signal, to its Signal: Connections with one item, [0] opened, Signal pulled and To trapdoor; the Viewport shows the hall at its own size, the lever outlined with its move arrows beside a dark gap in the boards, the pit the trapdoor covers
on_enter
Runs once, on the step another Area starts overlapping this one, with that Area’s NodeId. The blade’s Area is live only on the slash’s hitbox frames, so it enters the lever’s when the swing reaches it, and that is the moment the lever is pulled.
play_sound
Plays the clip a trigger of this node’s Audio Source is named for: play_sound("click") plays metalClick.ogg, the Clip of the row named click. It is the same list the frame events used in chapter 4.
emit
Tells whoever the scene connected: a Signal row with this name, pulled, on this node, sends it to the node in its To. The trapdoor hears it on the next step, never in the same one, so neither needs to know about the other.
Signal and To
A row of the Signal component’s Connections: the name of what this node emits, spelled as the script emits it, and the node that hears it. A node can have several rows, and several can share a name: chapter 6 adds two more pulled rows.

The trapdoor

The trapdoor is a plank lying across the pit. Until the lever is pulled it is solid, and he stands on it. On the signal its script makes it not solid, so it holds nothing up, and plays its own clip: the lid swings down on its hinge, bounces as it hits the pit’s wall, and settles. A property clip does this: it keys the node’s Rotation, as chapter 3’s did the curtain’s position.

  1. Select trapdoor, press Add component and choose PropertyAnimationComponent. Click the Animation tab, then its Properties mode, and press New property clips. It writes scenes/trapdoor.propanim and sets the component’s Clips to it.
  2. Press + and name the new clip open. Switch Record on: from now on, a value you change in the Inspector is keyed into the open clip, at the playhead, and the trapdoor itself is not changed.
  3. Set the time box, 0.00 s, beside the Play and Stop buttons, to 0.35, the playhead, and set Rotation in the Inspector to 83.08, a little more than a quarter turn. A row, rotation, appears with a key at 0.35, and the Inspector and the Viewport show the pose. The clip is 0.35 seconds long, the time of its last key.
  4. Set the playhead to 0. A track holds its first key’s value until the playhead reaches it, so Rotation reads 83.08. Set it to 0: the second key. Switch Record off.
  5. In the rotation row, choose Eased in the first list and OutBounce in the second. A recorded track starts as Linear.
  6. Press Capture RESET. The mode adds a clip called RESET with one key at 0, holding the rotation the scene gives the trapdoor, 0, whatever the playhead says.
  7. In the component, set Clip to open and Looping to Once. Playing stays off: the clip waits for its script.
  8. Press Add component and choose AudioSourceComponent. Press the + beside Triggers, open the new row, set Name to open and Clip to impactPlank_medium_000.ogg.
  9. In Assets, press Create, choose New script, type trapdoor, and press Enter. In the Code tab, replace everything in trapdoor.rgs with the script below, and press Ctrl+S. Then select trapdoor, press Add component and choose Trapdoor, and press Ctrl+S.
rgs trapdoor.rgs
/// The trapdoor. When the lever is pulled it stops holding anything up and
/// drops open with a bang.
component Trapdoor {
    fn on_signal() {
        if heard("pulled") {
            set_flag("ColliderComponent", "solid", false);
            play_property_clip("open");
            play_sound("open");
        }
    }
}
The Animation tab in its Properties mode on the trapdoor: two clip tabs, open, lit, and RESET; the playhead at 0.20 s; one row, rotation, with Eased and OutBounce chosen, and two keys on its lane, one at the start and one at 0.35 s, the playhead between them
The editor with the open clip’s playhead at 0.2 s: the Viewport, framed on the trapdoor, shows its plank tipped steeply down towards the lower right; the Inspector reads Rotation 62.73° and the Audio Source’s one trigger; below, the Animation tab shows the rotation row with its two keys
The same moment as a whole picture. At 0.2 seconds the lid has hit once and bounced back up to 62.73 degrees.
set_flag
Writes a tick box of another component on this node. set_flag("ColliderComponent", "solid", false) unticks the trapdoor’s Solid: from the next step nothing stands on it.
play_property_clip
Plays one of this node’s Property Animation clips from its start, the one already playing or one that has finished too. It does not need Playing on: the call starts the clip.
OutBounce
An ease that settles with decaying bounces, like something dropped on a floor: it reaches the last key early, falls back from it and reaches it again, each bounce smaller. The lid hits the pit’s wall, bounces up a little, and settles on 83.08 degrees at 0.35 seconds.

The scene, as the chapter leaves it

NodeComponentFieldValue
knightNodeGroupsknight
AreaComponentShape → Size12, 30
Shape → Offset0, -15
Knighthurtinghidden, starts false
spikesNodeParenttheatre, under platform
Groupsspikes
Position720, 288
SpriteComponentTexturespikes.png
Size32, 12
Anchor0.5, 1
Sort Layer4
AreaComponentShape → Size28, 6
Shape → Offset0, -3
leverAreaComponentShape → Size16, 28
Shape → Offset0, -14
SignalComponentConnections: Signal, Topulled, trapdoor
AudioSourceComponentTriggers: Name, Clipclick, metalClick.ogg
Leverpulledhidden, starts false
trapdoorPropertyAnimationComponentClipstrapdoor.propanim
Clipopen
LoopingOnce
AudioSourceComponentTriggers: Name, Clipopen, impactPlank_medium_000.ogg
Trapdoorevery fieldas it comes
FileClipLengthTrackKeys
trapdoor.propanimopen0.35 srotation: Eased, OutBounce0 → 0; 0.35 → 83.08
RESET0 srotation: Linear0 → 0
FileActionKeys
input.ronattackKeyJ, KeyX

Check it works

The knight at the left of the stage with the curtain raised, in the middle of a slash: his sword arm out in front of him and a pale curved sweep ahead of the blade, his red cape flying behind; five footlights burn along the boards, and the window and a lantern on either side stand behind
J at the left of the hall: the slash, at the step its blade is out.
Six close pictures of the knight running into the spikes, side by side: running, cape streaming; mid-stride with the spikes coming in at the right; bowed over with his cape over his shoulder beside the spikes; thrown up off the boards with his head back; landing upright; and running on
The spikes, in six steps of one run, D held from the start: steps 140, 148, 156, 162, 168 and 174, 2.33 to 2.9 seconds in. He is hurt as he reaches them, the recoil lifts him at about 162, and he lands running.
Four close pictures side by side of the knight’s sword sweep reaching the lever while the plank lid over the pit swings: in the first the sweep crosses the lever and the lid lies flat over the pit; in the second the lid is tipped down at its right end; in the third it hangs steeply; in the fourth it hangs almost straight down. The lever’s handle does not move
The lever, in four steps of one run: steps 228, 232, 236 and 252, 3.8, 3.87, 3.93 and 4.2 seconds in. He jumped the spikes at step 134 and pressed J at 220.