Skip to content

Tutorials · Ship Evader · Chapter 9

Score and HUD

45 minutes Uses: Canvas, Rect, Text, Panel, set_text, text, add_number, kept_number, saved_number, hide, show

By the end

  • The pack’s font, for every label in the game
  • A HUD: a canvas over the field, holding the score, the best and three life icons
  • A score kept for the whole game, and a best saved between launches
  • Labels that scripts write, and only when their number changes
  • A hidden STAGE banner, ready for the next chapter
On this page

The game’s font

  1. Choose File → Project settings… and open its Font tab.
  2. Set Face to fonts/kenvector_future.ttf, the pack’s font, and press Enter.
  3. Set Size to 48, and leave Distance field unticked.
Project settings on its Font tab: Face fonts/kenvector_future.ttf, found, Size 48, and Distance field unticked

Size is how many pixels the letters are drawn from. 48 suits the biggest label, the STAGE banner, and every label is drawn from the same letters at its own scale, so the HUD’s scales below keep the letters sharp in a 540 × 960 window.

  1. Close the window with the × at the right of its title bar: it floats over the Viewport.

Counting the score

The score is a kept number, as the lives are. An enemy shot down adds its points; the ship adds 10 for every whole second it is alive.

  1. Click the Code tab, then enemy.rgs in its Scripts list, replace everything in it with the script below, and press Ctrl+S.
rgs enemy.rgs
/// An enemy's hits: each laser that meets it costs it a hit point, the last one
/// removes it and adds its points to the score, a hit it lives through flashes
/// it white, and meeting the ship removes it too, for no points.
component Enemy : Area {
    /// How many lasers it takes to remove it.
    hit_points: i32 = 1,
    /// How long the white flash of a hit takes to fade.
    #[suffix("s")]
    flash_time: f32 = 0.12,
    /// What shooting it down adds to the score.
    points: f32 = 100.0,

    /// When its Area meets another that it notices, on the node `other`: a
    /// laser's or the ship's.
    fn on_enter(other: NodeId) {
        // The groups `other` is in say which it is.
        if in_group(other, "shot") {
            hit_points -= 1;
            if hit_points <= 0 {
                // Shot down: its points go on the score. The game keeps the
                // score as it keeps the lives, so the HUD can show it.
                add_number("score", points);
                destroy_self();
            } else {
                // A hit it lives through washes it white at once, and the
                // white fades back to its own colours over flash_time.
                set_number("SpriteComponent", "flash", 1.0);
                anneal_number("SpriteComponent", "flash", 0.0, flash_time, .Linear);
            }
        } else if in_group(other, "ship") {
            // Rammed: it goes, and scores nothing. The ship loses a life.
            destroy_self();
        }
    }
}

add_number adds to a kept number, and counts a number nothing has kept yet as 0. It adds rather than reading and keeping the sum, so two enemies shot down on one step both count. In ship.rgs, add one line to the comment at the top, two fields after blink_fade, and the seconds at the end of loop, after the firing:

