Skip to content

Tutorials · Animation and Cutscenes · Chapter 6

Tweens

25 minutes Uses: Signal, AudioSource, anneal_scale, anneal_turn, clamp, ease, grounded, heard, play_sound, pulse_by, quench, vec2, velocity

By the end

  • squash.rgs: the knight lands flattened by how fast he fell, and springs back
  • handle.rgs: the lever’s handle snaps over, past its mark and back, with a creak, when the lever is pulled
  • bob.rgs: the platform rises and falls for good, until the lever is pulled and stops it where it stands
  • The curves a tween can follow, and which of them these three use
On this page

Squash on landing

In chapter 3 you keyed a number on a clip: where it should be, and when. A script can move a number without a clip, by asking for a tween: from where it is to where you say, over so many seconds, along a curve. The engine calls the ask an anneal, and a script makes one with a single call, which then runs on its own: you do not tick it. This chapter has three, each on a node that already exists, and none of them touches a clip.

The first is a landing. The knight’s land clip is a crouch, but a landing wants more weight than one pose gives it. So on landing a script flattens his whole node, wider and shorter, in a twentieth of a second, and springs it back over a quarter of one. The faster he fell, the harder the squash.

  1. In Assets, press Create, choose New script, type squash, and press Enter. In the Code tab, replace everything in squash.rgs with the script below, and press Ctrl+S.
rgs squash.rgs
/// Squashes the knight as he lands, the harder the faster he fell, then
/// springs him back.
component Squash : Body {
    /// How fast he was falling the step before he landed.
    #[hidden]
    falling: f32 = 0.0,

    fn on_land() {
        let amount = ease(.OutQuad, clamp(falling / 900.0, 0.0, 1.0));
        await anneal_scale(vec2(1.0 + 0.3 * amount, 1.0 - 0.3 * amount), 0.05, .OutQuad);
        anneal_scale(vec2(1.0, 1.0), 0.25, .OutBack);
    }

    fn loop() {
        if !grounded() {
            falling = velocity().y;
        }
    }
}
  1. Select knight, press Add component and choose Squash. Its one field, falling, is hidden: the script keeps it, and it starts 0.
component Squash : Body
A script with a Body base: it goes on a node that has a Character Body, which the knight has, and it hears the body’s events.
on_land
A Body hook. It runs on the step the body touches the ground after being in the air: once for each landing.
grounded and velocity
grounded() is whether the body stood on something as the step began. velocity() is how fast it was moving then, in world units a second, and down is positive. loop keeps velocity().y in falling on every step he is in the air.
clamp
clamp(falling / 900.0, 0.0, 1.0) is the fall speed as a share of the body’s terminal speed, 900, kept between 0 and 1: a standing fall is 0, the fastest fall 1.
ease
Runs a number from 0 to 1 through a curve and gives back where it lands, at once, with no waiting. ease(.OutQuad, …) bends the share so that a middling fall already counts for most of it: a jump lands at about 620, a share of 0.69, which comes out as 0.9.
vec2
Builds a Vec2, a pair of numbers, from two: here a scale, in squash.rgs, and an offset, in bob.rgs. vec2(1.0, 1.0) is his own size: no squash at all.
anneal_scale
Scales this node to the Vec2 it is given, over the seconds it is given, along a curve. The knight’s node stands on his feet, so he is flattened onto the boards, not into them. vec2(1.0 + 0.3 * amount, 1.0 - 0.3 * amount) is wider by up to three tenths and shorter by the same.
await
In front of an anneal it stops the hook until the anneal has finished, and then carries on. The squash is awaited, so the spring back starts only when it is done. The spring back is not: the hook ends there, and the anneal runs on by itself.
.OutQuad and .OutBack
Curves, written with a leading point. .OutQuad ends slow: the squash gets to its depth quickly and eases into it. .OutBack overshoots and settles: the knight springs past his height by a little and comes back to it.
Six close pictures of the knight landing from a jump, side by side: in the air with his cape flung up behind him; in a crouch on the boards at the landing; flattened and wide, the widest and lowest of the six; coming back up, a little taller; blurred between the crouch and standing as one clip hands over to the next; and standing upright, his cape hanging behind him
The landing, in six steps of one run, Space held from step 10 to 45: steps 49, 51, 53, 56, 59 and 66, 0.82 to 1.1 seconds in. The third picture is about the deepest squash, 0.27.

A lever that snaps

The lever of chapter 5 clicks and drops the trapdoor, but its handle has stood still. Now the handle is thrown over when the lever is pulled, with a creak. It is a node of its own, the lever’s child, lever_handle, so it has its own script, and the lever tells it with a second Signal row.

  1. Copy creak1.ogg above into audio, and press Refresh in Assets.
  2. Select lever. In its SignalComponent, press the + beside Connections twice more. Open the second row and set Signal to pulled and To to lever_handle. Open the third, set Signal to pulled and To to platform: the third is for the last section.
  3. Select lever_handle, press Add component and choose AudioSourceComponent. Leave Bus on Sfx. Press the + beside Triggers, open the new row, set Name to creak and Clip to creak1.ogg.
  4. In Assets, press Create, choose New script, type handle, and press Enter. In the Code tab, replace everything in handle.rgs with the script below, and press Ctrl+S. Then select lever_handle, press Add component and choose Handle.
