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

Hello again!

Crash-tan, saluting with a wink: purple hair, an orange hoodie with her name on it, and teal trainers.

It’s me, Crash-tan! Last time I showed you my language. This time I’m showing you my engine: the part of me that draws, moves sprites around, knocks them into each other and plays the music while it happens.

I’ll assume you’ve been through Learn Crash BASIC, so you know what a FUNCTION, a list and a !! modifier are. If something here looks new, it’s about the engine, and I’ll explain it.

How this page works

The lesson is on this side. On the other side there’s an editor with real code in it, and the game’s screen under that. It’s all real: that’s my whole engine over there, running in your browser, the same one that runs your games on a phone or a PC.

Every piece of code in a lesson has a Try it button. Press it and the code goes into the editor and runs. Then change it! Press Run (or Ctrl + Enter) to run it again, Stop to stop a game that’s still going, and Reset when you’d like the lesson’s version back.

SCREEN 320, 240
PRINT "Hello! I'm Crash-tan."
CIRCLE (160, 140), 40, 14, 14
Hello! I'm Crash-tan.

SCREEN opens a screen 320 pixels wide and 240 high, and everything after it draws on that. PRINT writes on the screen too. Under a piece of code in a lesson, the box marked “prints” is what it writes. CIRCLE takes a center, a radius, the color of its edge and the color to fill it with: 14 is yellow.

Crash-tan’s Tip: A game that wants keys needs the screen to have the focus. Click the screen, then press away. Clicking back in the editor gives the keys back to the editor.

Break something

Go on: change CIRCLE to CIRCEL over there and run it. My error messages say what I was looking at and what to try next, and it’s nicer to meet them now, while nothing’s at stake.

SCREEN 320, 240
CIRCEL (160, 120), 40, 14

Over in the editor, I check what you type as you go, and a line under the code tells you about the first thing I’d trip over before you even press Run.

A game is a program that keeps going

Everything above ran and was done. A game doesn’t end until somebody stops it: it draws a frame, looks at the keys, moves things, and does it again. Here’s the smallest one I can think of. It goes until you press Stop.

SCREEN 320, 240
x = 0
DO
    IF COOLDOWN(1)
        MEMORYBUFFER "scene"
            CIRCLE (x, 120), 12, 12, 12
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
        x = (x + 2) MOD 320
    END IF
LOOP

That’s most of what the next few lessons are about: drawing, the frame, and the loop that keeps a game going. Then I’ll show you the parts of me that do the busy work for you, so your loop can stay small.

Drawing on the screen

The screen

SCREEN comes first in almost every game. It says how many pixels the game has to draw on, and I scale those up to fill whatever window, phone or TV the game is shown on, so a 320 × 240 game looks chunky and sharp everywhere.

SCREEN 320, 240
PRINT "I have", screenwidth, "by", screenheight, "pixels."
LINE (0, 0) -> (319, 239), 15
I have 320 by 240 pixels.

Pixels count from the top left: (0, 0) is that corner, x grows to the right and y grows downwards. A point is written as a pair, (x, y).

Colors

A color can be a number from my palette. The first sixteen are the classic ones: 0 is black, 1 blue, 2 green, 4 red, 12 a brighter red, 14 yellow and 15 white.

SCREEN 320, 240
FOR c = 0 ... 15
    LINE (c * 20, 60) -> (c * 20 + 19, 180), c !! box fill
NEXT

LINE takes a vector, one point -> another, and draws a line from the first to the second. With !! box it draws the box those two points are the corners of, and fill fills it in.

You can change any palette entry to any color you like, by its red, green and blue:

SCREEN 320, 240
PALETTE 20, 255, 128, 0
CIRCLE (160, 120), 60, 20, 20

Crash-tan’s Tip: If you’d rather say every color yourself, SET PALETTE FALSE turns the palette off, and then a color is the number 0xAARRGGBB: 0xFFFF8000 is that same orange.

Shapes

CIRCLE takes its center, its radius, the color of its edge, and if you like, the color to fill it with. PSET sets one pixel. POLYGON takes a list of points, an edge color and a fill color.

SCREEN 320, 240
CIRCLE (60, 60), 40, 9
CIRCLE (60, 60), 20, 9, 9
FOR x = 0 ... 99
    PSET (180 + x, 40 + x MOD 20), 14
NEXT
POLYGON [(160, 220), (210, 130), (260, 220)], 10, 2

Words on the screen

PRINT writes where the text cursor is and moves it down a line. LOCATE moves the cursor to a row and a column, counting from 1, and COLOR picks the text’s color and, if you like, the color behind it.

SCREEN 320, 240
COLOR 14
LOCATE 2, 2
PRINT "SCORE  1200"
COLOR 15, 4
LOCATE 15, 15
PRINT "GAME OVER"
SCORE  1200
GAME OVER

Clearing

CLS clears the screen, to black or to the color you give it. A game clears and draws again every frame, which is the next lesson.

SCREEN 320, 240
CLS 1
CIRCLE (160, 120), 50, 15
PRINT "a blue screen with a circle on it"
a blue screen with a circle on it

Layers

A screen can have layers, one above the other: draw the sky once on layer 0, and the things that move on layer 1, and clearing layer 1 leaves the sky alone. CREATE LAYER makes one and SET LAYER says which one drawing goes to.

SCREEN 320, 240
CLS 1
CIRCLE (260, 50), 25, 14, 14
CREATE LAYER 1, 320, 240
SET LAYER 1
x = 0
WHILE x <= 300
    IF COOLDOWN(1)
        MEMORYBUFFER "block"
            LINE (x, 180) -> (x + 30, 200), 12 !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "block"
        END FRAME
        x = x + 1
    END IF
WEND

CLS on layer 1 clears only layer 1, so the sun stays put while the red block slides past it. COOLDOWN(1) moves it one step every tick, sixty a second, and FRAME and MEMORYBUFFER get each step to the screen in one piece. More about all three next.

Frames

The screen you see is redrawn about sixty times a second. Each of those is a frame, and a game’s whole job is to have the next one ready, at the right pace.

The frame you draw

Everything you draw is sent to me as you draw it. If I showed it straight away, you’d catch the screen half drawn: cleared, with nothing on it yet. So put a frame’s drawing inside FRAME, and it reaches the screen all at once, when the block ends.

Shapes have one more step. Each CIRCLE or LINE on its own is a trip to the renderer, and a frame can have hundreds. A MEMORYBUFFER records every shape drawn inside it into one image instead, and draws it all in one go; PUTIMAGE then puts that image on the screen.

SCREEN 320, 240
angle = 0
DO
    IF COOLDOWN(1)
        x = 160 + cos(angle) * 80
        y = 120 + sin(angle) * 80
        MEMORYBUFFER "scene"
            CIRCLE (x, y), 10, 14, 14
            CIRCLE (160, 120), 4, 15, 15
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
        angle = angle + 0.05
    END IF
LOOP

The order matters. The shapes go into the MEMORYBUFFER first, before the frame starts. Then one FRAME holds everything that changes the screen: the CLS, the PUTIMAGE, and any PRINT. The screen is never seen cleared and waiting, so nothing flickers.

A MEMORYBUFFER is the screen’s size unless you give it one, and it starts out see-through, so only the shapes show.

From 1.x: There’s no SCREENLOCK and SCREENUNLOCK to pair up. A FRAME block can’t be left unlocked.

Keeping time

That IF COOLDOWN(1) is what keeps the circle going round at a steady speed. I count time in ticks, sixty to a second, and COOLDOWN(n) is TRUE once every n ticks and FALSE in between. So the loop goes round as often as it likes, but the circle only moves sixty times a second, on a fast machine or a slow one.

Each COOLDOWN keeps its own time, so one loop can do things at different paces:

SCREEN 320, 240
x = 0
blink = FALSE
DO
    IF COOLDOWN(1) THEN x = (x + 2) MOD 320      // sixty times a second
    IF COOLDOWN(30) THEN blink = NOT blink       // twice a second
    IF COOLDOWN(1)
        MEMORYBUFFER "scene"
            CIRCLE (x, 120), 10, 14, 14
            IF blink THEN CIRCLE (160, 60), 6, 12, 12
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
    END IF
LOOP

Try COOLDOWN(120) for the blink: once every two seconds.

Crash-tan’s Tip: Pace a loop with COOLDOWN. DELAY stops your program for a while, and a game that’s stopped can’t notice a key or a collision until it starts again.

How fast is it going?

fps is how many frames a second the screen is getting, and timer is the time in seconds, with fractions. Moving things by how much time went by keeps them at the same speed even when a frame comes late.

SCREEN 320, 240
x = 0
last = timer
DO
    IF COOLDOWN(1)
        now = timer
        x = x + 100 * (now - last)   // 100 pixels a second
        last = now
        IF x > 320 THEN x = 0
        MEMORYBUFFER "scene"
            LINE (x, 110) -> (x + 20, 130), 10 !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
            LOCATE 1, 1
            PRINT "fps", round(fps)
        END FRAME
    END IF
LOOP

Drawing every frame for you

Sometimes the drawing doesn’t belong in your loop at all. CALL RENDER gives me a function to call once every frame, all by myself, and your program goes on doing whatever it likes.

SCREEN 320, 240
ATOMIC ticks = 0

FUNCTION draw()
    MEMORYBUFFER "pulse"
        CIRCLE (160, 120), 20 + (ticks MOD 60), 12
    END MEMORYBUFFER
    FRAME
        CLS
        PUTIMAGE (0, 0), "pulse"
        LOCATE 1, 1
        PRINT "tick", ticks
    END FRAME
END FUNCTION

CALL RENDER draw, "pulse"
DO
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP

draw builds its picture and shows it in one FRAME, the same as a loop would. It runs on my render thread while your loop runs on its own, and the two share ticks. ATOMIC makes that safe: whichever side looks at it sees a whole number, never one caught half-changed.

Crash-tan’s Tip: STOP RENDER "pulse" stops calling it. Give each one a name you’ll remember, because a game can have several.

Keys, the mouse and a finger

Keys

keydown("Left") is 1 while the left arrow is held and 0 when it isn’t. That’s what a game wants: not “a key was pressed” but “is it held right now?” Click the screen so it hears the keys, then steer:

SCREEN 320, 240
x = 160
y = 120
DO
    IF COOLDOWN(1)
        IF keydown("Left") OR keydown("A") THEN x = x - 2
        IF keydown("Right") OR keydown("D") THEN x = x + 2
        IF keydown("Up") OR keydown("W") THEN y = y - 2
        IF keydown("Down") OR keydown("S") THEN y = y + 2
        MEMORYBUFFER "scene"
            CIRCLE (x, y), 8, 14, 14
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
    END IF
LOOP

A key is named as it’s written on it: "A", "5", "Space", "Escape", and the arrows "Left", "Right", "Up" and "Down". A name that’s no key is never down, so if a key seems dead, check how you spelled it.

Typing

inkey is the key that’s held, as the letter it types, so Shift + A is "A" and A alone is "a". It’s "" when nothing is held. For a whole line, a name or an answer, input asks, waits for Enter, and gives you what was typed.

SCREEN 320, 240
name = input("What's your name? ")
PRINT "Hi,", name

The mouse

mousex and mousey are where the pointer is, in the game’s own pixels, however big the screen is shown. mousedown is 1 while the left button is held.

SCREEN 320, 240
CLS
DO
    IF COOLDOWN(1) AND mousedown
        MEMORYBUFFER "dot", 9, 9
            CIRCLE (4, 4), 4, 14, 14
        END MEMORYBUFFER
        FRAME
            PUTIMAGE (mousex - 4, mousey - 4), "dot"
        END FRAME
    END IF
LOOP

There’s no CLS in that loop, so whatever you draw stays: hold the button and scribble.

The wheel

mousewheel() is how many notches the wheel has turned since you last asked: up is more than 0, down is less, and 0 means it hasn’t moved. Roll it to grow the circle and shrink it again:

SCREEN 320, 240
size = 20
DO
    IF COOLDOWN(1)
        size = size + mousewheel() * 4
        IF size < 4 THEN size = 4
        IF size > 100 THEN size = 100
        FRAME
            CLS
            CIRCLE (160, 120), size, 14, 14
            LOCATE 2, 2
            PRINT "size", size
        END FRAME
    END IF
LOOP

Asking takes the notches, so a second mousewheel() straight after the first is 0. Ask once a frame, and keep the answer in a variable if more than one thing wants it. mousewheelx() is the same sideways, for a wheel that tilts or a trackpad, with right more than 0. A trackpad turns a little at a time, and the little bits add up to whole notches.

Crash-tan’s Tip: Rolling the wheel away from you is always up, even on a Mac set to scroll the “natural” way. Up should zoom in, or move a list up, whichever way the player’s pages scroll.

A finger

On a phone or a tablet, a finger is the mouse: mousex, mousey and mousedown follow it, so the scribbler above works there with no change. tap is 1 while a finger (or the mouse button) is held still, and stops being 1 once it starts to drag, which tells a tap from a swipe.

Crash-tan’s Tip: ismobile is 1 on a phone or a tablet. Use it to show buttons on the screen where a PC would use keys.

Gamepads

A gamepad works the way the keys do: joybutton(0) is 1 while button 0 is held, and joyaxis(0) is where the left stick is pushed, from -1 to 1. joyconnected says whether there’s one plugged in at all.

I don’t look at gamepads until a game asks me to, with GAMEPAD PHYSICAL ON. Say it next to SCREEN, so a pad is ready by the time anyone presses a button.

SCREEN 320, 240
GAMEPAD PHYSICAL ON
DO
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 2, 2
            IF joyconnected
                PRINT "stick", fixed_str(joyaxis(0), 2)
            ELSE
                PRINT "No gamepad. Plug one in and press a button!"
            END IF
        END FRAME
    END IF
LOOP

Images and sprites

Drawing everything again every frame works, but most of a game is things that move: the hero, the bad guys, the bullets. Those are sprites. You tell me where a sprite is and what it looks like, and I draw it every frame, on top of the layers, without your loop lifting a finger.

An image to show

A sprite shows an image. A game usually loads its images from files, with LOADIMG "hero.png", "hero". Here we’ll draw one. Everything drawn inside a MEMORYBUFFER goes into an image of its own instead of onto the screen: give it a name, and how big it is.

SCREEN 320, 240
MEMORYBUFFER "ball", 16, 16
    CIRCLE (8, 8), 7, 14, 14
    CIRCLE (5, 5), 2, 15, 15
END MEMORYBUFFER
PUTIMAGE (40, 40), "ball"
PUTIMAGE (80, 40), "ball" !! scale((3, 3))
PRINT "a ball,", imagewidth("ball"), "pixels across"
a ball, 16 pixels across

PUTIMAGE stamps an image onto the layer, once. A sprite is the image that stays.

A sprite

A sprite has a number. ENABLE SPRITE gets it ready, SET SPRITE says everything about it after !!, and SHOW SPRITE puts it on the screen.

SCREEN 320, 240
MEMORYBUFFER "ball", 16, 16
    CIRCLE (8, 8), 7, 14, 14
END MEMORYBUFFER

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(160) y(120) origin((0.5, 0.5)) scale((2, 2))
SHOW SPRITE 0

x = 160
DO
    IF COOLDOWN(1)
        IF keydown("Left") THEN x = x - 2
        IF keydown("Right") THEN x = x + 2
        SET SPRITE 0 !! x(x)
    END IF
LOOP

There’s no CLS and no drawing in the loop: the loop only says where the sprite is now. Click the screen and use the arrow keys.

origin((0.5, 0.5)) says that the sprite’s place is its middle. Without it, x and y are its top-left corner.

Crash-tan’s Tip: A sprite needs an image before it can show anything. After ENABLE SPRITE it’s already at (0, 0), so image alone will do, and x and y can follow. One that was never enabled has no place yet: give it x, y and image together. After that, SET SPRITE changes only what you name: x(20) moves it across and leaves its height, its image and the rest as they were. MOVE SPRITE 0, (x, y) moves it both ways at once.

When there’s a lot to say about a sprite, say it over a few lines. A line that starts with !! carries on the one above it:

SET SPRITE 0 !! image("ball") x(160) y(120)
    !! origin((0.5, 0.5)) scale((2, 2))
    !! behavior("bounce")

Where a sprite says it is

spritex(n) and spritey(n) say where a sprite’s top-left corner is, whatever its origin. A second number picks another point across it: 0.5 is the middle, 1 the far side.

SCREEN 320, 240
NEWIMAGE 32, 32, 14, "box"
ENABLE SPRITE 0
SET SPRITE 0 !! image("box") x(160) y(120) origin((0.5, 0.5))
SHOW SPRITE 0
PRINT spritex(0), spritex(0, 0.5), spritex(0, 1)
144 160 176

So to move a sprite from where it is, read it back at its origin:

SET SPRITE 0 !! x(spritex(0, 0.5) + 1)

Crash-tan’s Tip: With origin((0.5, 0.5)), x(spritex(0) + 1) doesn’t move it one pixel right. It moves it half its width left, every time!

Many sprites, one image

A sprite sheet is one image cut into frames, and a sprite shows one frame of it at a time. A file loads as one with !! spritesheet and the size of a frame. TOSPRITESHEET makes one from images you’ve drawn, a frame each, in order.

SCREEN 320, 240
MEMORYBUFFER "red", 16, 16
    CIRCLE (8, 8), 7, 4, 4
END MEMORYBUFFER
MEMORYBUFFER "green", 16, 16
    CIRCLE (8, 8), 7, 10, 10
END MEMORYBUFFER
TOSPRITESHEET "lights", ["red", "green"]

FOR n = 0 ... 9
    ENABLE SPRITE n
    SET SPRITE n !! image("lights") frame(n MOD 2) x(20 + n * 30) y(120)
    SHOW SPRITE n
NEXT

DO
    IF COOLDOWN(12)
        FOR n = 0 ... 9
            SET SPRITE n !! frame(int(rnd * 2))
        NEXT
    END IF
LOOP

Frame 0 is the red light and frame 1 the green one. Changing a sprite’s frame is how a character walks: the frames of the walk, one after another.

Finding a sprite a number

Numbering sprites yourself is fine for a hero and a few bad guys. For bullets and sparks that come and go, reserve_sprite() claims a number nobody is using and gives it to you. When you’re done with one, DISABLE SPRITE stops it drawing and colliding, and lets its number go.

SCREEN 320, 240
first = reserve_sprite()
second = reserve_sprite()
PRINT "got", first, "and", second
DISABLE SPRITE first
PRINT "is", first, "still in use?", spriteenabled(first)
got 0 and 1
is 0 still in use? 0

Moving lots of sprites from your loop works, but it isn’t how I like it best. I can move them myself, every tick, from a description of how they move. That’s a behavior, and it’s next.

Behaviors

A behavior is a description of how a sprite moves and looks: “drift right two pixels a tick”, “bob up and down”, “fade in”. You write it once, put it on as many sprites as you like, and I run it for every one of them, every tick, on my own. Your program doesn’t have to be in a loop at all.

SCREEN 320, 240
MEMORYBUFFER "ship", 16, 16
    POLYGON [(0, 16), (8, 0), (16, 16)], 11, 3
END MEMORYBUFFER

BEHAVIOR "drift"
    VELOCITY 1, 0
    OSCILLATE 0, 10 !! speed(2)
END BEHAVIOR

FOR n = 0 ... 4
    ENABLE SPRITE n
    SET SPRITE n !! image("ship") x(0) y(40 + n * 40) behavior("drift")
    SHOW SPRITE n
NEXT
PAUSE

The PAUSE at the end only keeps the game open until you press a key. Everything that moves, moves by itself.

VELOCITY 1, 0 moves one pixel right every tick. OSCILLATE 0, 10 swings up and down ten pixels either way, and !! speed(2) swings it faster.

What a behavior can say

There are lots of words to use inside a BEHAVIOR. A few to start with:

VELOCITY x, yMove this much every tick.
IMPULSE x, yA push, once, that it keeps going at.
ACCELERATION x, ySpeed up this much every tick. ACCELERATION 0, 0.4 is a fall.
BOUNCE x, yBounce off what it runs into, keeping this share of its speed.
ROTATE angle, ticksTurn to an angle, or with !! loop, keep turning.
ALPHA from, to, ticksFade.
SCALE from_x, from_y, to_x, to_y, ticksGrow or shrink.
OSCILLATE x, ySwing back and forth.
ANIMATE image, framesStep through a sprite sheet’s frames.
ORBIT distanceGo round a point (!! center((x, y))) or another sprite.

A sprite can have several behaviors at once, and they add up:

SCREEN 320, 240
MEMORYBUFFER "gem", 24, 24
    POLYGON [(12, 0), (24, 12), (12, 24), (0, 12)], 13, 5
END MEMORYBUFFER

BEHAVIOR "spin"
    ROTATE 360, 90 !! origin((0.5, 0.5)) loop
END BEHAVIOR
BEHAVIOR "pulse"
    ALPHA 0.3, 1, 40 !! loop
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("gem") x(148) y(108) scale((3, 3)) behavior("spin")
SET SPRITE 0 !! behavior("pulse")
SHOW SPRITE 0
PAUSE

Behaviors with settings

The same behavior can go different speeds on different sprites. PARAM reads a setting the sprite gives it, with the value to use when it gives none, and the sprite gives it with behavior_params.

SCREEN 320, 240
NEWIMAGE 8, 8, 15, "dot"

BEHAVIOR "fall"
    VELOCITY 0, PARAM("speed", 1)
END BEHAVIOR

FOR n = 0 ... 15
    ENABLE SPRITE n
    SET SPRITE n !! image("dot") x(10 + n * 19) y(0)
        !! behavior("fall") behavior_params({speed: 0.5 + n * 0.25})
    SHOW SPRITE n
NEXT
PAUSE

From 1.x: A parameter was written @speed. Here it’s PARAM("speed", 1), and the second value is what it is when a sprite doesn’t say.

Ticks and frames

I run behaviors on ticks of my own, sixty a second unless you say otherwise with SET BEHAVIOR RATE. The screen’s frames are a separate thing: a slow machine might show fewer frames, but a sprite still moves the same distance every second.

Behaviors are also where sprites find out they’ve hit something, which is the next lesson.

Bumping into things

I keep track of which sprites overlap which, every tick, so you don’t have to check every pair yourself. When two start to touch, I tell each one, and when they come apart, I tell them that too.

Walls

A boundary is a rectangle that’s part of the level: a floor, a wall, the edge of the screen. BOUNDARY ADD gives it a name and two corners with -> between them. A solid sprite can’t go through a solid boundary; with enclosure, it can’t get out of one.

SCREEN 320, 240
MEMORYBUFFER "ball", 12, 12
    CIRCLE (6, 6), 5, 14, 14
END MEMORYBUFFER

BOUNDARY ADD "box", (20, 20) -> (300, 220) !! solid enclosure
LINE (20, 20) -> (299, 219), 8 !! box

BEHAVIOR "fly"
    IMPULSE 3, 2
    BOUNCE 1, 1
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(100) y(60) solid behavior("fly")
SHOW SPRITE 0
PAUSE

IMPULSE 3, 2 is a push, once, when the behavior starts, and the ball keeps the speed it was given. BOUNCE 1, 1 turns it around when it runs into something solid, and keeps all of its speed: BOUNCE 0.8, 0.8 would lose a fifth of it at every bounce.

Crash-tan’s Tip: VELOCITY sets the speed again every tick, so a sprite with VELOCITY can’t be turned around by a bounce. For things that bounce, push them with IMPULSE.

Finding out

ONCOLLISION, inside a behavior, runs when a sprite with that behavior touches something. It’s given who it is (me), what it touched (other, a sprite’s number, or -1 for a boundary), the other thing’s tag (other_tag), and for a boundary, which edge of it was hit. It runs on my tick, beside your program, so here it only counts, and the loop does the drawing.

SCREEN 320, 240
MEMORYBUFFER "ball", 12, 12
    CIRCLE (6, 6), 5, 15, 15
END MEMORYBUFFER

BOUNDARY ADD "box", (0, 0) -> (320, 240) !! solid enclosure
hits = 0
last = ""

BEHAVIOR "fly"
    IMPULSE 4, 3
    BOUNCE 1, 1
    ONCOLLISION
        hits = hits + 1
        last = edge
    END ONCOLLISION
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(160) y(120) solid behavior("fly")
SHOW SPRITE 0
DO
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 1, 1
            PRINT "hits:", hits, "  last edge:", last
        END FRAME
    END IF
LOOP

Sprites touching sprites

Two sprites don’t have to be solid to touch. I tell them whenever they overlap. AFTERCOLLISION runs when they come apart again.

SCREEN 320, 240
MEMORYBUFFER "blue", 16, 16
    LINE (0, 0) -> (15, 15), 9 !! box fill
END MEMORYBUFFER
MEMORYBUFFER "red", 16, 16
    LINE (0, 0) -> (15, 15), 12 !! box fill
END MEMORYBUFFER
TOSPRITESHEET "block", ["blue", "red"]

BEHAVIOR "slide"
    VELOCITY PARAM("vx", 1), 0
    ONCOLLISION
        IF other >= 0 THEN SET SPRITE me !! frame(1)
    END ONCOLLISION
    AFTERCOLLISION
        IF other >= 0 THEN SET SPRITE me !! frame(0)
    END AFTERCOLLISION
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("block") frame(0) x(20) y(112)
    !! behavior("slide") behavior_params({vx: 1})
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("block") frame(0) x(284) y(112)
    !! behavior("slide") behavior_params({vx: -1})
SHOW SPRITE 1
PAUSE

The blocks are blue, turn red while they overlap, and go blue again once they’ve passed through each other.