rgs part of ship.rgs
/// The player's ship: it flies where `move` points, never leaves the field,
/// fires lasers while `fire` is held, and keeps its position for whatever aims
/// at it. Whatever its Area meets costs it a life: it flashes white, then
/// blinks, and nothing can hit it while it does. On its last life it is gone.
/// Each whole second it stays alive adds 10 to the score.
component Ship : Area {
rgs part of ship.rgs
    /// What each whole second alive adds to the score.
    points_a_second: f32 = 10.0,
    /// Seconds alive since the last whole one was scored.
    #[hidden]
    alive: f32 = 0.0,
rgs part of ship.rgs
        // Points for staying alive, a whole second's worth at a time, so the
        // score stays a whole number. The ship counts its own seconds: once it
        // is gone its loop runs no more, and the count stops.
        alive += dt;
        if alive >= 1.0 {
            // Take the second away rather than start again at 0: what is over
            // it counts toward the next one, so the count never falls behind.
            alive -= 1.0;
            add_number("score", points_a_second);
        }
  1. Press Ctrl+S to save ship.rgs.
  2. Click the Viewport tab, then double-click artifacts/diver.artifact, select its node, diver, and press Ctrl+S: the editor gives a node its script’s new fields when you select it, so the artifact stores the Enemy’s new Points, 100.
  3. Double-click artifacts/gunner.artifact, select gunner, set its Enemy’s Points to 300, press Enter, and press Ctrl+S.
  4. Double-click scenes/game.ron, select ship, and press Ctrl+S: the scene stores the Ship’s two new fields.
The gunner’s artifact open, narrowed by points: the Area’s Shape and its Enemy’s Hit Points 3 and Points 300

A file stores a script’s new field only once it is opened in the editor, its node selected, and the file saved, which is why each file holding an Enemy or the Ship is saved again. One left unsaved still plays, on the field’s default.

A canvas over the field

A HUD is drawn on a canvas: screen space over the field, laid out in units of its own and never moved by the camera or a shake. Its children are placed by Rects, anchored to its corners or its centre.

  1. Select Game, press + Add node, choose Node, and rename it hud.
  2. Give it the group hud, and press Enter.
  3. Press Add component and choose CanvasComponent.
  4. Set Reference Size to 720 and 1280, Scale Mode to ScaleWithHeight, and keep Use Safe Area ticked, Space on Screen and Layer at 0.
The hud selected: its Groups row holds hud, and its Canvas a Reference Size of 720 by 1280, Scale Mode ScaleWithHeight, Use Safe Area ticked and Space Screen

Make it with Node and not the Canvas preset, which adds a Theme the game does not use. A Reference Size of the field’s own 720 by 1280 means a canvas unit is a world unit, in a window of any size.

The score and the best

Two scripts write the labels: one the kept score, one the saved best.

  1. In Assets, press Create, choose New script, type score, and press Enter.
  2. Make best the same way.
  3. In the Code tab’s Scripts list, click score.rgs, replace everything in it with the first script below, and press Ctrl+S.
  4. Click best.rgs in the same list, replace everything in it with the second, and press Ctrl+S.
rgs score.rgs
/// The score, on the HUD: the points the game keeps under the name "score",
/// written onto this node's Text as SCORE and the number, whenever it changes.
component Score {
    /// The score the label says. Below any score, so the first step writes it.
    #[hidden]
    shown: f32 = -1.0,

    fn loop() {
        let score = kept_number("score", 0.0);
        // Only when the score changes: the text is written each time it is
        // set, the same text included, and the score stands still on most
        // steps.
        if score != shown {
            shown = score;
            set_text("TextComponent", "text", "SCORE " + text(score));
        }
    }
}
rgs best.rgs
/// The best score, on the HUD: the one saved under the name "best", written
/// onto this node's Text as BEST and the number. It changes only when the
/// saved value does, which is when a game ends.
component Best {
    /// The best the label says. Below any score, so the first step writes it.
    #[hidden]
    shown: f32 = -1.0,

    fn loop() {
        // The saved best, not the score: a score passing it during the game
        // is not a best until the game ends and saves it.
        let best = saved_number("best", 0.0);
        if best != shown {
            shown = best;
            set_text("TextComponent", "text", "BEST " + text(best));
        }
    }
}
set_text and text
Write text into a component’s text field, here the Text’s; and write a number as text, 1200 with no decimal point. + joins two texts.
shown
The number the label says. The label is written only when the number changes, which is most steps never.
saved_number
Reads the best saved so far, as the game’s root does in the last chapter. BEST shows the saved best, not the score, so a score passing it is not a best until the game ends and saves it.
  1. Click the Viewport tab, select hud, press + Add node, and choose Label.
  2. Rename it score.
  3. In its Rect, set Anchor Min and Anchor Max to 0 and 0, the top-left corner; Offset Min to 40 and 40; Offset Max to 440 and 88; and Pivot to 0 and 0.
  4. In its Text, set Text to SCORE 0, Scale to 0.8 and Align to Left, keeping Color white.
  5. Press Add component and choose Score.
  6. Select hud and add another Label under it, called best: anchors 0 and 0, Offset Min 40 and 88, Offset Max 440 and 128, Pivot 0 and 0; Text BEST 0, Scale 0.7, Align Left.
  7. Press Add component and choose Best.
The score label selected: its Rect anchored at the top left with Offset Min 40, 40 and Offset Max 440, 88, and its Text, SCORE 0 at Scale 0.8, aligned left, and the Viewport shows SCORE 0 and BEST 0 at the top left of the field

Three lives, top right

Each life is an icon of its own with a fixed place, so losing one leaves the others where they are. A script on each hides it once the lives fall to its index.

  1. In Assets, press Create, choose New script, type life, and press Enter.
  2. In the Code tab’s Scripts list, click life.rgs, replace everything in it with the script below, and press Ctrl+S.
rgs life.rgs
/// One of the lives on the HUD: an icon, shown while the ship still has that
/// life and hidden once it is lost.
component Life {
    /// Which life this is, counting from 0 at the left. The icons are 0, 1 and
    /// 2, and the ship loses the rightmost first.
    // A number with decimals, though it only ever counts, because it is
    // compared with the lives, which the game keeps as one.
    index: f32 = 0.0,
    /// The lives it last counted. Below any count, so the first step decides.
    #[hidden]
    counted: f32 = -1.0,

    fn loop() {
        let lives = kept_number("lives", 3.0);
        // Only when the lives change, as the labels do.
        if lives != counted {
            counted = lives;
            // An icon at or above the lives left is a life lost: with 2 left,
            // icons 0 and 1 stay and icon 2 goes. Each icon has a place of its
            // own on the canvas, so hiding one leaves the others where they
            // are.
            if index >= lives {
                hide();
            } else {
                show();
            }
        }
    }
}
  1. Click the Viewport tab, select hud, press + Add node, and choose Panel.
  2. Rename it life0.
  3. In its Rect, set Anchor Min and Anchor Max to 1 and 0, the top-right corner; Offset Min to -173 and 56; Offset Max to -140 and 82; Pivot to 1 and 0.
  4. In its Panel, click Color.
  5. If the button at the top left of the picker that opens reads U8, press it, so it reads F: the picker then shows each value from 0 to 1, as this tutorial gives every colour, until you close the editor.
  6. Make it white: 1, 1, 1, alpha 1.
  7. Drag art/playerLife1_blue.png from Assets onto Texture, and set Border to 0.
  8. Press Add component and choose Life, keeping Index 0.
  9. Right-click life0 and choose Duplicate.
  10. Rename the copy life1, set its Offset Min to -123 and 56, Offset Max to -90 and 82, and its Index to 1.
  11. Duplicate life1 as life2: Offset Min -73 and 56, Offset Max -40 and 82, Index 2.
The life0 icon selected: its Rect anchored at the top right, Offset Min -173, 56 and Offset Max -140, 82; its Panel showing playerLife1_blue; and its Life with Index 0
The game after a hit: the score risen, BEST 0 under it, and two ship icons left at the top right
  1. Select hud, add a Label under it, called banner, and untick its Visible.
  2. Set its Anchor Min and Anchor Max to 0.5 and 0.5, the centre; Offset Min to -300 and -50; Offset Max to 300 and 50; Pivot 0.5 and 0.5.
  3. Set its Text to STAGE 1 and its Align to Center.
  4. Set its Scale to 1.3, press Enter, and press Ctrl+S.
The banner selected, its Visible unticked: its Rect centred on the canvas, 600 by 100, and its Text STAGE 1 at Scale 1.3, centred

It does nothing yet. The next chapter gives it a script and a group, and it fades in each time the game moves up a stage.

Kept and saved

The score is kept: it lasts until the game closes, through a new game. The best is saved: it lasts from one launch to the next. The game’s root, as the last chapter made it, saves the better of the two with save_number as each game ends, and sets the score back to 0 for the next.

Check it works