Skip to content

Tutorials · Crypt Knight · Chapter 11

The Crypt

60 minutes Uses: Area, CharacterBody, Sprite, AnimatedSprite, TileMap, heard, emit_to, min, pulse_by, set_number, set_flag, hide

By the end

  • scenes/crypt.ron, made from the Gate’s scene, with its own tilemap and no parallax, and The Crypt as the Levels list’s second level
  • Three creatures cut from their strips: a hound that walks, a fire skull that burns and a wolf that runs
  • Three small scripts, Patrol, Dash and Bob, one for each way they move
  • A knight that is hurt by an enemy’s touch, hops back from it, and heals from a heart he picks up
  • The sword on an altar, and a sign that says what it does
On this page

What the Crypt is

The Crypt is a level about timing. The knight cannot attack yet: he evades. It is the same size as the Gate, 102 cells wide and 14 tall, and it has five things to get past, in this order from the left: a hound patrolling the start floor, a fire skull bobbing over a pit, a wolf that dashes along a trench, a second hound on the far side, and, at the end, the sword. Two hearts lie on the way.

All of it is chapters you have done, again: a tilemap, Areas, bodies driven by a script, and a script each. The new part is the creatures, and what touching one costs the knight. The Crypt is also indoors, so it has no town behind it: the camera’s dark background shows behind its stone, and with nothing to draw behind it, it costs the machine less.

A scene made from the Gate

The Crypt needs a camera that follows the knight, a knight, a HUD, a way out and safe ground, which the Gate already has, so the Crypt’s scene is the Gate’s, copied and cut down.

  1. In Assets, open scenes, right-click gate.ron and choose Duplicate. Right-click the copy, gate-copy.ron, choose Rename, type crypt.ron, and press Enter.
  2. Double-click crypt.ron to open it. In the Hierarchy, select the root, Gate, and rename it Crypt in the Inspector.
  3. Select sky, then Ctrl-click mountains, forest, town, sign_walk, sign_jump, sign_high, sign_drop, sign_spikes, spikes and relic, and press Delete. The Crypt has no town to look at, and no spikes, tablets or relic of the Gate’s. Keep sign_air: it becomes the sword’s sign below.
  4. Also delete safe_step, safe_ledge, safe_lower, safe_after_pit and safe_far. Keep pit, safe_start and the rest: the camera, ground, knight, bounds, hud and exit.

Nothing about the camera, the Confiner, the knight or the HUD needs changing: the Crypt is the Gate’s size, and the knight starts at the same place, 72 and 160. What it does need is a floor.

The Crypt’s floor plan

The Crypt paints with the same tile sheet as the Gate, on a tilemap file of its own. The Gate’s own is copied, and emptied.

  1. In Assets, open art, right-click gothic-castle-tileset.tilemap and choose Duplicate. Right-click the copy, gothic-castle-tileset-copy.tilemap, choose Rename, type crypt.tilemap, and press Enter.
  2. Select ground in the Crypt’s Hierarchy, and drag art/crypt.tilemap onto the Tilemap slot of its TileMapComponent. Open the Tilemap tab, and turn on Paint mode. It still holds the Gate.
  3. Choose the Erase tool, and set its Size to 16. Zoom out in the Viewport until the whole level, 102 cells across, shows. Under Layers, click back, and drag from cell (0, 6) to cell (101, 6): a brush 16 wide and tall, along the middle of the level, takes every row of it. Do the same on ground, and on ledges. The Viewport shows an empty level.

Now paint the Crypt, as chapter 4 painted the Gate: the bodies on ground with #66, then the caps with #45, and the ledges, all with the Rect tool, each row of a table one drag from a point inside its first cell to a point inside its last.

ground, #66: bodiesFrom cellTo cell
The left wall(0, 0)(1, 13)
The start floor, up to the first pit(2, 11)(40, 13)
The first pillar(31, 9)(32, 10)
Between the pit and the trench(45, 11)(50, 13)
The trench’s floor(51, 13)(74, 13)
After the trench, and the far side(75, 11)(99, 13)
The second pillar(88, 9)(89, 10)
The right wall(100, 0)(101, 13)
ground, #45: capsFrom cellTo cell
The start floor(2, 10)(30, 10)
The first pillar(31, 8)(32, 8)
Before the pit(33, 10)(40, 10)
Between the pit and the trench(45, 10)(50, 10)
The trench’s floor(51, 12)(74, 12)
After the trench(75, 10)(87, 10)
The second pillar(88, 8)(89, 8)
The far side(90, 10)(99, 10)