Crash-tan’s Tip: Being shown and being solid have nothing to do with touching. A hidden sprite still touches things, and a sprite that isn’t solid still hears about every overlap. Solid only means it can’t go through.

Asking instead

Sometimes you just want to know, right now, from your own loop. spritetouching(0, "TOP") says whether sprite 0 is up against the top of a boundary (standing on a floor, say), and gives the boundary’s tag, or "". point_in_box(x, y, left, top, width, height) asks whether a point is inside a box, which is handy for clicking on things.

SCREEN 320, 240
DO
    IF COOLDOWN(1)
        over = point_in_box(mousex, mousey, 110, 90, 100, 60)
        shade = 9
        IF over THEN shade = 12
        MEMORYBUFFER "button"
            LINE (110, 90) -> (210, 150), shade !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "button"
        END FRAME
    END IF
LOOP

Sound and music

Crash-tan’s Tip: Browsers keep a page quiet until you’ve clicked on it. Press Try it or Run and I’m allowed to make noise.

Beeps and tones

BEEP is the classic. SOUND plays a tone: how high, in hertz, and for how many seconds.

SCREEN 320, 240
BEEP
SOUND 440, 0.2
SOUND 660, 0.2
SOUND 880, 0.4
PRINT "ta-da!"
PAUSE
ta-da!

When a program ends, its sounds stop with it, so PAUSE at the end keeps it going until you press a key. A game never needs one: it’s still running.

Tunes

PLAY plays a tune written as notes, the way the old BASICs did it: T sets the tempo, O the octave, L how long a note is (4 is a quarter note, 8 an eighth), and then the notes themselves.

SCREEN 320, 240
PLAY "T160 O4 L8 C D E F G4 G4 A A A A G2"
PRINT "done"
PAUSE
done

PLAY waits for its tune to finish. In a game you’d rather it didn’t, so the game goes on while it plays: !! background.

SCREEN 320, 240
PLAY "T120 O3 L4 C E G > C < G E C2" !! background
x = 0
DO
    IF COOLDOWN(1)
        MEMORYBUFFER "scene"
            CIRCLE (x, 120), 10, 13, 13
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
        x = (x + 3) MOD 320
    END IF
LOOP

Music

For a whole song with several instruments, describe it in a MUSIC block: the patterns, the tracks in each one, and the order they play in. Then PLAYMUSIC plays it, in the background, for as long as you like.

SCREEN 320, 240
MUSIC "theme"
    TEMPO 140
    PATTERN "verse"
        TRACK !! channel(1) notes("o4 l8 c e g e c e g e")
        TRACK !! channel(2) notes("o2 l2 c g")
    END PATTERN
    PATTERN "chorus"
        TRACK !! channel(1) notes("o4 l8 f a > c < a g b > d < b")
        TRACK !! channel(2) notes("o2 l2 f g")
    END PATTERN
    ARRANGEMENT
        PLAY "verse" !! repeat(2)
        PLAY "chorus"
    END ARRANGEMENT
END MUSIC
PLAYMUSIC "theme"
PRINT "playing", musicplaying()
PAUSE

There’s a lot more where that came from: instruments you design yourself, envelopes and filters, samples, fading one song into another. The reference has every word, and the music lessons will come later in this book.

Tile maps

A level is mostly the same few pictures over and over: floor, wall, grass, water. A tile map is a grid of numbers, one for each square of the level, and an image of tiles to draw them from. You change a number and I change the picture.

The tiles

The tiles are one image cut into squares, the same way a sprite sheet is. Here are three, sixteen pixels square, drawn side by side in a MEMORYBUFFER: grass, a wall and water.

SCREEN 320, 240
MEMORYBUFFER "tiles", 48, 16
    LINE (0, 0) -> (15, 15), 2 !! box fill
    LINE (16, 0) -> (31, 15), 8 !! box fill
    LINE (17, 1) -> (30, 7), 7 !! box
    LINE (32, 0) -> (47, 15), 1 !! box fill
END MEMORYBUFFER
PUTIMAGE (140, 100), "tiles" !! scale((2, 2))

The map

The grid is something your program keeps: a grid of whole numbers, rows of columns. A TILEMAP says which image the tiles come from, how big a tile is, how big the map is, and which grid it’s drawn from. CREATE MAP puts it on a layer.

SCREEN 320, 240
MEMORYBUFFER "tiles", 48, 16
    LINE (0, 0) -> (15, 15), 2 !! box fill
    LINE (16, 0) -> (31, 15), 8 !! box fill
    LINE (17, 1) -> (30, 7), 7 !! box
    LINE (32, 0) -> (47, 15), 1 !! box fill
END MEMORYBUFFER

// 15 rows of 20 tiles fill a 320 x 240 screen.
ground = (15, 20):INTEGER
FOR row = 0 ... 14
    FOR column = 0 ... 19
        ground(row, column) = 1
    NEXT
NEXT
FOR column = 0 ... 19
    ground(0, column) = 2
    ground(14, column) = 2
NEXT
FOR row = 5 ... 9
    FOR column = 7 ... 12
        ground(row, column) = 3
    NEXT
NEXT

TILEMAP "field"
    TILESET "tiles"
    TILESIZE 16
    SIZE 20, 15
    TILES 0 !! from(ground)
END TILEMAP
CREATE MAP "field" !! layer_to(0)
corner = maptile(0, 0)
pond = maptile(9, 7)
SET LAYER 1
PRINT "tile at the top left:", corner, " in the pond:", pond
tile at the top left: 1  in the pond: 2

A number in the grid picks a tile from the image, counting from 1: 1 is grass, 2 the wall, 3 the water, and 0 is no tile at all. maptile(x, y) asks which tile a square shows, counting the image’s tiles from 0 the way the frames of a sprite sheet are counted, so the wall is 1 there and the water 2.

Crash-tan’s Tip: I draw a map’s layer afresh every frame, so words printed on that layer are gone as soon as they’re there. Print on a layer above the map, the way this goes up to layer 1. maptile asks about the map on the layer you’re on, so ask it first, then go up.

Changing the map

The grid is still yours after the map is made. Change it, then UPDATE MAP shows the changes all at once. Click a square to drain the pond, or to flood the grass:

SCREEN 320, 240
MEMORYBUFFER "tiles", 48, 16
    LINE (0, 0) -> (15, 15), 2 !! box fill
    LINE (16, 0) -> (31, 15), 8 !! box fill
    LINE (17, 1) -> (30, 7), 7 !! box
    LINE (32, 0) -> (47, 15), 1 !! box fill
END MEMORYBUFFER

ground = (15, 20):INTEGER
FOR row = 0 ... 14
    FOR column = 0 ... 19
        ground(row, column) = 1
    NEXT
NEXT
TILEMAP "field"
    TILESET "tiles"
    TILESIZE 16
    SIZE 20, 15
    TILES 0 !! from(ground)
END TILEMAP
CREATE MAP "field" !! layer_to(0)

was_down = FALSE
DO
    IF COOLDOWN(1)
        IF mousedown AND NOT was_down
            column = int(mousex / 16)
            row = int(mousey / 16)
            IF ground(row, column) = 1 THEN ground(row, column) = 3 ELSE ground(row, column) = 1
            UPDATE MAP "field"
        END IF
        was_down = mousedown
    END IF
LOOP

Bigger than the screen

A map can be much bigger than the screen. SET SCROLL slides the layer it’s on, and the screen shows the part you’ve scrolled to.

SCREEN 320, 240
MEMORYBUFFER "tiles", 48, 16
    LINE (0, 0) -> (15, 15), 2 !! box fill
    LINE (16, 0) -> (31, 15), 8 !! box fill
    LINE (32, 0) -> (47, 15), 1 !! box fill
END MEMORYBUFFER

// Sixty tiles across: three screens wide.
ground = (15, 60):INTEGER
FOR row = 0 ... 14
    FOR column = 0 ... 59
        ground(row, column) = 1 + (int(column / 4) + int(row / 3)) MOD 3
    NEXT
NEXT
TILEMAP "long"
    TILESET "tiles"
    TILESIZE 16
    SIZE 60, 15
    TILES 0 !! from(ground)
END TILEMAP
CREATE MAP "long" !! layer_to(0)
SET LAYER 0

x = 0
DO
    IF COOLDOWN(1)
        IF keydown("Right") AND x < 640 THEN x = x + 4
        IF keydown("Left") AND x > 0 THEN x = x - 4
        SET SCROLL x, 0
    END IF
LOOP

Click the screen and use the arrow keys to scroll along.

Saving your game

A game remembers things between one time it’s played and the next: the high score, how far you got, which keys you like. It keeps them in files, in the game’s own folder.

Crash-tan’s Tip: Here in the book, a game’s files live in the page, and they’re gone when you reload it. In a real game they’re kept: on a PC in the game’s folder, in a browser in its storage, on a phone in the app’s.

A file of text

SAVETEXT saves some text as a file, in place of whatever was there, and loadtext reads it back. fileexists says whether there’s a file to read.

SCREEN 320, 240
IF fileexists("best.txt")
    PRINT "best so far:", loadtext("best.txt")
ELSE
    PRINT "no best yet"
END IF
SAVETEXT "best.txt", "1200"
PRINT "best so far:", loadtext("best.txt")
no best yet
best so far: 1200

Now press Run again: this time there’s a best.txt from the run before, so there’s a best to read straight away.

Saving everything at once

A game’s state is usually more than one number. Put it in a map, and serialize turns the whole thing into text (JSON, which other programs can read too), and deserialize turns it back.

SCREEN 320, 240
player = {name: "Ada", level: 3, items: ["key", "lamp"]}
SAVETEXT "save.json", serialize(player)

back = deserialize(loadtext("save.json"))
PRINT back.name, "is on level", back.level, "with", back.items
Ada is on level 3 with [key, lamp]

A line at a time

For a file that grows, like a log or a list of scores, open it and write lines into it. OPEN gives you the file: !! output starts it empty, !! append adds to the end, and without either it’s opened to read. WRITE writes one line, and CLOSE finishes, which is when it’s saved.

SCREEN 320, 240
scores = OPEN("scores.txt") !! output
WRITE scores, "Ada", 1200
WRITE scores, "Bob", 900
CLOSE scores

scores = OPEN("scores.txt") !! append
WRITE scores, "Cy", 1500
CLOSE scores

scores = OPEN("scores.txt")
WHILE NOT eof(scores)
    PRINT READLINE(scores)
WEND
CLOSE scores
Ada 1200
Bob 900
Cy 1500

From 1.x: A file is a value now, so there’s no #1 to keep track of. Keep it in a variable like anything else.

A whole game

Time to put it together! Stars fall from the sky. Catch them in your basket for a point each, and let three hit the ground and the game’s over.

Every piece of it is something from an earlier lesson:

  • the basket and the stars are sprites, showing images drawn in a MEMORYBUFFER;
  • the stars fall by themselves, with a behavior;
  • ONCOLLISION tells a star it’s been caught, or that it hit the ground;
  • the keys move the basket, and COOLDOWN keeps the loop’s time;
  • CALL RENDER draws the score every frame, under the sprites;
  • BEEP and SOUND say what happened.
SCREEN 320, 240
RANDOMIZE

// --- What things look like ---------------------------------------------
MEMORYBUFFER "basket", 40, 12
    LINE (0, 4) -> (39, 11), 6 !! box fill
END MEMORYBUFFER

MEMORYBUFFER "star", 11, 11
    POLYGON [(5, 0), (6, 4), (10, 4), (7, 6), (8, 10), (5, 8), (2, 10), (3, 6), (0, 4), (4, 4)], 14, 14
END MEMORYBUFFER

// The ground: a star that reaches it is lost.
BOUNDARY ADD "ground", (0, 236) -> (320, 240)

// --- The score ---------------------------------------------------------
ATOMIC caught = 0
ATOMIC missed = 0

FUNCTION draw()
    FRAME
        CLS
        LOCATE 1, 1
        PRINT "caught", caught, "   missed", missed, "of 3"
        IF missed >= 3
            LOCATE 14, 16
            PRINT "GAME OVER"
        END IF
    END FRAME
END FUNCTION
CALL RENDER draw, "score"

// --- Falling stars ------------------------------------------------------
BEHAVIOR "fall"
    VELOCITY 0, PARAM("speed", 1)
    ONCOLLISION
        // A star touching something touches it every tick until it's gone:
        // count it only the first time, and not once the game's over.
        IF spriteenabled(me) AND missed < 3
            IF other_tag = "basket"
                caught = caught + 1
                DISABLE SPRITE me
            ELSE IF other_tag = "ground"
                missed = missed + 1
                DISABLE SPRITE me
            END IF
        END IF
    END ONCOLLISION
END BEHAVIOR

FUNCTION drop_star()
    id = reserve_sprite()
    IF id < 0 THEN RETURN
    speed = 1 + rnd * 1.5 + caught / 10
    SET SPRITE id !! image("star") x(10 + rnd * 290) y(0)
        !! tag("star") behavior("fall") behavior_params({speed: speed})
    SHOW SPRITE id
END FUNCTION

// --- The basket, and the game ------------------------------------------
basket = reserve_sprite()
x = 140
SET SPRITE basket !! image("basket") x(x) y(220) tag("basket")
SHOW SPRITE basket

DO WHILE missed < 3
    // Sixty times a second: move the basket.
    IF COOLDOWN(1)
        IF keydown("Left") AND x > 0 THEN x = x - 4
        IF keydown("Right") AND x < 280 THEN x = x + 4
        SET SPRITE basket !! x(x)
    END IF
    // Every 45 ticks, three quarters of a second: another star.
    IF COOLDOWN(45) THEN drop_star()
LOOP
SOUND 220, 0.5

Click the screen, then use the arrow keys.

The score is drawn on layer 0, the bottom one, and the sprites are drawn above it. A layer of its own would have to be cleared to see-through rather than to black, or it would hide the sprites under it.

Make it yours

It’s yours now. Some things to try:

  • Faster. Drop a star every 30 ticks instead of 45, or make them fall faster as the score goes up.
  • A sound for a catch. Put a BEEP in the ONCOLLISION where a star is caught.
  • Bad stars. Drop a red one now and then, with a tag of its own, that costs you a point if you catch it.
  • A best score. When the game’s over, save caught with SAVETEXT if it’s better than what loadtext gives back.
  • A sky. Draw something on layer 0 before the game starts: it stays put under everything, the way the sun did in Drawing on the screen.

Pictures from files

Drawing with circles and lines takes you a long way, but sooner or later you’ll want a picture somebody painted: a hero, a skyline, a big orange explosion. Every game in this book has a few of those in its folder, and I’ll load them by name.

Loading a picture

LOADIMG takes a file from the game’s folder and gives it a name. After that, the name is all I need. imagewidth and imageheight say how big it is.

SCREEN 320, 240
LOADIMG "hero.png", "tan"
LOADIMG "city.png", "city"
PRINT "the hero:", imagewidth("tan"), "by", imageheight("tan")
PRINT "the city:", imagewidth("city"), "by", imageheight("city")
PUTIMAGE (0, 0), "city"
PUTIMAGE (200, 150), "tan"
the hero: 40 by 40
the city: 256 by 256

That’s the hero in the corner, in front of my city. PUTIMAGE stamps a picture onto the layer with its top-left corner where you say, and the parts of the file that are see-through stay see-through.

Putting it down your way

PUTIMAGE takes modifiers after !!. scale stretches it, scale_to makes it an exact size in pixels, flip_x mirrors it, and alpha makes it partly see-through, from 0 (gone) to 1 (solid).

SCREEN 320, 240
LOADIMG "hero.png", "tan"
LOADIMG "city.png", "city"
PUTIMAGE (0, 0), "city" !! scale_to((320, 240))
PUTIMAGE (10, 160), "tan"
PUTIMAGE (80, 160), "tan" !! flip_x
PUTIMAGE (150, 160), "tan" !! alpha(0.4)
PUTIMAGE (210, 40), "tan" !! scale((2, 2))

The city is 256 pixels square and the screen isn’t, so scale_to((320, 240)) stretches it to fill the screen.

rotate turns a picture by a number of degrees, about its middle. origin says which point of the picture goes where you put it, from (0, 0), its top left, to (1, 1), its bottom right. With origin((0.5, 0.5)), the place you give is the picture’s middle, so a picture that spins stays put:

SCREEN 320, 240
LOADIMG "hero.png", "tan"
LOADIMG "city.png", "city"
angle = 0
DO
    IF COOLDOWN(1)
        fade = 0.6 + sin(angle / 20) * 0.4
        FRAME
            CLS
            PUTIMAGE (0, 0), "city" !! scale_to((320, 240))
            PUTIMAGE (160, 120), "tan" !! origin((0.5, 0.5)) rotate(angle) alpha(fade)
        END FRAME
        angle = angle + 3
    END IF
LOOP

There are no shapes here, so there’s no MEMORYBUFFER: the pictures go straight into the FRAME.

A sheet of frames

explosion.png is one picture with sixteen little pictures in it, four rows of four, each 32 pixels square. !! spritesheet((32, 32)) loads it cut into frames, numbered from 0 at the top left, across and then down. PUTIMAGE takes the frame to show after the name.

SCREEN 320, 240
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
PRINT "a frame is", imagewidth("boom"), "by", imageheight("boom")
FOR f = 0 ... 15
    PUTIMAGE (8 + f * 19, 104), "boom", f
NEXT
a frame is 32 by 32

Once a picture is a sheet, imagewidth and imageheight give the size of one frame. All sixteen in a row are the whole bang, from the first spark to the last wisp of smoke.

A sprite shows a sheet the same way, with frame. Here are four explosions, each a few frames ahead of the next. When one is done, it moves somewhere else and goes off again.

SCREEN 320, 240
RANDOMIZE
LOADIMG "city.png", "city"
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
PUTIMAGE (0, 0), "city" !! scale_to((320, 240))

frames = [0, 4, 8, 12]
FOR n = 0 ... 3
    ENABLE SPRITE n
    SET SPRITE n !! image("boom") frame(frames(n)) x(rnd * 256) y(rnd * 176)
        !! scale((2, 2))
    SHOW SPRITE n
NEXT

DO
    IF COOLDOWN(3)
        FOR n = 0 ... 3
            frames(n) = frames(n) + 1
            IF frames(n) = 16
                frames(n) = 0
                SET SPRITE n !! x(rnd * 256) y(rnd * 176)
            END IF
            SET SPRITE n !! frame(frames(n))
        NEXT
    END IF
LOOP

COOLDOWN(3) moves each one on a frame every three ticks, so a whole explosion takes 48 ticks: a little under a second.

Crash-tan’s Tip: Try COOLDOWN(1) for a quick pop, or COOLDOWN(6) for a slow, lazy cloud.

Cutting out a piece

GETIMAGE copies part of the layer into a new picture. It takes two corners with -> between them. The second corner is just past the piece, so (96, 0) -> (160, 128) is 64 pixels across and 128 down.

SCREEN 320, 240
LOADIMG "city.png", "city"
PUTIMAGE (0, 0), "city"
GETIMAGE (96, 0) -> (160, 128), "tower"
PRINT "the tower is", imagewidth("tower"), "by", imageheight("tower")
CLS
PUTIMAGE (96, 0), "tower" !! scale((2, 1.75))
the tower is 64 by 128

I drew the whole city, cut the middle out, cleared the screen and put the piece back, bigger. It’s handy for a snapshot of the level behind a pause menu, or for keeping a picture you built up from lots of others.

Pixel by pixel

For something tiny, you can skip the file and write the pixels yourself. ASIMAGE takes rows of palette colors, with -1 for see-through, then a name and the picture’s width and height.

SCREEN 320, 240
heart = [[-1, 4, -1, 4, -1], [4, 12, 4, 12, 4], [4, 12, 12, 12, 4], [-1, 4, 12, 4, -1], [-1, -1, 4, -1, -1]]
ASIMAGE heart, "heart", 5, 5
FOR n = 0 ... 2
    PUTIMAGE (8 + n * 48, 8), "heart" !! scale((8, 8))
NEXT
PRINT "three lives"
three lives

Tidying up

A picture stays loaded until you free it. FREE IMAGE lets it go, which is worth doing with big pictures once a level is over. exists says whether something is loaded under a name.

SCREEN 320, 240
LOADIMG "city.png", "level one"
PRINT "loaded?", exists("level one")
FREE IMAGE "level one"
PRINT "still loaded?", exists("level one"), " width now:", imagewidth("level one")
loaded? 1
still loaded? 0  width now: 0

Once it’s gone, the name is free for the next level’s picture.

Words and fonts

A game talks: a score in the corner, a name over a door, a friend with something to tell you. You’ve met PRINT and LOCATE already. This lesson is about making words look the way you want and putting them exactly where you want them.

A font of your own

My own letters are chunky and plain. LOADFONT loads a TrueType or OpenType font from the game’s folder under a name, and SETFONT says how big to write and in which font. FONT RESET goes back to mine.

SCREEN 320, 240
LOADFONT "font.ttf", "story"
SETFONT 24, "story"
PRINT "Once upon a time"
SETFONT 12
PRINT "...in a city far, far away"
FONT RESET
PRINT "back to my letters"
Once upon a time
...in a city far, far away
back to my letters

SETFONT without a font name changes only the size, so the second line is still in "story", just smaller.

Exactly where

LOCATE counts rows and columns, which is lovely for lining things up but coarse when you want a word at a particular pixel. !! origin gives the pixel the rows and columns count from: LOCATE 1, 1 !! origin((200, 8)) puts the cursor at x 200, y 8.

COLOR sets the color of the words, and PRINT !! same_line leaves the cursor at the end of what it printed, so the next PRINT carries on along the same line. Together, they make a score with a label in one color and the number in another:

SCREEN 320, 240
score = 0
lives = 3
DO
    IF COOLDOWN(1)
        score = score + 7
        FRAME
            CLS
            LOCATE 1, 1 !! origin((8, 8))
            COLOR 14
            PRINT "SCORE " !! same_line
            COLOR 15
            PRINT score
            LOCATE 1, 1 !! origin((240, 8))
            COLOR 12
            PRINT "LIVES " !! same_line
            COLOR 15
            PRINT lives
        END FRAME
    END IF
LOOP

Crash-tan’s Tip: COLOR 15, 1 gives the words a blue background as well, which keeps them easy to read over a busy picture.

Somebody talking

Two more of PRINT’s modifiers are for conversations. words wraps a long line at the edge of the screen between words, never in the middle of one. rate types it out a letter at a time, this many milliseconds apart, the way role-playing games always have.

PRINT doesn’t wait for the typing to finish, so your game carries on while it goes. typing is TRUE until the last letter is out. Here the hero bobs up and down while I talk, and stops when I’m done:

SCREEN 320, 240
LOADIMG "hero.png", "tan"
MEMORYBUFFER "box"
    LINE (0, 172) -> (319, 239), 1 !! box fill
    LINE (0, 172) -> (319, 239), 15 !! box
END MEMORYBUFFER
PUTIMAGE (0, 0), "box"

ENABLE SPRITE 0
SET SPRITE 0 !! image("tan") x(240) y(132)
SHOW SPRITE 0

COLOR 15, 1
LOCATE 1, 1 !! origin((8, 180))
PRINT "Hi! I'm Crash-tan, and this is my city. Mind the explosions: they're mostly harmless." !! rate(40) words

bob = 0
DO WHILE typing
    IF COOLDOWN(5)
        bob = 1 - bob
        SET SPRITE 0 !! y(132 - bob * 3)
    END IF
LOOP
SET SPRITE 0 !! y(132)
LOCATE 1, 1 !! origin((200, 224))
PRINT "press a key"
PAUSE

Try a rate of 10 for somebody in a hurry, or 120 for somebody very, very sleepy.

Words you can move

Printed words stay where they’re printed. TEXTTOIMAGE writes words into a picture instead: the text, a name for the picture, and if you like, a size and a font. A picture can be stamped with PUTIMAGE, scaled, spun, or shown by a sprite, so here’s a title that drops in and then floats:

SCREEN 320, 240
LOADFONT "font.ttf", "story"
LOADIMG "city.png", "city"
PUTIMAGE (0, 0), "city" !! scale_to((320, 240))
TEXTTOIMAGE "CRASH CITY", "title", 32, "story"

ENABLE SPRITE 0
SET SPRITE 0 !! image("title") x(160) y(-20) origin((0.5, 0.5))
SHOW SPRITE 0

y = -20
t = 0
DO
    IF COOLDOWN(1)
        IF y < 90 THEN y = y + 3
        t = t + 1
        SET SPRITE 0 !! y(y + sin(t / 15) * 4)
    END IF
LOOP

The words are a sprite now, so I draw them every frame without any PRINT at all.

A screen of letters

Some games are nothing but letters: a text adventure, a roguelike, a terminal in a spaceship. SCREEN TEXT opens a screen measured in characters, columns across and rows down, in place of pixels.

SCREEN TEXT 40, 25
COLOR 14
LOCATE 2, 14
PRINT "THE DARK CAVE"
COLOR 7
LOCATE 5, 3
PRINT "You are standing at the mouth of a cave."
LOCATE 6, 3
PRINT "A cold wind blows from inside."
COLOR 10
LOCATE 9, 3
PRINT "> go north"
THE DARK CAVE
You are standing at the mouth of a cave.
A cold wind blows from inside.
> go north

!! modern swaps my plain ASCII letters for my modern character set.

A pointer of your own

SET CURSOR turns any picture into the mouse pointer, and !! hotspot says which of its pixels is the tip. SET CURSOR !! off brings the ordinary pointer back.

SCREEN 320, 240
MEMORYBUFFER "arrow", 12, 12
    POLYGON [(0, 0), (11, 5), (5, 6), (4, 11)], 15, 14
