Tutorials · Ship Evader · Chapter 10
Getting harder
By the end
- A director that moves the game up a stage every 20 s, to stage 10
- Gaps, a share of gunners and speeds worked out from the stage by everything that reads it
- A STAGE banner that fades in, holds and fades out when the stage changes
- Why a changed default does not reach a value a file already stores
On this page
A director counts the stages
One node keeps the stage, and everything that gets harder reads it. The director counts its own seconds, moves the game up a stage every 20, keeps the stage for the rest to read, and tells the HUD. Make a script called director:
- In Assets, press Create, choose New script, type
director, and press Enter. The editor writesdirector.rgsinto the project folder. - Click the Code tab, then
director.rgsin its Scripts list, replace everything in it with the script below, and press Ctrl+S.
/// The director: it moves the game up a stage every 20 s, up to the last, and
/// keeps the stage for whatever gets harder with it. The sky reads it for its
/// gaps and its share of gunners, and each enemy and meteor for how fast it
/// comes down. Each time the stage changes, the director tells the HUD, whose
/// banner shows it.
///
/// It counts the seconds itself, in a field of its own. A new game is a fresh
/// copy of the scene, with a fresh director, so it starts again at stage 1.
component Director {
/// Seconds each stage lasts. At least 2: a stage that begins while the
/// banner still shows the last one is never announced, and the banner shows
/// each stage for 1.6 s.
#[suffix("s")]
#[range(2..)]
stage_time: f32 = 20.0,
/// The last stage. The game gets no harder after it.
// A number with decimals, though it only ever counts, because it is
// compared with the stage, which the game keeps as one.
#[range(1..)]
last_stage: f32 = 10.0,
/// The stage the game is at: 0 until the first step begins stage 1.
#[hidden]
stage: f32 = 0.0,
/// Seconds until the next stage begins.
#[hidden]
next_stage: f32 = 0.0,
fn loop(dt: f32) {
// Once the last stage has begun, there is nothing left to count to. And
// once the last life is lost the game is over: no stage begins while it
// waits to start again. The first game has kept no lives yet, and has 3.
if stage < last_stage && kept_number("lives", 3.0) > 0.0 {
next_stage -= dt;
if next_stage <= 0.0 {
stage += 1.0;
// A stage's worth of seconds more. Added rather than set, as
// the ship counts its seconds: what the step ran past carries
// into the next stage, so the stages never fall behind.
next_stage += stage_time;
// Kept, like the score, so that any script can read it. It
// lands at the end of this step.
keep_number("stage", stage);
// Everything in the group "hud" hears it on the next step, by
// when the stage kept here has landed.
emit_to("hud", "stage");
}
}
}
}
- Click the Viewport tab, then select
Game, press + Add node, choose Node, and rename itdirector. - Press Add component and choose Director, keeping Stage Time
20and Last Stage10.
It counts only while the ship has lives, so no stage begins once the last is lost. A new game is a fresh copy of the scene, with a fresh director, so it starts again at stage 1. Click the Code tab, then game.rgs in its Scripts list, and keep the stage at 1 beside the lives and the score, before the scene changes, so the sky reads stage 1 on the new game’s first step:
fn loop() {
await signal("game over");
await wait(restart_delay);
// Kept values outlast the scene, so the new game's are set here,
// before the scene changes, and not by the new scene as it starts: it
// begins with them already in place. The stage too: the sky reads it on
// the new game's first step, before the new director has kept one. The
// best score so far is saved for the next launch too.
save_number("best", max(saved_number("best", 0.0), kept_number("score", 0.0)));
keep_number("lives", 3.0);
keep_number("score", 0.0);
keep_number("stage", 1.0);
change_scene("game");
}
- Press Ctrl+S to save
game.rgs.
Sooner, and more gunners
The sky works out its gaps and its share of gunners from the stage, with fields of its own for how much each stage changes them.
- Click the Code tab, then
sky.rgsin its Scripts list, replace everything in it with the script below, and press Ctrl+S.
/// The sky: at stage 1 it drops a meteor at once and then one every 1.6 s,
/// each of a random size, and an enemy at once and then one every 2.0 s, each
/// somewhere along the top of the field. At each stage past the first it drops
/// them a little sooner, and more of the enemies are gunners, from none at
/// stage 1.
component Sky {
/// Seconds between two meteors at stage 1.
#[suffix("s")]
#[range(0.1..)]
meteor_interval: f32 = 1.6,
/// Seconds until the next meteor.
#[hidden]
meteor_countdown: f32 = 0.0,
/// Seconds between two enemies at stage 1.
#[suffix("s")]
#[range(0.1..)]
enemy_interval: f32 = 2.0,
/// The share of the enemies that are gunners at stage 1: 0 is none of
/// them, and 1 is every one.
#[range(0.0..1.0)]
gunner_share: f32 = 0.0,
/// Seconds until the next enemy.
#[hidden]
enemy_countdown: f32 = 0.0,
/// How much shorter each stage past the first makes the gap between two
/// meteors.
#[suffix("s")]
#[range(0.0..)]
meteor_interval_cut: f32 = 0.12,
/// How much shorter each stage past the first makes the gap between two
/// enemies.
#[suffix("s")]
#[range(0.0..)]
enemy_interval_cut: f32 = 0.15,
/// How much each stage past the first adds to the share of gunners.
#[range(0.0..1.0)]
gunner_share_growth: f32 = 0.05,
/// The largest share of the enemies that are gunners, however far the
/// stages go.
#[range(0.0..1.0)]
most_gunners: f32 = 0.4,
fn loop(dt: f32) {
// The stage the director keeps, or 1 until it has kept one. Each stage
// past the first shortens the gaps and brings more gunners; at stage 1
// this is 0.
let stages_past_first = kept_number("stage", 1.0) - 1.0;
meteor_countdown -= dt;
if meteor_countdown <= 0.0 {
// Anywhere from the left edge (view.x) to the right one (view.z),
// 50 units above the top (view.y): up is toward negative y.
let view = camera_view();
let at = vec2(random(view.x, view.z), view.y - 50.0);
// Each of the three sizes as likely as the others.
let size = random(0.0, 3.0);
if size < 1.0 {
spawn("meteor_big", at);
} else if size < 2.0 {
spawn("meteor_medium", at);
} else {
spawn("meteor_small", at);
}
// A little sooner at each stage past the first, but never sooner
// than 0.1 s, however many stages there are: the sky never drops
// something on every step.
let gap = meteor_interval - meteor_interval_cut * stages_past_first;
meteor_countdown = max(gap, 0.1);
}
enemy_countdown -= dt;
if enemy_countdown <= 0.0 {
// Along the top, the way a meteor comes in.
let view = camera_view();
let at = vec2(random(view.x, view.z), view.y - 50.0);
// A little more of them gunners at each stage past the first, up to
// the most there can be. random(0.0, 1.0) is below 0.25 a quarter
// of the time, so a share of 0.25 makes one enemy in four a gunner.
let share = gunner_share + gunner_share_growth * stages_past_first;
if random(0.0, 1.0) < min(share, most_gunners) {
spawn("gunner", at);
} else {
spawn("diver", at);
}
// Sooner too, and never sooner than 0.1 s, as the meteors are.
let gap = enemy_interval - enemy_interval_cut * stages_past_first;
enemy_countdown = max(gap, 0.1);
}
}
}
Each stage past the first takes 0.12 s off the gap between rocks and 0.15 s off the gap between enemies, from 1.6 s and 2.0 s at stage 1 to 0.52 s and 0.65 s at stage 10. max keeps either gap at 0.1 s or more, whatever the Inspector says. The share of gunners grows by 5% a stage from none, and min holds it at 40% at most.
- Click the Viewport tab, then select
sky. Its Sky shows four new rows: Meteor Interval Cut0.12, Enemy Interval Cut0.15, Gunner Share Growth0.05and Most Gunners0.4. - Type
0into its Gunner Share, press Enter, and press Ctrl+S.
Faster, too
Each enemy and rock reads the stage as it arrives and comes down faster: 6% faster for each stage past the first, 1.54 times as fast at stage 10. Only what comes down speeds up: a gunner’s weave, a rock’s drift and spin, and the shots and lasers keep their speeds.
- In the Code tab’s Scripts list, click
diver.rgs, replace everything in it with the script below, and press Ctrl+S.
/// A diver: it dives straight down at a speed of its own, faster at each stage
/// past the first, and removes itself once it is 100 units past the bottom.
component Diver {
/// The slowest it may dive at stage 1.
#[suffix("u/s")]
#[range(1..)]
slowest_dive: f32 = 240.0,
/// The fastest it may dive at stage 1.
#[suffix("u/s")]
#[range(1..)]
fastest_dive: f32 = 320.0,
/// How fast it dives, rolled when it is made.
#[hidden]
dive_speed: f32 = 0.0,
/// How much faster it dives at each stage past the first: 0.06 is 6%
/// faster a stage.
#[range(0.0..)]
speed_up: f32 = 0.06,
fn on_create() {
// How many times as fast as at stage 1 it dives: 1 at stage 1, and
// speed_up more at each stage past it. The director keeps the stage,
// and until it has kept one the game is at stage 1.
let pace = 1.0 + speed_up * (kept_number("stage", 1.0) - 1.0);
dive_speed = random(slowest_dive, fastest_dive) * pace;
}
fn loop(dt: f32) {
// Down is toward positive y: the world's y grows downward.
translate(vec2(0.0, dive_speed * dt));
// camera_view().w is the bottom edge of the field. `#[range(1..)]` on
// both dive speeds means every diver gets past it, and a stage only
// ever makes it faster.
if position().y > camera_view().w + 100.0 {
destroy_self();
}
}
}
- Click
gunner.rgsin the same list, replace everything in it with the script below, and press Ctrl+S.
/// A gunner: it drifts down the field weaving from side to side, faster down at
/// each stage past the first, fires a shot every 1.5 s from the moment it is on
/// screen, and removes itself once it is 100 units past the bottom.
component Gunner {
/// How fast it drifts down at stage 1.
#[suffix("u/s")]
#[range(1..)]
fall_speed: f32 = 70.0,
/// How far it weaves to either side of its lane.
#[suffix("u")]
weave: f32 = 120.0,
/// Seconds it takes to weave from one side to the other.
#[suffix("s")]
#[range(0.5..)]
sweep_time: f32 = 2.4,
/// Seconds between two shots.
#[suffix("s")]
shot_interval: f32 = 1.5,
/// The x it weaves about, set when it is made.
#[hidden]
lane: f32 = 0.0,
/// Seconds since it was made.
#[hidden]
age: f32 = 0.0,
/// Seconds until its next shot, counted down while it is on screen.
#[hidden]
shot_countdown: f32 = 0.0,
/// How much faster it drifts down at each stage past the first: 0.06 is 6%
/// faster a stage.
#[range(0.0..)]
speed_up: f32 = 0.06,
/// How fast it drifts down, set when it is made.
#[hidden]
drift_speed: f32 = 0.0,
fn on_create() {
// Its lane is where it came in, but at least `weave` in from the left
// and right edges (view.x and view.z), so that all of its weave is on
// screen. It comes in above the top, out of sight, so nobody sees it
// step across into its lane.
let view = camera_view();
lane = clamp(position().x, view.x + weave, view.z - weave);
// How many times as fast as at stage 1 it drifts down: 1 at stage 1,
// and speed_up more at each stage past it. The director keeps the
// stage, and until it has kept one the game is at stage 1. Only its
// drift down: its weave keeps its width and its time, and the shots it
// aims at the ship keep their speed. A stage speeds up what comes down
// the field, not what is fired.
let pace = 1.0 + speed_up * (kept_number("stage", 1.0) - 1.0);
drift_speed = fall_speed * pace;
}
fn loop(dt: f32) {
let view = camera_view();
// It sways along a sine wave: a whole turn of the wave, tau() radians,
// is two sweeps, one each way. `#[range(0.5..)]` on `sweep_time` means
// this never divides by zero.
age += dt;
let turns = age / (2.0 * sweep_time);
let sway = weave * (turns * tau()).sin();
// Down is toward positive y: the world's y grows downward.
move_to(vec2(lane + sway, position().y + drift_speed * dt));
// On screen: below the top edge (view.y) and above the bottom one
// (view.w).
if position().y > view.y && position().y < view.w {
shot_countdown -= dt;
if shot_countdown <= 0.0 {
// The shot aims itself at the ship.
spawn("enemy_shot", position());
shot_countdown = shot_interval;
}
}
// `#[range(1..)]` on `fall_speed` means every gunner gets this far, and
// a stage only ever makes it faster.
if position().y > view.w + 100.0 {
destroy_self();
}
}
}
- Click
meteor.rgsin the same list, replace everything in it with the script below, and press Ctrl+S.
/// A meteor: it falls at a speed of its own, faster at each stage past the
/// first, drifting a little to one side and spinning, and removes itself once
/// it is 100 units past the bottom.
component Meteor {
/// The slowest it may fall at stage 1.
#[suffix("u/s")]
#[range(1..)]
slowest_fall: f32 = 90.0,
/// The fastest it may fall at stage 1.
#[suffix("u/s")]
#[range(1..)]
fastest_fall: f32 = 150.0,
/// The fastest it may drift, to either side.
#[suffix("u/s")]
drift: f32 = 30.0,
/// The slowest it may spin, either way.
#[suffix("rad/s")]
slowest_spin: f32 = 0.4,
/// The fastest it may spin, either way.
#[suffix("rad/s")]
fastest_spin: f32 = 2.0,
/// How fast it moves, sideways and down, rolled when it is made.
#[hidden]
velocity: Vec2 = vec2(0.0, 0.0),
/// How much faster it falls at each stage past the first: 0.06 is 6% faster
/// a stage.
#[range(0.0..)]
speed_up: f32 = 0.06,
fn on_create() {
// How many times as fast as at stage 1 it falls: 1 at stage 1, and
// speed_up more at each stage past it. The director keeps the stage,
// and until it has kept one the game is at stage 1.
let pace = 1.0 + speed_up * (kept_number("stage", 1.0) - 1.0);
// Down is toward positive y: the world's y grows downward. Only its
// fall is faster: its drift keeps its pace, and so does its spin below,
// which lasts as long as the fall, however quick.
velocity = vec2(random(-drift, drift), random(slowest_fall, fastest_fall) * pace);
// How long it will take to fall to 100 units past the bottom edge
// (the bottom edge is camera_view().w). `#[range(1..)]` on the two
// fall speeds means this never divides by zero.
let lifetime = (camera_view().w + 100.0 - position().y) / velocity.y;
let spin = random(slowest_spin, fastest_spin);
// Half of the meteors spin the other way.
if random(0.0, 1.0) < 0.5 {
spin = -spin;
}
// No builtin turns a node a little each step, so the whole spin is
// one long turn that lasts as long as the fall.
anneal_turn(spin * lifetime, lifetime, .Linear);
}
fn loop(dt: f32) {
translate(velocity * dt);
if position().y > camera_view().w + 100.0 {
destroy_self();
}
}
}
- Click the Viewport tab, then open each of the five artifacts that carry them,
diver,gunner,meteor_big,meteor_mediumandmeteor_small, select its node, and press Ctrl+S: each stores its new Speed Up,0.06. The editor gives a node its script’s new fields when you select it. - Double-click
scenes/game.ronto go back.
The STAGE banner
The banner from the last chapter shows each new stage: it fades in, holds a second and fades out, then hides. Make a script called banner:
- In Assets, press Create, choose New script, type
banner, and press Enter. The editor writesbanner.rgsinto the project folder. - Click the Code tab, then
banner.rgsin its Scripts list, replace everything in it with the script below, and press Ctrl+S.
/// The STAGE banner, on the HUD: each time the stage changes, it says which
/// stage the game is at. It fades in, holds a moment and fades out, and then it
/// is hidden until the next.
///
/// The director tells the group "hud" when the stage changes. A signal sent to
/// a group reaches the nodes in it and not their children, so this node is in
/// the group itself, as is the canvas that holds it.
component Banner {
/// How long the banner takes to fade in.
#[suffix("s")]
fade_in: f32 = 0.2,
/// How long it holds, once it has faded in.
#[suffix("s")]
hold: f32 = 1.0,
/// How long it takes to fade out.
#[suffix("s")]
fade_out: f32 = 0.4,
fn loop() {
await signal("stage");
// The text is written here, when the stage changes, and at no other
// time.
let stage = kept_number("stage", 1.0);
set_text("TextComponent", "text", "STAGE " + text(stage));
// White, all the way see-through, and white, solid.
let clear = vec4(1.0, 1.0, 1.0, 0.0);
let solid = vec4(1.0, 1.0, 1.0, 1.0);
set_colour("TextComponent", "color", clear);
show();
await anneal_colour("TextComponent", "color", solid, fade_in, .Linear);
await wait(hold);
await anneal_colour("TextComponent", "color", clear, fade_out, .Linear);
// Hidden at rest, not left see-through: a Text is still drawn at an
// alpha of 0, letter by letter, only to show nothing.
hide();
}
}
- Click the Viewport tab:
game.ronis still open. - Select
banner, give it the grouphud, and press Enter. - Press Add component and choose Banner, keeping Fade In
0.2, Hold1and Fade Out0.4. - Press Ctrl+S.
The director tells the group hud. A signal sent to a group reaches the nodes in it and not their children, so the banner is in the group itself, beside the canvas, which hears it and does nothing. It hides at rest rather than staying see-through, because a Text is still drawn, letter by letter, at an alpha of 0.