Then the ledges, on the ledges layer, which is one-way, with #45: three, over the trench, that the knight jumps up through and lands on.

ledges, #45From cellTo cell
The first ledge(53, 8)(56, 8)
The second ledge(60, 8)(63, 8)
The third ledge(67, 8)(70, 8)

And the sword’s pedestal, on the back layer, with the Brush: #48 in cell (94, 8) and #69 under it in cell (94, 9). Press Save at the top of the Tilemap panel, and Ctrl+S.

  • The pits. Cells 41 to 44 have no floor at all: a bottomless pit, as the Gate’s gap, with the fire skull over it. The trench’s floor, cells 51 to 74, lies two cells lower than the rest, with a wall at each end.
  • The pillars, cells 31 and 32, and 88 and 89, stand two cells over the floor. Each is a wall a patrolling hound turns back at.
  • The three ledges over the trench are a way across it, and where a heart floats.

Safe ground and a pit

The Crypt keeps five stretches of safe ground and one pit. The pit is the Gate’s, moved; one safe strip is the start’s, resized, and its copies make the rest.

  1. Select pit. Set its Position to 688 and 248, and its Area’s Shape Size to 96 and 32. The skull is over this pit.
  2. Select safe_start, rename it safe_1, and set its Position and Size from the table below. Right-click it and choose Duplicate four times, and rename and set each of the copies.
  3. Press Ctrl+S.
NodePositionSizeCells
safe_1256, 160416, 163 to 28
safe_2592, 16096, 1634 to 39
safe_3768, 16064, 1646 to 49
safe_41304, 160176, 1676 to 86
safe_51520, 160128, 1691 to 98

Three creatures from their strips

The Crypt’s enemies are three creatures from the same collection: a hell hound, which walks; a wolf, which runs; and a fire skull, which burns. Each is one strip of frames. Copy the three pictures into art, as you copied the knight’s in chapter 3, and press Refresh.

Each plays from one picture, so it needs no packing: the Sprite sheet cuts it into a clip, and writes the clip into an animation file beside the picture. The three are cut alike, with the grid set to a frame.

  1. Double-click art/hell-hound-walk.png in Assets. The Sprite sheet tab opens on it. Under Grid, set Cell to 64 and 32. Drag across the whole top row, cell 0, 0 to cell 11, 0: Selected 12 x 1.
  2. Under Clips, press From selection. Name the clip walk, set 12 fps and Loop, and press Generate 1. A file, art/hell-hound-walk.anim, appears beside the picture.
  3. Double-click art/wolf-runing-cycle.png. Set Cell to 56 and 32, drag from cell 0, 0 to cell 3, 0: Selected 4 x 1. From selection, name run, 10 fps, Loop, Generate 1. A file, art/wolf-runing-cycle.anim.
  4. Double-click art/fire-skull.png. Set Cell to 96 and 112, drag from cell 0, 0 to cell 7, 0: Selected 8 x 1. From selection, name burn, 10 fps, Loop, Generate 1. A file, art/fire-skull.anim.

A frame’s pivot is the middle of its bottom edge, as chapter 7 set out, which is right for a hound and a wolf: their feet are on the bottom row of their frames. The skull flies, and sits in the middle of its picture, so its sprite is drawn at its own size, as the one picture.

A hound that patrols

A patroller is a body in the room that walks back and forth: a Character Body, driven by its script as the knight’s is, that turns after a set distance or when it meets a wall. It cannot see a ledge, so every patrol in the Crypt ends at a wall or at its own distance. The art faces left.

  1. In Assets, press Create, choose New script, type patrol, and press Enter.
  2. In the Code tab’s Scripts list, click patrol.rgs, replace everything in it with the script below, and press Ctrl+S.
