Hello again!
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 FALSEturns the palette off, and then a color is the number0xAARRGGBB:0xFFFF8000is 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
SCREENLOCKandSCREENUNLOCKto pair up. AFRAMEblock 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.DELAYstops 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:
ismobileis 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 SPRITEit’s already at (0, 0), soimagealone will do, andxandycan follow. One that was never enabled has no place yet: give itx,yandimagetogether. After that,SET SPRITEchanges 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, y | Move this much every tick. |
IMPULSE x, y | A push, once, that it keeps going at. |
ACCELERATION x, y | Speed up this much every tick. ACCELERATION 0, 0.4 is a fall. |
BOUNCE x, y | Bounce off what it runs into, keeping this share of its speed. |
ROTATE angle, ticks | Turn to an angle, or with !! loop, keep turning. |
ALPHA from, to, ticks | Fade. |
SCALE from_x, from_y, to_x, to_y, ticks | Grow or shrink. |
OSCILLATE x, y | Swing back and forth. |
ANIMATE image, frames | Step through a sprite sheet’s frames. |
ORBIT distance | Go 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’sPARAM("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:
VELOCITYsets the speed again every tick, so a sprite withVELOCITYcan’t be turned around by a bounce. For things that bounce, push them withIMPULSE.
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.
maptileasks 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
#1to 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;
ONCOLLISIONtells a star it’s been caught, or that it hit the ground;- the keys move the basket, and
COOLDOWNkeeps the loop’s time; CALL RENDERdraws the score every frame, under the sprites;BEEPandSOUNDsay 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
BEEPin theONCOLLISIONwhere 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
caughtwithSAVETEXTif it’s better than whatloadtextgives 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, orCOOLDOWN(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, 1gives 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:
CLSon 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 SCROLLitsangleevery 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 POLYGONtakessolid,tagandvelocitytoo, 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,STEPand friends, but none of your program’s variables: anything it needs from your game comes in through aUSEsetting.
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" !! fastonly 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
CLSand an effect from Shaders and transitions, and wait fortransitionto 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_leveland 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:
EMITPARTICLEis also a function,emitparticle, that gives back the particle’s sprite number, in case you want to do something more to it.
Blink and shake
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
ROTATEin"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 SHOW | Stop drawing it, or start again. It’s still there. |
STEP GHOST | Let things pass through it. |
STEP SOLID | Make 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
MUSICblock. Those are parts of a song. APATTERNon 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 SPRITEof their own, with nothing else after the!!.clear_pintakes 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. |
loop | Go back to the first point and round again. |
pingpong | Go 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:
GRAVITYonly ever pulls on the elevation. For a platform game, where down is down the screen, useACCELERATION 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
xinto that range: on a screen 320 across,x / 160 - 1is -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:
FILTERtakes some of the sound away."lowpass"keeps the low part and muffles the highs above itscutoff, in hertz;resonancemakes it ring at the cutoff.FILTERENVELOPEmoves the cutoff over each note, the wayENVELOPEmoves the volume, up as far as itspeak.VIBRATOmakes the pitch waver, andTREMOLOthe volume:rateis how many times a second,depthhow much.PANsits it to the left (-1) or the right (1), andVOLUMEsays 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
LOADWAVcan 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, 4is four quarter notes, and3, 4is a waltz.PATTERNs: a few bars each, with a name. A pattern’slengthis 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 itstimesig.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", 1silences a track, and!! muted(FALSE)brings it back.CREATE MUSIC PATTERNadds a new pattern,CREATE TRACKgives it tracks, andADD ARRANGEMENTputs 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_heldmakes 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:
loadjsonis 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.
mapdatagives 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)andmaptiley(y)are for a place on the screen, likemousexandmousey. They add on how far the map is scrolled.mapworldtilex(x)andmapworldtiley(y)are for a place in the level, like a sprite’sxandy. 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 variablesLOCAL.
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 program | its own thread, the one it starts on |
CALL RENDER | my render thread, once a frame |
Behaviors, ONCOLLISION | my tick, sixty times a second |
CALL TIMER, ASYNC !! after or every | my timer, when the time comes |
CALL EVENT | me, a moment after the EVENT |
CALL THREAD, ASYNC | a 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
ATOMICvariable 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), thenscore = 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", FALSEhides a button, andTRUEbrings 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.5makes the whole gamepad half see-through.GAMEPAD SETIMAGE "A", "jump"draws a button with an image of yours.GAMEPAD DPADsays 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 OFFhides it, for a cutscene or a menu you tap, andGAMEPAD RESETforgets everyGAMEPAD 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, touchy | Where a finger is touching, or -1 when none is. Only a finger: the mouse doesn’t count. |
mousex, mousey | Where the mouse is, or a finger. |
tap | 1 while a finger (or the mouse button) is held down and hasn’t moved. |
dragx, dragy | How 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:
astaris 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, andnearest(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 | ||
|---|---|---|---|
int | down | 3 | -4 |
floor | down | 3 | -4 |
ceil | up | 4 | -3 |
fix | towards zero | 3 | -3 |
round | to the nearest | 4 | -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:
bitandandANDare different things.ANDasks whether both sides are true:12 AND 10is 1.bitandkeeps 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
USEand@namerather than gluing them into the SQL yourself. A player calledBob'); 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
syncwithWITH CLIENT, because it would be waiting on every client at once, and a player’s server can’tsyncwithWITH PLAYERfor its own slot, because it would be waiting on itself. Both are errors. Leavesyncoff 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:
fetchworks 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:
fetchwaits 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 withCALL 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.jsonis 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.