END MEMORYBUFFER
SET CURSOR "arrow" !! hotspot((0, 0))
PRINT "Move the mouse over the screen, then press a key."
PAUSE
SET CURSOR !! off

The arrow’s tip is its top-left pixel, (0, 0), so that’s the pixel the pointer is pointing at.

Layers in depth

You met layers back in Drawing on the screen: sheets of glass, one above the other, each with its own drawing. They can do a lot more than sit there. A layer can fade, hide, show only a window of itself, slide, repeat forever, and even lie down flat like a floor.

Making layers

Layer 0 is always there. CREATE LAYER makes another, with a number and a size, and a bigger number sits above a smaller one. Sprites go on top of them all.

SET LAYER says which layer drawing goes to from now on. For a few lines of drawing on another layer, WITH LAYER is tidier: inside the block, drawing goes to that layer, and after it, I go back to the one you were on.

SCREEN 320, 240
LOADIMG "city.png", "city"
LOADIMG "hero.png", "tan"
CLS 1
CREATE LAYER 1, 320, 240
CREATE LAYER 2, 320, 240
WITH LAYER 1
    PUTIMAGE (32, 0), "city"
END WITH LAYER
WITH LAYER 2
    PUTIMAGE (140, 184), "tan"
END WITH LAYER
PRINT "blue on 0, city on 1, the hero on 2"
blue on 0, city on 1, the hero on 2

A new layer starts out see-through, so the blue of layer 0 shows down the sides of the city, and the city shows around the hero.

Crash-tan’s Tip: CLS on a layer clears it to solid black, which hides everything under it, sprites included if it’s the top one. Clear the bottom layer, and leave the ones above for things that stay put or that you draw once.

How a layer shows

SET LAYER takes modifiers that change how a layer shows, without touching what’s drawn on it. alpha makes the whole layer see-through, from 0 to 1. visible(FALSE) hides it and visible shows it again. clip shows only the part of it inside a rectangle of the screen, given as two corners with -> between them.

Here, the city on layer 1 shows only through a window that slides back and forth, and a red warning stripe on layer 2 blinks on and off:

SCREEN 320, 240
LOADIMG "city.png", "city"
MEMORYBUFFER "stripe", 320, 16
    LINE (0, 0) -> (319, 15), 4 !! box fill
END MEMORYBUFFER

CREATE LAYER 1, 320, 240
WITH LAYER 1
    PUTIMAGE (0, 0), "city" !! scale_to((320, 240))
END WITH LAYER
CREATE LAYER 2, 320, 240
WITH LAYER 2
    PUTIMAGE (0, 200), "stripe"
END WITH LAYER
SET LAYER 2 !! alpha(0.7)

x = 0
dx = 2
shown = TRUE
DO
    IF COOLDOWN(1)
        x = x + dx
        IF x <= 0 OR x >= 220 THEN dx = -dx
        SET LAYER 1 !! clip((x, 40) -> (x + 100, 190))
    END IF
    IF COOLDOWN(30)
        shown = NOT shown
        SET LAYER 2 !! visible(shown)
    END IF
LOOP

SET LAYER 1 !! clip(...) makes layer 1 the one drawing goes to as well, so if your loop draws too, say which layer you mean first. SET LAYER 1 !! clear_clip shows the whole layer again.

Sliding along

SET SCROLL slides the current layer, by x across and y down, and getscroll gives back where a layer has been slid to: the current one, or the one whose number you give it.

SCREEN 320, 240
CREATE LAYER 1, 320, 240
WITH LAYER 1
    SET SCROLL 40, 10
END WITH LAYER
PRINT "layer 0:", getscroll(0), " layer 1:", getscroll(1)
layer 0: (0, 0)  layer 1: (40, 10)

Slide a layer far enough and you run off the end of its picture. wrap_x fixes that: the layer repeats, side by side, forever. The city is 256 pixels across, so here it repeats every 256 pixels and never runs out.

SCREEN 320, 240
LOADIMG "city.png", "city"
CREATE LAYER 1, 256, 256
SET LAYER 1 !! wrap_x
PUTIMAGE (0, 0), "city"
x = 0
DO
    IF COOLDOWN(1)
        x = x + 1
        SET SCROLL x, 0
    END IF
LOOP

The layer is the city’s size, not the screen’s, so it wraps right where the picture ends. wrap_y does the same up and down.

Far away and close by

Look out of a train window: the hills far away hardly move, and the posts by the track whip past. Scroll layers at different speeds and you get the same thing. That’s parallax, and it’s the cheapest way I know to make a flat game look deep.

The city goes on layer 1 and moves at a quarter of the speed. A street of my own, with a lamp post, goes on layer 2 at full speed. Both wrap. The hero stands on the street as a sprite, and the arrow keys walk them along.

SCREEN 320, 240
LOADIMG "city.png", "city"
LOADIMG "hero.png", "tan"
MEMORYBUFFER "street", 160, 240
    LINE (0, 200) -> (159, 239), 8 !! box fill
    LINE (0, 200) -> (159, 203), 7 !! box fill
    LINE (78, 110) -> (81, 199), 7 !! box fill
    CIRCLE (80, 106), 6, 14, 14
END MEMORYBUFFER

CREATE LAYER 1, 256, 256
SET LAYER 1 !! wrap_x
PUTIMAGE (0, 0), "city"
CREATE LAYER 2, 160, 240
SET LAYER 2 !! wrap_x
PUTIMAGE (0, 0), "street"

ENABLE SPRITE 0
SET SPRITE 0 !! image("tan") x(140) y(160)
SHOW SPRITE 0

x = 0
DO
    IF COOLDOWN(1)
        IF keydown("Right") THEN x = x + 3
        IF keydown("Left") THEN x = x - 3
        WITH LAYER 1
            SET SCROLL x / 4, 0
        END WITH LAYER
        WITH LAYER 2
            SET SCROLL x, 0
        END WITH LAYER
    END IF
LOOP

Click the screen and hold an arrow key. The hero stays in the middle and the world goes by: the lamp posts quickly, the city slowly. Put another layer in between at half the speed, and it looks deeper still.

Lying down flat

A layer can be a plane: a picture I turn, zoom, and even lay down like a floor going off into the distance. The old consoles called it Mode 7.

SET LAYER 1 !! plane("affine") makes layer 1 a flat plane that turns and zooms. SET SCROLL then says which point of the layer is in the middle of the screen, and its !! angle turns the layer that many degrees about it. zoom is how much of the layer fits on the screen: zoom(2) shows twice as much, so everything looks half the size.

SCREEN 320, 240
LOADIMG "city.png", "city"
CREATE LAYER 1, 256, 256
SET LAYER 1 !! plane("affine") zoom(1) wrap_x wrap_y
PUTIMAGE (0, 0), "city"
turn = 0
DO
    IF COOLDOWN(1)
        turn = turn + 1
        SET LAYER 1 !! plane("affine") zoom(1.5 + sin(turn / 40))
        SET SCROLL (128, 128) !! angle(turn)
    END IF
LOOP

With wrap_x and wrap_y, the city repeats in every direction, so there’s never an edge to see however far out it zooms.

plane("perspective") lays the layer down as a floor. horizon is the screen row where the floor meets the sky, and above it the layer is see-through. height is how high the camera is over the floor: higher, and you see more of it at once. SET SCROLL is now where the camera stands on the layer, and angle is which way it faces.

Here’s a checkered floor, built in a MEMORYBUFFER and repeated forever, with the city behind it on layer 0. Up drives forward, and Left and Right turn.

SCREEN 320, 240
LOADIMG "city.png", "city"
PUTIMAGE (0, 0), "city" !! scale_to((320, 240))
MEMORYBUFFER "checks", 64, 64
    LINE (0, 0) -> (31, 31), 5 !! box fill
    LINE (32, 32) -> (63, 63), 5 !! box fill
    LINE (32, 0) -> (63, 31), 13 !! box fill
    LINE (0, 32) -> (31, 63), 13 !! box fill
END MEMORYBUFFER

CREATE LAYER 1, 64, 64
SET LAYER 1 !! plane("perspective") horizon(100) height(64) wrap_x wrap_y
PUTIMAGE (0, 0), "checks"

heading = 0
cx = 0
cy = 0
DO
    IF COOLDOWN(1)
        IF keydown("Left") THEN heading = heading + 2
        IF keydown("Right") THEN heading = heading - 2
        IF keydown("Up")
            cx = cx - sin(heading * pi / 180) * 3
            cy = cy + cos(heading * pi / 180) * 3
        END IF
        SET SCROLL (cx, cy) !! angle(heading)
    END IF
LOOP

angle is in degrees, and sin and cos work in radians, so pi / 180 turns one into the other. The camera faces down the layer when heading is 0, and the two lines under keydown("Up") move it a step whichever way it’s facing.

Crash-tan’s Tip: Give SET SCROLL its angle every time on a plane. Leave it out and the angle goes back to 0. plane("flat") makes the layer an ordinary one again.

Putting them away

FREE LAYER throws a layer away, drawing and all. RESET LAYERS throws away every layer but 0, and goes back to drawing on 0: handy between levels, when the next one wants layers of its own.

SCREEN 320, 240
CREATE LAYER 1, 320, 240
CREATE LAYER 2, 320, 240
CREATE LAYER 3, 320, 240
FREE LAYER 3
PRINT "layer 3 is gone"
RESET LAYERS
PRINT "only layer 0 is left"
layer 3 is gone
only layer 0 is left

Shapes that change

A sprite is a picture, and a picture is always a rectangle. A polygon can be any shape at all: a ship, a rock, a beam of light, a blob of jelly. This lesson is about polygons that turn, grow, wear a picture, and change shape point by point while the game runs.

Turning and growing

You met POLYGON in Drawing on the screen: a list of points, an edge color and a fill color. After !! it can be turned, scaled and moved first, without your having to work out where any of the points go.

SCREEN 320, 240
ship = [(0, -14), (-9, 10), (0, 5), (9, 10)]
angle = 0
DO
    IF COOLDOWN(1)
        angle = angle + 2
        MEMORYBUFFER "scene"
            POLYGON ship, 15, 9 !! offset((60, 120))
            POLYGON ship, 15, 10 !! rotate(angle) rotate_origin((0, 0)) offset((160, 120))
            POLYGON ship, 15, 12 !! scale((2, 2)) rotate(-angle) rotate_origin((0, 0)) offset((260, 120))
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
    END IF
LOOP

The ship’s points are written around (0, 0), its nose at the top. offset moves the finished shape, and it’s always done last, so it doesn’t matter where in the !! you put it. rotate turns it, in degrees, about its center unless rotate_origin names another point, and scale makes it bigger or smaller: (2, 2) is twice the size both ways.

A picture on a shape

A polygon can wear an image, too, stretched across it. Give POLYGON an image after its fill, and !! uv says which part of the image goes at each point: (0, 0) is the image’s top left corner and (1, 1) its bottom right. An edge color of -1 means no edge.

SCREEN 320, 240
LOADIMG "city.png", "city"
corners = [(0, 0), (1, 0), (1, 1), (0, 1)]
t = 0
DO
    IF COOLDOWN(1)
        t = t + 0.05
        lean = sin(t) * 40
        quad = [(80 + lean, 30), (240 + lean, 30), (260, 210), (60, 210)]
        FRAME
            CLS
            POLYGON quad, -1, 15, "city" !! uv(corners)
        END FRAME
    END IF
LOOP

The top of the city sways from side to side while the bottom stays put, and the picture bends to fit.

That POLYGON goes straight into the FRAME, without a MEMORYBUFFER. A buffer builds its picture one pixel at a time, and stretching an image across a shape is a job for the graphics chip, so a buffer can’t do it:

SCREEN 320, 240
LOADIMG "city.png", "city"
MEMORYBUFFER "scene"
    POLYGON [(0, 0), (100, 0), (100, 100)], -1, 15, "city" !! uv([(0, 0), (1, 0), (1, 1)])
END MEMORYBUFFER

Shapes I keep

Everything above is drawn again every frame by your loop. A sprite, though, is something I draw myself, and a polygon can be one of those too.

For that, its points go in a MUTATION: a list of points that you and I share. SET POLYGON binds it to a polygon number, and from then on I draw it every frame, the way I draw sprites. Its modifiers are a lot like a sprite’s: location and origin place it, fill and color color it, texture and uv put a picture on it, and behavior moves it.

SCREEN 320, 240
LOADIMG "city.png", "city"
poster = MUTATION((0, 0), (100, 0), (100, 100), (0, 100))
corners = MUTATION((0, 0), (1, 0), (1, 1), (0, 1))
SET POLYGON 0 !! bind(poster) texture("city") uv(corners) location((160, 120)) origin((0.5, 0.5))

BEHAVIOR "spin"
    ROTATE 360, 240 !! loop
END BEHAVIOR
SET POLYGON 0 !! behavior("spin")

shown = TRUE
DO
    IF COOLDOWN(60)
        shown = NOT shown
        IF shown THEN SHOW POLYGON 0 ELSE HIDE POLYGON 0
    END IF
LOOP

The picture spins by itself, and the loop does nothing but blink it once a second with HIDE POLYGON and SHOW POLYGON. DESTROY POLYGON gets rid of it: its shape on the screen, and its behaviors. The MUTATION it was bound to is still yours afterwards, with all its points.

Crash-tan’s Tip: SET POLYGON takes solid, tag and velocity too, so a polygon I keep can bump into things the way a sprite does.

Changing a shape

The points in a MUTATION are shared: when you change one, the shape on the screen changes with it. Here’s a blob of jelly, twelve points around a circle, each pushed in and out a little every tick:

SCREEN 320, 240
blob = MUTATION()
FOR i = 0 ... 11
    a = i * PI / 6
    SET MUTATIONPOINT blob, i, (160 + cos(a) * 50, 120 + sin(a) * 50)
NEXT
rest = clone(blob)
SET POLYGON 0 !! bind(blob) color(15) fill(10)

t = 0
DO
    IF COOLDOWN(1)
        t = t + 0.1
        FOR i = 0 ... 11
            wobble = 1 + sin(t + i * 2) * 0.08
            SET MUTATIONPOINT blob, i, (160 + (rest(i).x - 160) * wobble, 120 + (rest(i).y - 120) * wobble)
        NEXT
    END IF
LOOP

SET MUTATIONPOINT moves a point, and given a number one past the end, it adds one there, which is how the blob gets its twelve. Adding points is for a shape that isn’t bound yet: once a polygon is drawing it, its number of points is fixed, so what I draw is never in two minds. Build the shape first, then bind it. blob(i) = (x, y) moves a point too, and blob(i) reads one.

clone makes a copy that has nothing to do with the original any more, so rest remembers where every point started out while blob wobbles. The copy is a plain list: it isn’t bound to anything, and nobody draws it.

To ask about a shape, mutationcount is how many points it has, and mutationpointx and mutationpointy are where one of them is:

SCREEN 320, 240
triangle = MUTATION((0, 0), (50, 0), (25, 43))
SET MUTATIONPOINT triangle, 2, (25, 30)
PRINT mutationcount(triangle), "points"
PRINT "the top is at", mutationpointx(triangle, 2), mutationpointy(triangle, 2)
3 points
the top is at 25.0 30.0

Behaviors for every point

SET MUTATION BEHAVIOR gives a shape a behavior, and I move every point of it by that behavior, together, every tick:

SCREEN 320, 240
BEHAVIOR "drift"
    VELOCITY 2, 0
END BEHAVIOR

rock = MUTATION((20, 100), (40, 95), (50, 115), (30, 130), (15, 118))
SET POLYGON 0 !! bind(rock) color(7) fill(8)
SET MUTATION BEHAVIOR rock !! behavior("drift")

n = 0
WHILE n < 30
    IF COOLDOWN(1) THEN n = n + 1
WEND
PRINT "has it moved?", rock(0).x > 20
rock(0) = (60, 100)
n = 0
WHILE n < 30
    IF COOLDOWN(1) THEN n = n + 1
WEND
PRINT "half a second later, it's at", rock(0)
has it moved? 1
half a second later, it's at (60, 100)

Reading a point while the behavior moves it is fine, as often as you like. Writing one is you saying where the shape is now, so the behavior stops, and the rock stays where you put its point.

A shape doesn’t have to be bound to a polygon for a behavior to move it. Here, a searchlight: the lamp is a shape of one point that stays put, the far end of the beam is a shape of two points that a behavior sweeps back and forth, and the loop joins them with + and draws the triangle they make.

SCREEN 320, 240
LOADIMG "city.png", "city"
PALETTE 20, 255, 255, 160, 100

BEHAVIOR "sweep"
    PATH [(0, 0), (260, 0)] !! speed(2) pingpong
END BEHAVIOR

lamp = MUTATION((160, 10))
beam = MUTATION((60, 240), (0, 240))
SET MUTATION BEHAVIOR beam !! behavior("sweep")

DO
    IF COOLDOWN(1)
        MEMORYBUFFER "light"
            POLYGON lamp + beam, 20, 20
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (32, 0), "city"
            PUTIMAGE (0, 0), "light"
        END FRAME
    END IF
LOOP

PATH takes the beam from where it starts to 260 pixels to the right, and pingpong brings it back again. Color 20 is a pale yellow with an alpha of 100, so the city shows through the light.

Shaders and transitions

Everything so far has been about what I draw. This lesson is about how it looks once it’s drawn: a whole layer tinted for nighttime, a sprite that flashes white when it’s hit, a screen that melts away between one part of a game and the next.

A shader

A shader is a little program that runs once for every pixel of a layer or a sprite, on the graphics chip, and decides what color that pixel ends up. You write one in a SHADER block, and give it a name.

SCREEN 320, 240
SHADER "night"
    USE NUMBER dark DEFAULT 0.5
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * (1.0 - dark), c.g * (1.0 - dark), c.b, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("night") shader_params({dark: 0.6})
PAUSE

Inside WHEN DRAWING, IMAGE is the pixel’s color as it was drawn, and whatever you SET COLOR to is what shows. Colors in here are RGBA with each part from 0 to 1, so c.r * 0.4 is red at forty percent. This one turns the red and the green down and leaves the blue, and the city goes blue and dark.

USE NUMBER is a setting the shader takes, with the value it has when nobody says. SET LAYER with !! shader(...) puts the shader on the layer, and shader_params gives it its settings by name.

Crash-tan’s Tip: A shader’s body is a language of its own, the one the graphics chip speaks, and I hand it over as you wrote it. It has MIX, SIN, CLAMP, STEP and friends, but none of your program’s variables: anything it needs from your game comes in through a USE setting.

Changing a setting as you go

The settings can change while the shader runs. Here the sun goes down and comes back up, by changing nothing but dark. The settings go with the shader they’re for, so every tick says both:

SCREEN 320, 240
SHADER "night"
    USE NUMBER dark DEFAULT 0.5
    WHEN DRAWING
        c = IMAGE
        SET COLOR = RGBA(c.r * (1.0 - dark), c.g * (1.0 - dark), c.b, c.a)
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
hour = 0
DO
    IF COOLDOWN(1)
        hour = hour + 0.01
        SET LAYER 0 !! shader("night") shader_params({dark: 0.35 + sin(hour) * 0.35})
    END IF
LOOP

SET LAYER 0 !! clear_shader takes it off again.

A shader can also keep time by itself. TIME is how many seconds the game has been going, and OWNTEXTURE reads the pixel from somewhere else in the same picture. Read it from a little to the side, by a different amount on every row, and the city ripples like it’s under water:

SCREEN 320, 240
SHADER "water"
    USE NUMBER size DEFAULT 0.01
    WHEN DRAWING
        c = OWNTEXTURE(VEC2(UV.x + SIN(UV.y * 30.0 + TIME * 3.0) * size, UV.y))
        SET COLOR = c
    END WHEN
END SHADER

LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
SET LAYER 0 !! shader("water")
PAUSE

UV is where the pixel is, from (0, 0) at the top left to (1, 1) at the bottom right. Nothing in the program moves: the ripple is all the shader’s.

A shader on a sprite

A sprite takes a shader the same way, with SET SPRITE. Here are three heroes: one as it is, and two sharing a shader, at half strength and all the way.

SCREEN 320, 240
SHADER "flash"
    USE NUMBER amount DEFAULT 0.0
    WHEN DRAWING
        c = IMAGE
        white = RGBA(1.0, 1.0, 1.0, c.a)
        SET COLOR = MIX(c, white, amount)
    END WHEN
END SHADER

LOADIMG "hero.png", "crashtan"
ENABLE SPRITE 0
SET SPRITE 0 !! image("crashtan") x(40) y(80) scale((2, 2))
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("crashtan") x(120) y(80) scale((2, 2))
    !! shader("flash") shader_params({amount: 0.5})
SHOW SPRITE 1
ENABLE SPRITE 2
SET SPRITE 2 !! image("crashtan") x(200) y(80) scale((2, 2))
    !! shader("flash") shader_params({amount: 1})
SHOW SPRITE 2
PAUSE

MIX(c, white, amount) is the way from one color to another: none of the way at 0, all of it at 1. Keeping c.a, the pixel’s alpha, keeps the see-through parts of the sprite see-through, so it’s the hero that turns white and not the whole square.

Flashing from a behavior

A shader can change over time on its own, too. USE SHADER, in a behavior, runs a shader while the behavior does, and takes one of its settings from one value to another over some ticks. With !! loop it goes back and forth for as long as the sprite has the behavior: a glow for a shield, or a power that’s about to run out.

SCREEN 320, 240
SHADER "flash"
    USE NUMBER amount DEFAULT 0.0
    WHEN DRAWING
        c = IMAGE
        white = RGBA(1.0, 1.0, 1.0, c.a)
        SET COLOR = MIX(c, white, amount)
    END WHEN
END SHADER

BEHAVIOR "shielded"
    USE SHADER "flash" !! param("amount") start(0) to(0.7) duration(30) loop
END BEHAVIOR

LOADIMG "hero.png", "crashtan"
ENABLE SPRITE 0
SET SPRITE 0 !! image("crashtan") x(120) y(80) scale((2, 2))
    !! behavior("shielded")
SHOW SPRITE 0
PAUSE

The hero glows white and back, every half second, with the shield on. Without !! loop, it would go from start to to once, and stay there.

Clearing with style

CLS clears a layer at once. With !! effect, it takes its time about it: "fade", "melt", "block", "dissolve", "wipex" or "wipey". My renderer runs the effect by itself, and while it does, transition says which one it is. When it’s over, transition is "".

SCREEN 320, 240
LOADIMG "city.png", "city"
PUTIMAGE (32, 0), "city"
CLS 0 !! effect("melt")
PRINT "now running:", transition
now running: MELT

Here’s every one of them in turn. The city goes up, stays for a second, and goes away with the next effect:

SCREEN 320, 240
LOADIMG "city.png", "city"
effects = ["fade", "melt", "block", "dissolve", "wipex", "wipey"]
n = 0
showing = FALSE
DO
    IF COOLDOWN(60) AND transition = ""
        IF showing
            CLS 0 !! effect(effects(n))
            n = (n + 1) MOD LEN(effects)
        ELSE
            FRAME
                CLS
                PUTIMAGE (32, 0), "city"
                LOCATE 1, 1
                PRINT "next:", effects(n)
            END FRAME
        END IF
        showing = NOT showing
    END IF
LOOP

!! speed says how fast an effect goes, and !! size how big the blocks are for "block". Each layer runs a transition of its own, and transition(1) asks about layer 1, whichever layer you’re drawing on.

Crash-tan’s Tip: A transition doesn’t stop your program: it runs on my renderer while your code goes on. When the next thing should wait for it, wait in your loop until transition = "", the way this one does before it draws the city again.

Clearing to a picture

!! to_image clears a layer to a picture instead of a color. With an effect, that’s a transition from what’s there into the picture: a dissolve from black into the title screen, say.

SCREEN 320, 240
LOADIMG "city.png", "city"
CLS 0 !! effect("dissolve") to_image("city")
PAUSE

Shaders and transitions are how a game looks between its parts. The parts themselves, the title screen, the level, the game over, are scenes, and they’re next.

Scenes

A game is more than the part where you play it. There’s a title screen first, then the level, then a game over screen, and then the title again. Each part makes sprites, starts behaviors and draws things of its own, and none of that should still be lying around when the next part starts.

A SCENE block takes care of that. I remember the game world as it is when the block starts, and when it ends, I clear away everything the scene made and put the world back the way it was.

In and out again

SCREEN 320, 240
NEWIMAGE 8, 8, 15, "dot"
ENABLE SPRITE 0
SET SPRITE 0 !! image("dot") x(10) y(10)
SHOW SPRITE 0
score = 0

SCENE "bonus"
    SET SPRITE 0 !! x(200)
    extra = reserve_sprite()
    SET SPRITE extra !! image("dot") x(20) y(20)
    SHOW SPRITE extra
    score = 50
    PRINT "in the scene: sprite 0 is at x", spritex(0), "and there's a sprite", extra
END SCENE

PRINT "after it: sprite 0 is at x", spritex(0)
PRINT "is sprite", extra, "still there?", spriteenabled(extra)
PRINT "score", score
in the scene: sprite 0 is at x 200 and there's a sprite 1
after it: sprite 0 is at x 10
is sprite 1 still there? 0
score 50

Sprite 0 was there before the scene, so it’s part of the world: the scene moved it, and at END SCENE it went back. Sprite 1 was made in the scene, so it went with it, and its number is free for the next one. Your variables are yours and I leave them alone, which is how the score gets out of a scene to the next one.

What a scene cleans up is everything it started: the sprites, layers and boundaries it made, the behaviors it put on sprites, the shaders, and any CALL RENDER, thread or timer. Palette colors it changed get their old values back.

