Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Floors, ledges and walls

In Bumping into things a ball bounced around a box. A platformer asks more of my boundaries: a floor to stand on, ledges to jump up onto, walls to stop you, and a way to know which of them you’re touching right now. Let’s build one.

Something to stand on

A floor is a solid boundary, the same as the walls of the box. A ledge you can jump up through from below and still land on is solid from one side only: one_way("top") makes its top the only face that stops anything.

Gravity is a behavior: ACCELERATION 0, 0.4 speeds a sprite up downward a little more every tick. Two heroes fall from the top of the screen, one beside the ledge and one over it:

SCREEN 320, 240
LOADIMG "hero.png", "hero"
BOUNDARY ADD "floor", (0, 224) -> (320, 240) !! solid
BOUNDARY ADD "ledge", (100, 150) -> (220, 154) !! solid one_way("top")

BEHAVIOR "gravity"
    ACCELERATION 0, 0.4
END BEHAVIOR

FOR n = 0 ... 1
    ENABLE SPRITE n
    SET SPRITE n !! image("hero") x(40 + n * 100) y(0)
        !! bbox((0.25, 0.125) -> (0.75, 1)) solid
        !! behavior("gravity")
    SHOW SPRITE n
NEXT

// Give them a second to land.
t = 0
DO WHILE t < 60
    IF COOLDOWN(1) THEN t = t + 1
LOOP
FOR n = 0 ... 1
    PRINT "sprite", n, "is on the", spritetouching(n, "TOP"), "at y =", spritetouchingy(n, "TOP")
NEXT
sprite 0 is on the floor at y = 184.0
sprite 1 is on the ledge at y = 110.0

spritetouching(n, "TOP") gives the tag of whatever sprite n is standing on, or "" while it’s in the air. The edge you name is the face of the boundary: standing on a floor touches its "TOP". Running into a wall from the left touches the wall’s "LEFT", and bumping your head on a ceiling touches its "BOTTOM". "ANY" checks all four.

spritetouchingy is the y the sprite has to be at to stand exactly on that face, and spritetouchingx the x to be flush against a wall. They’re for snapping a sprite into place, say when it lands.

bbox is the part of the sprite that bumps into things, from (0, 0), its top left, to (1, 1), its bottom right. hero.png is 40 pixels square, with room either side for arms and legs when the hero runs, but a wall should stop the hero’s body, not the empty air beside it. (0.25, 0.125) -> (0.75, 1) is the middle half across, from the top of the head down to the feet.

Crash-tan’s Tip: I only work out what a sprite is touching while it has a behavior that moves it. A sprite with no behavior at all touches nothing, wherever you put it.

Run and jump

Walking is a VELOCITY with the speed as a setting. Giving a sprite a behavior it already has, with new behavior_params, changes the setting, so the keys only have to say how fast.

A jump is an IMPULSE: one push upward, which gravity then wears away until you come back down. An impulse pushes once, when its behavior starts, so to jump again the behavior comes off and goes back on.

The hero stands, runs and jumps with the sheets from Animation and particles, one behavior each, and pose remembers which one is on. In hero_jump.png, frames 0 to 2 are going up and 3 to 5 coming down.

SCREEN 320, 240
LOADIMG "hero_idle.png", "idle" !! spritesheet((40, 40))
LOADIMG "hero_run.png", "run" !! spritesheet((40, 40))
LOADIMG "hero_jump.png", "jump" !! spritesheet((40, 40))

// The level: a floor, two ledges to jump up through, and the screen's edge.
BOUNDARY ADD "floor", (0, 224) -> (320, 240) !! solid
BOUNDARY ADD "ledge", (40, 170) -> (130, 174) !! solid one_way("top")
BOUNDARY ADD "ledge", (170, 120) -> (280, 124) !! solid one_way("top")
BOUNDARY ADD "edge", (0, 0) -> (320, 240) !! solid enclosure