rgs patrol.rgs
/// A patroller: walks back and forth, turning after `distance` or at a wall,
/// and faces the way it walks. Its art faces left.
component Patrol : Body {
    /// How fast it walks.
    #[suffix("u/s")]
    speed: f32 = 40.0,
    /// How far it walks before it turns.
    #[suffix("u")]
    distance: f32 = 128.0,
    /// The way it walks: -1 for left, 1 for right.
    way: f32 = 1.0,
    /// How far it has walked since it last turned.
    #[hidden]
    walked: f32 = 0.0,

    fn loop(dt: f32) {
        walked += speed * dt;
        if walked >= distance {
            way = -way;
            walked = 0.0;
        }
        set_number("CharacterBodyComponent", "velocity.x", way * speed);
        // Drawn facing left, so mirrored to walk right.
        set_flag("SpriteComponent", "flip.x", way > 0.0);
    }

    fn on_hit_wall() {
        way = -way;
        walked = 0.0;
    }
}
component Patrol : Body
A Body base: the script may have the body’s hooks, such as on_hit_wall, which runs when the body is stopped moving sideways. It is the script that sets the body’s speed: a body driven by a script keeps whatever its script last gave it.
walked and distance
A patroller counts how far it has gone, speed units a second times dt, in walked, and turns when it reaches distance: way flips from 1 to -1 and back, and walked starts again. A hound with a wall in front of it turns at once, in on_hit_wall, with the same two lines.
flip.x
The art faces left, so flip.x is on while the hound walks right, as the knight’s is off. The first hound, set way to 1, starts walking right.
  1. Click the Viewport tab. Select Crypt, press + Add node, choose Character, and rename it hound_1. Set its Position to 400 and 160, under Groups type enemy and press Enter.
  2. Remove its Shape with the bin icon at the right of the ShapeComponent header.
  3. In its CharacterBodyComponent, set Size to 32 and 22, Driven By to Script, Gravity to 900 and Terminal Velocity to 400. Leave the rest.
  4. Press Add component and choose SpriteComponent: set Texture by dragging art/hell-hound-walk.png onto it, Size to 64 and 32, Anchor to 0.5 and 1, and Sort Layer to 4.
  5. Press Add component and choose AnimatedSpriteComponent: drag art/hell-hound-walk.anim onto Animations, set Clip to walk, and leave Frame Size unticked: the Sprite’s own size, 64 by 32, is the frame’s.
  6. Press Add component and choose AreaComponent. In its Shape, set Size to 28 and 18, Offset to 0 and -9, and its own Layer to 1, and untick 0 in its Mask: it is an enemy, and enemies are on layer 1 and look for nothing, as hazards do.
  7. Press Add component and choose Patrol. Leave its Speed at 40, Distance at 128 and Way at 1.
  8. Right-click hound_1 and choose Duplicate. Rename the copy hound_2, set its Position to 1300 and 160, and its Patrol’s Distance to 112.
  9. Press Ctrl+S.
The first hound framed and selected, its picture filling the Viewport over the floor, its Character Body the green box round it and its Area the yellow box inside that; its Inspector narrowed to its Character Body: Size 32 by 22, Driven By Script, Gravity 900 and Terminal Velocity 400, with the Animation panel under the Viewport listing its one clip, walk, 12 frames at 12 fps, Loop, and the Hierarchy listing the Crypt’s nodes

The first hound’s walk is 128 units from x 400, and the first pillar’s face is at x 496: it turns there, after 80 units, and from then on every 128. The second walks 112 from x 1300, and meets the second pillar, whose face is at x 1408, after 92.

A skull that bobs

The fire skull is not a body: it has no floor to stand on. It hangs over the pit and bobs, rising and sinking for ever. A pulse does that: it eases a node away from where it is and back, again and again.

  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.
rgs bob.rgs
/// Bobs up and down for ever: `rise` units up and back, `period` seconds each
/// way.
component Bob {
    /// How far up it rises.
    #[suffix("u")]
    rise: f32 = 72.0,
    /// Seconds to rise, and as long to sink back.
    #[suffix("s")]
    period: f32 = 1.2,

    fn on_create() {
        // Up is toward negative y.
        pulse_by(vec2(0.0, -rise), period, .InOutSine);
    }
}