Crash-tan’s Tip: SCENE "name" !! fast only clears away what the scene made, without putting back the world as it was. It’s quicker, for the times when nothing from before needs to survive.

A scene ends when its block does

A scene is the code between SCENE and END SCENE, so it lasts as long as that code runs. A title screen is a scene with a loop in it that waits for a key. A level is a scene with the game’s loop in it, and the level is over when the loop is.

That also makes a scene fit nicely in a function, one for each part of the game, and leaving the function by RETURN ends the scene too:

SCREEN 320, 240
NEWIMAGE 8, 8, 14, "dot"

FUNCTION level()
    SCENE "level"
        star = reserve_sprite()
        SET SPRITE star !! image("dot") x(160) y(120)
        SHOW SPRITE star
        RETURN star
    END SCENE
END FUNCTION

used = level()
PRINT "the level used sprite", used
PRINT "and now it's", spriteenabled(used)
the level used sprite 0
and now it's 0

Title, play, game over

Here’s a whole small game in three scenes. Rocks fall from the sky, and you dodge them for as long as you can. The main loop at the bottom is the whole game in four lines: title, play, game over, and round again.

SCREEN 320, 240
RANDOMIZE
LOADIMG "hero.png", "crashtan"
LOADIMG "city.png", "city"
MEMORYBUFFER "rock", 16, 16
    CIRCLE (8, 8), 7, 6, 6
END MEMORYBUFFER

ATOMIC score = 0
ATOMIC hit = FALSE
best = 0

// Wait for Space to be let go of, and then pressed.
FUNCTION wait_for_space()
    released = FALSE
    DO
        IF COOLDOWN(1)
            IF NOT keydown("Space") THEN released = TRUE
            IF released AND keydown("Space") THEN RETURN
        END IF
    LOOP
END FUNCTION

// --- The title screen ---------------------------------------------------
BEHAVIOR "bob"
    OSCILLATE 0, 8 !! speed(2)
END BEHAVIOR

FUNCTION show_title()
    SCENE "title"
        FRAME
            CLS
            PUTIMAGE (32, 0), "city"
            LOCATE 4, 16
            PRINT "D O D G E"
            LOCATE 25, 10
            PRINT "press Space to start"
            LOCATE 27, 14
            PRINT "best:", best
        END FRAME
        mascot = reserve_sprite()
        SET SPRITE mascot !! image("crashtan") x(140) y(100) behavior("bob")
        SHOW SPRITE mascot
        wait_for_space()
    END SCENE
END FUNCTION

// --- Playing ----------------------------------------------------------
BEHAVIOR "fall"
    VELOCITY 0, PARAM("speed", 2)
    ONCOLLISION
        IF other_tag = "player" THEN hit = TRUE
        IF other_tag = "floor" THEN DISABLE SPRITE me
    END ONCOLLISION
END BEHAVIOR

FUNCTION draw_play()
    FRAME
        CLS
        PUTIMAGE (32, 0), "city"
        LOCATE 1, 1
        PRINT "score", score
    END FRAME
END FUNCTION

FUNCTION play_level()
    SCENE "play"
        score = 0
        hit = FALSE
        BOUNDARY ADD "floor", (0, 260) -> (320, 270)
        CALL RENDER draw_play, "hud"
        player = reserve_sprite()
        x = 140
        SET SPRITE player !! image("crashtan") x(x) y(200) tag("player")
        SHOW SPRITE player
        DO WHILE NOT hit
            IF COOLDOWN(1)
                IF keydown("Left") AND x > 0 THEN x = x - 3
                IF keydown("Right") AND x < 280 THEN x = x + 3
                SET SPRITE player !! x(x)
                score = score + 1
            END IF
            IF COOLDOWN(20)
                rock = reserve_sprite()
                IF rock >= 0
                    SET SPRITE rock !! image("rock") x(rnd * 304) y(-16)
                        !! behavior("fall") behavior_params({speed: 2 + score / 600})
                    SHOW SPRITE rock
                END IF
            END IF
        LOOP
    END SCENE
END FUNCTION

// --- Game over ------------------------------------------------------------
FUNCTION show_game_over()
    IF score > best THEN best = score
    SCENE "game over"
        FRAME
            CLS 4
            LOCATE 12, 16
            PRINT "GAME OVER"
            LOCATE 14, 14
            PRINT "you scored", score
            LOCATE 25, 9
            PRINT "press Space for the title"
        END FRAME
        wait_for_space()
    END SCENE
END FUNCTION

// --- The game ---------------------------------------------------------------
DO
    show_title()
    play_level()
    show_game_over()
LOOP

Click the screen, press Space, and use the arrow keys.

Look at what doesn’t happen. play_level never takes the rocks away when you’re hit, or turns off its CALL RENDER, or removes the floor, and the title never hides its bobbing hero. Every one of those was made inside a scene, so END SCENE cleared it away. The next scene starts with nothing left over from the last, every time round.

score and best are ordinary variables, so they carry on from one scene to the next: play_level sets the score, show_game_over shows it and keeps the best, and show_title shows the best.

Crash-tan’s Tip: A scene puts the world back as it was when the scene started, so make the things that belong to the whole game, like images and behaviors, before the first scene. Anything that belongs to one part of the game goes inside that part’s scene.

Make it yours

  • A transition. Start the game over scene with a CLS and an effect from Shaders and transitions, and wait for transition to be "" before you draw on it.
  • A flash. When the hero is hit, give it the "hurt" behavior from the same lesson, and wait a moment before the scene ends.
  • Another level. After a minute of dodging, end play_level and go to a second scene where the rocks fall faster.

Animation and particles

Back in Images and sprites, a sprite changed its frame because the loop told it to. That’s fine for one light blinking, but a game is full of things that animate: a walk, a spinning coin, a flame. A behavior can step through the frames for me, so your loop never has to count them.

A sheet of frames

explosion.png is in every game’s folder. It’s a sheet of 32 × 32 frames, so it loads with !! spritesheet((32, 32)), and framecount says how many frames that made.

SCREEN 320, 240
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
PRINT "the explosion has", framecount("boom"), "frames"
the explosion has 16 frames

Playing them

ANIMATE goes in a behavior. It takes the image, the frames to play in order, and after !!, how many frames a second to show them at. Frames 0 ... 15 is all sixteen, and it starts again from the first when it gets to the end.

SCREEN 320, 240
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))

BEHAVIOR "burning"
    ANIMATE "boom", 0 ... 15 !! framerate(12)
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("boom") frame(0) x(160) y(120)
    !! origin((0.5, 0.5)) scale((3, 3))
    !! behavior("burning")
SHOW SPRITE 0
PAUSE

Try framerate(30) for a faster fire, or [4, 5, 6, 7] to play only the middle of it. The frames don’t have to be in order, either: [0, 1, 2, 1] goes there and back.

Standing and running

A hero has more than one animation: one for standing about and one for running, each a sheet of its own. hero_idle.png and hero_run.png are sheets of 40 × 40 frames. Give each its own behavior, and when the hero starts or stops running, take one behavior off and put the other on. ANIMATE changes the sprite’s picture to its own sheet as it starts, so the sprite always shows the one that’s playing.

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

BEHAVIOR "standing"
    ANIMATE "idle" !! framerate(6)
END BEHAVIOR
BEHAVIOR "running"
    ANIMATE "run" !! framerate(12)
END BEHAVIOR

x = 120
ENABLE SPRITE 0
SET SPRITE 0 !! image("idle") frame(0) x(x) y(80) scale((2, 2))
    !! behavior("standing")
SHOW SPRITE 0

running = FALSE
DO
    IF COOLDOWN(1)
        way = keydown("Right") - keydown("Left")
        x = x + way * 2
        IF x < 0 THEN x = 0
        IF x > 240 THEN x = 240
        SET SPRITE 0 !! x(x)
        IF way < 0 THEN SET SPRITE 0 !! flip_x
        IF way > 0 THEN SET SPRITE 0 !! flip_x(FALSE)

        IF (way <> 0) <> running
            running = way <> 0
            IF running
                SET SPRITE 0 !! remove_behavior("standing")
                SET SPRITE 0 !! behavior("running")
            ELSE
                SET SPRITE 0 !! remove_behavior("running")
                SET SPRITE 0 !! behavior("standing")
            END IF
        END IF
    END IF
LOOP

Click the screen and hold an arrow key. With no frames listed, ANIMATE plays every frame in the sheet. The loop only swaps the behaviors when running starts or stops: giving the sprite "running" again every tick would start the run over from its first frame, every tick.

scale((2, 2)) shows each pixel of the art as two by two, and that’s the way to make pixel art bigger: by a whole number. Half sizes and odd sizes smear the pixels.

Once, and gone

An explosion shouldn’t go round and round. !! noloop plays the frames once and holds the last one. Then the sprite has to go, and that’s what a particle is for.

EMITPARTICLE makes a sprite that looks after itself: where it starts, the image, a tag, the behavior that drives it, and how many ticks it lives. When its time’s up it’s gone, and its number goes back to me. You don’t ENABLE, SHOW or DISABLE anything.

SCREEN 320, 240
RANDOMIZE
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
LOADWAV "explosion.wav", "bang"

BEHAVIOR "explode"
    ANIMATE "boom", 0 ... 15 !! framerate(30) noloop
END BEHAVIOR

DO
    IF COOLDOWN(40)
        EMITPARTICLE (20 + rnd * 250, 20 + rnd * 180), "boom", "boom", "explode", 32 !! scale((2, 2))
        PLAYWAV "bang", 0, 0.3
    END IF
LOOP

Sixteen frames at thirty a second take 32 ticks, so each explosion lives exactly as long as its animation. The !! scale((2, 2)) after it is the same scale a sprite takes: a particle takes most of what SET SPRITE does.

Sparks

A particle can be driven by any behavior, and one behavior can send each particle its own way: give it settings with params, the way behavior_params does for a sprite. Here’s a fountain of sparks, each one thrown up at its own angle, pulled down, and fading as it goes.

SCREEN 320, 240
RANDOMIZE
MEMORYBUFFER "spark", 4, 4
    LINE (0, 0) -> (3, 3), 14 !! box fill
END MEMORYBUFFER

BEHAVIOR "spark"
    IMPULSE PARAM("vx", 0), PARAM("vy", -3)
    ACCELERATION 0, 0.1
    ALPHA 1, 0, 50
END BEHAVIOR

DO
    IF COOLDOWN(2)
        vx = rnd * 3 - 1.5
        vy = -2 - rnd * 3
        EMITPARTICLE (160, 200), "spark", "spark", "spark", 50 !! params({vx: vx, vy: vy})
    END IF
    IF COOLDOWN(30)
        FRAME
            CLS
            LOCATE 1, 1
            PRINT "sparks in the air:", spritecount("spark")
        END FRAME
    END IF
LOOP

A new spark every two ticks, each living fifty: there are never more than twenty-five at once, and I tidy up every one of them.

Crash-tan’s Tip: EMITPARTICLE is also a function, emitparticle, that gives back the particle’s sprite number, in case you want to do something more to it.

Two more things a behavior can do to how a sprite looks. BLINK 6 hides and shows it every six ticks, the classic “I just got hit, I can’t be hurt for a moment” flicker. SHAKE 2 jiggles it up to two pixels either way. Put them in a behavior of their own, and add it and take it away again with behavior and remove_behavior.

SCREEN 320, 240
LOADIMG "hero.png", "tan"

BEHAVIOR "hurt"
    BLINK 6
    SHAKE 2
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("tan") x(160) y(120) origin((0.5, 0.5)) behavior("hurt")
SHOW SPRITE 0

hurt = 60
DO
    IF COOLDOWN(1)
        IF keydown("Space") AND hurt = 0
            SET SPRITE 0 !! behavior("hurt")
            hurt = 60
        END IF
        IF hurt > 0
            hurt = hurt - 1
            IF hurt = 0 THEN SET SPRITE 0 !! remove_behavior("hurt")
        END IF
    END IF
LOOP

The hero starts out hurt. Once they’ve settled, click the screen and press Space, and they flicker and tremble for another second.

That’s a lot of moving and looking that the loop doesn’t have to think about. Next, a way to tell a sprite to do one thing, then another, then another.

Patterns

A behavior is everything a sprite does at once, every tick: move this way, spin, fade, all together, for as long as it’s on. That’s perfect for a spinning coin. But a bad guy in a shooter has a plan: fly in, stop, fire, fly away. Those happen one after another, and that’s a pattern.

One step after another

A PATTERN is a list of steps, and I go down it in order. STEP with a behavior’s name runs that behavior until it’s done, or with !! duration(n), for n ticks. Then I move on to the next step.

SCREEN 320, 240
NEWIMAGE 12, 12, 10, "box"

BEHAVIOR "right"
    VELOCITY 2, 0
END BEHAVIOR
BEHAVIOR "down"
    VELOCITY 0, 2
END BEHAVIOR

PATTERN "corner"
    STEP "right" !! duration(60)
    STEP "down" !! duration(40)
END PATTERN

ENABLE SPRITE 0
SET SPRITE 0 !! image("box") x(20) y(20) pattern("corner")
SHOW SPRITE 0
PAUSE

The box goes right for a second, then down for two thirds of one, then stops, because the pattern’s run out of steps. A sprite gets its pattern with pattern(...), the way it gets a behavior with behavior(...).

With !! loop after its name, a pattern starts again from its first step when it gets to the end. Try PATTERN "corner" !! loop, and add two more steps to bring the box back round.

A bad guy with a plan

The other STEP words don’t run a behavior: they do one thing to the sprite, there and then. STEP BEVENT fires an event, which is how a pattern tells your program something’s happened. STEP DISABLE takes the sprite out of the game, and gives its number back.

Here’s that plan. A ship flies down the screen to its place, sways there for a second, fires, sways a little more, flies off the top and is gone.

SCREEN 320, 240
RANDOMIZE
MEMORYBUFFER "ship", 20, 14
    POLYGON [(0, 0), (20, 0), (10, 14)], 12, 4
END MEMORYBUFFER
MEMORYBUFFER "shot", 4, 8
    LINE (0, 0) -> (3, 7), 14 !! box fill
END MEMORYBUFFER

BEHAVIOR "arrive"
    PATH [(0, 0), (0, 90)] !! speed(3)
END BEHAVIOR
BEHAVIOR "sway"
    OSCILLATE 8, 0 !! speed(3)
END BEHAVIOR
BEHAVIOR "leave"
    VELOCITY 0, -4
END BEHAVIOR
BEHAVIOR "falling"
    VELOCITY 0, 3
END BEHAVIOR

PATTERN "attack"
    STEP "arrive"
    STEP "sway" !! duration(60)
    STEP BEVENT "fire"
    STEP "sway" !! duration(30)
    STEP "leave" !! duration(40)
    STEP DISABLE
END PATTERN

FUNCTION on_fire(ship)
    EMITPARTICLE (spritex(ship, 0.5), spritey(ship, 1)), "shot", "shot", "falling", 80
END FUNCTION
CALL EVENT on_fire, "fire"

DO
    IF COOLDOWN(45)
        id = reserve_sprite()
        IF id >= 0
            SET SPRITE id !! image("ship") x(20 + rnd * 260) y(-20)
                !! tag("enemy") pattern("attack")
            SHOW SPRITE id
        END IF
    END IF
LOOP

PATH with no loop is done once it’s been to its last point, so the "arrive" step lasts exactly as long as the trip. The event comes to on_fire with the sprite that fired it, so the shot starts under the right ship.

Crash-tan’s Tip: A pattern runs one step at a time, but a step’s behavior can do as much at once as any behavior. Put ROTATE in "leave" and the ship spins as it goes.

Waiting for a signal

STEP WAITFOR stands still until an event fires. Fire it with EVENT from your program, and every sprite waiting for it goes at once. !! timeout(n) gives up after n ticks, so with an event nobody ever fires, it’s a plain wait.

STEP GOTOPATTERN carries on with another pattern, from its first step, and STEP FADE fades the sprite out: STEP FADE 2 takes about half a second, and a bigger number is faster.

SCREEN 320, 240
NEWIMAGE 14, 14, 11, "drone"

BEHAVIOR "hover"
    OSCILLATE 0, 4 !! speed(4)
END BEHAVIOR
BEHAVIOR "dive"
    ACCELERATION 0, 0.15
END BEHAVIOR

PATTERN "wait"
    STEP WAITFOR "attack"
    STEP GOTOPATTERN "dive"
END PATTERN
PATTERN "dive"
    STEP "dive" !! duration(50)
    STEP FADE 2
    STEP DISABLE
END PATTERN

FOR n = 0 ... 7
    ENABLE SPRITE n
    SET SPRITE n !! image("drone") x(30 + n * 35) y(40)
        !! behavior("hover") pattern("wait")
    SHOW SPRITE n
NEXT

ticks = 0
DO
    IF COOLDOWN(1)
        ticks = ticks + 1
        IF ticks = 60 THEN EVENT "attack"
        FRAME
            CLS
            LOCATE 1, 1
            IF spriteenabled(0) THEN PRINT "sprite 0 is running", spritepattern(0)
        END FRAME
    END IF
LOOP

The drones hover from the start: behavior("hover") is on the sprite itself, and a sprite’s behaviors go on whatever step its pattern’s on. After a second the program fires "attack", and the whole row dives together.

spritepattern(n) gives the name of the pattern a sprite’s running, or "". haspattern(n) asks whether it’s running one at all, and haspattern(n, "dive") whether it’s running that one.

Changing what a sprite is

The last few steps change how the sprite takes part in the game, part way through its plan:

STEP HIDE, STEP SHOWStop drawing it, or start again. It’s still there.
STEP GHOSTLet things pass through it.
STEP SOLIDMake it solid again.
STEP RETAG "name"Give it another tag, the one collisions report.

Here’s a ghost that drifts across the screen, and every so often vanishes, goes on drifting unseen with nothing able to touch it, and comes back.

SCREEN 320, 240
MEMORYBUFFER "ghost", 16, 18
    CIRCLE (8, 8), 7, 15, 15
    LINE (1, 8) -> (15, 17), 15 !! box fill
    PSET (5, 7), 0
    PSET (11, 7), 0
END MEMORYBUFFER

BEHAVIOR "drift"
    VELOCITY 1, 0
END BEHAVIOR

PATTERN "haunt" !! loop
    STEP "drift" !! duration(60)
    STEP GHOST
    STEP RETAG "faded"
    STEP HIDE
    STEP "drift" !! duration(40)
    STEP SHOW
    STEP SOLID
    STEP RETAG "ghost"
END PATTERN

ENABLE SPRITE 0
SET SPRITE 0 !! image("ghost") x(0) y(110) tag("ghost") pattern("haunt")
SHOW SPRITE 0
DO
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 1, 1
            PRINT "tag:", spritetag(0), "  shown:", spritevisible(0)
        END FRAME
    END IF
LOOP

A bullet’s ONCOLLISION that looks at other_tag would know not to count a hit on a "faded" ghost.

Keeping them

A pattern can be saved to a file with SAVE PATTERN, and LOAD PATTERN brings back every pattern in one, ready to use. So can a behavior, with SAVE BEHAVIOR and LOAD BEHAVIOR: a level editor can write the plans, and the game can read them.

SCREEN 320, 240
BEHAVIOR "drift"
    VELOCITY 1, 0
END BEHAVIOR
PATTERN "patrol" !! loop
    STEP "drift" !! duration(60)
END PATTERN
SAVE PATTERN "patrol", "patrol.json"
PRINT "saved:", fileexists("patrol.json")

LOAD PATTERN "patrol.json"
NEWIMAGE 8, 8, 15, "dot"
ENABLE SPRITE 0
SET SPRITE 0 !! image("dot") x(20) y(100) pattern("patrol")
PRINT "running:", spritepattern(0)
saved: 1
running: patrol

Crash-tan’s Tip: The music has patterns too, inside a MUSIC block. Those are parts of a song. A PATTERN on its own is always for sprites.

Patterns and behaviors move one sprite at a time. Next, sprites that move together, joined up.

Sprites joined together

Some things in a game are made of parts: a tank and its turret, a windmill and its sails, an arm with a hand on the end. Each part is a sprite, and I can join them up, so that moving one moves the lot. A set of sprites joined like that is a rig.

Carried along

!! parent(n) puts a sprite on another one. From then on, wherever the parent goes, the child goes too, keeping the same distance from it. The child can still have behaviors of its own.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
MEMORYBUFFER "flag", 12, 8
    LINE (0, 0) -> (11, 7), 12 !! box fill
END MEMORYBUFFER

BEHAVIOR "roll"
    PATH [(0, 0), (200, 0)] !! speed(2) pingpong
END BEHAVIOR
BEHAVIOR "wave"
    OSCILLATE 0, 3 !! speed(6)
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(40) y(130) scale_to((32, 32)) behavior("roll")
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("flag") x(50) y(110) parent(0) behavior("wave")
SHOW SPRITE 1
PAUSE

The ball rolls back and forth, and the flag rides on top, waving as it goes. ball.png is 128 pixels across, so scale_to((32, 32)) shows it at 32.

A plain child only follows its parent about. With rigid as well, it turns with the parent too, going round it when the parent spins:

SCREEN 320, 240
LOADIMG "ball.png", "ball"
MEMORYBUFFER "sail", 50, 10
    LINE (0, 0) -> (49, 9), 15 !! box fill
END MEMORYBUFFER

BEHAVIOR "spin"
    ROTATE 360, 240 !! origin((0.5, 0.5)) loop
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(160) y(120)
    !! origin((0.5, 0.5)) scale_to((24, 24))
    !! behavior("spin")
SHOW SPRITE 0
FOR n = 1 ... 4
    angle = (n - 1) * 90
    ENABLE SPRITE n
    SET SPRITE n !! image("sail") origin((0.5, 0.5)) rotate(angle)
        !! x(160 + cos(angle * pi / 180) * 40) y(120 + sin(angle * pi / 180) * 40)
        !! parent(0) rigid
    SHOW SPRITE n
NEXT
PAUSE

Only the hub has a behavior. The four sails are placed around it once, and from then on they go round because it does.

Pulled toward a point

A pin pulls a sprite toward a place every tick. pin_target((x, y)) is where, and pin_mix is how hard, from 0 to 1: at 1 it’s there at once, and at 0.1 it goes a tenth of the way each tick, so it eases in and slows as it arrives.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(160) y(120) origin((0.5, 0.5)) scale_to((20, 20))
SHOW SPRITE 0
DO
    IF COOLDOWN(1) THEN SET SPRITE 0 !! pin_target((mousex, mousey)) pin_mix(0.1)
LOOP

Move the mouse over the screen and the ball glides after it.

Crash-tan’s Tip: A pin, a limit and IK each go in a SET SPRITE of their own, with nothing else after the !!. clear_pin takes a pin off again.

Kept within limits

A limit keeps a child within a range of its parent. limit_x(-50 ... 50) lets it go fifty pixels either side of its parent and no further, and limit_y does the same up and down. Here a knob swings a hundred pixels each way, and the limit stops it at the ends of its rail.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
NEWIMAGE 120, 4, 8, "rail"

BEHAVIOR "swing"
    OSCILLATE 100, 0 !! speed(3)
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("rail") x(160) y(120) origin((0.5, 0.5))
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("ball") x(160) y(120)
    !! origin((0.5, 0.5)) scale_to((16, 16)) parent(0)
    !! behavior("swing")
SET SPRITE 1 !! limit_x(-52 ... 52)
SHOW SPRITE 1
PAUSE

limit_axis("x") keeps a child level with its parent, so it can only slide from side to side.

Reaching

IK, inverse kinematics, works the other way round from a parent. You say where the end of a chain should be, and I bend every joint of it to get there. ik_target((x, y)) goes on the last sprite of a chain, and ik_length says how many sprites back up the chain I’m allowed to bend.

Here’s a tentacle of nine balls, each a rigid child of the one before. The first stays where it is, and the tip reaches for the mouse. The lines between the balls are drawn into a MEMORYBUFFER every frame, and go on the bottom layer, under the sprites.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
links = 8

FOR n = 0 ... links
    ENABLE SPRITE n
    SET SPRITE n !! image("ball") x(160 + n * 18) y(120)
        !! origin((0.5, 0.5)) scale_to((14, 14))
    IF n > 0 THEN SET SPRITE n !! parent(n - 1) rigid
    SHOW SPRITE n
NEXT

DO
    IF COOLDOWN(1)
        SET SPRITE links !! ik_target((mousex, mousey)) ik_length(links)
        MEMORYBUFFER "bones"
            FOR n = 1 ... links
                LINE (spritex(n - 1, 0.5), spritey(n - 1, 0.5)) -> (spritex(n, 0.5), spritey(n, 0.5)), 8
            NEXT
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "bones"
        END FRAME
    END IF
LOOP

When the mouse is further away than the tentacle is long, it stretches out straight toward it, as far as it reaches.

Saving a rig

SAVE RIG writes a sprite and everything joined to it to a file: their images, places, behaviors and patterns. LOAD RIG builds it again, and with !! onto(n) it builds it on sprite n, with new numbers for the parts. Make one guard, save it, and you can have as many as you like.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
MEMORYBUFFER "helmet", 16, 8
    LINE (0, 0) -> (15, 7), 4 !! box fill
END MEMORYBUFFER

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(60) y(120) scale_to((24, 24))
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("helmet") x(64) y(112) parent(0) tag("helmet")
SHOW SPRITE 1
SAVE RIG 0, "guard.json"

FOR n = 1 ... 3
    body = reserve_sprite()
    SET SPRITE body !! image("ball") x(60 + n * 60) y(120) scale_to((24, 24))
    SHOW SPRITE body
    LOAD RIG "guard.json" !! onto(body)