MEMORYBUFFER "level"
    LINE (0, 224) -> (319, 239), 2 !! box fill
    LINE (40, 170) -> (129, 173), 6 !! box fill
    LINE (170, 120) -> (279, 123), 6 !! box fill
END MEMORYBUFFER
FRAME
    CLS
    PUTIMAGE (0, 0), "level"
END FRAME

BEHAVIOR "gravity"
    ACCELERATION 0, 0.4
END BEHAVIOR
BEHAVIOR "walk"
    VELOCITY PARAM("speed", 0), 0
END BEHAVIOR
BEHAVIOR "jump"
    IMPULSE 0, -8
END BEHAVIOR
BEHAVIOR "standing"
    ANIMATE "idle" !! framerate(6)
END BEHAVIOR
BEHAVIOR "running"
    ANIMATE "run" !! framerate(12)
END BEHAVIOR
BEHAVIOR "in the air"
    ANIMATE "jump" !! framerate(10) noloop
END BEHAVIOR

hero = reserve_sprite()
SET SPRITE hero !! image("idle") frame(0) x(20) y(180)
    !! bbox((0.25, 0.125) -> (0.75, 1)) solid
    !! behavior("gravity")
SET SPRITE hero !! behavior("walk")
SET SPRITE hero !! behavior("standing")
SHOW SPRITE hero

speed = 0
jump_held = FALSE
pose = "standing"
DO
    IF COOLDOWN(1)
        want = 0
        IF keydown("Left") THEN want = want - 2
        IF keydown("Right") THEN want = want + 2
        IF want <> speed
            speed = want
            SET SPRITE hero !! behavior("walk") behavior_params({speed: speed})
            IF speed < 0 THEN SET SPRITE hero !! flip_x
            IF speed > 0 THEN SET SPRITE hero !! flip_x(FALSE)
        END IF

        // Only jump from the ground, and once for each press.
        standing = spritetouching(hero, "TOP") <> ""
        jump = keydown("Up") OR keydown("Space")
        IF jump AND NOT jump_held AND standing
            SET SPRITE hero !! remove_behavior("jump")
            SET SPRITE hero !! behavior("jump")
        END IF
        jump_held = jump

        // Show what the hero's doing, changing only when that changes.
        now = "standing"
        IF speed <> 0 THEN now = "running"
        IF NOT standing THEN now = "in the air"
        IF now <> pose
            SET SPRITE hero !! remove_behavior(pose)
            SET SPRITE hero !! behavior(now)
            pose = now
        END IF
    END IF
LOOP

Click the screen, then run with the arrow keys and jump with Up or Space. You go straight up through a ledge from underneath and land on top of it.

Two boundaries can share a tag, like the two ledges here: whichever one you’re on, spritetouching says "ledge".

Crash-tan’s Tip: jump_held makes you let go of the key before you can jump again. Without it, holding Space would bounce you the moment you touched the ground.

Walls

A wall is one more solid boundary. Walk into it and it stops you, and spritetouching with "LEFT" says you’ve met the wall’s left face:

SCREEN 320, 240
LOADIMG "hero.png", "hero"
BOUNDARY ADD "floor", (0, 224) -> (320, 240) !! solid
BOUNDARY ADD "wall", (200, 0) -> (220, 224) !! solid

BEHAVIOR "gravity"
    ACCELERATION 0, 0.4
END BEHAVIOR
BEHAVIOR "walk"
    VELOCITY 3, 0
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(20) y(192)
    !! bbox((0.25, 0.125) -> (0.75, 1)) solid
    !! behavior("gravity")
SET SPRITE 0 !! behavior("walk")
SHOW SPRITE 0

t = 0
DO WHILE t < 90
    IF COOLDOWN(1) THEN t = t + 1
LOOP
PRINT "stopped by the", spritetouching(0, "LEFT"), "at x =", spritetouchingx(0, "LEFT")
PRINT "still standing on the", spritetouching(0, "TOP")
stopped by the wall at x = 170.0
still standing on the floor

Walls from a map