rgs handle.rgs
/// The lever's handle: thrown over when the lever is pulled, past its mark
/// and back, with a creak.
component Handle {
    fn on_signal() {
        if heard("pulled") {
            play_sound("creak");
            anneal_turn(-1.0, 0.4, .OutBack);
        }
    }
}
anneal_turn
Turns this node by an amount in radians, over the seconds it is given, along a curve. -1.0 is one radian, about 57 degrees, anticlockwise: the handle starts tilted 28.65 degrees to the right, and ends up as far to the left. .OutBack takes it past that, and back.
heard("pulled")
The same signal the trapdoor heard in chapter 5: all three nodes the lever’s Signal names get it, on the same step, and each script reacts in its own way. play_sound plays the creak trigger.
Six pictures of the lever beside the knight, side by side: the handle tilted to the right; the knight’s sword sweep reaching it, the handle upright; the handle leaning left a little; leaning further, past where it will rest; much the same, at the far end of its swing; and, in the last, the knight’s head and shoulder passing in front of the handle, which has come back a little and rests leaning left
The handle, in six steps of the run from chapter 5: steps 226, 232, 236, 240, 246 and 258, 3.77 to 4.3 seconds in. It is upright in the second picture, swings past its mark in the fourth and fifth, and has come back to it, 28.65 degrees to the left, in the last.

A platform that bobs

The stone platform in the middle of the hall, at 872, 232, between the spikes and the lever, is solid, and a jump reaches it. It should not be still. A pulse is an anneal that turns round at the end and does it again, for good; it never finishes, and nothing can await it. The script starts one when the platform is made, and stops it when the lever is pulled.

  1. In Assets, press Create, choose New script, type bob, and press Enter. In the Code tab, replace everything in bob.rgs with the script below, and press Ctrl+S. Then select platform, press Add component and choose Bob, and press Ctrl+S.
rgs bob.rgs
/// The platform bobs up and down for good, until the lever is pulled: then
/// it stops where it stands.
component Bob {
    fn on_create() {
        pulse_by(vec2(0.0, -24.0), 1.5, .InOutSine);
    }

    fn on_signal() {
        if heard("pulled") {
            quench();
        }
    }
}
on_create
Runs once, on the first step the node exists: for a node in the room, the first step after you press Play.
pulse_by
Moves this node by a Vec2 and back, forever, taking the seconds it is given each way. vec2(0.0, -24.0) is 24 units up, since y grows downward, and 1.5 seconds each way makes a cycle of 3. .InOutSine is soft at both ends, so the platform slows as it turns.
quench
Stops every anneal and pulse on this node, wherever each stands. The platform stays wherever it was when the signal arrived, anywhere between its lowest and its top, and does not return.
Six close pictures, half a second apart, of the platform, a block of three mossy stones: at its lowest; a little higher; higher; at its highest, near the top of the picture; coming down; and lower again
The platform, half a second apart in one run: steps 180, 210, 240, 270, 300 and 330, 3 to 5.5 seconds in. A cycle is 3 seconds: it is at the bottom at 180, at the top at 270, and on its way down, a quarter of its height above the bottom, at 330.

The curves

Every anneal, every pulse and the ease call take a curve, written with a point: .OutQuad. The set is Easing, with a picture of each. A name is a family and a form. The families are these:

FamilyIts shape
LinearNo shaping. Constant speed. It has no forms.
SineThe gentlest. .InOutSine is the softest S-curve, the bob’s.
QuadThe workhorse. .OutQuad ends slow, the squash’s.
Cubic, Quart, QuintThe same shape, each steeper than the one before.
ExpoHolds near one end, then leaps.
CircAlong a quarter of a circle, not a power.
BackPulls back before it moves, or overshoots and settles. .OutBack is the spring-back’s and the handle’s.
ElasticSprings past the end and rings down, like a plucked string.
BounceSettles with decaying bounces. .OutBounce is the trapdoor’s, in chapter 5.
SpringOvershoots and settles with a wobble.

Each family but Linear comes in four forms, by where the curve is slow. In starts slow. Out ends slow. InOut is slow at both ends, and quickest in the middle. OutIn is quick at both ends, with a pause in the middle. So .InQuad, .OutQuad, .InOutQuad and .OutInQuad are the Quad family. Chapters 3 and 5 gave keys three of these names as their ease: InOutCubic, InOutSine and OutBounce.

The scene, as the chapter leaves it

NodeComponentFieldValue
knightSquashfallinghidden, starts 0
leverSignalComponentConnections, second row: Signal, Topulled, lever_handle
Connections, third row: Signal, Topulled, platform
lever_handleAudioSourceComponentTriggers: Name, Clipcreak, creak1.ogg
Handleevery fieldas it comes
platformBobevery fieldas it comes

The first row of the lever’s Connections, pulled to trapdoor, is chapter 5’s.

Check it works