NEXT
PRINT "helmets:", spritecount("helmet")
helmets: 4

Four: the first guard’s, and one for each guard loaded onto a new body.

That’s every way I know to join sprites up. Next, all the other ways I know to move them.

More ways to move

VELOCITY, IMPULSE and ACCELERATION push a sprite about. That’s enough for a ball, but plenty of things in a game move with somewhere in mind: after the hero, round a planet, along a track. I have words for those too, and they all go in a behavior.

Following

FOLLOW chases another sprite and keeps a distance behind it. Give each piece of a snake the piece in front of it to follow, and you have a snake.

SCREEN 320, 240
MEMORYBUFFER "head", 14, 14
    CIRCLE (7, 7), 6, 10, 10
END MEMORYBUFFER
MEMORYBUFFER "body", 14, 14
    CIRCLE (7, 7), 5, 2, 2
END MEMORYBUFFER

BEHAVIOR "wander"
    PATH [(40, 40), (280, 60), (240, 200), (60, 180), (40, 40)] !! absolute speed(2) loop
END BEHAVIOR
BEHAVIOR "tail"
    FOLLOW PARAM("leader", 0), 12 !! speed(3)
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("head") x(40) y(40) behavior("wander")
SHOW SPRITE 0
FOR n = 1 ... 8
    ENABLE SPRITE n
    SET SPRITE n !! image("body") x(40) y(40)
        !! behavior("tail") behavior_params({leader: n - 1})
    SHOW SPRITE n
NEXT
PAUSE

FOLLOW takes the sprite to follow and how far behind to stay, and !! speed(3) is how fast it can catch up, in pixels a tick. The head’s going at 2, so the body keeps up.

Paths

The head’s going round a PATH: a list of points to visit in order. Usually the points are offsets from where the sprite starts, so the same path works anywhere; with !! absolute they’re places on the screen. After !! too:

speed(n)Pixels a tick.
loopGo back to the first point and round again.
pingpongGo back along the path the way it came, and forth again.
seek_first(n)Go to the first point first, at this speed.

Without loop or pingpong, the path is done when it gets to its last point, and the sprite stays there.

The head’s path ends on (40, 40), where it started, so each time round it comes all the way home before it sets off again.

Orbits

ORBIT goes round and round at a distance. !! center((x, y)) goes round a point; !! center_sprite(n) goes round a sprite, wherever that sprite goes. So a moon can go round a planet that’s going round a sun.

SCREEN 320, 240
MEMORYBUFFER "sun", 24, 24
    CIRCLE (12, 12), 11, 14, 14
END MEMORYBUFFER
MEMORYBUFFER "planet", 12, 12
    CIRCLE (6, 6), 5, 9, 9
END MEMORYBUFFER
MEMORYBUFFER "moon", 6, 6
    CIRCLE (3, 3), 2, 7, 7
END MEMORYBUFFER

BEHAVIOR "year"
    ORBIT 80 !! center((160, 120)) speed(1)
END BEHAVIOR
BEHAVIOR "month"
    ORBIT 20 !! center_sprite(1) speed(6) clockwise(FALSE)
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("sun") x(160) y(120) origin((0.5, 0.5))
SHOW SPRITE 0
ENABLE SPRITE 1
SET SPRITE 1 !! image("planet") x(240) y(120) origin((0.5, 0.5)) behavior("year")
SHOW SPRITE 1
ENABLE SPRITE 2
SET SPRITE 2 !! image("moon") x(260) y(120) origin((0.5, 0.5)) behavior("month")
SHOW SPRITE 2
PAUSE

speed is in degrees a tick, so the planet takes 360 ticks, six seconds, to go round once, and the moon goes round it nine times in that.

Sliding to a stop

FRICTION takes a share of a sprite’s speed away every tick: 0.03 is three hundredths of it. With nothing pushing, it slows down and stops, like a puck on ice.

Here the loop does the pushing. spritevelocityx and spritevelocityy say how fast a sprite’s going right now, and !! velocity((x, y)) sets it: each arrow key adds a little. Let go, and the friction wins.

SCREEN 320, 240
MEMORYBUFFER "puck", 16, 16
    CIRCLE (8, 8), 7, 15, 8
END MEMORYBUFFER
BOUNDARY ADD "rink", (0, 0) -> (320, 240) !! solid enclosure

BEHAVIOR "ice"
    FRICTION 0.03, 0.03
    BOUNCE 0.8, 0.8
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("puck") x(152) y(112) solid behavior("ice") velocity((4, 2))
SHOW SPRITE 0

DO
    IF COOLDOWN(1)
        vx = spritevelocityx(0)
        vy = spritevelocityy(0)
        IF keydown("Left") THEN vx = vx - 0.3
        IF keydown("Right") THEN vx = vx + 0.3
        IF keydown("Up") THEN vy = vy - 0.3
        IF keydown("Down") THEN vy = vy + 0.3
        SET SPRITE 0 !! velocity((vx, vy))
        FRAME
            CLS
            LOCATE 1, 1
            PRINT "speed", round(vx * 10) / 10, round(vy * 10) / 10
        END FRAME
    END IF
LOOP

Moving, and holding still

MOVE x, y moves a sprite by that much every tick. With !! absolute_x, the x is where to hold it instead: every tick, I put it back at that x. absolute_y does the same for y. Here’s a lift that rises a pixel a tick, and whatever x it was placed at, it rides up at 145.

SCREEN 320, 240
NEWIMAGE 30, 6, 6, "lift"

BEHAVIOR "lift"
    MOVE 145, -1 !! absolute_x
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("lift") x(20) y(230) behavior("lift")
SHOW SPRITE 0
t = 0
DO WHILE t < 30
    IF COOLDOWN(1) THEN t = t + 1
LOOP
PRINT "x is", spritex(0), "and y is going up:", spritey(0) < 230
x is 145 and y is going up: 1

Up off the ground

In 3D, a sprite has an elevation as well as x and y: how high it is off the ground. IMPULSE, ACCELERATION, FRICTION and MOVE all take a third number for it, ELEVATE takes it from one height to another over some ticks, and GRAVITY pulls it back down to the ground, which is 0. spriteelevation says how high it is. That’s for a sprite on a plane layer, the tilted, Mode 7 kind of floor (SET LAYER 1 !! plane("perspective")), where I draw it at its height.

On a flat screen, up and down is y, and the ground is something solid. Here a ball hops on a solid floor: IMPULSE 0, -6 throws it up once, ACCELERATION 0, 0.3 is its gravity, and the floor is what it lands on.

SCREEN 320, 240
MEMORYBUFFER "ball", 16, 16
    CIRCLE (8, 8), 7, 12, 12
END MEMORYBUFFER
LINE (0, 184) -> (319, 239), 8 !! box fill
BOUNDARY ADD "floor", (0, 184) -> (320, 240) !! solid

BEHAVIOR "fall"
    ACCELERATION 0, 0.3
END BEHAVIOR
BEHAVIOR "hop"
    IMPULSE 0, -6
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! image("ball") x(152) y(168) solid behavior("fall")
SET SPRITE 0 !! behavior("hop")
SHOW SPRITE 0

DO
    IF COOLDOWN(1) AND keydown("Space") AND spritetouching(0, "TOP") = "floor"
        SET SPRITE 0 !! remove_behavior("hop")
        SET SPRITE 0 !! behavior("hop")
    END IF
LOOP

Nothing in the loop moves the ball. It only lets Space hop again once the ball is standing on the floor. Click the screen and press Space.

Crash-tan’s Tip: GRAVITY only ever pulls on the elevation. For a platform game, where down is down the screen, use ACCELERATION 0, 0.3.

A little different every time

A swarm where every bee flies the same way at the same speed doesn’t look like a swarm. variance(base, low, high) in a behavior is a number that’s different for each sprite it’s on: base, plus something from low to high. varianceint is the same with whole numbers, and !! nonzero keeps it off 0.

SCREEN 320, 240
NEWIMAGE 4, 4, 14, "bee"

BEHAVIOR "swarm"
    ORBIT variance(60, -30, 30) !! center((160, 120)) speed(variance(2, -1, 1)) start_angle(variance(180, -180, 180))
END BEHAVIOR

FOR n = 0 ... 39
    ENABLE SPRITE n
    SET SPRITE n !! image("bee") x(160) y(120) behavior("swarm")
    SHOW SPRITE n
NEXT
PAUSE

Forty sprites and one behavior, and no two of them fly alike: each one circles at its own distance and its own speed, starting from its own place on the circle. They all start in the middle, so without start_angle they’d set off from the same side.

Faster ticks

I tick behaviors sixty times a second. SET BEHAVIOR RATE changes that for every sprite at once, and everything a behavior does a tick, it does more or less often. Half the rate is slow motion.

SCREEN 320, 240
NEWIMAGE 10, 10, 13, "dot"

BEHAVIOR "circle"
    ORBIT PARAM("r", 60) !! center((160, 120)) speed(3)
END BEHAVIOR

FOR n = 0 ... 5
    ENABLE SPRITE n
    SET SPRITE n !! image("dot") x(180 + n * 15) y(120)
        !! behavior("circle") behavior_params({r: 20 + n * 15})
    SHOW SPRITE n
NEXT

slow = FALSE
DO
    IF COOLDOWN(90)
        slow = NOT slow
        IF slow
            SET BEHAVIOR RATE 20
        ELSE
            SET BEHAVIOR RATE 60
        END IF
    END IF
LOOP

Every second and a half it switches between full speed and a third of it. COOLDOWN keeps its own time, sixty a second, whatever the behaviors are doing.

Sound files

PLAY and its notes are perfect for blips and tunes. Some sounds, though, nobody could write as notes: an explosion, a voice, a real drum. Those come from a recording, in a file in your game’s folder, and I play them as they were recorded.

There are two kinds. A WAV is for short sounds that go off again and again, like a jump or a blast. An MP3 is for longer ones, like a voice or a whole song: it’s much smaller than a WAV of the same length.

Short sounds

LOADWAV loads a WAV under a name, once, at the start. After that, PLAYWAV plays it whenever you like, by that name. Click the screen and press Space:

SCREEN 320, 240
LOADWAV "explosion.wav", "boom"
flash = 0
was_down = FALSE
DO
    IF COOLDOWN(1)
        down = keydown("Space")
        IF down AND NOT was_down
            PLAYWAV "boom"
            flash = 40
        END IF
        was_down = down
        IF flash > 0 THEN flash = flash - 2
        MEMORYBUFFER "scene"
            IF flash > 0 THEN CIRCLE (160, 120), flash, 14, 12
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
            LOCATE 1, 1
            PRINT "Press Space!"
        END FRAME
    END IF
LOOP

keydown says the key is held for as long as it’s held, and holding Space shouldn’t set off sixty explosions a second. So the loop remembers whether Space was down last time, and only plays the sound when it goes down.

PLAYWAV can take two more things: whether to loop the sound, and how loud to play it, from 0 to 1. !! at((x, 0)) says where it’s heard: x runs from -1, all the way left, to 1, all the way right, and 0 is the middle.

SCREEN 320, 240
LOADWAV "explosion.wav", "boom"
PLAYWAV "boom", FALSE, 0.5 !! at((0.8, 0))
PRINT "a quieter boom, over on the right"
PAUSE
a quieter boom, over on the right

Crash-tan’s Tip: To hear a sound where something is on the screen, turn its x into that range: on a screen 320 across, x / 160 - 1 is -1 at the left edge, 0 in the middle and 1 at the right.

STOPWAV "boom" stops it, which matters most for a sound that loops. STOPWAV with no name stops every WAV that’s playing.

Crash-tan’s Tip: Load every sound before the game starts, beside the images. Loading reads a file, and a file read in the middle of a fight is a hiccup the player can feel.

Long sounds

MP3s go the same way, with their own words: LOADMP3 and PLAYMP3. My book comes with two voice clips, ready.mp3 and go.mp3, which are just what the start of a race needs:

SCREEN 320, 240
LOADMP3 "ready.mp3", "ready"
LOADMP3 "go.mp3", "go"
PLAYMP3 "ready"
ticks = 0
DO
    IF COOLDOWN(1)
        ticks = ticks + 1
        IF ticks = 60 THEN PLAYMP3 "go"
        FRAME
            CLS
            LOCATE 15, 18
            IF ticks < 60
                PRINT "READY?"
            ELSE
                PRINT "GO!"
            END IF
        END FRAME
    END IF
LOOP

ticks counts sixty to a second, so “go” comes a second after “ready”. Counting ticks is how a game says “a little later” without stopping.

PLAYMP3 also takes whether to loop and how loud, the same as PLAYWAV. That’s how background music from a file goes: PLAYMP3 "song", TRUE, 0.6.

Fading

A song that stops dead sounds like a mistake. FADEMP3 fades an MP3 out over as many seconds as you say, or fades one in, starting it from silence if it isn’t playing yet. One faded in goes round and round until it’s faded out or stopped.

FADEMP3 doesn’t wait for its fade: your program carries on at once. So this one counts ticks, sixty a second, to give each fade its time, and to hear the song in between.

SCREEN 320, 240
LOADMP3 "ready.mp3", "ready"
FADEMP3 "in", "ready", 0.5
PRINT "fading in"
ticks = 0
DO WHILE ticks < 120
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
FADEMP3 "out", "ready", 1
PRINT "and out again"
ticks = 0
DO WHILE ticks < 60
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
fading in
and out again

STOPMP3 stops one at once, by name, or every MP3 with no name.

Turning it down

SET AUDIO changes a sound while it plays: its volume, from 0 to 1, and its pan, from -1 (all the way left) to 1 (all the way right). It works on a WAV, an MP3 or the music, by name. Here a looping sound follows the mouse from one ear to the other:

SCREEN 320, 240
LOADWAV "explosion.wav", "boom"
PLAYWAV "boom", TRUE
DO
    IF COOLDOWN(1)
        side = mousex / 160 - 1
        SET AUDIO "boom" !! pan(side) volume(0.6)
        FRAME
            CLS
            LOCATE 1, 1
            PRINT "pan", fixed_str(side, 2)
        END FRAME
    END IF
LOOP

Letting go

A loaded sound stays in memory until the game ends. That’s fine for the sounds a game uses all the time. For a big one it needs only once, like the song for the title screen, FREE MP3 (or FREE WAV) stops it and lets the memory go. Here the title screen plays its song for a second, then the game starts and lets it go.

SCREEN 320, 240
LOADMP3 "ready.mp3", "title"
PLAYMP3 "title"
ticks = 0
DO WHILE ticks < 60
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
FREE MP3 "title"
PRINT "the title song is gone"
the title song is gone

A file or notes?

Both have their place, and most games use both.

  • A file for anything recorded: voices, real instruments, explosions, and a song somebody made in a music program.
  • Notes for sounds that should change with the game. A coin’s blip can go up a note for every coin in a row, and a song written as notes can speed up when the timer runs low. Notes take no space at all, and there’s no file to forget to put in the folder.

The next two lessons are about notes going a long way: instruments of your own, and whole songs.

Instruments

Every note I play, I play on an instrument. So far that’s been instrument 0, the piano. In a tune, i and a number picks another one, and it plays every note after it until the next i.

SCREEN 320, 240
PLAY "t150 o4 l8 i0 c e g > c < i17 c e g > c < i7 o2 c e g > c"
PRINT instrumentname(0)
PRINT instrumentname(17)
PRINT instrumentname(7)
PAUSE
FM Piano
FM Vibraphone
FM Bass

I come with a whole bank of them, numbered from 0: pianos, strings, basses, drums, and the sounds games need, like a coin pickup and a laser. instrumentname(n) tells you what number n is.

A bank from a file

A .cbi file is a bank of instruments, and each instrument in it takes its number, in place of the one I had there. A file called instruments.cbi in your game’s folder is special: I load it by myself, before your program starts. Every game in this book has one, so these are already its instruments, not mine:

SCREEN 320, 240
PRINT instrumentname(51)
PLAY "t160 o5 l16 i51 c e g > c < g e c4"
PAUSE
MM2 Lead

In my own bank, number 51 is called “Futuristic Lead”. LOADINSTRUMENTS loads any other bank, whenever you like: LOADINSTRUMENTS "spooky.cbi" for the haunted level, say. With !! instrument(51) it loads only that one and leaves the rest alone.

One of your own

INSTRUMENT and a number describes an instrument of your own. Any number will do: I start mine at 200, so they don’t land on top of the bank’s.

SCREEN 320, 240
INSTRUMENT 200
    NAME "Pluck"
    VOLUME 0.8
    ENVELOPE !! attack(0.005) decay(0.15) sustain(0) release(0.1)
    LAYER "triangle"
    END LAYER
END INSTRUMENT

INSTRUMENT 201
    NAME "Pad"
    VOLUME 0.6
    ENVELOPE !! attack(0.4) decay(0.2) sustain(0.8) release(0.8)
    LAYER "sawtooth"
    END LAYER
END INSTRUMENT

PLAY "t120 o4 l4 i200 c e g e"
PLAY "t120 o4 l4 i201 c e g e"
PRINT instrumentname(200), "and", instrumentname(201)
PAUSE
Pluck and Pad

The same four notes, and two very different sounds. A LAYER is a wave: "sine", "square", "triangle", "sawtooth", "pulse" or "noise". The ENVELOPE is how each note goes:

  • attack: how many seconds it takes to get loud when the note starts;
  • decay: how long it then takes to settle down to…
  • sustain: the level it holds, from 0 to 1, for as long as the note lasts;
  • release: how long it takes to fade once the note ends.

The pluck gets loud at once and dies away, like a plucked string. The pad swells in slowly and lingers.

Shaping the sound

A few more lines go in an instrument, a setting each:

  • FILTER takes some of the sound away. "lowpass" keeps the low part and muffles the highs above its cutoff, in hertz; resonance makes it ring at the cutoff.
  • FILTERENVELOPE moves the cutoff over each note, the way ENVELOPE moves the volume, up as far as its peak.
  • VIBRATO makes the pitch waver, and TREMOLO the volume: rate is how many times a second, depth how much.
  • PAN sits it to the left (-1) or the right (1), and VOLUME says how loud.
SCREEN 320, 240
INSTRUMENT 202
    NAME "Wah"
    FILTER "lowpass" !! cutoff(400) resonance(3)
    FILTERENVELOPE !! attack(0.1) decay(0.3) sustain(0.2) release(0.2) peak(2000)
    LAYER "sawtooth"
    END LAYER
END INSTRUMENT

INSTRUMENT 203
    NAME "Singer"
    VIBRATO !! rate(5) depth(0.2) delay(0.3)
    TREMOLO !! rate(6) depth(0.3)
    PAN -0.5
    LAYER "triangle"
    END LAYER
END INSTRUMENT

PLAY "t130 o3 l8 i202 c c g g a a g4"
PLAY "t90 o5 l2 i203 c g"
PRINT instrumentname(202), instrumentname(203), instrumentpan(203)
PAUSE
Wah Singer -0.5

VIBRATO’s delay waits a moment before it starts, the way a singer holds a long note steady and then lets it shake.

Layers

An instrument can have several layers, and they all play every note together. Each one can be tuned a little away from the note (detune, in hundredths of a note), play octaves up or down (octave), or be quieter than the rest (volume). Inside a layer, UNISON plays it as several copies at once, each a little out of tune with the others, for a big, wide sound.

SCREEN 320, 240
INSTRUMENT 204
    NAME "Big Lead"
    ENVELOPE !! attack(0.01) decay(0.2) sustain(0.7) release(0.3)
    LAYER "sawtooth"
        UNISON !! voices(5) detune(15) spread(0.8)
    END LAYER
    LAYER "square" !! octave(-1) volume(0.4)
    END LAYER
    LAYER "pulse" !! duty(0.25) detune(7) volume(0.3)
    END LAYER
END INSTRUMENT

PLAY "t140 o4 l8 i204 e g > c4 < b g a2"
PRINT instrumentname(204), "has", instrumentlayercount(204), "layers"
PAUSE
Big Lead has 3 layers

Drums

A drum is an instrument too. "noise" is a hiss, and a hiss that stops almost at once is a snare. PERCUSSION says it’s a drum.

SCREEN 320, 240
INSTRUMENT 205
    NAME "Snare"
    PERCUSSION 1
    ENVELOPE !! attack(0) decay(0.1) sustain(0) release(0.05)
    LAYER "noise"
    END LAYER
END INSTRUMENT
PLAY "t120 o4 l8 i205 c r c r c c c r"
PRINT instrumentpercussion(205)
PAUSE
1

Real drums sound better than any hiss, though, and a layer can play a recording instead of a wave. SAMPLE gives it the file, and basenote the note it was recorded at: 60 is o4 c, so that note plays the drum just as it was recorded, and higher notes play it higher.

My book comes with three drums, kick.wav, snare.wav and hat.wav. Here they are as a kit:

SCREEN 320, 240
INSTRUMENT 210
    NAME "Kick"
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "kick.wav" !! basenote(60)
    END LAYER
END INSTRUMENT
INSTRUMENT 211
    NAME "Snare"
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "snare.wav" !! basenote(60)
    END LAYER
END INSTRUMENT
INSTRUMENT 212
    NAME "Hat"
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "hat.wav" !! basenote(60)
    END LAYER
END INSTRUMENT

beat = "i210 c i212 c i211 c i212 c "
PLAY "t110 o4 l8 " + beat + beat + beat + beat
PRINT "boom tss crack tss"
PAUSE
boom tss crack tss

PLAY plays one note after another, so this beat is one drum at a time. In the next lesson, a song plays the kick, the snare and the hats on tracks of their own, all at once.

Crash-tan’s Tip: A sound you’ve already loaded with LOADWAV can go on an instrument too: SETINSTRUMENTWAV 210, "boom".

WAVETABLE is a layer’s other way to use a file: a file of many single waves, one after another, and the layer plays the one at position, from 0 to 1. Moving the position while a note plays turns one sound into another. There’s no such file in this book’s folder, so this one’s only to look at:

INSTRUMENT 206
    NAME "Morph"
    LAYER "sine"
        WAVETABLE "waves.wav" !! framesize(256) position(0.5)
    END LAYER
END INSTRUMENT

Changing one as you go

SET INSTRUMENT changes an instrument you’ve made, and only what you write in it. Everything else stays as it was. Here the mouse opens and closes the filter while a bass line plays: click the screen and move across it.

SCREEN 320, 240
INSTRUMENT 207
    NAME "Bass"
    FILTER "lowpass" !! cutoff(300) resonance(4)
    ENVELOPE !! attack(0.01) decay(0.2) sustain(0.5) release(0.1)
    LAYER "sawtooth"
    END LAYER
END INSTRUMENT

cutoff = 300
DO
    IF COOLDOWN(30)
        cutoff = 200 + mousex * 10
        SET INSTRUMENT 207
            FILTER "lowpass" !! cutoff(cutoff) resonance(4)
        END SET INSTRUMENT
        PLAY "t120 o2 l16 i207 c c > c < c" !! background
    END IF
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 1, 1
            PRINT "cutoff", cutoff
        END FRAME
    END IF
LOOP

Every half a second the loop sets the filter where the mouse says and plays the next few notes with it.

Keeping them

When an instrument sounds right, SAVEINSTRUMENTS saves it in a bank of your own (!! instrument(n) saves just that one), and LOADINSTRUMENTS brings it back, in this game or the next.

SCREEN 320, 240
INSTRUMENT 208
    NAME "Blip"
    ENVELOPE !! attack(0) decay(0.1) sustain(0) release(0.05)
    LAYER "square" !! volume(0.5)
    END LAYER
END INSTRUMENT
SAVEINSTRUMENTS "mine.cbi" !! instrument(208)
DELETE INSTRUMENT 208
PRINT instrumentexists(208)
LOADINSTRUMENTS "mine.cbi"
PRINT instrumentexists(208), instrumentname(208)
PLAY "t200 o6 l16 i208 c e g"
PAUSE
0
1 Blip

Call your bank instruments.cbi and put it in the game’s folder, and I’ll load it by myself every time the game starts.

Music

Back in Sound and music, a MUSIC block played a little song with two tracks. Now that you can make instruments, it’s time to give them a whole band to play in.

The parts of a song

A song in a MUSIC block is made of a few things:

  • TEMPO: how fast it goes, in beats a minute.
  • TIMESIG: how many beats a bar has, and which note is a beat. 4, 4 is four quarter notes, and 3, 4 is a waltz.
  • PATTERNs: a few bars each, with a name. A pattern’s length is how many beats it lasts.
  • TRACKs, inside a pattern: each one a line of notes, all playing at once.
  • The ARRANGEMENT: which patterns play, in what order.

A track says everything about itself after !!: its notes, the instrument that plays them, the channel it plays on (give each track of a pattern its own), how loud it is (volume, 0 to 100), where it sits from left to right (pan, -1 to 1), and a name, if you’d like to find it again.

SCREEN 320, 240
MUSIC "waltz" !! loop
    TEMPO 150
    TIMESIG 3, 4
    PATTERN "a" !! length(6)
        TRACK !! name("tune") channel(1) instrument(0) notes("o5 l4 c e g f2 d4")
        TRACK !! name("bass") channel(2) instrument(7) volume(70) notes("o2 l4 c e g g b > d")
    END PATTERN
    PATTERN "b" !! length(6)
        TRACK !! name("tune") channel(1) instrument(0) notes("o4 l4 b > d f e2.")
        TRACK !! name("bass") channel(2) instrument(7) volume(70) notes("o2 l4 g b > d < c e g")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
        PLAY "b"
    END ARRANGEMENT
END MUSIC
PRINT musictempo("waltz"), "beats a minute,", musicbeats("waltz"), "to a bar"
PRINT musicpatterncount("waltz"), "patterns,", trackcount("waltz", "a"), "tracks in each"