Drawing a level as boundaries one rectangle at a time gets old fast. If the level is a tile map, BOUNDARY ADD FROM LAYER looks at the map and makes every tile you can see into solid ground. Here the grass and the stone come from greyscale.png, and the empty squares stay open:

SCREEN 320, 240
LOADIMG "greyscale.png", "tiles"
LOADIMG "hero.png", "hero"

ground = (8, 10):INTEGER
FOR x = 0 ... 9
    ground(6, x) = 10        // grass along the bottom
NEXT
FOR x = 2 ... 4
    ground(4, x) = 2         // a stone ledge
NEXT
FOR x = 6 ... 8
    ground(2, x) = 2         // and a higher one
NEXT
TILEMAP "level"
    TILESET "tiles"
    TILESIZE 32
    SIZE 10, 8
    TILES 0 !! from(ground)
END TILEMAP
CREATE MAP "level" !! layer_to(1)
BOUNDARY ADD FROM LAYER "ground", 1 !! solid

BEHAVIOR "gravity"
    ACCELERATION 0, 0.4
END BEHAVIOR
FOR n = 0 ... 2
    ENABLE SPRITE n
    SET SPRITE n !! image("hero") x(16 + n * 96) y(0)
        !! bbox((0.25, 0.125) -> (0.75, 1)) layer(1) solid
        !! behavior("gravity")
    SHOW SPRITE n
NEXT

t = 0
DO WHILE t < 60
    IF COOLDOWN(1) THEN t = t + 1
LOOP
SET LAYER 2
FOR n = 0 ... 2
    PRINT "sprite", n, "landed at y =", spritey(n)
NEXT
sprite 0 landed at y = 152
sprite 1 landed at y = 88
sprite 2 landed at y = 24

Boundaries made from a layer belong to that layer, and only sprites on the same layer bump into them: that’s why the sprites say layer(1). A boundary from BOUNDARY ADD belongs to no layer unless you give it one with !! layer(n), and with none it stops sprites on every layer.

Crash-tan’s Tip: A layer you’ve drawn on works as well as a map: whatever you drew becomes solid. !! cell(8) says how finely to cut it, in pixels. Smaller follows the shapes more closely and makes more boundaries.

Walls from a file, and taking them away

My map editor saves a level’s boundaries in a file, and BOUNDARY ADD FROM adds every one of them. It’s JSON: a list of boundaries, each with its tag, its rectangle, and whether it’s solid. Here we write one with SAVETEXT, so you can see what’s in it.

BOUNDARY DELETE takes away every boundary with a tag, so a door that opens is one line. BOUNDARY CLEAR takes them all away, ready for the next level.

SCREEN 320, 240
LOADIMG "hero.png", "hero"
SAVETEXT "walls.json", "{\"boundaries\": [{\"tag\": \"floor\", \"rect\": [0, 224, 320, 240], \"solid\": true}, {\"tag\": \"door\", \"rect\": [150, 160, 160, 224], \"solid\": true}]}"
BOUNDARY ADD FROM "walls.json"

BEHAVIOR "gravity"
    ACCELERATION 0, 0.4
END BEHAVIOR
BEHAVIOR "walk"
    VELOCITY 2, 0
END BEHAVIOR
ENABLE SPRITE 0
SET SPRITE 0 !! image("hero") x(20) y(192)
    !! bbox((0.25, 0.125) -> (0.75, 1)) solid
    !! behavior("gravity")
SET SPRITE 0 !! behavior("walk")
SHOW SPRITE 0

t = 0
DO WHILE t < 90
    IF COOLDOWN(1) THEN t = t + 1
LOOP
PRINT "waiting at the", spritetouching(0, "LEFT")

BOUNDARY DELETE "door"
DO WHILE t < 150
    IF COOLDOWN(1) THEN t = t + 1
LOOP
PRINT "through the door:", spritex(0) > 160

BOUNDARY CLEAR
DO WHILE t < 210
    IF COOLDOWN(1) THEN t = t + 1
LOOP
PRINT "no floor any more:", spritey(0) > 240
waiting at the door
through the door: 1
no floor any more: 1