pulse_by moves the node by an offset and back, over a time, in a shape: here up 72 units, since the world’s y grows downward, over 1.2 seconds, and back, with .InOutSine, which eases at both ends. It runs for as long as the node exists.

  1. Click the Viewport tab. Select Crypt, press + Add node, choose Sprite, and rename it skull. Set its Position to 688 and 176, the middle of the pit, and under Groups type enemy.
  2. Drag art/fire-skull.png onto its Sprite’s Texture, and set Size to 96 and 112, Anchor to 0.5 and 0.5 and Sort Layer to 4.
  3. Press Add component and choose AnimatedSpriteComponent: drag art/fire-skull.anim onto Animations, and set Clip to burn.
  4. Press Add component and choose AreaComponent. In its Shape, set Size to 28 and 30, and Offset to 0 and 8, the bright core of the skull and not its flames; set its own Layer to 1, and untick 0 in its Mask.
  5. Press Add component and choose Bob. Leave its Rise at 72 and Period at 1.2.
  6. Press Ctrl+S.
The skull selected and framed in the Viewport: a burning skull in red and orange over a pit, with its Area, a small box in its middle, a stone floor either side and a red heart over the right one; the Inspector shows its Animated Sprite, fire-skull.anim, Clip burn, and the Animation panel its one clip, burn, 8 frames at 10 fps

At rest the skull is low in the pit, under the floor line, and at the top of its bob it has risen 72 units, to the knight’s height. A knight can jump across the pit while it is down, and is hit by it when it is up: time it.

A wolf that dashes

The wolf lives in the trench: it runs from one wall to the other, waits there, and runs back. The three ledges over the trench are the knight’s way across, over its back.

  1. In Assets, press Create, choose New script, type dash, and press Enter. In the Code tab, replace everything in dash.rgs with the script below, and press Ctrl+S.
rgs dash.rgs
/// A dasher: runs one way until a wall stops it, waits there, then runs back.
/// Its art faces right.
component Dash : Body {
    /// How fast it runs.
    #[suffix("u/s")]
    speed: f32 = 160.0,
    /// Seconds it waits at each wall.
    #[suffix("s")]
    rest: f32 = 1.0,
    /// The way it runs: -1 for left, 1 for right.
    way: f32 = 1.0,
    /// Seconds of waiting left.
    #[hidden]
    waiting: f32 = 0.0,

    fn loop(dt: f32) {
        if waiting > 0.0 {
            waiting -= dt;
            set_number("CharacterBodyComponent", "velocity.x", 0.0);
        } else {
            set_number("CharacterBodyComponent", "velocity.x", way * speed);
        }
        // Its legs stop while it waits.
        set_flag("AnimatedSpriteComponent", "playing", waiting <= 0.0);
        set_flag("SpriteComponent", "flip.x", way < 0.0);
    }

    fn on_hit_wall() {
        way = -way;
        waiting = rest;
    }
}
  1. Click the Viewport tab. Select Crypt, press + Add node, choose Character, and rename it wolf. Set its Position to 904 and 192, the floor of the trench, and under Groups type enemy. Remove its Shape.
  2. In its Character Body, set Size to 36 and 18, Driven By to Script, Gravity to 900 and Terminal Velocity to 400.
  3. Add a SpriteComponent: drag art/wolf-runing-cycle.png onto its Texture, Size 56 and 32, Anchor 0.5 and 1, Sort Layer 4.
  4. Add an AnimatedSpriteComponent: drag art/wolf-runing-cycle.anim onto Animations, Clip run.
  5. Add an AreaComponent: Shape Size 32 and 14, Offset 0 and -7, its own Layer 1, and 0 unticked in its Mask.
  6. Add Dash, and leave its Speed at 160 and Rest at 1.
  7. Press Ctrl+S.

The wolf’s art faces right, the hound’s left, so the two scripts flip the other way round. While it waits at a wall its legs stop: set_flag turns the Animated Sprite’s Playing off, as a wolf running on the spot reads as a bug.

The knight meets an enemy

The knight’s Area looks for layer 1, where the hounds, the skull and the wolf are, so he already hears them. What he does about it is the script’s. An enemy costs a heart, like a hazard, but it does not put him back on safe ground: it hops him back, up and away, and takes his steering from him for a quarter of a second. He blinks, as after a spike, and the camera shakes.

Open knight.rgs. First, the comment at the top, which now says so: replace its first seven lines with these eight.