// Once through: twelve beats at 150 a minute is under five seconds.
PLAYMUSIC "waltz"
ticks = 0
DO WHILE ticks < 300
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
150 beats a minute, 3 to a bar
2 patterns, 2 tracks in each

!! loop on MUSIC starts the song again when it gets to the end, which is what music under a game usually wants.

A beat

Here’s the drum kit from the last lesson, playing in a song. The kick, the snare and the hats each get a track, so they play at the same time.

repeat on a track plays its notes again and again until the pattern ends, so the hats only need writing once. And in the arrangement, with plays patterns alongside each other: the drums go on under the bass, then under the bass and the tune together.

SCREEN 320, 240
INSTRUMENT 210
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "kick.wav" !! basenote(60)
    END LAYER
END INSTRUMENT
INSTRUMENT 211
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "snare.wav" !! basenote(60)
    END LAYER
END INSTRUMENT
INSTRUMENT 212
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "hat.wav" !! basenote(60)
    END LAYER
END INSTRUMENT

MUSIC "groove" !! loop
    TEMPO 110
    PATTERN "drums" !! length(4)
        TRACK !! name("kick") channel(10) instrument(210) notes("o4 l4 c r c r")
        TRACK !! name("snare") channel(11) instrument(211) notes("o4 l4 r c r c")
        TRACK !! name("hat") channel(12) instrument(212) volume(50) repeat notes("o4 l8 c")
    END PATTERN
    PATTERN "bass" !! length(4)
        TRACK !! channel(2) instrument(53) notes("o2 l8 c c > c < c e e g g")
    END PATTERN
    PATTERN "tune" !! length(4)
        TRACK !! channel(1) instrument(51) volume(60) notes("o5 l8 g e c e g4 a4")
    END PATTERN
    ARRANGEMENT
        PLAY "drums" !! repeat(2)
        PLAY "drums" !! repeat(2) with("bass")
        PLAY "drums" !! repeat(4) with("bass", "tune")
    END ARRANGEMENT
END MUSIC

PLAYMUSIC "groove"
PRINT "playing:", musicplaying()
PAUSE

Playing, stopping and pausing

PLAYMUSIC starts a song, from the beginning, and your program carries on while it plays. !! loop (or !! loop(FALSE)) on PLAYMUSIC says whether it loops this time, whatever the song says. STOPMUSIC stops it.

PAUSEMUSIC stops the song where it is, and RESUMEMUSIC carries on from there, which is just what a game’s pause screen needs. musicplaying() is TRUE while a song is playing, and FALSE once it’s stopped or paused.

SCREEN 320, 240
MUSIC "theme" !! loop
    TEMPO 140
    PATTERN "a"
        TRACK !! channel(1) instrument(0) notes("o4 l8 c e g e c e g e")
        TRACK !! channel(2) instrument(7) notes("o2 l2 c g")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
    END ARRANGEMENT
END MUSIC

// A second of each, to hear it.
FUNCTION hold(ticks)
    t = 0
    DO WHILE t < ticks
        IF COOLDOWN(1) THEN t = t + 1
    LOOP
END FUNCTION

PLAYMUSIC "theme"
PRINT musicplaying()
hold(60)
PAUSEMUSIC
PRINT musicplaying()
hold(60)
RESUMEMUSIC
PRINT musicplaying()
hold(60)
STOPMUSIC
PRINT musicplaying()
1
0
1
0

Crash-tan’s Tip: A song that doesn’t loop still counts as playing once it’s come to its end, until STOPMUSIC. If you need to know it’s done, ask where it’s got to instead.

Where’s it got to?

musicposition() is how far into the song it is, and musiclength("theme") how long the whole song is, both in ticks of the song, four to a beat. One divided by the other is how far along it is, from 0 to 1. (A length can be 0, and nobody divides by 0, so the loop checks first.) Here it is as a bar across the screen:

SCREEN 320, 240
MUSIC "theme"
    TEMPO 160
    PATTERN "a"
        TRACK !! channel(1) instrument(0) notes("o4 l8 c e g e c e g e d f a f d f a f")
    END PATTERN
    ARRANGEMENT
        PLAY "a" !! repeat(4)
    END ARRANGEMENT
END MUSIC

PLAYMUSIC "theme"
length = musiclength("theme")
DO
    IF COOLDOWN(1)
        done = 0
        IF length > 0 THEN done = min(musicposition() / length, 1)
        MEMORYBUFFER "bar"
            LINE (20, 110) -> (300, 130), 8 !! box
            LINE (20, 110) -> (20 + 280 * done, 130), 10 !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "bar"
            LOCATE 1, 1
            PRINT round(done * 100), "%"
        END FRAME
    END IF
LOOP

Fading

FADEMUSIC "in" starts a song from silence and brings it up over as many seconds as you say. FADEMUSIC "out" takes it down to nothing. It’s a gentle way to start a level, and a gentler way to end one than STOPMUSIC.

SCREEN 320, 240
MUSIC "theme" !! loop
    TEMPO 120
    PATTERN "a"
        TRACK !! channel(1) instrument(3) notes("o4 l2 c e g e")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
    END ARRANGEMENT
END MUSIC

FADEMUSIC "in", "theme", 2
ticks = 0
DO
    IF COOLDOWN(1)
        ticks = ticks + 1
        IF ticks = 300 THEN FADEMUSIC "out", "theme", 2
        FRAME
            CLS
            LOCATE 1, 1
            IF ticks < 300
                PRINT "here it comes..."
            ELSE
                PRINT "and away it goes"
            END IF
        END FRAME
    END IF
LOOP

Changing a song as the game goes

A song isn’t fixed once it’s written. A few words change it while the game runs:

  • SET MUSIC "theme" !! tempo(180) changes its tempo, whether it loops, or its timesig.
  • SET TRACK "theme", "a", 0 !! notes("...") changes a track: the song, the pattern, then which track, counting from 0. Only what you name changes.
  • MUTE TRACK "theme", "a", 1 silences a track, and !! muted(FALSE) brings it back.
  • CREATE MUSIC PATTERN adds a new pattern, CREATE TRACK gives it tracks, and ADD ARRANGEMENT puts it on the end of the arrangement.

I play a song from a recording I make of it when it starts, so a song that’s already playing goes on as it was. What you change is heard the next time it starts. That makes the moment between two levels the time to change it: the next level’s music is the same song, faster.

SCREEN 320, 240
MUSIC "theme" !! loop
    TEMPO 120
    PATTERN "a"
        TRACK !! channel(1) instrument(0) notes("o4 l8 c e g e c e g e")
        TRACK !! channel(2) instrument(7) notes("o2 l2 c g")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
    END ARRANGEMENT
END MUSIC

// Two seconds of each level's music.
FUNCTION hold(ticks)
    t = 0
    DO WHILE t < ticks
        IF COOLDOWN(1) THEN t = t + 1
    LOOP
END FUNCTION

FUNCTION start_level(level)
    SET MUSIC "theme" !! tempo(100 + level * 20)
    IF level >= 3
        SET TRACK "theme", "a", 1 !! notes("o2 l4 c c g g")
    END IF
    PLAYMUSIC "theme"
END FUNCTION

FOR level = 1 ... 3
    start_level(level)
    PRINT "level", level, "at", musictempo("theme")
    hold(120)
NEXT
PRINT tracknotes("theme", "a", 1)
STOPMUSIC
level 1 at 120
level 2 at 140
level 3 at 160
o2 l4 c c g g

Here’s a song built from nothing, as the game goes, the way a game might add a part for each boss you beat:

SCREEN 320, 240
MUSIC "boss" !! loop
    TEMPO 150
    PATTERN "base" !! length(4)
        TRACK !! channel(2) instrument(61) notes("o2 l8 c c c c d# d# f f")
    END PATTERN
    ARRANGEMENT
        PLAY "base"
    END ARRANGEMENT
END MUSIC

CREATE MUSIC PATTERN "boss", "rage", 4
CREATE TRACK "boss", "rage" !! track_name("lead") channel(1) instrument(59) notes("o5 l8 c d# g > c < g d# c4")
CREATE TRACK "boss", "rage" !! track_name("bass") channel(2) instrument(61) notes("o2 l8 c c c c d# d# f f")
ADD ARRANGEMENT "boss", "rage" !! repeat(2)
APPLY MUSIC "boss"

PRINT musicpatterncount("boss"), "patterns"
FOR step = 0 ... arrangementcount("boss") - 1
    PRINT arrangementpattern("boss", step), "x", arrangementrepeat("boss", step)
NEXT

// Once through, the base and the rage twice, then stop.
PLAYMUSIC "boss"
ticks = 0
DO WHILE ticks < 300
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
STOPMUSIC
2 patterns
base x 1
rage x 2

Changes wait in your program until the song is next played. APPLY MUSIC hands them to my audio engine straight away instead, without playing anything.

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

Levels from files

In Tile maps the program built its level square by square. A real game draws its levels in a map editor and saves each one as a file, and the program only has to load it. Your game’s folder has one: level.map.json, the courtyard from my Adventures of Sir Blocksley demo, drawn with the tiles in greyscale.png.

One line

A map has layers of its own, and CREATE MAP puts them on the screen’s. It takes the file, which of the map’s layers to show, and with !! layer_to the screen layers they go on, one each. A map’s layers count up from 0, the lowest. This map has two, and this puts them on screen layers 1 and 2, which leaves screen layer 0 under them for anything else. The words go on layer 3, over the top, as in Tile maps:

SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
SET LAYER 3
PRINT "the level is", mapwidth(1), "tiles across and", mapheight(1), "down"
the level is 20 tiles across and 23 down

The file says everything else: which tile set to use and how big a tile is, so there’s no LOADIMG and no TILESIZE. mapwidth and mapheight say how big a map is, in tiles, given the screen layer it’s on. This one is bigger than the screen, so you’d SET SCROLL down it the way you did in Tile maps.

One map layer goes on one screen layer: CREATE MAP "level.map.json", 1 !! layer_to(3) puts the map’s layer 1 on screen layer 3. Leave the map’s layers out and I start from 0, as many as layer_to has room for: CREATE MAP "level.map.json" !! layer_to(1...2) is the same as the line above.

From 1.x: That’s CREATE MAP("level.map.json", 0 TO 1) AS LAYER 1 TO 2.

What’s in the file

The file is JSON, and loadjson reads any JSON file into maps and lists. Here’s the level’s:

SCREEN 320, 240
level = loadjson("level.map.json")
PRINT "tiles from", level.tileset, "and", level.tileWidth, "pixels square"
PRINT level.mapWidth, "by", level.mapHeight
FOR layer = level.layers
    PRINT "a", layer.layerType, "layer called", layer.name
END FOR
tiles from greyscale.png and 32 pixels square
20 by 23
a tile layer called Layer 1
a tile layer called Layer 2
a metadata layer called Metadata

Two layers of tiles, and one more of metadata, which is the interesting one. It isn’t drawn. Each square of it holds a bit of text that says what’s there: "P" where the player starts, "C" for a coin, "X" for the exit, whatever your game needs. The map editor lets you write on it, and the game reads it back.

Crash-tan’s Tip: loadjson is for any data your game keeps in files: a list of levels, the enemies in each, what a shop sells. Build the file by hand or with a tool, and read it with one line.

Starting from the file

A TILEMAP can start from a file too. BASEMAP says which, and then the map’s size, tile set and layers all come from it. Each layer that says !! into(grid) fills that grid with the file’s tiles, so your program can look at the level square by square:

SCREEN 320, 240
ground = (23, 20):INTEGER
walls = (23, 20):INTEGER
TILEMAP "level"
    BASEMAP "level.map.json"
    TILES 0 !! name("ground") into(ground)
    TILES 1 !! name("walls") into(walls)
END TILEMAP
CREATE MAP "level", 0...1 !! layer_to(1...2)

count = 0
FOR row = 0 ... 22
    FOR column = 0 ... 19
        IF walls(row, column) <> 0 THEN count = count + 1
    NEXT
NEXT
SET LAYER 3
PRINT count, "squares have a wall on them"
PRINT "the top left is", ground(0, 0), "in the grid and", maptile(0, 0, 1), "on the map"
121 squares have a wall on them
the top left is 10 in the grid and 9 on the map

The grids are 23 rows of 20, the size of the map. A TILEMAP’s layers go on the screen the same way a file’s do, and when it has more than one TILES layer, each needs a name, which picks it too: CREATE MAP "level", "ground" !! layer_to(1). maptile(x, y, 1) asks about the map on screen layer 1, where its layer 0 went. The grid counts tiles from 1, with 0 for none, the same as the grids in Tile maps, while maptile counts them from 0.

Markers

The courtyard’s metadata layer is already written on. "P" is where Sir Blocksley starts, "K" is the king, "B" is someone standing about, "*" runs along every wall, and a number marks each way out.

mapmeta(x, y) reads the marker on one square of the map on the layer you’re drawing on, so SET LAYER one the map is on, and ask before going up to print. mapdata goes through a whole region and gives every square back, each with its x, its y and its meta, so finding the start and the people is one loop:

SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
SET LAYER 1
start = mapmeta(9, 8)
cells = mapdata((0, 0) -> (19, 22))

SET LAYER 3
PRINT "on (9, 8):", start
FOR cell = cells
    IF cell.meta = "P" THEN PRINT "start at", cell.x, cell.y
    IF cell.meta = "K" THEN PRINT "the king at", cell.x, cell.y
    IF cell.meta = "B" THEN PRINT "someone at", cell.x, cell.y
END FOR
on (9, 8): P
someone at 3 4
the king at 9 5
someone at 15 6
start at 9 8

Your program can put markers down too. A META layer that says !! from(grid) is drawn from a grid of text you keep, the same way TILES is drawn from a grid of numbers:

SCREEN 320, 240
markers = (23, 20):STRING
markers(8, 9) = "P"
markers(7, 3) = "C"
markers(3, 14) = "C"

TILEMAP "level"
    BASEMAP "level.map.json"
    META "markers" !! from(markers)
END TILEMAP
CREATE MAP "level", 0...1 !! layer_to(1...2)
SET LAYER 1
cells = mapdata((0, 0) -> (19, 22))
start = mapmeta(9, 8)
gone = mapmeta(0, 1)

SET LAYER 3
FOR cell = cells
    IF cell.meta = "C" THEN PRINT "a coin at", cell.x, cell.y
END FOR
PRINT "on (9, 8):", start
PRINT "on (0, 1):", gone
a coin at 14 3
a coin at 3 7
on (9, 8): P
on (0, 1): 

Your grid takes the place of the file’s markers: (0, 1) had a "3" on it, and now it has nothing. That’s why the grid puts "P" back.

Change the grid and UPDATE MAP shows the change, for markers as for tiles: when a coin’s picked up, rub out its "C".

Crash-tan’s Tip: Most squares have no marker, so look for the ones you want. mapdata gives back every square in the region, marker or not.

Which square is that?

A sprite’s place, or the mouse’s, is in pixels, and the map is in squares. Four functions turn one into the other, and the difference between them is scrolling.

  • maptilex(x) and maptiley(y) are for a place on the screen, like mousex and mousey. They add on how far the map is scrolled.
  • mapworldtilex(x) and mapworldtiley(y) are for a place in the level, like a sprite’s x and y. They don’t.
SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
FOR layer = 1 ... 2
    SET LAYER layer
    SET SCROLL 0, 160
NEXT
SET LAYER 1
on_screen = maptiley(20)
in_level = mapworldtiley(20)
SET LAYER 3
PRINT "20 pixels down the screen is row", on_screen
PRINT "20 pixels down the level is row", in_level
20 pixels down the screen is row 5
20 pixels down the level is row 0

With the map scrolled 160 pixels, five rows of 32, the top of the screen shows row 5 of the level.

A walk for coins

Here’s all of it together: the level from the file, the start and the coins from the markers, and the hero walking about picking them up. When the hero stands on a square marked "C", the marker’s rubbed out and the coin goes.

SCREEN 640, 480
LOADIMG "hero.png", "hero"
MEMORYBUFFER "coin", 10, 10
    CIRCLE (5, 5), 4, 14, 14
END MEMORYBUFFER

// The start and the coins, as the map editor would mark them.
markers = (23, 20):STRING
markers(8, 9) = "P"
FOR spot = [(3, 7), (14, 3), (9, 9), (15, 8), (3, 2)]
    markers(spot.y, spot.x) = "C"
END FOR

TILEMAP "level"
    BASEMAP "level.map.json"
    META "markers" !! from(markers)
END TILEMAP
CREATE MAP "level", 0...1 !! layer_to(1...2)
SET LAYER 1

// A coin sprite on every "C". coin_at remembers which sprite is where.
coin_at = (23, 20):INTEGER
coins = 0
FOR cell = mapdata((0, 0) -> (19, 14))
    IF cell.meta = "C"
        id = reserve_sprite()
        SET SPRITE id !! image("coin") x(cell.x * 32 + 11) y(cell.y * 32 + 11)
            !! layer(2)
        SHOW SPRITE id
        coin_at(cell.y, cell.x) = id
        coins = coins + 1
    ELSE IF cell.meta = "P"
        x = cell.x * 32 + 16
        y = cell.y * 32 + 16
    END IF
END FOR

hero = reserve_sprite()
SET SPRITE hero !! image("hero") x(x) y(y)
    !! origin((0.5, 0.5)) layer(2)
SHOW SPRITE hero

found = 0
DO WHILE found < coins
    IF COOLDOWN(1)
        IF keydown("Left") AND x > 8 THEN x = x - 2
        IF keydown("Right") AND x < 632 THEN x = x + 2
        IF keydown("Up") AND y > 8 THEN y = y - 2
        IF keydown("Down") AND y < 472 THEN y = y + 2
        SET SPRITE hero !! x(x) y(y)

        column = mapworldtilex(x)
        row = mapworldtiley(y)
        IF mapmeta(column, row) = "C"
            markers(row, column) = ""
            UPDATE MAP "level"
            DISABLE SPRITE coin_at(row, column)
            found = found + 1
        END IF
    END IF
LOOP
SOUND 660, 0.3

Click the screen and walk with the arrow keys. The level isn’t scrolled, so mapworldtilex and maptilex would agree here; use the world ones for a sprite anyway, and the game keeps working once the camera starts to move.

Timers, events and threads

So far a game has been one loop, with my behaviors and CALL RENDER running beside it. This lesson is about the other ways I can call your functions for you: every so often, when something happens, or on a thread of their own while your loop gets on with the game.

Every so often

CALL TIMER calls a function every so often, for as long as the game runs or until STOP TIMER stops it. It takes the function, how long between calls, the unit ("seconds" unless you say "milliseconds" or "minutes"), and a name to stop it by.

SCREEN 320, 240
ATOMIC left = 10

FUNCTION tick()
    left = left - 1
    IF left = 0 THEN STOP TIMER "countdown"
END FUNCTION
CALL TIMER tick, 1, "seconds", "countdown"

DO
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 8, 17
            IF left > 0 THEN PRINT left ELSE PRINT "Lift off!"
        END FRAME
    END IF
LOOP

The timer only counts. The loop draws, sixty times a second, whatever left is now. A timer can stop itself, as tick does when it gets to 0.

With !! after, the function’s called once, after the time, instead of every time:

SCREEN 320, 240
ATOMIC rang = FALSE

FUNCTION ring()
    rang = TRUE
END FUNCTION
CALL TIMER ring, 0.5, "seconds", "alarm" !! after

ticks = 0
DO UNTIL rang
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
PRINT "ring ring!"
ring ring!

!! args([...]) gives the function the values to be called with, every time.

Crash-tan’s Tip: A timer for “a second from now” is easier to read than counting ticks in your loop, and it keeps counting while your loop is busy elsewhere, in a menu, say.

When something happens

An event is a name for something that happens in your game: "coin", "door", "boss_down". CALL EVENT says which function to call when it happens, and EVENT makes it happen, with !! args for what to tell the function.

SCREEN 320, 240
ATOMIC score = 0

FUNCTION on_coin(points)
    score = score + points
END FUNCTION
CALL EVENT on_coin, "coin"

EVENT "coin" !! args([10])
EVENT "coin" !! args([25])

ticks = 0
DO WHILE score < 35 AND ticks < 60
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
PRINT "score", score
score 35

EVENT doesn’t wait for on_coin. It hands the event to me, and I call the function a moment later, on a thread of my own. That’s why the loop waits for the score to arrive before it prints.

What an event is good for is that whoever fires it doesn’t need to know who’s listening. A sprite’s ONCOLLISION can say EVENT "coin" when it touches a coin, and the score, the sound and the “+10” floating up can each be someone else’s business. In a game for several players, EVENT can fire on the other players’ machines too.

A thread of its own

Some jobs take a while: building a big level, working out the best move. If your loop did them, the game would freeze until they were done. CALL THREAD runs a function on a thread of its own, and your loop keeps drawing. Here the long job is only adding up a lot of numbers, two hundred times over, but it could be anything.

SCREEN 320, 240
ATOMIC done = 0

FUNCTION long_job()
    FOR step = 1 ... 200
        LOCAL sum = 0
        FOR i = 1 ... 20000
            sum = sum + sin(i)
        NEXT
        done = step
    NEXT
END FUNCTION
CALL THREAD long_job, "worker"

DO
    IF COOLDOWN(1)
        IF keydown("Escape") THEN STOP THREAD "worker"
        MEMORYBUFFER "bar"
            LINE (60, 110) -> (260, 130), 8 !! box
            LINE (60, 110) -> (60 + done, 130), 10 !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "bar"
            LOCATE 10, 13
            PRINT "steps done:", done, "of 200"
        END FRAME
    END IF
LOOP

STOP THREAD "worker" stops it before it’s done (press Escape), and STOP THREAD with no name stops every one.

Crash-tan’s Tip: Two calls running at once share every variable that isn’t LOCAL, the same as any two functions do. If a function might run while another copy of it runs, on a timer that comes round again or in two threads, make its own working variables LOCAL.

ASYNC

ASYNC in front of an ordinary call starts it on a thread of its own and gives you a future: a promise of the answer. RESOLVE waits for the answer and gives it to you, or all of them at once for several futures.

SCREEN 320, 240
FUNCTION add_up(n)
    LOCAL total = 0
    FOR i = 1 ... n
        total = total + i
    NEXT
    RETURN total
END FUNCTION

first = ASYNC add_up(1000)
second = ASYNC add_up(2000)
// Both are working now. Wait for both answers:
PRINT RESOLVE(first, second)
[500500, 2001000]

ASYNC has two modifiers that make it a timer. !! after(time, unit) starts the call once, later, and its future becomes the answer. !! every(time, unit) starts it again and again, and its future is what you hold on to so that STOP can stop it. The unit is milliseconds unless you say otherwise.

SCREEN 320, 240
FUNCTION shout(word)
    RETURN word + "!"
END FUNCTION
later = ASYNC shout("time's up") !! after(1, "seconds")
PRINT RESOLVE(later)

ATOMIC beats = 0
FUNCTION beat()
    beats = beats + 1
END FUNCTION
drum = ASYNC beat() !! every(100)
ticks = 0
DO WHILE beats < 3
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
STOP drum
PRINT "three beats, then", RESOLVE(drum)
time's up!
three beats, then NULL

A stopped future resolves to NULL, so anyone still waiting on it isn’t left waiting forever.

Which thread, and ATOMIC

Here’s who calls what, and where:

Your programits own thread, the one it starts on
CALL RENDERmy render thread, once a frame
Behaviors, ONCOLLISIONmy tick, sixty times a second
CALL TIMER, ASYNC !! after or everymy timer, when the time comes
CALL EVENTme, a moment after the EVENT
CALL THREAD, ASYNCa new thread, just for that call

All of them run at the same time as your program. That’s what ATOMIC is for. A variable that two of them change, like left above (the timer changes it and the loop reads it) or a score that a collision and the loop both change, is marked ATOMIC once, at the top of the program. Then left = left - 1 is one step: no other thread can sneak in between the reading and the writing, and nothing gets lost.

A variable only one of them ever uses doesn’t need it. Nor does one that’s set before the others start and only read after that.

Crash-tan’s Tip: A line that changes an ATOMIC variable using its own value can’t call a function, because every other thread that wants the variable is waiting on that line. Work the value out on the line before: bonus = points_for(level), then score = score + bonus.

Drawing somewhere else for a moment

WITH LAYER draws on another layer until END WITH LAYER, then goes back to the one you were on. It’s handy in a function that draws a part of the screen, the score, say, which shouldn’t leave whoever called it drawing on the wrong layer:

SCREEN 320, 240
CREATE LAYER 1, 320, 240

MEMORYBUFFER "ground"
    LINE (0, 200) -> (319, 239), 2 !! box fill
END MEMORYBUFFER
MEMORYBUFFER "border"
    LINE (4, 4) -> (315, 235), 15 !! box
END MEMORYBUFFER

FRAME
    CLS
    PUTIMAGE (0, 0), "ground"
    WITH LAYER 1
        PUTIMAGE (0, 0), "border"
        LOCATE 2, 3
        PRINT "lives 3"
    END WITH LAYER
    // Back on layer 0.
    LOCATE 13, 15
    PRINT "Ready?"
END FRAME
PAUSE

Layer 1 isn’t cleared, so it’s see-through apart from the border and the lives, and layer 0 shows under it.

Phones, tablets and gamepads

A phone has no arrow keys, and a living-room console has no keyboard at all. In Keys, the mouse and a finger you saw that a finger works the mouse. Now for the rest: buttons on the screen, the ways a finger can steer, and a gamepad in your hands.

A gamepad on the screen

GAMEPAD ON puts a gamepad over the game: a D-pad on the left, A, B, X and Y on the right, and START in the middle. Each button presses a key, and GAMEPAD MAP says which one. So a game that reads keys with keydown works on a phone without changing a line of its loop.

SCREEN 320, 240
GAMEPAD ON
GAMEPAD MAP "UP", "Up"
GAMEPAD MAP "DOWN", "Down"
GAMEPAD MAP "LEFT", "Left"
GAMEPAD MAP "RIGHT", "Right"
GAMEPAD MAP "A", "Space"

x = 160
y = 80
DO
    IF COOLDOWN(1)
        IF keydown("Left") THEN x = x - 2
        IF keydown("Right") THEN x = x + 2
        IF keydown("Up") THEN y = y - 2
        IF keydown("Down") THEN y = y + 2
        shade = 14
        IF keydown("Space") THEN shade = 12
        MEMORYBUFFER "scene"
            CIRCLE (x, y), 10, shade, shade
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
    END IF
LOOP

The buttons are called "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y" and "START". A button you haven’t mapped presses nothing.

Making it yours

Most games don’t need every button. A few more words tidy the gamepad up:

  • GAMEPAD BUTTON "B", FALSE hides a button, and TRUE brings it back.
  • GAMEPAD BUTTON "A" !! at((0.85, 0.8)) moves one. The place is a share of the screen, from 0 to 1 each way, so it lands in the same spot on any size of phone.
  • GAMEPAD ALPHA 0.5 makes the whole gamepad half see-through.
  • GAMEPAD SETIMAGE "A", "jump" draws a button with an image of yours.
  • GAMEPAD DPAD says how the D-pad works: "buttons", four of them, the way it starts; "joystick", a stick you push any way, diagonals and all; or "drag", where you put your thumb down anywhere and slide it.
  • GAMEPAD OFF hides it, for a cutscene or a menu you tap, and GAMEPAD RESET forgets every GAMEPAD MAP.

Here’s a gamepad for a runner, where all you do is jump: one big button, faint, in the corner where a right thumb rests.

SCREEN 320, 240
MEMORYBUFFER "jump", 32, 32
    CIRCLE (16, 16), 15, 12, 4
    POLYGON [(16, 6), (26, 20), (6, 20)], 15, 15
END MEMORYBUFFER

FOR button = ["UP", "DOWN", "LEFT", "RIGHT", "B", "X", "Y", "START"]
    GAMEPAD BUTTON button, FALSE
END FOR
GAMEPAD BUTTON "A" !! at((0.88, 0.82))
GAMEPAD SETIMAGE "A", "jump"
GAMEPAD ALPHA 0.6
GAMEPAD MAP "A", "Space"
GAMEPAD ON

y = 200
speed = 0
DO
    IF COOLDOWN(1)
        IF keydown("Space") AND y = 200 THEN speed = -6
        speed = speed + 0.3
        y = min(y + speed, 200)
        MEMORYBUFFER "scene"
            LINE (0, 211) -> (319, 239), 2 !! box fill
            CIRCLE (60, y), 10, 14, 14
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
    END IF
LOOP

Steering with a finger

Some games are better with no buttons at all. Here’s what I know about a finger on the screen:

touchx, touchyWhere a finger is touching, or -1 when none is. Only a finger: the mouse doesn’t count.
mousex, mouseyWhere the mouse is, or a finger.
tap1 while a finger (or the mouse button) is held down and hasn’t moved.
dragx, dragyHow far a finger (or the mouse) has been dragged since it went down, from -1 to 1, and 0 when it isn’t dragging.

dragx and dragy make a joystick out of the whole screen: put a finger down anywhere and slide it. Halfway across the screen is all the way, 1 or -1. Try it with the mouse, too: hold the button and drag.

SCREEN 320, 240
x = 160
y = 120
DO
    IF COOLDOWN(1)
        x = clamp(x + dragx * 4, 0, 320)
        y = clamp(y + dragy * 4, 0, 240)
        shade = 11
        IF tap THEN shade = 15
        MEMORYBUFFER "scene"
            POLYGON [(x, y - 8), (x + 6, y + 6), (x - 6, y + 6)], shade, shade
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
        END FRAME
    END IF
LOOP

Press and hold without moving and the ship lights up: that’s tap. As soon as you drag, tap goes back to 0 and the ship starts to move.

Where am I running?

ismobile is 1 on a phone or a tablet, and isconsole is 1 on a console in the living room. Use them to pick the controls and the words on the screen:

SCREEN 320, 240
IF ismobile
    GAMEPAD ON
    PRINT "Tap A to start"
ELSE IF isconsole
    GAMEPAD PHYSICAL ON
    PRINT "Press A to start"
ELSE
    PRINT "Press Space to start"
END IF

Crash-tan’s Tip: On a console there’s no screen to touch, so I never show the on-screen gamepad there, even if you ask. Its players have a real gamepad, which is next.

A gamepad in your hands

GAMEPAD PHYSICAL ON, which you met in Keys, the mouse and a finger, switches on real gamepads. Once it’s on, GAMEPAD MAP works for them as it does for the one on the screen: the same mapping and the same keys, so one loop serves the keyboard, the screen and the gamepad.

For more than keys, ask the gamepad itself. joybutton(n) is 1 while a button’s held: 0 is A, 1 is B, 2 is X, 3 is Y, and 9 is START. joyaxis(0) and joyaxis(1) are the left stick, from -1 to 1, and joyaxis(2) and joyaxis(3) the right one. Each takes a second number to read the second gamepad, the third, or the fourth.

RUMBLE shakes it. !! strength is how hard, from 0 to 1, and !! duration is for how long, in milliseconds.

SCREEN 320, 240
GAMEPAD PHYSICAL ON
GAMEPAD MAP "A", "Space"

x = 160
was_down = FALSE
DO
    IF COOLDOWN(1)
        stick = joyaxis(0)
        IF abs(stick) > 0.2 THEN x = clamp(x + stick * 4, 10, 310)
        IF keydown("Left") THEN x = x - 3
        IF keydown("Right") THEN x = x + 3

        // A, or Space on the keyboard: a shake you can feel.
        down = keydown("Space")
        IF down AND NOT was_down THEN RUMBLE !! strength(0.8) duration(150)
        was_down = down

        MEMORYBUFFER "scene"
            CIRCLE (x, 120), 12, 14, 14
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "scene"
            LOCATE 1, 1
            PRINT "gamepads:", joyconnected
        END FRAME
    END IF
LOOP

Crash-tan’s Tip: A stick never rests at exactly 0. Ignoring anything under 0.2, as that loop does, keeps a sprite from creeping about on its own.

Enemies that think

A bad guy that walks straight at you gets stuck on the first wall. One that finds its way round, notices when it can see you, and wanders about when it can’t, feels alive. I have a set of functions for exactly that, and none of them needs more than a line.

Thinking on a grid

Most of them think about the level as a grid: a list of rows, where 0 is a square you can walk on and anything else is in the way. That’s the same shape as the grid of a tile map, and they read it the same way, grid(y, x), a row and then a column. A place on the grid is a coordinate (x, y): a column and then a row.

maze makes one for you, walled all round. Give it a seed and you get the same maze every time:

SCREEN 320, 240
grid = maze(15, 7, 42)
FOR y = 0 ... 6
    row = ""
    FOR x = 0 ... 14
        IF grid(y, x) = 0 THEN row = row + "." ELSE row = row + "#"
    END FOR
    PRINT row
END FOR
###############
#.............#
#.#.#########.#
#.#.#...#.#...#
#.###.###.#.###
#.....#.......#
###############

Without a seed it’s a different maze each time. After the seed it can take a style: "BACKTRACKER", long winding corridors and the one it starts with; "PRIM", lots of short dead ends; "DIVISION", straight walls like rooms; or "CAVE", a cave. And a last number from 0 to 1 knocks out that share of the dead ends, so there’s more than one way round: maze(21, 15, 7, "PRIM", 0.3).

Finding the way

astar finds the shortest path across a grid from one square to another, round anything in the way. It gives back the squares to walk through, from the start to the end, or [] when there’s no way there at all.

SCREEN 320, 240
// A wall down column 3, with a gap at the bottom.
grid = [[0, 0, 0, 1, 0, 0],
        [0, 0, 0, 1, 0, 0],
        [0, 0, 0, 1, 0, 0],
        [0, 0, 0, 0, 0, 0]]
path = astar(grid, (0, 0), (5, 0))
PRINT path
PRINT "the first step is to", path(1)
PRINT "into the wall:", astar(grid, (0, 0), (3, 0))
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 2), (5, 1), (5, 0)]
the first step is to (1, 1)
into the wall: []

It goes diagonally as well as straight, so the path cuts across to the gap and back up. An enemy only needs path(1), the next square, and asks again later, when you’ve moved.

Crash-tan’s Tip: astar is quick, but there’s no need to ask it every tick. An enemy that looks again each time it takes a step is every bit as clever, and leaves your loop more time for everything else.

Two more look at the grid around a square. neighbors gives the squares next to one that you can step on (up, down, left and right), and floodfill counts every square you can reach from one, which tells you whether a level you’ve made has bits nobody can get to:

SCREEN 320, 240
grid = [[0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0]]
PRINT "next to (1, 1):", neighbors(grid, (1, 1))
PRINT "reachable from the left:", floodfill(grid, (0, 0), 0)
PRINT "reachable from the right:", floodfill(grid, (4, 0), 0)
next to (1, 1): [(1, 0), (1, 2), (0, 1)]
reachable from the left: 6
reachable from the right: 6

Can it see you?

raycast draws a straight line between two squares and says whether anything is in the way: 1 if the line is clear, 0 if it isn’t. That’s line of sight. A guard that can see you comes running, and one that can’t keeps to its rounds.

SCREEN 320, 240
grid = [[0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 0, 0, 0]]
PRINT "along the top:", raycast(grid, (0, 0), (4, 0))
PRINT "through the pillar:", raycast(grid, (0, 1), (4, 1))
along the top: 1
through the pillar: 0

Moving about

Not everything lives on a grid. For things that move freely, these work in pixels, and give back a velocity: how far to move this tick, across and down.

  • seek(from, to, speed) heads straight for something.
  • flee(from, away_from, speed) heads straight away from it.
  • wander(place, heading, radius, distance) gives a new place a little way ahead, wobbling at random: a sheep that’s grazing.
  • distance(a, b) is how far apart two places are, and nearest(place, places) is which of a list of places is closest, as its index.
SCREEN 320, 240
v = seek((0, 0), (30, 40), 5)
PRINT "toward:", v, "  away:", flee((0, 0), (30, 40), 5)
PRINT "how far:", distance((0, 0), (30, 40))
PRINT "nearest:", nearest((0, 0), [(50, 50), (5, 5), (90, 0)])
toward: (3, 4)   away: (-3, -4)
how far: 50.0
nearest: 1

Here are three of them at once, around the mouse: a chick that follows it, a mouse-shy sheep that runs off when it comes close, and one more sheep that just wanders.

SCREEN 320, 240
MEMORYBUFFER "chick", 10, 10
    CIRCLE (5, 5), 4, 14, 14
END MEMORYBUFFER
MEMORYBUFFER "sheep", 14, 10
    CIRCLE (7, 5), 6, 15, 15
END MEMORYBUFFER

chick = (60, 120)
shy = (160, 120)
grazer = (260, 120)
heading = 0
FOR n = 0 ... 2
    ENABLE SPRITE n
NEXT
SET SPRITE 0 !! image("chick") x(chick.x) y(chick.y) origin((0.5, 0.5))
SET SPRITE 1 !! image("sheep") x(shy.x) y(shy.y) origin((0.5, 0.5))
SET SPRITE 2 !! image("sheep") x(grazer.x) y(grazer.y) origin((0.5, 0.5))
FOR n = 0 ... 2
    SHOW SPRITE n
NEXT

DO
    IF COOLDOWN(1)
        mouse = (mousex, mousey)
        IF distance(chick, mouse) > 12 THEN chick = chick + seek(chick, mouse, 1.5)
        IF distance(shy, mouse) < 60 THEN shy = shy + flee(shy, mouse, 2)
        heading = heading + (rnd - 0.5) * 20
        grazer = wander(grazer, heading, 0.3, 0.5)
        shy = (clamp(shy.x, 0, 320), clamp(shy.y, 0, 240))
        grazer = (clamp(grazer.x, 0, 320), clamp(grazer.y, 0, 240))
        SET SPRITE 0 !! x(chick.x) y(chick.y)
        SET SPRITE 1 !! x(shy.x) y(shy.y)
        SET SPRITE 2 !! x(grazer.x) y(grazer.y)
    END IF
LOOP

A little luck

An enemy that does the same thing every time is easy to beat. choose picks one thing from a list, each as likely as the next, and weighted picks with odds: the weights [6, 3, 1] make the first thing six times as likely as the last. Press Space to open a chest:

SCREEN 320, 240
loot = ["a coin", "a potion", "a sword"]
held = FALSE
DO
    IF COOLDOWN(1)
        IF keydown("Space") AND NOT held
            found = weighted(loot, [6, 3, 1])
            mood = choose(["Ooh,", "Hey,", "Nice,"])
            PRINT mood, found + "!"
        END IF
        held = keydown("Space")
    END IF
LOOP

The chaser

Here’s an enemy that thinks. The level is a maze with a few loops in it, so there’s somewhere to run. The hero moves a square at a time with the arrow keys. The chaser asks astar for the way to the hero and takes the next step. When raycast says it can see the hero, it goes red and speeds up.

SCREEN 320, 240
RANDOMIZE
LOADIMG "hero.png", "hero"

// The level: a maze, 20 squares by 15, drawn as a tile map.
grid = maze(20, 15, 0, "BACKTRACKER", 0.4)
MEMORYBUFFER "tiles", 16, 16
    LINE (0, 0) -> (15, 15), 8 !! box fill
    LINE (1, 1) -> (14, 7), 7 !! box
END MEMORYBUFFER
TILEMAP "maze"
    TILESET "tiles"
    TILESIZE 16
    SIZE 20, 15
    TILES 0 !! from(grid)
END TILEMAP
CREATE MAP "maze" !! layer_to(0)

MEMORYBUFFER "calm", 16, 16
    CIRCLE (8, 8), 6, 9, 9
END MEMORYBUFFER
MEMORYBUFFER "angry", 16, 16
    CIRCLE (8, 8), 7, 12, 12
END MEMORYBUFFER
TOSPRITESHEET "chaser", ["calm", "angry"]

// The hero starts at the top left, the chaser at the bottom right.
her = (1, 1)
chaser = (17, 13)
hero = reserve_sprite()
SET SPRITE hero !! image("hero") x(her.x * 16 + 8) y(her.y * 16 + 8)
    !! origin((0.5, 0.5)) scale((0.5, 0.5))
SHOW SPRITE hero
enemy = reserve_sprite()
SET SPRITE enemy !! image("chaser") frame(0) x(chaser.x * 16) y(chaser.y * 16)
SHOW SPRITE enemy

FUNCTION walkable(place)
    RETURN grid(place.y, place.x) = 0
END FUNCTION

DO WHILE her <> chaser
    // The hero: a square every 8 ticks while an arrow is held.
    IF COOLDOWN(8)
        target = her
        IF keydown("Left") THEN target = (her.x - 1, her.y)
        IF keydown("Right") THEN target = (her.x + 1, her.y)
        IF keydown("Up") THEN target = (her.x, her.y - 1)
        IF keydown("Down") THEN target = (her.x, her.y + 1)
        IF walkable(target) THEN her = target
        SET SPRITE hero !! x(her.x * 16 + 8) y(her.y * 16 + 8)
    END IF

    // The chaser: a square every 6 ticks if it can see the hero, every 14 if not.
    sees = raycast(grid, chaser, her)
    IF sees
        SET SPRITE enemy !! frame(1)
    ELSE
        SET SPRITE enemy !! frame(0)
    END IF
    IF (sees AND COOLDOWN(6)) OR (NOT sees AND COOLDOWN(14))
        path = astar(grid, chaser, her)
        IF len(path) > 1 THEN chaser = path(1)
        SET SPRITE enemy !! x(chaser.x * 16) y(chaser.y * 16)
    END IF
LOOP
SOUND 110, 0.5

Each COOLDOWN keeps its own time, so the two speeds don’t trip over each other. Make the chaser smarter or slower, give it a friend, or make it wander until it first sees you.

Handy maths for games

A game is full of little sums. Which way is the player? How far away is the treasure? How do I make the camera glide instead of jump? You won’t need much maths for any of it, and I’ve got the pieces ready for you.

Which way is it?

atn2(y, x) gives the angle of a step x across and y down. Give it the difference between where something is and where it’s looking, and you know which way to face.

SCREEN 320, 240
turret = (160, 120)
target = (260, 220)
d = target - turret
angle = atn2(d.y, d.x)
PRINT "in radians:", fixed_str(angle, 3)
PRINT "in degrees:", fixed_str(angle * 180 / pi, 1)
in radians: 0.785
in degrees: 45.0

Taking one point from another gives the step between them, a pair you read with .x and .y. The angle comes back in radians, where a whole turn is 2 * pi. Angles start pointing right and grow clockwise, because y grows downwards on the screen. Sprites turn in degrees, so * 180 / pi converts.

Here’s a turret that follows the mouse, and fires where it’s pointing when you click:

SCREEN 320, 240
MEMORYBUFFER "turret", 32, 32
    CIRCLE (16, 16), 10, 8, 8
    LINE (16, 13) -> (31, 18), 7 !! box fill
END MEMORYBUFFER
MEMORYBUFFER "shot", 6, 6
    CIRCLE (3, 3), 2, 14, 14
END MEMORYBUFFER

// Just off each edge of the screen: a shot that reaches one is done.
BOUNDARY ADD "top", (-40, -40) -> (360, -20)
BOUNDARY ADD "bottom", (-40, 260) -> (360, 280)
BOUNDARY ADD "left", (-40, -40) -> (-20, 280)
BOUNDARY ADD "right", (340, -40) -> (360, 280)

BEHAVIOR "fly"
    VELOCITY PARAM("vx", 0), PARAM("vy", 0)
    ONCOLLISION
        IF other < 0 THEN DISABLE SPRITE me
    END ONCOLLISION
END BEHAVIOR

turret = reserve_sprite()
SET SPRITE turret !! image("turret") x(160) y(120) origin((0.5, 0.5))
SHOW SPRITE turret

angle = 0
DO
    IF COOLDOWN(1)
        angle = atn2(mousey - 120, mousex - 160)
        SET SPRITE turret !! rotate(angle * 180 / pi)
    END IF
    IF COOLDOWN(10) AND mousedown
        shot = reserve_sprite()
        IF shot >= 0
            SET SPRITE shot !! image("shot") origin((0.5, 0.5))
                !! x(160 + cos(angle) * 16) y(120 + sin(angle) * 16)
                !! behavior("fly") behavior_params({vx: cos(angle) * 4, vy: sin(angle) * 4})
            SHOW SPRITE shot
        END IF
    END IF
LOOP

The barrel is drawn pointing right, which is angle 0, so turning the sprite by the angle points it at the mouse. Going the other way, cos(angle) and sin(angle) turn an angle back into a step: how far across and how far down one pixel in that direction is. Times 4, that’s a shot’s speed, and times 16, the end of the barrel.

How far is it?

mag is how long a step is, so mag(b - a) is how far it is from a to b. It’s how a guard notices you, or how a magnet decides a coin is close enough to pull.

SCREEN 320, 240
player = (100, 100)
coins = [(130, 140), (300, 20), (90, 60)]
FOR coin = coins
    away = mag(coin - player)
    IF away < 50
        PRINT coin, "is", round(away), "away: pull it in"
    ELSE
        PRINT coin, "is", round(away), "away: leave it"
    END IF
NEXT
(130, 140) is 50 away: leave it
(300, 20) is 215 away: leave it
(90, 60) is 41 away: pull it in

distance(a, b) says the same thing in words, if you’d rather.

Smooth moves

lerp(a, b, t) is the point a share t of the way from a to b: 0 is a, 1 is b and 0.5 is halfway. Every tick, move something a small share of the way to where it’s going, and it moves fast when it’s far away and slows down as it gets there. That’s easing.

clamp(value, low, high) keeps a value between two others, so things stay where they’re allowed to be.

Here’s a camera that eases after the hero on a map bigger than the screen. Click the screen and use the arrow keys:

SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
LOADIMG "hero.png", "crashtan"

hero = reserve_sprite()
x = 288
y = 256
SET SPRITE hero !! image("crashtan") x(x) y(y) layer(2)
SHOW SPRITE hero

camera_x = 160
camera_y = 168
DO
    IF COOLDOWN(1)
        IF keydown("Left") THEN x = x - 3
        IF keydown("Right") THEN x = x + 3
        IF keydown("Up") THEN y = y - 3
        IF keydown("Down") THEN y = y + 3
        // The map is 640 x 736 pixels and the hero is 40 x 40.
        x = clamp(x, 0, 600)
        y = clamp(y, 0, 696)
        SET SPRITE hero !! x(x) y(y)

        // Where the camera would like to be: the hero in the middle,
        // but never past the edges of the map.
        camera_x = lerp(camera_x, clamp(x + 20 - 160, 0, 640 - 320), 0.08)
        camera_y = lerp(camera_y, clamp(y + 20 - 120, 0, 736 - 240), 0.08)
        FOR layer = 1 ... 2
            SET LAYER layer
            SET SCROLL camera_x, camera_y
        NEXT
    END IF
LOOP

The map’s two layers are on screen layers 1 and 2, so both scroll together. The hero is a sprite on layer 2, and scrolling that layer takes it along. Try 0.3 in place of 0.08 for a camera that keeps up better, or 0.02 for a lazy one. lerp works on pairs too: lerp((0, 0), (100, 50), 0.5) is (50, 25).

Whole numbers

Positions and speeds are rarely whole, but tiles, lives and scores are. There are a few ways to lose the fraction:

3.7-3.7
intdown3-4
floordown3-4
ceilup4-3
fixtowards zero3-3
roundto the nearest4-4
SCREEN 320, 240
x = 75.5
y = 140.2
PRINT "standing on tile", int(x / 32), int(y / 32)
hp = 7
PRINT "hearts to draw:", ceil(hp / 2)
PRINT "score:", round(1234.56)
PRINT int(3.7), floor(3.7), ceil(3.7), fix(3.7), round(3.7)
PRINT int(-3.7), floor(-3.7), ceil(-3.7), fix(-3.7), round(-3.7)
standing on tile 2 4
hearts to draw: 4
score: 1235
3 3 4 3 4
-4 -4 -3 -3 -4

Random, but the same every time

rnd is a random number from 0 up to (not quite) 1. int(rnd * 6) + 1 rolls a die. My random numbers come from a seed, and RANDOMIZE sets it: with no seed it uses the clock, so every game is different.

With a seed, it’s the same numbers every time. That’s how a game makes a huge level from one number, and makes it again just the same:

SCREEN 320, 240
FUNCTION rocks(level)
    RANDOMIZE level
    placed = []
    FOR n = 1 ... 3
        PUSH placed, (int(rnd * 320), int(rnd * 240))
    NEXT
    RETURN placed
END FUNCTION

PRINT "level 7:", rocks(7)
PRINT "level 8:", rocks(8)
PRINT "level 7:", rocks(7)
RANDOMIZE
level 7: [(9, 73), (45, 130), (87, 228)]
level 8: [(113, 183), (313, 108), (193, 47)]
level 7: [(9, 73), (45, 130), (87, 228)]

Level 7 has its rocks in the same places both times. The RANDOMIZE at the end goes back to the clock, so what comes after isn’t the same every game.

Lots of yes-or-no in one number

A number is made of bits, each one a yes or a no. That makes one number a tidy place to keep lots of flags: which keys you’ve found, which levels are done, which doors are open. Number the flags from 0.

bitset(n, b) turns flag b on, bittest(n, b) asks whether it’s on, and bitclear(n, b) turns it off. shl(1, b) is the number with only flag b on, and bitor and bitand put flags together and pick them out. shl and shr slide every bit up or down a number of places.

SCREEN 320, 240
RED_KEY = 0
BLUE_KEY = 1
GOLD_KEY = 2

keys = 0
keys = bitset(keys, RED_KEY)
keys = bitset(keys, GOLD_KEY)
PRINT "the keys, as one number:", keys
PRINT "got the blue key?", bittest(keys, BLUE_KEY)
PRINT "got the gold key?", bittest(keys, GOLD_KEY)

// The big door needs red and gold.
door = bitor(shl(1, RED_KEY), shl(1, GOLD_KEY))
PRINT "the big door opens?", bitand(keys, door) = door
PRINT "every flag moved down one:", shr(keys, 1)
the keys, as one number: 5
got the blue key? 0
got the gold key? 1
the big door opens? 1
every flag moved down one: 2

The keys are 5: flag 0 is worth 1 and flag 2 is worth 4. Moved down one place, the red key falls off the end and the gold key’s flag becomes flag 1, worth 2. Five keys, or fifty levels, still save as one number with SAVETEXT.

Crash-tan’s Tip: bitand and AND are different things. AND asks whether both sides are true: 12 AND 10 is 1. bitand keeps the bits both sides have: bitand(12, 10) is 8.

Words and data

Games are full of words: names on a high-score table, lines of dialog, what someone typed into a chat. And they’re full of data: levels, item lists, scores. This lesson is the toolbox for both.

Cutting and shaping text

len is how many characters a string has. left, right and mid cut a piece out of one, counting from 1. ucase and lcase change its case, and trim takes the spaces off both ends (ltrim and rtrim take them off one).