rgs part of knight.rgs
/// The knight: runs where `move` points, jumps on `jump`, jumps lower when
/// `jump` is let go early, drops through a ledge when `down` is held as it
/// jumps, jumps once more in the air once the relic is taken, faces the way it
/// last moved, and shows the clip for what it is doing. It keeps where it last
/// stood on safe ground. A spike or a pit costs it a heart and puts it back
/// there; an enemy costs it a heart and hops it back. A hurt shakes the camera
/// and blinks the knight, and nothing can hurt it while it blinks. A heart it
/// picks up heals one. With no hearts left, the level is lost.

Two more fields, after blink_time: how long the hop takes his steering, and the seconds of it still to run.

rgs part of knight.rgs
    /// How long an enemy's hurt takes the knight's steering away while it hops
    /// back.
    #[suffix("s")]
    hop_time: f32 = 0.25,
    /// Seconds of the hop still to run.
    #[hidden]
    hopping: f32 = 0.0,

Then loop. It takes dt now, which counts the hop down, and a heart picked up says "heal". Replace everything from fn loop() { down to, and including, the block if x != 0.0 { facing = x; }, with this. The line after it, set_flag("SpriteComponent", "flip.x", facing < 0.0);, stays.

rgs part of knight.rgs
    fn loop(dt: f32) {
        // A heart picked up says "heal" to the group "knight", heard a step
        // later.
        if heard("heal") {
            hearts = min(hearts + 1.0, 5.0);
            keep_number("hearts", hearts);
        }
        // One more jump in the air once the relic is taken: double_jump is
        // kept as 0 or 1.
        set_number("CharacterBodyComponent", "air_jumps", kept_number("double_jump", 0.0));
        let x = direction("move").x;
        if hopping > 0.0 {
            // Hopping back from a hurt: the hop keeps its own speed.
            hopping -= dt;
        } else {
            set_number("CharacterBodyComponent", "velocity.x", x * speed);
            let drop = pressed("jump") && held("down");
            set_flag("CharacterBodyComponent", "drop_through", drop);
            if pressed("jump") && !drop {
                jump();
            }
            if !held("jump") {
                cut_jump();
            }
            if x != 0.0 {
                facing = x;
            }
        }

Last, on_enter. Replace the whole hook with this.

rgs part of knight.rgs
    /// When the knight's Area meets another: a spike, a pit or an enemy costs
    /// a heart.
    fn on_enter(other: NodeId) {
        let hazard = in_group(other, "hazard");
        if hazard || in_group(other, "enemy") {
            hearts -= 1.0;
            keep_number("hearts", hearts);
            if hearts <= 0.0 {
                fail();
            } else {
                if hazard {
                    move_to(kept_point("safe", position()));
                    set_point("CharacterBodyComponent", "velocity", vec2(0.0, 0.0));
                } else {
                    // Up, and back the opposite way to the way it faces.
                    set_point("CharacterBodyComponent", "velocity", vec2(-facing * 90.0, -160.0));
                    hopping = hop_time;
                }
                // The camera's node is in the group "camera".
                emit_to("camera", "shake");
                pulse_colour("SpriteComponent", "color", vec4(1.0, 1.0, 1.0, 0.25), 0.08, .Linear);
                // A hook that is waiting ignores every call until it finishes,
                // so while this one waits, nothing can hurt the knight.
                await wait(blink_time);
                // Setting the colour ends the blink.
                set_colour("SpriteComponent", "color", vec4(1.0, 1.0, 1.0, 1.0));
            }
        }
    }
let hazard
A local, true when the thing met is in the group hazard. A hazard puts him back on safe ground, as in chapter 9; anything else that costs a heart, an enemy, hops him instead.
velocity of -facing * 90 and -160
The hop: 90 units a second away from the way he faces, and 160 up. set_point writes a vector into the body’s velocity. For the 0.25 seconds of hopping, loop leaves his sideways speed alone, so the hop carries him instead of the keys.
Why away from where he faces
A script cannot read another node’s position, so the knight cannot tell which side an enemy struck him from. Throwing him back away from the way he faces is right for a hit from the front, which is nearly every hit; a hit from behind throws him toward the enemy, once.
heard "heal" and min
A heart he picks up, below, tells the group knight to "heal"; the knight hears it a step later and adds a heart, never above five.

Press Ctrl+S. The knight in the Gate and the knight in the Crypt are one script: each gains the two new fields, with their defaults, the next time its scene opens.

Two hearts to pick up

  1. In Assets, press Create, choose New script, type heart, and press Enter. In the Code tab, replace everything in heart.rgs with the script below, and press Ctrl+S.
rgs heart.rgs
/// A heart lying in the crypt: it bobs, and the knight takes it by walking
/// into it, which heals one heart. Then it is gone.
component Heart : Area {
    fn on_create() {
        pulse_by(vec2(0.0, -3.0), 0.5, .InOutSine);
    }

    fn on_enter(other: NodeId) {
        if in_group(other, "knight") {
            // The knight is in the group "knight", and hears this a step
            // later.
            emit_to("knight", "heal");
            hide();
        }
    }
}
  1. Click the Viewport tab. Select Crypt, press + Add node, choose Shape, and rename it heart_pickup_1. Set its Position to 776 and 148, and its Rotation to 45.
  2. In its Shape, keep Kind on Rectangle, set Size to 7 and 7, Anchor to 0.5 and 0.5, Outline to 0, Color to 0.6, 0.02, 0.06 with an alpha of 1, and Sort Layer to 6: a small red diamond, the colour of the HUD’s.
  3. Add an AreaComponent: Shape Size 12 and 12. Leave its Layer and Mask: it is on layer 0 and looks for the knight, who is there.
  4. Add Heart.
  5. Right-click heart_pickup_1 and choose Duplicate. Rename the copy heart_pickup_2, and set its Position to 992 and 116: over the second ledge.
  6. Press Ctrl+S.

A heart bobs 3 units over half a second, and is taken by touch, with the same hidden-Area trick as the relic. The knight is told to heal by the group: a script writes only to its own node, so the heart cannot reach into the knight, and says so instead.

The sword on its altar

At the end of the Crypt, past the second hound, an altar holds the sword. Taking it will let him attack, in chapter 12. It is the relic of chapter 10 again, with another power: the script is the same one.

  1. Select Crypt, press + Add node, choose Shape, and rename it sword. Set its Position to 1512 and 118.
  2. In its Shape, keep Kind on Rectangle, set Size to 3 and 16, Anchor to 0.5 and 0.5, Outline to 0, Color to 0.85, 0.88, 0.92 with an alpha of 1, and Sort Layer to 6: a thin bar, standing on its end.
  3. Add an AreaComponent: Shape Size 16 and 48, Offset 0 and 18.
  4. Add Relic, and set its Power to sword.
  5. Select sign_air, the Gate’s sign you kept, and rename it sign_sword. Set its Position to 1512 and 98, the Text of its World Text to J or X to strike., the Size of its Shape to 110 and 13, and its Area’s Offset to 0 and 34: 17 characters make 110, and the Area is lower, to reach the floor.
  6. Press Ctrl+S.
The far side of the Crypt framed in the Viewport: a thin white bar, the sword, hanging over a stone pedestal under the tall outline of the sign’s Area, a dark slab, the exit, at the right, and a thin strip along the floor, the last safe ground, selected; the Inspector shows that strip’s Area, Size 128 by 16, Offset 0 and -8, Layer 1 and Mask Nothing

The exit is the Gate’s, at the same place, 1584 and 160, and it already wins the level.

The Crypt on the list

  1. Click the Levels tab. Type The Crypt into the box in the panel’s bar and press Add level.
  2. With its row selected, press Scene and choose crypt.ron.
  3. Drag abandoned_castle_music_-_starninjas_6.ogg from audio in Assets onto the Music button of its row: the same loop as The Gate’s.
The Levels panel with two levels in order: The Gate, scenes/gate.ron, startup, and The Crypt, scenes/crypt.ron, each with the castle loop as its Music, and Then open: (none)

The Gate’s exit now opens The Crypt. The Crypt’s exit is the last level’s: the run ends there, until chapter 13 adds the third and chapter 15 the victory screen.

Check it works

The game’s window in the Crypt: the knight running along a stone floor under a flat dark sky with five hearts at the top left, a hound walking toward him and a stone pillar at the right edge
The knight in the air over the hound, thrown up and back after its touch, with four hearts at the top left and a stone pillar behind them