SCREEN 320, 240
typed = "   crash-tan   "
name = trim(typed)
PRINT "[" + name + "] has", len(name), "letters"
PRINT ucase(left(name, 1)) + mid(name, 2)
PRINT "initials:", ucase(left(name, 1) + mid(name, 7, 1))
PRINT "ends with", right(name, 3)
[crash-tan] has 9 letters
Crash-tan
initials: CT
ends with tan

str turns a number into text and val turns text back into a number. string(n, c) repeats a character and space(n) is that many spaces, which lines up a table of scores nicely:

SCREEN 320, 240
names = ["Ada", "Bob", "Crash-tan"]
points = [1200, 900, 15000]
FOR i = 0 ... 2
    dots = string(16 - len(names(i)) - len(str(points(i))), ".")
    PRINT names(i) + " " + dots + " " + str(points(i))
NEXT
PRINT "next level at", val("2000") + points(0)
Ada ......... 1200
Bob .......... 900
Crash-tan .. 15000
next level at 3200

Finding and changing

instr finds one string in another and says where, or 0 when it isn’t there. replace swaps every copy of one string for another. split cuts a string into a list at a separator, and join glues a list back together.

Here they are reading what a player typed into a chat box:

SCREEN 320, 240
FUNCTION chat(line)
    IF instr(line, "/") = 1
        words = split(mid(line, 2), " ")
        PRINT "command:", words(0), "with", len(words) - 1, "more:", join(words, "+")
    ELSE
        PRINT "says:", replace(line, "heck", "h*ck")
    END IF
END FUNCTION

chat("/give sword 3")
chat("what the heck is that")
command: give with 2 more: give+sword+3
says: what the h*ck is that

Letters as numbers

Every character has a number. asc gives a character’s number and chr the character for a number, so going from one letter to the next is adding one. That’s how an arcade machine lets you pick your initials with the joystick:

SCREEN 320, 240
letter = "A"
FOR press = 1 ... 3
    letter = chr(asc(letter) + 1)
NEXT
PRINT "three presses up from A is", letter, "which is number", asc(letter)
PRINT "255 in hex is", hex(255)
three presses up from A is D which is number 68
255 in hex is FF

hex writes a number in hexadecimal, which is handy for showing colors the way 0xFF8000 writes them.

Is it the right shape?

match checks a string against a pattern where * is anything and ? is any one character: good for file names. regex takes a full regular expression, for checking what someone typed. This one checks a name for the high-score table: a letter, then two to eleven more letters, digits or dashes.

SCREEN 320, 240
FOR name = ["Ada", "x", "Crash-tan", "99 Bob"]
    ok = regex(name, "^[A-Za-z][A-Za-z0-9-]{2,11}$")
    PRINT name, "fits the table?", ok
NEXT
PRINT match("level3.map.json", "level?.map.json"), match("hero.png", "*.wav")
Ada fits the table? 1
x fits the table? 0
Crash-tan fits the table? 1
99 Bob fits the table? 0
1 0

What day is it?

date is today, as month/day/year, and time is the time now, as hours:minutes:seconds, on the player’s clock.

SCREEN 320, 240
PRINT "Today is", date, "and it's", time
// The same seed all day, a new one tomorrow.
RANDOMIZE val(replace(date, "/", ""))
PRINT "today's lucky number:", int(rnd * 100)

That’s a daily challenge: everyone who plays today gets the same lucky number, and tomorrow it changes. It’s the seed trick from Handy maths for games.

Data from a file

loadjson reads a JSON file into maps and lists. The level in your game’s folder is one:

SCREEN 320, 240
level = loadjson("level.map.json")
PRINT "tiles", level.tilewidth, "by", level.tileheight
PRINT "map", level.mapwidth, "by", level.mapheight, "tiles"
PRINT "first row:", len(level.layers(0).data(0)), "tiles"
tiles 32 by 32
map 20 by 23 tiles
first row: 20 tiles

The file says mapWidth. I don’t mind how a field’s name is capitalized, so level.mapwidth finds it.

Asking data questions

When data gets deep, jmes asks it questions in JMESPath, a little language for JSON. [?points > `1000`] keeps the items that pass a test, .name picks one field out of each, and max_by and sort_by find the best and put them in order.

SCREEN 320, 240
scores = [{name: "Ada", points: 1200}, {name: "Bob", points: 900}, {name: "Cy", points: 1500}]
PRINT "over 1000:", jmes(scores, "[?points > `1000`].name")
PRINT "the best:", jmes(scores, "max_by(@, &points).name")
PRINT "low to high:", jmes(scores, "sort_by(@, &points)[*].name")

level = loadjson("level.map.json")
PRINT "layers:", jmes(level, "layers[*].name")
over 1000: [Ada, Cy]
the best: Cy
low to high: [Bob, Ada, Cy]
layers: [Layer 1, Layer 2, Metadata]

A high-score table

For lots of records that you’d like to sort and search, a database does the work. My databases speak SQL. An SQL block names a statement: USE lines say what it’s given and of what kind, and in the SQL, @name is where each goes. open_database opens the file (!! writable to change it as well as read it), and call_sql runs a statement, with what it’s given after !!.

SCREEN 320, 240
SQL "make"
    CREATE TABLE IF NOT EXISTS scores (name TEXT, points INTEGER)
END SQL
SQL "add"
    USE STRING name
    USE NUMBER points
    INSERT INTO scores (name, points) VALUES (@name, @points)
END SQL
SQL "top"
    USE NUMBER how_many DEFAULT 3
    SELECT name, points FROM scores ORDER BY points DESC LIMIT @how_many
END SQL

db = open_database("scores.db") !! writable
call_sql(db, "make")
call_sql db, "add" !! name("Ada") points(1200)
call_sql db, "add" !! name("Bob") points(900)
call_sql db, "add" !! name("Cy") points(1500)
call_sql db, "add" !! name("Dee") points(300)

PRINT "TOP THREE"
FOR row = call_sql(db, "top")
    PRINT row.name, row.points
NEXT
best = call_sql(db, "top") !! how_many(1)
PRINT "champion:", best(0).name
CLOSE db
TOP THREE
Cy 1500
Ada 1200
Bob 900
champion: Cy

A statement that reads gives back its rows, a map each. how_many has a DEFAULT, so it can be left out. The table is kept in the file scores.db, so the next time the game opens it, the scores are still there, and new ones join them.

Crash-tan’s Tip: Always give values with USE and @name rather than gluing them into the SQL yourself. A player called Bob'); DROP TABLE scores; -- is then just a player with a strange name.

Playing together

Games are better with friends. CrashNet is my multiplayer: a server runs the game, and everyone’s CrashPlayer connects to it. You still write one program. I run copies of it in different places, and each copy knows which one it is.

One program, four places

In a CrashNet game, your program runs as:

  • the host, on the server. There’s one, it has no screen, and it’s in charge of the shared world: the level, the enemies, the round.
  • a player’s server, also on the server, one for each player. It’s in charge of that player: their sprites, their score.
  • a client, in each player’s own CrashPlayer. It draws, plays the sound, and hears the keys.

The fourth place is on its own, a single-player game, like every game in this book so far. gamecontext says which of the four a copy is, as "HOST", "PLAYER", "CLIENT" or "SINGLE", and me is the player it runs for.

SCREEN 320, 240
PRINT "I'm running as", gamecontext
PRINT "for", me.name, "in slot", me.slot
PRINT "host?", me.ishost, " server?", me.isserver, " client?", me.isclient
PRINT "online?", crashnet.connected
I'm running as SINGLE
for Player in slot 1
host? 0  server? 0  client? 1
online? 0

On my own, I’m a one-player client called Player in slot 1, so a game written for CrashNet runs alone with no changes.

Saying what’s shared

A crashnet.json file in the game’s folder, beside main.crash, sets up CrashNet. Everyone reads it before the game starts.

{
  "maxPlayers": 4,
  "tickRate": 20,
  "playerData": { "score": "INTEGER", "team": "STRING" },
  "hostData": { "round": "INTEGER" }
}

playerData is what every player has, and hostData is what the game as a whole has. A field is INTEGER, FLOAT, STRING or BOOLEAN. tickRate is how many times a second the changes go out to everyone.

Everyone’s records

me is this player’s record, players(n) is the player in slot n, and players.count is how many there are. host is the game’s record. Change a field and it goes out to everyone.

SCREEN 320, 240
me.score = 10
me.score = me.score + 5
host.round = 1
PRINT me.name, "has", me.score, "in round", host.round
PRINT players.count, "playing; slot 1 is", players(1).name
Player has 15 in round 1
1 playing; slot 1 is Player

Not everyone may change everything. A player’s server changes its own player’s fields, and the host may change anyone’s, and its own. A client only reads them. Playing alone, I let you change any field you like.

Who’s here?

CALL PLAYER calls a function whenever a player joins or leaves, with that player’s record: their slot, their name, and joined, which is 1 when they’ve come and 0 when they’ve gone. CALL HOST calls a function when this copy becomes the host, at the start or when the host leaves and someone takes over. STOP PLAYER and STOP HOST stop them by name.

A guard says which copies a function is for: here, only the host and a single-player game hear about players.

SCREEN 320, 240
FUNCTION roster(player)
    IF player.joined
        PRINT player.name, "joined in slot", player.slot
    ELSE
        PRINT player.name, "left"
    END IF
END FUNCTION
FUNCTION promoted()
    PRINT "I'm the host now"
END FUNCTION

CALL PLAYER roster, "roster" !! guard(["host", "single"])
CALL HOST promoted, "promoted"
PRINT "waiting in the lobby with", players.count, "player"
// ... and when the game starts:
STOP PLAYER "roster"
STOP HOST "promoted"
waiting in the lobby with 1 player

Playing alone, nobody comes or goes, so neither function is ever called.

Asking the server

A client can’t change the world. It asks its player’s server, with an EVENT and !! target("server"). The server has a CALL EVENT for it, does what was asked, and its changes go out to everyone. target("host") asks the host instead.

SCREEN 320, 240
ATOMIC jumps = 0
ATOMIC climbed = 0
FUNCTION jump(height)
    climbed = climbed + height
    jumps = jumps + 1
END FUNCTION
CALL EVENT jump, "jump" !! guard(["player", "single"])

EVENT "jump" !! target("server") args([20])
EVENT "jump" !! target("server") args([35])
DO WHILE jumps < 2
    IF COOLDOWN(1) THEN SYNC
LOOP
PRINT jumps, "jumps,", climbed, "high in all"
2 jumps, 55 high in all

Playing alone, every target is here, so the event lands in this same copy.

Two events can be handled at the same time, so the second jump can start before the first is done. That’s why jump only adds to ATOMIC variables and the program prints once both are in: two PRINTs in jump could come out in either order.

SYNC marks the end of a tick’s worth of changes, so they go out to everyone together.

Running code over there

WITH CLIENT runs one of your functions on the clients: from the host, on every one, and from a player’s server, on that player’s own. WITH PLAYER runs one on a player’s server. !! args(...) is what it’s called with, and !! sync waits for it to finish there.

SCREEN 320, 240
FUNCTION banner(text)
    LOCATE 2, 2
    PRINT text
END FUNCTION
FUNCTION award(points)
    me.score = points
END FUNCTION

WITH PLAYER 1, award !! args(50) sync
WITH CLIENT banner !! args("Player 1 wins with " + str(me.score) + "!")
PAUSE

Think of that as the host’s code. It waits for player 1’s server to give out the points, then has every client show who won. That’s how the host gets something drawn: a server has no screen, so a PRINT or a FRAME on the server is an error. It goes the other way for files: the client can’t save, so it asks a server to. Playing alone, both run right here.

Crash-tan’s Tip: The host can’t sync with WITH CLIENT, because it would be waiting on every client at once, and a player’s server can’t sync with WITH PLAYER for its own slot, because it would be waiting on itself. Both are errors. Leave sync off and they go without waiting.

Rooms

A big game is split into rooms, and a player only sees the room they’re in. What’s made inside a WITH ROOM block belongs to that room, and SET ROOM moves this player into one (SET ROOM "" leaves them all). Anything made outside every room is seen everywhere: the score, say.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
WITH ROOM "cave"
    bat = reserve_sprite()
    SET SPRITE bat !! image("ball") x(40) y(40) scale((0.25, 0.25))
    SHOW SPRITE bat
END WITH ROOM
SET ROOM "cave"
PRINT me.name, "is in the", me.room
Player is in the cave

The whole thing

Here’s a game for several players: everyone gets a ball, and the arrow keys move yours. It runs alone too, as one player. Click the screen and steer.

SCREEN 320, 240
LOADIMG "ball.png", "ball"
ATOMIC mine = -1

// A player's server makes that player's ball, and shares it with everyone.
FUNCTION make_ball()
    mine = reserve_sprite()
    SET SPRITE mine !! image("ball") x(40 * me.slot) y(100)
        !! scale((0.25, 0.25)) sync
    SHOW SPRITE mine
END FUNCTION

// ...and moves it when its client asks.
FUNCTION move(dx, dy)
    IF mine < 0 THEN RETURN
    SET SPRITE mine !! x(clamp(spritex(mine) + dx, 0, 288))
        !! y(clamp(spritey(mine) + dy, 0, 208))
END FUNCTION
CALL EVENT move, "move" !! guard(["player", "single"])

IF gamecontext = "PLAYER" OR gamecontext = "SINGLE" THEN make_ball()

DO
    IF COOLDOWN(1)
        IF gamecontext = "CLIENT" OR gamecontext = "SINGLE"
            dx = 0
            dy = 0
            IF keydown("Left") THEN dx = -3
            IF keydown("Right") THEN dx = 3
            IF keydown("Up") THEN dy = -3
            IF keydown("Down") THEN dy = 3
            IF dx <> 0 OR dy <> 0 THEN EVENT "move" !! target("server") args([dx, dy])
        END IF
        SYNC
    END IF
LOOP

Every copy loads the image: the servers need to know how big it is, even though they never draw it. A sprite a server makes is shared with the clients when it’s set with !! sync. A sprite a client makes is its own, and nobody else sees it, which is just right for a menu or a sparkle.

Trying it out

CrashNet games run on a crashnet-server. To try one on your own computer, start a server in simulation mode, which lets anyone in, then point a CrashPlayer at it for each player:

crashnet-server -s my_game
crashplayer --remote ws://localhost:9005 --name "Ada"
crashplayer --remote ws://localhost:9005 --name "Bob"

Crash-tan’s Tip: Send the server what a player did (“jump”, “fire”, “buy the sword”) instead of every key they press. It’s far less to send, and the server stays the one that decides what happened.

The internet

A game can talk to a web server: fetch today’s news for the title screen, send a score to a leaderboard, download a level someone made. It all goes through one function, fetch.

Crash-tan’s Tip: fetch works in my players for PCs, Macs and phones, and not in a web browser, this book included. The last section says why.

Asking a server

fetch(url, method) sends a request and waits for the answer. It gives back a map with two things in it: statuscode, the server’s answer as a number, and message, what it sent back, as text. 200 means “here you are”, and 404 means “there’s nothing here”.

SCREEN 320, 240
answer = fetch("https://example.com/", "GET")
IF answer.statuscode = 200
    PRINT "the page is", len(answer.message), "characters long"
    PRINT "it starts:", left(answer.message, 15)
ELSE IF answer.statuscode = 0
    PRINT "no answer:", answer.message
ELSE
    PRINT "the server said", answer.statuscode
END IF

What that prints depends on whether you’re online right now. Every request can fail, so every game that uses fetch checks what came back.

When nobody answers

A request that never reaches a server, because you’re offline, or the address is wrong, or the server is down, doesn’t stop your game. statuscode is 0, and message says why.

SCREEN 320, 240
answer = fetch("not a web address", "GET")
PRINT "statuscode:", answer.statuscode
PRINT "was there a reason?", len(answer.message) > 0
statuscode: 0
was there a reason? 1

I give a request 30 seconds. One that hasn’t been answered by then is a 0 too.

Crash-tan’s Tip: fetch waits for its answer, and while it waits your loop doesn’t go round. Fetch what you need before the game starts, or on a thread of its own with CALL THREAD, the way the next example does.

Receiving JSON

Most web servers that games talk to answer in JSON, and deserialize turns that into maps and lists, the same as it does for a save file. Here a thread fetches some JSON while the title screen keeps going:

SCREEN 320, 240
ATOMIC news = "loading the news..."

FUNCTION get_news()
    answer = fetch("https://httpbin.org/json", "GET")
    IF answer.statuscode = 200
        data = deserialize(answer.message)
        news = data.slideshow.title
    ELSE
        news = "no news today"
    END IF
END FUNCTION
CALL THREAD get_news, "news"

DO
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 8, 12
            PRINT "SUPER CRASH WORLD"
            LOCATE 12, 4
            PRINT news
        END FRAME
    END IF
LOOP

httpbin.org is a site for trying out web requests, and /json always sends the same small piece of JSON. If it can’t be reached, the title screen says so and carries on.

Sending JSON

To send something, use "POST" and give fetch a body. A map or a list is sent as JSON, labeled as JSON, so a server knows what it’s getting. A fourth value adds headers, which is how a server that wants a key gets one.

SCREEN 320, 240
score = {name: "Ada", points: 1200}
answer = fetch("https://httpbin.org/post", "POST", score, {"X-Game": "Super Crash World"})
IF answer.statuscode = 200
    echo = deserialize(answer.message)
    PRINT "the server got:", echo.json
ELSE
    PRINT "the score didn't get there; try again later"
    SAVETEXT "unsent.json", serialize(score)
END IF

httpbin.org’s /post sends back what it was given, so you can see the score arrive. A real leaderboard would answer with your place on it. When it can’t be reached, the score is saved, ready to send next time.

Other methods work the same way: "PUT", "PATCH" and "DELETE", for servers that use them.

In the browser

fetch works in my players for PCs, Macs and phones. It doesn’t work in a web browser. To run there, my web player needs the page it’s on served with headers that keep it apart from other sites, and those same headers stop fetch from reaching a server. So in a browser, every fetch answers with a statuscode of 0, the same as a server that isn’t there.

That includes this book, which runs in your browser: Try the examples on this page and you’ll see the 0 path every time. That’s why every example here deals with a 0. A game that does the same keeps working in the browser, just without the internet.

Video

An intro before the title screen, a cutscene between levels, a looping background behind the menu: sometimes the thing to show is a video. I play MP4 and WebM files, sound and all.

Crash-tan’s Tip: The games in this book have a short video in their folder, clip.mp4: three seconds of me under the stars, with no sound. Put a video of your own in your game’s folder to hear one play.

Loading and playing

LOADVIDEO loads a video from the game’s folder and gives it a name, the way LOADIMG does for an image. PLAYVIDEO plays it on a layer, and your program carries on while it plays. !! loop starts it again each time it ends, and !! mute plays it without its sound.

SCREEN 320, 240
LOADVIDEO "clip.mp4", "menu"
PLAYVIDEO "menu", 1 !! loop mute

That’s a background for a menu: round and round, quietly, while the menu is drawn and the keys are read.

A cutscene

WAITVIDEO waits until a video has finished, so the program stops there and starts again when the video’s over. That’s a cutscene: play it, wait, then the next level.

When a video ends, its last picture stays, so the end of a cutscene doesn’t flash to an empty screen. This one plays on layer 0, the layer PRINT writes on, so “Level 1” goes on top of that last picture. On a higher layer, the picture would cover it.

SCREEN 320, 240
LOADVIDEO "clip.mp4", "intro"
PLAYVIDEO "intro", 0
WAITVIDEO "intro"
PRINT "Level 1"

The controls

PAUSEVIDEO pauses a video and RESUMEVIDEO carries on from where it was. SEEKVIDEO jumps to a time, in seconds, so SEEKVIDEO "intro", 0 goes back to the start. STOPVIDEO stops it for good, and a WAITVIDEO that was waiting for it stops waiting.

Players like to skip a cutscene they’ve seen. While the program waits on WAITVIDEO, a thread of its own watches the keys: Space skips, P pauses and R starts again.

SCREEN 320, 240
LOADVIDEO "clip.mp4", "intro"

FUNCTION controls()
    paused = FALSE
    DO
        IF COOLDOWN(1)
            IF keydown("Space") THEN STOPVIDEO "intro"
            IF keydown("R") THEN SEEKVIDEO "intro", 0
        END IF
        IF COOLDOWN(15) AND keydown("P")
            IF paused THEN RESUMEVIDEO "intro" ELSE PAUSEVIDEO "intro"
            paused = NOT paused
        END IF
    LOOP
END FUNCTION

PLAYVIDEO "intro", 0
CALL THREAD controls, "controls"
WAITVIDEO "intro"
STOP THREAD "controls"
PRINT "Level 1"

The COOLDOWN(15) on P means holding it down doesn’t flick between paused and playing sixty times a second.

When the video isn’t there

A game can check for its video before playing it, and go straight on without one. There’s no intro.mp4 in the book’s games, so this one skips to the level.

SCREEN 320, 240
IF fileexists("intro.mp4")
    LOADVIDEO "intro.mp4", "intro"
    PLAYVIDEO "intro", 0
    WAITVIDEO "intro"
END IF
PRINT "Level 1"
Level 1

Videos aren’t layers

Every platform plays video its own way, to use the hardware that’s made for it: in the browser it’s the browser’s own video player. So a video doesn’t always take on what you’ve done to its layer. Don’t count on a layer’s shader, transparency or clipping to change how a video looks.

In-app purchases

Some games are free to play and sell extras: a bag of coins, a new costume, no more ads. I talk to the store for you, whichever store it is, so your game asks the same way everywhere.

The store

The things for sale are listed in a file called iap.json, in the game’s folder. Each has an id your game buys it by, a name, a price, and whether it’s consumable, bought again and again like coins, or non-consumable, bought once and kept, like “no more ads”.

{
  "version": 1,
  "products": [
    { "id": "coins_100", "type": "consumable", "name": "100 Coins",
      "price": { "amount": 99, "currency": "usd" },
      "apple_product_id": "com.example.coins100",
      "google_product_id": "coins_100" },
    { "id": "no_ads", "type": "non_consumable", "name": "No More Ads",
      "price": { "amount": 299, "currency": "usd" },
      "apple_product_id": "com.example.noads",
      "google_product_id": "no_ads" }
  ],
  "providers": { "ios": "apple", "android": "google",
                 "web": "iap_server", "desktop": "iap_server" },
  "iap_server": { "endpoint": "https://example.com/iap" }
}

providers says which store each kind of machine uses:

  • On a phone or a tablet, it’s the App Store on iPhone and iPad, and Google Play on Android. A product is only on sale there if it has the id you gave it in that store, and the store shows its own window to pay in.
  • In a web browser, and on a PC, it’s an IAP server: a web server of your own (or one you use) that knows how to sell things. The price in iap.json is the one it shows, and paying happens in the browser.

Is it open?

purchase.available is 1 when the game is talking to a store, and 0 when there’s no store or it can’t be reached. purchases is the list of what’s for sale, each with its id, name, description, price and whether it’s purchased.

SCREEN 320, 240
IF purchase.available
    FOR item = purchases
        PRINT item.name, item.price, item.purchased
    NEXT
ELSE
    PRINT "no store here"
END IF
PRINT "the store has", len(purchases), "things"
no store here
the store has 0 things

Here in the book there’s no store at all, so it’s closed, with nothing for sale. A game should always be fine with that: just don’t show the shop.

Buying

PURCHASE NEW and the id of what to buy opens the store’s window, and your game carries on. When it’s over, I fire the "purchase" event, with the id, whether it worked (1 or 0), and why not when it didn’t. Have a CALL EVENT ready for it before anyone can buy.

SCREEN 320, 240
ATOMIC coins = 0
ATOMIC news = "visiting the shop..."

FUNCTION bought(product, ok, why)
    IF ok
        IF product = "coins_100" THEN coins = coins + 100
        news = "thank you!"
    ELSE
        news = "no sale: " + why
    END IF
END FUNCTION
CALL EVENT bought, "purchase"

PURCHASE NEW "coins_100"
// A spinner while the store has its turn.
turn = 0
DO WHILE news = "visiting the shop..."
    IF COOLDOWN(1)
        MEMORYBUFFER "spinner"
            CIRCLE (160 + cos(turn) * 20, 120 + sin(turn) * 20), 5, 14, 14
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "spinner"
        END FRAME
        turn = turn + 0.1
    END IF
LOOP
PRINT news
PRINT "coins:", coins
no sale: declined
coins: 0

With no store, I can’t sell anything, so the event says no, and why. On a phone, the player would see the store’s window, and the event would come when they’d paid, or changed their mind.

Keeping count

purchased only means something for a non-consumable: it’s 1 once it’s been bought, and the store remembers. A consumable is always 0, because it can be bought again, so the store doesn’t keep count of your coins. Your game does: add them up when the event says they were bought, and save them, the way Saving your game does.

FUNCTION bought(product, ok, why)
    IF ok AND product = "coins_100"
        coins = coins + 100
        SAVETEXT "coins.txt", str(coins)
    END IF
END FUNCTION

Bought it before?

Someone who bought “no more ads” on their old phone wants it on their new one too. PURCHASE RESTORE asks the store for everything they’ve bought before, and the "purchase" event comes once for each one. Put it behind a “Restore purchases” button.

SCREEN 320, 240
ATOMIC ads = TRUE
FUNCTION bought(product, ok, why)
    IF ok AND product = "no_ads" THEN ads = FALSE
END FUNCTION
CALL EVENT bought, "purchase"
PURCHASE RESTORE
PRINT "asked the store for what you bought before"
asked the store for what you bought before

Only non-consumables come back: the coins were spent long ago.

Crash-tan’s Tip: Never decide a purchase worked yourself. Wait for the "purchase" event to say so: the window can be closed, the card can be turned down, and the player can be offline.