Chapter 18 still taught the machinery the listing lost when it moved onto SOLID and the persistent drawing layer: the frame-boundary PACE routine, the flattened live list, the SSHAPE pool accounting, and the sixty lines of BALLBRICKS/TESTCELL that computed a minimum translation axis by hand. Step 8 now registers each brick with SOLID, arms COLLISION 2 and reads the contact back with RCOLLISION, including the point that costs an evening if it is missed: the BUMP mask is by sprite, and the balls are sprites 5, 6 and 7, so their bits are 16, 32 and 64. The FOR-with-equal-bounds rule that every one-item case in the chapter depends on is now stated where it is first used rather than referred to from a step that no longer exists. The listing loses six declarations orphaned by the conversion and the comment blocks that still described the captures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2139 lines
57 KiB
Markdown
2139 lines
57 KiB
Markdown
# 18. Tutorial: Breakout with artwork
|
|
|
|
This chapter builds Breakout a second time, out of loaded PNG artwork, with a drawn
|
|
brick field, five powerups, three voices of sound and a HUD in colour. It is a bigger
|
|
program than [Chapter 17](17-tutorial-breakout.md)'s and it uses a completely different set
|
|
of verbs: everything here is drawn or loaded, and nothing is written into the text grid.
|
|
|
|
This is what you are building:
|
|
|
|

|
|
|
|
The finished listing is
|
|
[`examples/breakout/sprites/breakout.bas`](../examples/breakout/sprites/breakout.bas).
|
|
|
|
Read Chapter 17 first if you have not. The rules it teaches — declare every name up front,
|
|
loop with `GOTO`, parenthesise mixed `+` and `-`, build a random-number generator — all
|
|
apply here too, and are not repeated.
|
|
|
|
One of them is worth repeating, because this chapter leans on it harder than Chapter 17
|
|
does. **`#` is an integer, `%` is a float and `$` is a string.** That is the opposite way
|
|
round from Commodore BASIC, and it is why ball positions and velocities here are `BLX%` and
|
|
`BLVX%` while counters and flags are `BLN#` and `BLON#`. Get one wrong and the ball moves in
|
|
whole pixels, or stops.
|
|
|
|
[Chapter 3](03-the-language.md) is the language reference for the rest — `MOD`, `INSTR`,
|
|
`MID`, `LEN` — and [Chapter 4](04-control-flow.md) is `BEGIN`/`BEND`, `DO ... LOOP UNTIL`
|
|
and `GOSUB`.
|
|
|
|
```sh norun
|
|
$ ./build-akgl/basic examples/breakout/sprites/breakout.bas
|
|
```
|
|
|
|
| Key | Does |
|
|
|---|---|
|
|
| left / right | move the paddle |
|
|
| space | start a game, launch the ball, release a stuck ball |
|
|
| P | pause |
|
|
| S | sound on and off |
|
|
| Q or escape | quit |
|
|
|
|
A broken brick drops a gem about one time in seven. Catch it with the paddle; the colour
|
|
tells you which it is.
|
|
|
|
| Gem | Name | Does | For |
|
|
|---|---|---|---|
|
|
| red | EXPAND | doubles the paddle's width | 20 seconds |
|
|
| yellow | MULTI | throws two more balls off the one in play | until they are lost |
|
|
| green | SLOW | drops the ball's speed to about two thirds | 16 seconds |
|
|
| blue | STICKY | the ball sticks where it lands; space fires it | 18 seconds |
|
|
| purple | CATCH | a second bar appears higher up the field | 24 seconds |
|
|
|
|
## What you will do
|
|
|
|
- **[Step 1](#step-1-put-artwork-on-the-screen)** — load PNG artwork into sprites
|
|
- **[Step 2](#step-2-budget-the-eight-sprite-slots)** — decide what each of the eight
|
|
sprites is, before writing anything else
|
|
- **[Step 3](#step-3-make-room-for-what-you-draw)** — take the text layer out of the way,
|
|
so what you draw can be seen and stays seen
|
|
- **[Step 4](#step-4-make-the-brick-stamps)** — make one brick per colour and stamp the
|
|
field out of them
|
|
- **[Step 5](#step-5-draw-at-most-one-thing-per-frame)** — draw at most one thing per
|
|
frame, chosen by dirty flags
|
|
- **[Step 6](#step-6-erase-one-brick-not-the-whole-field)** — erase a broken brick in
|
|
place, so the field is drawn once and not rebuilt
|
|
- **[Step 7](#step-7-draw-lettering-with-a-stroke-font)** — draw lettering with a stroke
|
|
font, because text has no colour
|
|
- **[Step 8](#step-8-move-and-bounce-the-ball)** — move the ball, bounce it off the bars
|
|
without a square root, and register the bricks as collision geometry so the interpreter
|
|
finds the hits
|
|
- **[Step 9](#step-9-gems-and-powerups)** — drop gems and apply what catching one does
|
|
- **[Step 10](#step-10-three-voices-and-a-mute)** — three voices of sound and a mute that
|
|
costs nothing
|
|
- **[Step 11](#step-11-the-states)** — the frame loop's states: title, serve, play, lost,
|
|
cleared
|
|
- **[Step 12](#step-12-put-the-program-together)** — put the pieces in one file, in the
|
|
right order
|
|
|
|
---
|
|
|
|
## Step 1: Put artwork on the screen
|
|
|
|
**Goal: a picture from a file, on the screen, at its own size.**
|
|
|
|
`SPRSAV` takes an image file path as well as an array of pattern bytes, and a sprite
|
|
loaded from a file **keeps the image's own size** rather than being squeezed into 24 by
|
|
21. That is the whole of it:
|
|
|
|
```basic requires=akgl setup=breakout_art screenshot=breakout-artwork
|
|
I# = 0
|
|
SPRSAV "art/paddleBlu.png", 3
|
|
SPRSAV "art/paddleRed.png", 4
|
|
SPRSAV "art/ballBlue.png", 5
|
|
SPRSAV "art/element_red_polygon_glossy.png", 6
|
|
SPRSAV "art/element_green_polygon_glossy.png", 7
|
|
SPRSAV "art/element_purple_polygon_glossy.png", 8
|
|
FOR I# = 3 TO 8
|
|
SPRITE I#, 1, 2
|
|
NEXT I#
|
|
MOVSPR 3, 20, 20
|
|
MOVSPR 4, 20, 60
|
|
MOVSPR 5, 160, 30
|
|
MOVSPR 6, 30, 120
|
|
MOVSPR 7, 130, 120
|
|
MOVSPR 8, 230, 120
|
|
```
|
|
|
|

|
|
|
|
That is the whole game's cast: two bars, a ball and five gems.
|
|
|
|
**Write down the sizes now**, because every collision test later in this chapter is built
|
|
out of them:
|
|
|
|
| Artwork | Is | Shows up later as |
|
|
|---|---|---|
|
|
| `paddleBlu.png`, `paddleRed.png` | 104 by 24 | `PDW# = 104`, and the bar's height in `HITBAR` |
|
|
| `ballBlue.png`, `ballGrey.png` | 22 by 22 | `+ 21` on every edge of the ball's box |
|
|
| the five gems | 48 by 46 | the `+ 48` and `+ 46` in the catch test |
|
|
|
|
Three more things to note.
|
|
|
|
**The path is tried against the working directory first, then against the directory the
|
|
program was loaded from.** A `.bas` stored beside its own `art/` folder therefore runs from
|
|
anywhere.
|
|
|
|
**`SPRITE n, 1, 2` turns sprite `n` on in colour 2.** A sprite's colour *multiplies* the
|
|
artwork rather than replacing it, so colour 2 — white — is the one that leaves loaded
|
|
artwork looking like itself. Any other colour tints it.
|
|
|
|
**Put the art where the licence lets you.** The artwork here is Kenney's
|
|
[Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1), released under
|
|
[CC0](http://creativecommons.org/publicdomain/zero/1.0/), and
|
|
`examples/breakout/sprites/art/PROVENANCE.md` records which file is used for what. CC0
|
|
does not require crediting Kenney. Do it anyway.
|
|
|
|
## Step 2: Budget the eight sprite slots
|
|
|
|
**Goal: know what all eight sprites are before you write the program.**
|
|
|
|
There are eight sprite slots and no more. That is not a limit you will design your way
|
|
around later, so spend them on paper first. This game spends them like this:
|
|
|
|
| Slot | Is |
|
|
|---|---|
|
|
| 1 | free |
|
|
| 2 | free |
|
|
| 3 | the paddle |
|
|
| 4 | the catcher bar (the purple gem) |
|
|
| 5, 6, 7 | up to three balls |
|
|
| 8 | the falling gem |
|
|
|
|
Six are spent and **two are left over**, which is more room than the program needs. Two
|
|
consequences fall straight out of the six that are spent:
|
|
|
|
- **The bricks are drawn rather than made of artwork.** Sixty bricks will not fit in six
|
|
slots, and a sprite is the only way to get an image file onto the screen — `GSHAPE`
|
|
cannot stamp a sprite and `SPRSAV` cannot read one back out.
|
|
- **Only one gem falls at a time.** There is one slot for it, so a brick broken while a gem
|
|
is already falling drops nothing.
|
|
|
|
Deciding this first is what stops a feature costing an afternoon before it is abandoned.
|
|
|
|
## Step 3: Make room for what you draw
|
|
|
|
**Goal: something you drew, still on the screen on the next frame.**
|
|
|
|
Two things have to be true before a drawing is any use, and only one of them is automatic.
|
|
|
|
**A drawing persists.** `DRAW`, `BOX`, `CIRCLE` and `PAINT` render into a layer the frame
|
|
composites underneath the text and the sprites, so a picture you draw once is there on
|
|
every frame after. You do not redraw it and you do not have to keep it anywhere.
|
|
|
|
**But the text layer covers it.** It repaints every row it owns, opaque, every frame — and
|
|
by default it owns the whole window. So the first executable line of this program is:
|
|
|
|
```basic norun
|
|
WINDOW 0, 35, 49, 36
|
|
```
|
|
|
|
Two rows at the bottom, which is enough for the final score, and the other thirty-five
|
|
belong to the drawing verbs. `WINDOW l, t, r, b` takes character cells, and
|
|
`RWINDOW(0)` and `RWINDOW(1)` report what you ended up with.
|
|
|
|
That is the whole of it. Draw your field once and it stays; draw your HUD once and it
|
|
stays.
|
|
|
|
**Two consequences shape the rest of this chapter, and both are things you no longer have
|
|
to do.**
|
|
|
|
There is no drawing deadline. The host runs a fixed number of source lines and then
|
|
presents the frame, and a drawing longer than one batch used to be a problem — it does not
|
|
matter here, because a drawing that spans two batches simply arrives over two frames and
|
|
the layer keeps both halves.
|
|
|
|
And there is nothing to redraw. If you change one brick, erase that brick; the other
|
|
fifty-nine are still on the screen. Which is why this chapter has no routine that walks a
|
|
list of everything still standing.
|
|
|
|
## Step 4: Make the brick stamps
|
|
|
|
**Goal: six coloured bricks, and a field stamped out of them.**
|
|
|
|
Draw one brick per row colour, capture the six of them, and then stamp them wherever a
|
|
brick belongs with `GSHAPE`. Drawing six things and stamping sixty is far cheaper than
|
|
drawing sixty.
|
|
|
|
```basic requires=akgl screenshot=breakout-stamps size=100x130
|
|
DIM BRC#(6)
|
|
I# = 0
|
|
R# = 0
|
|
K# = 0
|
|
T1# = 0
|
|
T2# = 0
|
|
FOR I# = 0 TO 5
|
|
READ BRC#(I#)
|
|
NEXT I#
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
FOR R# = 0 TO 5
|
|
COLOR 1, BRC#(R#)
|
|
T1# = R# * 20
|
|
T2# = T1# + 8
|
|
FOR K# = 0 TO 7
|
|
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
|
|
NEXT K#
|
|
NEXT R#
|
|
|
|
DATA 3, 9, 8, 6, 4, 5
|
|
```
|
|
|
|

|
|
|
|
Reading that from the top: `GRAPHIC 1, 1` selects the graphics mode and clears it.
|
|
`COLOR 1, BRC#(R#)` sets **colour source 1** to the palette entry that row wants — a
|
|
drawing verb names a source, not a colour, and there are seven sources. `DRAW 1, x1, y1 TO
|
|
x2, y2` draws a line using source 1.
|
|
|
|
Each brick is 68 by 16 and is filled with horizontal lines. The inner loop draws **two
|
|
scan lines per pass**, `T1# + K#` and `T2# + K#`, so eight passes fill sixteen rows. That
|
|
costs about a hundred and thirty lines for the whole set instead of the two hundred and
|
|
seventy a line-at-a-time loop would take.
|
|
|
|
Capturing the six is six `SSHAPE`s:
|
|
|
|
```basic norun
|
|
SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$
|
|
SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$
|
|
SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$
|
|
SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$
|
|
SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
|
|
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
|
|
```
|
|
|
|
**`SSHAPE` has sixteen slots and nothing gives one back**, and this program spends eight of
|
|
them: six brick colours and the two erasers in Step 6. Spent once, at startup, and never
|
|
again — nothing here captures per frame, so there is no pool to run dry and no rebuild:
|
|
|
|
```basic norun
|
|
LABEL DRAWPROTOS
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
```
|
|
|
|
with the six `DRAW` loops, Step 6's two erasers, and eight `SSHAPE`s after it. Called once,
|
|
from the setup block.
|
|
|
|
**`PAINT` would be one statement instead of sixteen, and is not usable here.** It costs
|
|
nearly four milliseconds a call; sixty of those is seven frames' worth of time.
|
|
|
|
Now the whole screen — walls, the brick field stamped out of the six, and the artwork on
|
|
top:
|
|
|
|
```basic requires=akgl setup=breakout_art screenshot=breakout-screen size=800x600
|
|
DIM BRC#(6)
|
|
I# = 0
|
|
R# = 0
|
|
C# = 0
|
|
K# = 0
|
|
T1# = 0
|
|
T2# = 0
|
|
Z$ = ""
|
|
FOR I# = 0 TO 5
|
|
READ BRC#(I#)
|
|
NEXT I#
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
FOR R# = 0 TO 5
|
|
COLOR 1, BRC#(R#)
|
|
T1# = R# * 20
|
|
T2# = T1# + 8
|
|
FOR K# = 0 TO 7
|
|
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
|
|
NEXT K#
|
|
NEXT R#
|
|
SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$
|
|
SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$
|
|
SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$
|
|
SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$
|
|
SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
|
|
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
|
|
GRAPHIC 1, 1
|
|
WIDTH 2
|
|
COLOR 5, 16 : COLOR 1, 4
|
|
BOX 5, 2, 62, 797, 597
|
|
BOX 1, 6, 66, 793, 593
|
|
FOR C# = 0 TO 9
|
|
Z$ = S0$ : GSHAPE Z$, 42 + C# * 72, 108
|
|
Z$ = S1$ : GSHAPE Z$, 42 + C# * 72, 132
|
|
Z$ = S2$ : GSHAPE Z$, 42 + C# * 72, 156
|
|
Z$ = S3$ : GSHAPE Z$, 42 + C# * 72, 180
|
|
Z$ = S4$ : GSHAPE Z$, 42 + C# * 72, 204
|
|
Z$ = S5$ : GSHAPE Z$, 42 + C# * 72, 228
|
|
NEXT C#
|
|
SSHAPE Z$, 0, 60, 800, 600
|
|
SPRSAV Z$, 2
|
|
SPRITE 2, 1, 2
|
|
MOVSPR 2, 0, 60
|
|
SPRSAV "art/paddleBlu.png", 3
|
|
SPRSAV "art/ballBlue.png", 5
|
|
SPRITE 3, 1, 2
|
|
SPRITE 5, 1, 2
|
|
MOVSPR 3, 348, 540
|
|
MOVSPR 5, 389, 517
|
|
|
|
DATA 3, 9, 8, 6, 4, 5
|
|
```
|
|
|
|

|
|
|
|
Everything above the `SSHAPE Z$, 0, 60, 800, 600` is a drawing nobody would ever see.
|
|
Those four lines are what make it the screen.
|
|
|
|
A brick is 68 by 16 on a 72 by 24 pitch, ten columns by six rows, with the field's top-left
|
|
corner at (42, 108). Those numbers are worth writing down once; they come back in Steps 7
|
|
and 9.
|
|
|
|
The finished game keeps its six stamps in six separate scalars — `S0$` to `S5$` — rather
|
|
than in an array. An array works too.
|
|
|
|
## Step 5: Draw at most one thing per frame
|
|
|
|
**Goal: a frame loop that paces, draws one thing, and then plays the game.**
|
|
|
|
```basic norun
|
|
LABEL FRAME
|
|
GOSUB DRAWJOB
|
|
GOSUB READKEYS
|
|
IF STATE# = 0 THEN GOSUB TITLETICK
|
|
IF STATE# = 1 THEN GOSUB SERVETICK
|
|
IF STATE# = 2 THEN GOSUB PLAYTICK
|
|
IF STATE# = 3 THEN GOSUB LOSTTICK
|
|
IF STATE# = 4 THEN GOSUB CLEARTICK
|
|
IF RUNNING# = 0 THEN GOTO SHUTDOWN
|
|
GOTO FRAME
|
|
```
|
|
|
|
At most one drawing job a frame, then the game. Nothing here has a deadline any more — a
|
|
drawing that spans two batches arrives over two frames — but a queue of one still keeps the
|
|
work even, and it keeps the *decision* about what needs redrawing in one place instead of
|
|
scattered through the game.
|
|
|
|
`DRAWJOB` is a queue of one, chosen by dirty flags — stamps first, because everything else
|
|
draws with them, then the field, then the HUD:
|
|
|
|
```basic norun
|
|
LABEL DRAWJOB
|
|
IF DPLAY# = 0 THEN GOTO DRAWJOB3
|
|
GOSUB DRAWPLAY
|
|
DPLAY# = 0
|
|
RETURN
|
|
LABEL DRAWJOB3
|
|
IF DHUD# = 0 THEN RETURN
|
|
GOSUB DRAWHUD
|
|
DHUD# = 0
|
|
RETURN
|
|
```
|
|
|
|
The rest of the game never draws. It sets `DPLAY# = 1` or `DHUD# = 1` when something has
|
|
changed and gets on with its frame. One consequence is visible and deliberate: **the score
|
|
lags the bricks by one frame**, because the field is drawn first. At thirty frames a second
|
|
nobody can see it.
|
|
|
|
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks. Either works; the labels keep
|
|
each arm to one `RETURN` and read the same.
|
|
|
|
## Step 6: Erase one brick, not the whole field
|
|
|
|
**Goal: take a broken brick off the screen without redrawing the other fifty-nine.**
|
|
|
|
Because a drawing stays, removing something means covering it up. There is no
|
|
filled-rectangle verb — `BOX` outlines and `PAINT` costs about four milliseconds a call —
|
|
so the cheapest way to blank a region is a stamp of something blank:
|
|
|
|
```basic norun
|
|
COLOR 1, 1
|
|
FOR K# = 0 TO 15
|
|
DRAW 1, 0, 130 + K# TO 67, 130 + K#
|
|
NEXT K#
|
|
SSHAPE Z$, 0, 130, 68, 146 : BL$ = Z$
|
|
```
|
|
|
|
The same sixteen lines the brick stamps are made of, in the background colour. Erasing is
|
|
then one statement:
|
|
|
|
```basic norun
|
|
LABEL ERASEBRICK
|
|
Z$ = BL$
|
|
GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24
|
|
RETURN
|
|
```
|
|
|
|
Make a second one the width of the HUD strip while you are here, for the same reason: the
|
|
strip is rewritten whenever a number in it changes, and the old digits have to go
|
|
somewhere before the new ones are drawn.
|
|
|
|
**This is what makes the whole field a draw-once job.** Draw the walls and all sixty bricks
|
|
when a level is laid out, and after that touch only what changes. A program that had to
|
|
redraw the field to remove one brick would need a list of what is still standing, kept in
|
|
step with the array, walked in runs per row — and none of that is here, because none of it
|
|
is needed.
|
|
|
|
## Step 7: Draw lettering with a stroke font
|
|
|
|
**Goal: a HUD in more than one colour.**
|
|
|
|
The text grid draws in one colour and has no verb that changes it — `CHAR` accepts a
|
|
colour argument and ignores it. A coloured HUD therefore has to be *drawn*, which means
|
|
carrying a font.
|
|
|
|
The font here is four units wide and seven tall, one glyph per `DATA` line: **how many
|
|
strokes, then that many pairs of points**. A point is coded `X * 10 + Y`, so 0 is the
|
|
top-left corner, 30 the top right, 6 the bottom left and 36 the bottom right.
|
|
|
|
```basic requires=akgl screenshot=breakout-lettering size=260x110
|
|
DIM FNC#(5)
|
|
DIM FNI#(5)
|
|
DIM FNS#(60)
|
|
I# = 0
|
|
K# = 0
|
|
N# = 0
|
|
D# = 0
|
|
GP# = 0
|
|
GX# = 0
|
|
GX2# = 0
|
|
P1# = 0
|
|
P2# = 0
|
|
X1# = 0
|
|
Y1# = 0
|
|
X2# = 0
|
|
Y2# = 0
|
|
FOR I# = 0 TO 4
|
|
READ N#
|
|
FNC#(I#) = N#
|
|
FNI#(I#) = GX#
|
|
GOSUB READGLYPH
|
|
NEXT I#
|
|
GRAPHIC 1, 1
|
|
WIDTH 2
|
|
COLOR 1, 8
|
|
SZ# = 9
|
|
FOR I# = 0 TO 4
|
|
GOSUB DRAWGLYPH
|
|
NEXT I#
|
|
END
|
|
|
|
LABEL READGLYPH
|
|
FOR K# = 1 TO N# * 2
|
|
READ D#
|
|
FNS#(GX#) = D#
|
|
GX# = GX# + 1
|
|
NEXT K#
|
|
RETURN
|
|
|
|
LABEL DRAWGLYPH
|
|
GP# = FNI#(I#)
|
|
GX2# = 20 + I# * SZ# * 5
|
|
FOR K# = 1 TO FNC#(I#)
|
|
P1# = FNS#(GP#)
|
|
P2# = FNS#(GP# + 1)
|
|
GP# = GP# + 2
|
|
X1# = GX2# + (P1# / 10) * SZ#
|
|
Y1# = 20 + MOD(P1#, 10) * SZ#
|
|
X2# = GX2# + (P2# / 10) * SZ#
|
|
Y2# = 20 + MOD(P2#, 10) * SZ#
|
|
DRAW 1, X1#, Y1# TO X2#, Y2#
|
|
NEXT K#
|
|
RETURN
|
|
|
|
REM S
|
|
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
|
|
REM C
|
|
DATA 3, 0,30, 0,6, 6,36
|
|
REM O
|
|
DATA 4, 0,30, 6,36, 0,6, 30,36
|
|
REM R
|
|
DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36
|
|
REM E
|
|
DATA 4, 0,6, 0,30, 3,33, 6,36
|
|
```
|
|
|
|

|
|
|
|
`SZ#` is the scale, so the same table draws a 12-unit `BREAKOUT` on the title screen and a
|
|
4-unit `SCORE` in the HUD.
|
|
|
|
Turning a character into a glyph number is one call: `INSTR(ALPHA$, MID(TX$, TXI#, 1))`
|
|
over an alphabet string. That is why the order of the `DATA` lines matters — they have to
|
|
match the order of the characters in `ALPHA$`. The finished game uses a 41-character
|
|
alphabet covering `A` to `Z`, `0` to `9`, space, colon, dash, full stop and exclamation
|
|
mark.
|
|
|
|
**Building the strokes and drawing them are separate jobs, on different frames.** Turning a
|
|
string into strokes costs about sixteen lines a character and happens when a number
|
|
changes; the draw routine merely replays the list at two lines a stroke, and it is the one
|
|
with the deadline:
|
|
|
|
```basic norun
|
|
LABEL DRAWHUD
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
|
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
|
BOX 5, 0, 56, 799, 57
|
|
IF HN# = 1 THEN DRAW HC#(0), HX1#(0), HY1#(0) TO HX2#(0), HY2#(0)
|
|
IF HN# < 2 THEN GOTO HUDONE
|
|
FOR I# = 0 TO HN# - 1
|
|
DRAW HC#(I#), HX1#(I#), HY1#(I#) TO HX2#(I#), HY2#(I#)
|
|
NEXT I#
|
|
LABEL HUDONE
|
|
RETURN
|
|
```
|
|
|
|
`HX1#()` to `HY2#()` are the stroke endpoints and `HC#()` is the colour source each stroke
|
|
wants, so one pass over the list draws in as many colours as it likes: labels cyan, the
|
|
score yellow, the lives green, the level red. **Seven colour sources is the ceiling**,
|
|
which is why the purple gem's banner is the closest purple the palette has rather than the
|
|
gem's own.
|
|
|
|
Note the one-stroke case written out beside the loop. **A `FOR` with equal bounds does not
|
|
run its body at all in this dialect**, so `FOR I# = 0 TO HN# - 1` with one stroke in the
|
|
list draws nothing. Write the one-item case out beside the loop with a `GOTO` past the
|
|
loop, and do it wherever a list can hold exactly one thing — a one-character word, a
|
|
one-stroke glyph, a field with one brick left. [Chapter 13](13-differences.md) records the
|
|
rule; `TODO.md` §6 item 19 records why it stands.
|
|
|
|
## Step 8: Move and bounce the ball
|
|
|
|
**Goal: bounces at sensible angles, computed without a square root.**
|
|
|
|
There is no `SQR` in this dialect, so never compute a magnitude. Instead, tabulate eight
|
|
landing zones across the bar, each holding **very nearly a unit vector**, and make every
|
|
velocity one of them times the current speed:
|
|
|
|
```basic norun
|
|
DATA -0.85, -0.62, -0.40, -0.18, 0.18, 0.40, 0.62, 0.85
|
|
DATA -0.53, -0.78, -0.92, -0.98, -0.98, -0.92, -0.78, -0.53
|
|
```
|
|
|
|
Read those into `ZVX%()` and `ZVY%()`. A bar bounce is then: work out which zone the ball
|
|
landed in, and assign.
|
|
|
|
```basic norun
|
|
LABEL HITBAR
|
|
IF BLY%(B#) + 21 < T2# THEN RETURN
|
|
IF BLY%(B#) > T2# + 23 THEN RETURN
|
|
IF BLX%(B#) + 21 < T1# THEN RETURN
|
|
IF BLX%(B#) > T1# + T3# THEN RETURN
|
|
T4# = (BLX%(B#) + 11 - T1#) * 8 / T3#
|
|
IF T4# < 0 THEN T4# = 0
|
|
IF T4# > 7 THEN T4# = 7
|
|
BLVX%(B#) = ZVX%(T4#) * SPD%
|
|
BLVY%(B#) = ZVY%(T4#) * SPD%
|
|
BLY%(B#) = T2# - 23
|
|
SOUND 1, 4298, 5
|
|
HIT# = 1
|
|
RETURN
|
|
```
|
|
|
|
`T1#` is the bar's left edge, `T2#` its top and `T3#` its width, so the same routine serves
|
|
the paddle and the catcher bar — `BALLPADDLE` in Step 12 calls it twice with different
|
|
values and reads `HIT#` to find out whether the first call already caught the ball. The
|
|
four tests at the top are the overlap; each can bail out, and reaching the fifth line means
|
|
the ball is on the bar.
|
|
|
|
Step 9 adds two more lines before that `RETURN`, for the sticky gem.
|
|
|
|
### Which type a variable is decides the arithmetic
|
|
|
|
Two rules matter here and they matter a lot, because breaking either produces **no error at
|
|
all** — the program computes something else and carries on.
|
|
|
|
**A ball's velocity must be a float.** `BLVX%` has a `%` suffix; a `#` variable holds
|
|
`-0.85 * 6.0` as `-5`, and a ball whose velocity is quantised to whole pixels bleeds speed
|
|
away at every bounce.
|
|
|
|
**The left operand of an expression decides whether it is done in integers or floats.**
|
|
Put the float on the left:
|
|
|
|
```basic
|
|
LEVEL# = 4
|
|
SPD% = 5.6 + LEVEL# * 0.45
|
|
PRINT "INTEGER FIRST " + SPD%
|
|
SPD% = 5.6 + 0.45 * LEVEL#
|
|
PRINT "FLOAT FIRST " + SPD%
|
|
V% = 6.4
|
|
PRINT "0 - V% " + (0 - V%)
|
|
PRINT "0.0 - V% " + (0.0 - V%)
|
|
```
|
|
|
|
```output
|
|
INTEGER FIRST 5.600000
|
|
FLOAT FIRST 7.400000
|
|
0 - V% -6
|
|
0.0 - V% -6.400000
|
|
```
|
|
|
|
`LEVEL# * 0.45` is zero because `LEVEL#` is an integer, so reversing a ball is
|
|
`0.0 - BLVX%(B#)` and never `0 - BLVX%(B#)`. This is a decision of the dialect rather than
|
|
a defect — [Chapter 3](03-the-language.md#the-left-operand-decides-whether-the-arithmetic-is-integer-or-float)
|
|
has the two rules that keep you out of it, and
|
|
[Chapter 13](13-differences.md) names it as the difference from BASIC 7.0 most likely to
|
|
turn a working listing into a quietly wrong one.
|
|
|
|
### Bricks
|
|
|
|
A bounce off a wall or a brick only ever flips a sign, so a ball is always travelling at
|
|
exactly the `SPD%` that was in force when it last left a bar. That makes changing the speed
|
|
a **ratio of two speeds** rather than a change of magnitude, which is what the SLOW gem
|
|
needs:
|
|
|
|
```basic norun
|
|
LABEL RESCALE
|
|
IF BSPD% < 0.1 THEN BSPD% = SPD%
|
|
RAT% = SPD% / BSPD%
|
|
BSPD% = SPD%
|
|
FOR B# = 0 TO 2
|
|
IF BLON#(B#) = 1 THEN BEGIN
|
|
BLVX%(B#) = BLVX%(B#) * RAT%
|
|
BLVY%(B#) = BLVY%(B#) * RAT%
|
|
BEND
|
|
NEXT B#
|
|
RETURN
|
|
```
|
|
|
|
`RAT%` is a float on purpose: an integer one holds the 0.75 of a slowdown as 0 and stops
|
|
the ball dead.
|
|
|
|
Scaling by the *vertical component* instead — setting `BLVY%` to the new speed and taking
|
|
the horizontal along with it — is not a slowdown at all. A shallow ball's small vertical
|
|
gets stretched up and drags the large horizontal with it, so SLOW makes the ball faster. The
|
|
table above is why: the outermost zones keep only 0.53 of their speed in the vertical, so
|
|
scaling that up to the new speed multiplies everything by 1/0.53, and the ball ends up
|
|
nearly twice as fast as intended. Every zone inflates; only the middle two are close enough
|
|
to vertical for it not to show.
|
|
|
|
### Bricks are collision geometry, not arithmetic
|
|
|
|
A brick is a rectangle the interpreter knows about. Register one per brick as the level is
|
|
laid out:
|
|
|
|
```basic norun
|
|
LABEL FILLROW
|
|
FOR C# = 0 TO 9
|
|
BRK#(R# * 10 + C#) = 1
|
|
BRN# = BRN# + 1
|
|
SLX# = BRKX# + C# * 72
|
|
SLY# = BRKY# + R# * 24
|
|
SLI# = (R# * 10 + C#) + 1
|
|
SOLID SLI#, SLX#, SLY#, SLX# + 68, SLY# + 16
|
|
NEXT C#
|
|
RETURN
|
|
```
|
|
|
|
The id is the array index plus one, so what comes back out indexes `BRK#()` with no lookup.
|
|
Compute it into `SLI#` first rather than writing the expression inline — a parenthesised
|
|
first argument to a verb is worth avoiding, and it reads better anyway.
|
|
|
|
**Retire the previous level's rectangles before laying the next one out**, not after. That
|
|
is one line in `SETUPLEVEL`, and putting it in the wrong place is silent: registering all
|
|
sixty and then clearing them leaves a wall the ball passes straight through, with no error
|
|
and nothing to see.
|
|
|
|
Then arm a handler and let it do the work:
|
|
|
|
```basic norun
|
|
COLLISION 2, BRICKHIT
|
|
```
|
|
|
|
```basic norun
|
|
LABEL BRICKHIT
|
|
MB# = BUMP(2)
|
|
IF MB# = 0 THEN RETURN
|
|
B# = 0
|
|
IF (MB# AND 16) <> 0 THEN GOSUB ONEBRICK
|
|
B# = 1
|
|
IF (MB# AND 32) <> 0 THEN GOSUB ONEBRICK
|
|
B# = 2
|
|
IF (MB# AND 64) <> 0 THEN GOSUB ONEBRICK
|
|
RETURN
|
|
```
|
|
|
|
**The mask is by sprite, and the balls are sprites 5, 6 and 7** — so their bits are 16, 32
|
|
and 64, not 1, 2 and 4. That is the same `5 + B#` arithmetic `MOVSPR` uses for them
|
|
everywhere else, and getting it wrong costs nothing visible: the handler runs, tests bits
|
|
nothing sets, and returns.
|
|
|
|
```basic norun
|
|
LABEL ONEBRICK
|
|
IF BLON#(B#) = 0 THEN RETURN
|
|
N# = RCOLLISION(5 + B#, 1)
|
|
IF N# < 1 THEN RETURN
|
|
N# = N# - 1
|
|
IF BRK#(N#) = 0 THEN RETURN
|
|
DP% = RCOLLISION(5 + B#, 4)
|
|
BLX%(B#) = BLX%(B#) + (RCOLLISION(5 + B#, 2) * DP%)
|
|
BLY%(B#) = BLY%(B#) + (RCOLLISION(5 + B#, 3) * DP%)
|
|
AX# = RCOLLISION(5 + B#, 7)
|
|
IF AX# = 1 THEN BLVX%(B#) = 0.0 - BLVX%(B#)
|
|
IF AX# = 2 THEN BLVY%(B#) = 0.0 - BLVY%(B#)
|
|
R# = N# / 10
|
|
C# = MOD(N#, 10)
|
|
BRK#(N#) = 0
|
|
SOLID N# + 1
|
|
BRN# = BRN# - 1
|
|
SCORE# = SCORE# + BRV#(R#)
|
|
SOUND 1, 17175 - R# * 2100, 4
|
|
GOSUB ERASEBRICK
|
|
GOSUB HUDTEXT
|
|
RETURN
|
|
```
|
|
|
|
Fields 2, 3 and 4 push the ball exactly clear along the contact normal. Field 7 is which
|
|
axis to reverse — **the minimum translation axis**, which is why a ball clipping the end of
|
|
a row goes sideways rather than straight back down. Working that out by hand means building
|
|
an overlap rectangle and comparing its width to its height; it is one field here.
|
|
|
|
`SOLID N# + 1` retires the rectangle in the same statement that clears the array, so the
|
|
next frame cannot hit a brick that is no longer drawn. `GOSUB ERASEBRICK` takes it off the
|
|
screen with Step 6's blank stamp.
|
|
|
|
**Both records exist on purpose.** `BRK#()` is the program's — it is what "is the level
|
|
clear" counts and what the field redraw reads. The rectangles are the interpreter's. Keeping
|
|
them in step is two statements and the ids line up, which is the whole reason the id is the
|
|
index plus one.
|
|
|
|
## Step 9: Gems and powerups
|
|
|
|
**Goal: one falling gem that can be any of five things.**
|
|
|
|
A gem is one sprite, one type number and two timers. The type picks the artwork, the
|
|
banner colour and what catching it does:
|
|
|
|
```basic norun
|
|
LABEL SPAWNGEM
|
|
RNMAX# = 5
|
|
GOSUB NEXTRAND
|
|
GMTYP# = RNVAL# + 1
|
|
GMX% = BX1# + 10
|
|
GMY% = BY1#
|
|
IF GMTYP# = 1 THEN SPRSAV "art/element_red_polygon_glossy.png", 8
|
|
IF GMTYP# = 2 THEN SPRSAV "art/element_yellow_polygon_glossy.png", 8
|
|
IF GMTYP# = 3 THEN SPRSAV "art/element_green_polygon_glossy.png", 8
|
|
IF GMTYP# = 4 THEN SPRSAV "art/element_blue_polygon_glossy.png", 8
|
|
IF GMTYP# = 5 THEN SPRSAV "art/element_purple_polygon_glossy.png", 8
|
|
GMON# = 1
|
|
SPRITE 8, 1, 2
|
|
MOVSPR 8, GMX%, GMY%
|
|
RETURN
|
|
```
|
|
|
|
**Reloading slot 8 is how one sprite becomes five gems.** `SPRSAV` over a live slot replaces
|
|
its artwork, so there is no need for a slot per gem — which is just as well, because Step 2
|
|
did not leave one.
|
|
|
|
Catching one is one routine with five arms. Each sets a timer, sets the banner text, and
|
|
does whatever that gem does:
|
|
|
|
```basic norun
|
|
LABEL TAKEGEM
|
|
SOUND 2, 8579, 14, 1, 17175, 400
|
|
SCORE# = SCORE# + 50
|
|
GOSUB HUDTEXT
|
|
IF GMTYP# = 1 THEN BEGIN
|
|
PDW# = 208
|
|
PDEXP# = 600
|
|
SPRITE 3, 1, 2, 0, 1, 0
|
|
BAN$ = "EXPAND"
|
|
BEND
|
|
IF GMTYP# = 2 THEN BEGIN
|
|
BAN$ = "MULTI"
|
|
GOSUB SPLITBALLS
|
|
BEND
|
|
IF GMTYP# = 3 THEN BEGIN
|
|
SLOWT# = 480
|
|
SPD% = 4.2
|
|
GOSUB RESCALE
|
|
BAN$ = "SLOW"
|
|
BEND
|
|
IF GMTYP# = 4 THEN BEGIN
|
|
STKON# = 1
|
|
STKT# = 540
|
|
BAN$ = "STICKY"
|
|
BEND
|
|
IF GMTYP# = 5 THEN BEGIN
|
|
CTON# = 1
|
|
CTTM# = 720
|
|
SPRITE 4, 1, 2
|
|
BAN$ = "CATCH"
|
|
BEND
|
|
BANT# = 90
|
|
GOSUB SETBANNER
|
|
RETURN
|
|
```
|
|
|
|
EXPAND is `SPRITE 3, 1, 2, 0, 1, 0` — the x-expand flag, which is the only scaling a sprite
|
|
has, and doubling the artwork is exactly what it wants. `PDW#` is the width the collision
|
|
test uses, so it has to be doubled with it.
|
|
|
|
MULTI throws two more balls off the one in play, at slightly different angles so they do
|
|
not travel as a stack:
|
|
|
|
```basic norun
|
|
LABEL SPLITBALLS
|
|
IF BLN# > 2 THEN RETURN
|
|
IF BLON#(0) = 0 THEN RETURN
|
|
FOR B# = 1 TO 2
|
|
IF BLON#(B#) = 0 THEN BEGIN
|
|
BLON#(B#) = 1
|
|
BLST#(B#) = 0
|
|
BLX%(B#) = BLX%(0)
|
|
BLY%(B#) = BLY%(0)
|
|
BLVX%(B#) = ZVX%(B# * 5 - 4) * SPD%
|
|
BLVY%(B#) = ZVY%(B# * 5 - 4) * SPD%
|
|
BLN# = BLN# + 1
|
|
SPRITE 5 + B#, 1, 2
|
|
BEND
|
|
NEXT B#
|
|
RETURN
|
|
```
|
|
|
|
STICKY is three lines at the end of `HITBAR` in Step 8, replacing its final `RETURN` — the
|
|
ball stops where it landed and remembers its offset along the bar:
|
|
|
|
```basic norun
|
|
IF STKON# = 0 THEN RETURN
|
|
BLST#(B#) = 1
|
|
BLOFF# = BLX%(B#) - PDX%
|
|
RETURN
|
|
```
|
|
|
|
and space releases it, straight up the middle:
|
|
|
|
```basic norun
|
|
LABEL UNSTICK
|
|
FOR B# = 0 TO 2
|
|
IF BLST#(B#) = 1 THEN BEGIN
|
|
BLST#(B#) = 0
|
|
BLVX%(B#) = ZVX%(4) * SPD%
|
|
BLVY%(B#) = ZVY%(4) * SPD%
|
|
BEND
|
|
NEXT B#
|
|
RETURN
|
|
```
|
|
|
|
The CATCH bar **mirrors** the paddle rather than following it — the line is in `MOVEPADDLE`
|
|
in Step 12:
|
|
|
|
```basic norun
|
|
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
|
|
```
|
|
|
|
A second bar directly above the first is worth nothing; a mirrored one turns a dive across
|
|
the field into a save at both ends. Note the leading `0.0` rather than `0`, for the reason
|
|
in Step 8.
|
|
|
|
### Every timer in one place
|
|
|
|
A timer is a frame count decremented in one routine, so an expiry is exactly where the
|
|
effect is undone and there is only one place to look:
|
|
|
|
```basic norun
|
|
LABEL TIMERS
|
|
IF BANT# > 0 THEN BEGIN
|
|
BANT# = BANT# - 1
|
|
IF BANT# = 0 THEN BEGIN
|
|
BAN$ = ""
|
|
GOSUB SETBANNER
|
|
BEND
|
|
BEND
|
|
IF PDEXP# > 0 THEN BEGIN
|
|
PDEXP# = PDEXP# - 1
|
|
IF PDEXP# = 0 THEN BEGIN
|
|
PDW# = 104
|
|
SPRITE 3, 1, 2, 0, 0, 0
|
|
BEND
|
|
BEND
|
|
IF SLOWT# > 0 THEN BEGIN
|
|
SLOWT# = SLOWT# - 1
|
|
IF SLOWT# = 0 THEN BEGIN
|
|
GOSUB LEVELSPEED
|
|
GOSUB RESCALE
|
|
BEND
|
|
BEND
|
|
IF STKT# > 0 THEN BEGIN
|
|
STKT# = STKT# - 1
|
|
IF STKT# = 0 THEN BEGIN
|
|
STKON# = 0
|
|
GOSUB UNSTICK
|
|
BEND
|
|
BEND
|
|
IF CTTM# > 0 THEN BEGIN
|
|
CTTM# = CTTM# - 1
|
|
IF CTTM# = 0 THEN BEGIN
|
|
CTON# = 0
|
|
SPRITE 4, 0
|
|
BEND
|
|
BEND
|
|
RETURN
|
|
```
|
|
|
|
Each arm is the same shape: count down, and on the frame it reaches zero, put back what the
|
|
gem changed. SLOW expiring calls `LEVELSPEED` rather than assigning a number, so the speed
|
|
it returns to is the speed the current level should be running at.
|
|
|
|
## Step 10: Three voices and a mute
|
|
|
|
**Goal: sound that does not cut itself off, and a mute that costs one line.**
|
|
|
|
Give each kind of sound its own voice. Voice 1 is the ball hitting things, voice 2 is the
|
|
gem and the ball being lost, and voice 3 is reserved for `PLAY` — so a brick going cannot
|
|
cut a tune off mid-note.
|
|
|
|
**`SOUND`'s frequency argument is a SID register value, not hertz.** The pitch is
|
|
`register * 1022730 / 16777216` — see [Chapter 7](07-sound.md#sound) — so work the notes
|
|
out once and write them down: 17175 is C6, 8579 C5, 4298 C4, 3609 A3.
|
|
|
|
Pitch the brick tone by the row it came from and the wall plays itself down a scale as it
|
|
comes apart:
|
|
|
|
```basic norun
|
|
SOUND 1, 17175 - R# * 2100, 4
|
|
```
|
|
|
|
Mute with `VOL 0` rather than a flag tested at every call site:
|
|
|
|
```basic norun
|
|
LABEL PRESSMUTE
|
|
SNDON# = 1 - SNDON#
|
|
IF SNDON# = 1 THEN VOL 8
|
|
IF SNDON# = 0 THEN VOL 0
|
|
RETURN
|
|
```
|
|
|
|
A silenced voice costs nothing to issue, so one line here beats eleven scattered through
|
|
the game.
|
|
|
|
Set up the envelope and tempo once at startup:
|
|
|
|
```basic norun
|
|
VOL 8
|
|
ENVELOPE 0, 0, 6, 0, 4
|
|
TEMPO 12
|
|
```
|
|
|
|
`PLAY` takes a string of notes, which is what the fanfares are:
|
|
|
|
```basic norun
|
|
PLAY "V3 T2 U8 O4 QC QE QG O5 HC"
|
|
```
|
|
|
|
## Step 11: The states
|
|
|
|
**Goal: the five states the frame loop in Step 5 dispatches to.**
|
|
|
|
Each is a subroutine called once a frame while that state is current. This is a different
|
|
shape from Chapter 17's branch targets, and it works here because nothing jumps out of a
|
|
loop to reach it.
|
|
|
|
```basic norun
|
|
LABEL TITLETICK
|
|
STIMER# = STIMER# + 1
|
|
RETURN
|
|
|
|
LABEL SERVETICK
|
|
GOSUB MOVEPADDLE
|
|
BLX%(0) = PDX% + BLOFF#
|
|
BLY%(0) = PDY# - 23
|
|
MOVSPR 5, BLX%(0), BLY%(0)
|
|
RETURN
|
|
|
|
LABEL PLAYTICK
|
|
GOSUB MOVEPADDLE
|
|
GOSUB MOVEBALLS
|
|
GOSUB MOVEGEM
|
|
GOSUB TIMERS
|
|
IF BRN# = 0 THEN GOSUB LEVELDONE
|
|
IF BLN# = 0 AND STATE# = 2 THEN GOSUB BALLGONE
|
|
RETURN
|
|
|
|
LABEL LOSTTICK
|
|
STIMER# = STIMER# - 1
|
|
IF STIMER# > 0 THEN RETURN
|
|
IF LIVES# > 0 THEN GOSUB SERVEAGAIN
|
|
IF LIVES# < 1 THEN GOSUB GAMEISOVER
|
|
RETURN
|
|
|
|
LABEL CLEARTICK
|
|
STIMER# = STIMER# - 1
|
|
IF STIMER# > 0 THEN RETURN
|
|
LEVEL# = LEVEL# + 1
|
|
GOSUB SETUPLEVEL
|
|
RETURN
|
|
```
|
|
|
|
`STIMER#` is a frame countdown, which is how "hold this message up for a second and a half"
|
|
is written without stopping the game.
|
|
|
|
Laying a level out is one routine. Every third level is a row shorter, so the field changes
|
|
shape as well as pace:
|
|
|
|
```basic norun
|
|
LABEL SETUPLEVEL
|
|
GOSUB CLEARPOWERS
|
|
GOSUB LEVELSPEED
|
|
T3# = 6
|
|
IF MOD(LEVEL#, 3) = 2 THEN T3# = 5
|
|
IF MOD(LEVEL#, 3) = 0 THEN T3# = 4
|
|
BRN# = 0
|
|
FOR I# = 0 TO 59
|
|
BRK#(I#) = 0
|
|
NEXT I#
|
|
SOLID
|
|
FOR R# = 0 TO 5
|
|
IF R# < T3# THEN GOSUB FILLROW
|
|
NEXT R#
|
|
GOSUB RESETBALL
|
|
GOSUB HUDTEXT
|
|
STATE# = 1
|
|
DPLAY# = 1
|
|
RETURN
|
|
|
|
LABEL LEVELSPEED
|
|
SPD% = 5.6 + 0.45 * LEVEL#
|
|
IF SPD% > 11.0 THEN SPD% = 11.0
|
|
RETURN
|
|
```
|
|
|
|
`CLEARPOWERS` turns off every powerup, every timer and the two sprites they use, and is
|
|
called from level setup, from a re-serve and from the title screen — so no state can leak
|
|
from one game into the next.
|
|
|
|
## Step 12: Put the program together
|
|
|
|
**Goal: one file, in an order that runs.**
|
|
|
|
The file runs from the top, so three things about the order matter and nothing else does:
|
|
setup first, `DATA` in the order it will be read, and every `LABEL` present somewhere.
|
|
|
|
```text
|
|
WINDOW 0, 35, 49, 36 the text layer, out of the way -- Step 3
|
|
the declaration block, below
|
|
GOSUB LOADTABLES the row colours, values and bounce zones
|
|
GOSUB LOADFONT the stroke font
|
|
SEED# = MOD(1 + TI# * 7919, 2147483648)
|
|
the artwork from Step 1
|
|
VOL 8 / ENVELOPE / TEMPO from Step 10
|
|
COLLISION 2, BRICKHIT the brick handler -- Step 8
|
|
GOSUB DRAWPROTOS the stamps, once -- Steps 4 and 6
|
|
GOSUB TITLESCREEN
|
|
|
|
LABEL FRAME the frame loop from Step 5
|
|
|
|
the draw jobs, the input, the states, the ball, the gems,
|
|
the text builders, the generator
|
|
--- in any order ---
|
|
|
|
the tables as DATA read by LOADTABLES
|
|
the font as DATA read by LOADFONT
|
|
```
|
|
|
|
**`LOADTABLES` must run before `LOADFONT`, because the tables' `DATA` is written first.**
|
|
One `READ` cursor walks every `DATA` item in the file in the order they appear, no matter
|
|
which routine is reading, so whichever loader runs first gets whichever `DATA` comes first.
|
|
Get that backwards and the font table fills with brick colours, which does not fail — it
|
|
just draws nonsense.
|
|
|
|
### The declaration block
|
|
|
|
```basic norun
|
|
STATE# = 0
|
|
RUNNING# = 1
|
|
SCORE# = 0
|
|
LIVES# = 3
|
|
LEVEL# = 1
|
|
HISCORE# = 0
|
|
STIMER# = 0
|
|
LASTT# = 0
|
|
KEYV# = 0
|
|
SNDON# = 1
|
|
|
|
WALLL# = 12
|
|
WALLR# = 788
|
|
BRKX# = 42
|
|
BRKY# = 108
|
|
|
|
PDX% = 348
|
|
PDY# = 540
|
|
PDW# = 104
|
|
PDDIR# = 0
|
|
PDCO# = 0
|
|
PDEXP# = 0
|
|
CTON# = 0
|
|
CTX% = 348
|
|
CTTM# = 0
|
|
|
|
DIM BLON#(3)
|
|
DIM BLX%(3)
|
|
DIM BLY%(3)
|
|
DIM BLVX%(3)
|
|
DIM BLVY%(3)
|
|
DIM BLST#(3)
|
|
BLN# = 0
|
|
BLOFF# = 41
|
|
SPD% = 6.0
|
|
BSPD% = 6.0
|
|
RAT% = 1.0
|
|
SLOWT# = 0
|
|
STKON# = 0
|
|
STKT# = 0
|
|
|
|
GMON# = 0
|
|
GMTYP# = 0
|
|
GMX% = 0
|
|
GMY% = 0
|
|
|
|
DIM BRK#(60)
|
|
DIM BRC#(6)
|
|
DIM BRV#(6)
|
|
BRN# = 0
|
|
|
|
S0$ = "" : S1$ = "" : S2$ = ""
|
|
S3$ = "" : S4$ = "" : S5$ = ""
|
|
BL$ = "" : HBL$ = ""
|
|
Z$ = ""
|
|
DHUD# = 1
|
|
DPLAY# = 1
|
|
BAN$ = ""
|
|
BANT# = 0
|
|
|
|
ALPHA$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :-.!"
|
|
DIM FNS#(280)
|
|
DIM FNI#(41)
|
|
DIM FNC#(41)
|
|
|
|
DIM HX1#(120)
|
|
DIM HY1#(120)
|
|
DIM HX2#(120)
|
|
DIM HY2#(120)
|
|
DIM HC#(120)
|
|
HN# = 0
|
|
DIM PX1#(110)
|
|
DIM PY1#(110)
|
|
DIM PX2#(110)
|
|
DIM PY2#(110)
|
|
DIM PC#(110)
|
|
PN# = 0
|
|
BUILDH# = 0
|
|
|
|
TX$ = "" : TXX# = 0 : TXY# = 0
|
|
TXS# = 3 : TXC# = 1 : TLEN# = 0 : TXI# = 0
|
|
GC# = 0 : GN# = 0 : GP# = 0 : GX# = 0 : GK# = 0
|
|
P1# = 0 : P2# = 0
|
|
NUM$ = ""
|
|
|
|
DIM ZVX%(8)
|
|
DIM ZVY%(8)
|
|
|
|
I# = 0 : K# = 0 : N# = 0 : R# = 0 : C# = 0 : D# = 0 : B# = 0
|
|
T1# = 0 : T2# = 0 : T3# = 0 : T4# = 0
|
|
BX1# = 0 : BY1# = 0
|
|
LFT# = 0 : TOP# = 0 : RGT# = 0 : BOT# = 0
|
|
CC1# = 0 : CC2# = 0 : RR1# = 0 : RR2# = 0
|
|
HIT# = 0
|
|
SEED# = 1 : RNMAX# = 2 : RNVAL# = 0
|
|
MB# = 0 : AX# = 0 : DP% = 0.0
|
|
SLX# = 0 : SLY# = 0 : SLI# = 0
|
|
```
|
|
|
|
**`BL$` and `HBL$` are in that list for a reason worth learning from.** They are the two
|
|
eraser stamps, and they are built inside `DRAWPROTOS` — so left undeclared they were
|
|
created *in that routine's scope*, held a perfectly good handle while it ran, and were
|
|
empty everywhere else. Nothing failed; the first `GSHAPE` that used one simply refused with
|
|
"was given a string that did not come from SSHAPE", several routines away from the cause.
|
|
Chapter 17 Step 3 is about exactly this.
|
|
|
|
`DPLAY#` and `DHUD#` start at 1 so the first frame draws both.
|
|
|
|
That block is 121 of the interpreter's 128 variables, which is why several constants in the
|
|
geometry are spelled out where they are used rather than given names: a name costs a slot
|
|
whether it holds a constant or not.
|
|
|
|
### The tables and the generator
|
|
|
|
```basic norun
|
|
LABEL LOADTABLES
|
|
FOR I# = 0 TO 5
|
|
READ BRC#(I#)
|
|
NEXT I#
|
|
FOR I# = 0 TO 5
|
|
READ BRV#(I#)
|
|
NEXT I#
|
|
FOR I# = 0 TO 7
|
|
READ ZVX%(I#)
|
|
NEXT I#
|
|
FOR I# = 0 TO 7
|
|
READ ZVY%(I#)
|
|
NEXT I#
|
|
RETURN
|
|
|
|
LABEL NEXTRAND
|
|
SEED# = MOD(SEED# * 1103515245 + 12345, 2147483648)
|
|
RNVAL# = MOD(SHR(SEED#, 16), RNMAX#)
|
|
RETURN
|
|
```
|
|
|
|
```basic norun
|
|
DATA 3, 9, 8, 6, 4, 5
|
|
DATA 60, 50, 40, 30, 20, 10
|
|
DATA -0.85, -0.62, -0.40, -0.18, 0.18, 0.40, 0.62, 0.85
|
|
DATA -0.53, -0.78, -0.92, -0.98, -0.98, -0.92, -0.78, -0.53
|
|
```
|
|
|
|
Row colours, what a brick in each row is worth, and the eight bounce zones from Step 8.
|
|
|
|
`NEXTRAND` is a `GOSUB` over globals rather than a `DEF` function. Set `RNMAX#` to the
|
|
number of answers you want and read `RNVAL#`.
|
|
|
|
The font loader is the same shape, and it needs a subroutine of its own for the inner read
|
|
rather than a `BEGIN` block, because a `FOR` nested inside a block that is itself inside a
|
|
`FOR` does not find its own `NEXT`:
|
|
|
|
```basic norun
|
|
LABEL LOADFONT
|
|
GX# = 0
|
|
FOR I# = 0 TO 40
|
|
READ N#
|
|
FNC#(I#) = N#
|
|
FNI#(I#) = GX#
|
|
IF N# > 0 THEN GOSUB READGLYPH
|
|
NEXT I#
|
|
RETURN
|
|
|
|
LABEL READGLYPH
|
|
FOR K# = 1 TO N# * 2
|
|
READ D#
|
|
FNS#(GX#) = D#
|
|
GX# = GX# + 1
|
|
NEXT K#
|
|
RETURN
|
|
```
|
|
|
|
`FNC#(g)` is glyph `g`'s stroke count and `FNI#(g)` is where its points start in `FNS#()`.
|
|
|
|
The 41 `DATA` lines are one per character of `ALPHA$`, **in that order** — the `INSTR` in
|
|
`BUILDTEXT` gives a position in `ALPHA$` and that position is used directly as the glyph
|
|
number, so getting the two out of step silently draws the wrong letters. Here is the whole
|
|
font, in the format Step 8 gives:
|
|
|
|
```basic norun
|
|
REM A
|
|
DATA 4, 0,30, 0,6, 30,36, 3,33
|
|
REM B
|
|
DATA 6, 0,6, 0,30, 3,33, 6,36, 30,33, 33,36
|
|
REM C
|
|
DATA 3, 0,30, 0,6, 6,36
|
|
REM D
|
|
DATA 6, 0,6, 0,20, 20,31, 31,35, 35,26, 26,6
|
|
REM E
|
|
DATA 4, 0,6, 0,30, 3,33, 6,36
|
|
REM F
|
|
DATA 3, 0,6, 0,30, 3,33
|
|
REM G
|
|
DATA 5, 0,30, 0,6, 6,36, 33,36, 13,33
|
|
REM H
|
|
DATA 3, 0,6, 30,36, 3,33
|
|
REM I
|
|
DATA 1, 10,16
|
|
REM J
|
|
DATA 3, 30,36, 6,36, 3,6
|
|
REM K
|
|
DATA 3, 0,6, 30,3, 3,36
|
|
REM L
|
|
DATA 2, 0,6, 6,36
|
|
REM M
|
|
DATA 4, 0,6, 30,36, 0,13, 13,30
|
|
REM N
|
|
DATA 3, 0,6, 30,36, 0,36
|
|
REM O
|
|
DATA 4, 0,30, 6,36, 0,6, 30,36
|
|
REM P
|
|
DATA 4, 0,6, 0,30, 3,33, 30,33
|
|
REM Q
|
|
DATA 5, 0,30, 6,36, 0,6, 30,36, 24,36
|
|
REM R
|
|
DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36
|
|
REM S
|
|
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
|
|
REM T
|
|
DATA 2, 0,30, 10,16
|
|
REM U
|
|
DATA 3, 0,6, 30,36, 6,36
|
|
REM V
|
|
DATA 2, 0,16, 16,30
|
|
REM W
|
|
DATA 4, 0,6, 30,36, 6,13, 13,36
|
|
REM X
|
|
DATA 2, 0,36, 30,6
|
|
REM Y
|
|
DATA 3, 0,13, 30,13, 13,16
|
|
REM Z
|
|
DATA 3, 0,30, 30,6, 6,36
|
|
REM 0
|
|
DATA 5, 0,30, 6,36, 0,6, 30,36, 30,6
|
|
REM 1
|
|
DATA 2, 10,16, 1,10
|
|
REM 2
|
|
DATA 5, 0,30, 30,33, 3,33, 3,6, 6,36
|
|
REM 3
|
|
DATA 4, 0,30, 30,36, 3,33, 6,36
|
|
REM 4
|
|
DATA 3, 0,3, 3,33, 30,36
|
|
REM 5
|
|
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
|
|
REM 6
|
|
DATA 5, 0,30, 0,6, 3,33, 33,36, 6,36
|
|
REM 7
|
|
DATA 2, 0,30, 30,36
|
|
REM 8
|
|
DATA 5, 0,30, 6,36, 0,6, 30,36, 3,33
|
|
REM 9
|
|
DATA 5, 0,30, 0,3, 3,33, 30,36, 6,36
|
|
REM space
|
|
DATA 0
|
|
REM colon
|
|
DATA 2, 12,13, 14,15
|
|
REM dash
|
|
DATA 1, 3,33
|
|
REM full stop
|
|
DATA 1, 15,16
|
|
REM exclamation mark
|
|
DATA 2, 10,14, 15,16
|
|
```
|
|
|
|
Space is `DATA 0` — no strokes — which is what the `IF N# > 0` in `LOADFONT` and the
|
|
`IF GN# = 0 THEN RETURN` in `BUILDGLYPH` are for.
|
|
|
|
### Input
|
|
|
|
```basic norun
|
|
LABEL READKEYS
|
|
GET KEYV#
|
|
IF KEYV# = 0 THEN RETURN
|
|
IF KEYV# = 1073741904 THEN PDDIR# = 0 - 1 : PDCO# = 7
|
|
IF KEYV# = 1073741903 THEN PDDIR# = 1 : PDCO# = 7
|
|
IF KEYV# = 32 THEN GOSUB PRESSFIRE
|
|
IF KEYV# = 112 THEN GOSUB PRESSPAUSE
|
|
IF KEYV# = 115 THEN GOSUB PRESSMUTE
|
|
IF KEYV# = 113 THEN RUNNING# = 0
|
|
IF KEYV# = 27 THEN RUNNING# = 0
|
|
GOTO READKEYS
|
|
|
|
LABEL PRESSFIRE
|
|
IF STATE# = 0 THEN GOSUB STARTGAME : RETURN
|
|
IF STATE# = 5 THEN GOSUB TITLESCREEN : RETURN
|
|
IF STATE# = 1 THEN GOSUB LAUNCH : RETURN
|
|
IF STATE# = 2 THEN GOSUB UNSTICK
|
|
RETURN
|
|
|
|
LABEL PRESSPAUSE
|
|
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
|
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
|
RETURN
|
|
```
|
|
|
|
`READKEYS` empties the queue by tail-calling itself with `GOTO`, which costs no scope.
|
|
A press buys seven frames of paddle travel in `PDCO#` and a held key keeps renewing it —
|
|
the same countdown idea Chapter 17 uses, sized for this game's frame rate. Seven frames
|
|
matters because the first key repeat is a quarter of a second behind the press, and a
|
|
paddle that stalled for a quarter of a second would be unusable.
|
|
|
|
### The paddle and the balls
|
|
|
|
```basic norun
|
|
LABEL MOVEPADDLE
|
|
IF PDCO# > 0 THEN BEGIN
|
|
PDCO# = PDCO# - 1
|
|
PDX% = PDX% + PDDIR# * 9
|
|
BEND
|
|
IF PDCO# < 1 THEN PDDIR# = 0
|
|
IF PDX% < WALLL# THEN PDX% = WALLL#
|
|
IF PDX% > WALLR# - PDW# THEN PDX% = WALLR# - PDW#
|
|
MOVSPR 3, PDX%, PDY#
|
|
IF CTON# = 0 THEN RETURN
|
|
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
|
|
MOVSPR 4, CTX%, 470
|
|
RETURN
|
|
|
|
LABEL MOVEBALLS
|
|
FOR B# = 0 TO 2
|
|
IF BLON#(B#) = 1 THEN GOSUB MOVEONE
|
|
NEXT B#
|
|
RETURN
|
|
|
|
LABEL MOVEONE
|
|
IF BLST#(B#) = 1 THEN BEGIN
|
|
BLX%(B#) = PDX% + BLOFF#
|
|
BLY%(B#) = PDY# - 23
|
|
BEND
|
|
IF BLST#(B#) = 0 THEN BEGIN
|
|
BLX%(B#) = BLX%(B#) + BLVX%(B#)
|
|
BLY%(B#) = BLY%(B#) + BLVY%(B#)
|
|
GOSUB BALLWALLS
|
|
BEND
|
|
IF BLON#(B#) = 0 THEN RETURN
|
|
IF BLST#(B#) = 0 THEN BEGIN
|
|
GOSUB BALLPADDLE
|
|
BEND
|
|
MOVSPR 5 + B#, BLX%(B#), BLY%(B#)
|
|
RETURN
|
|
|
|
LABEL BALLWALLS
|
|
IF BLX%(B#) < WALLL# THEN BEGIN
|
|
BLX%(B#) = WALLL#
|
|
BLVX%(B#) = 0.0 - BLVX%(B#)
|
|
SOUND 1, 3609, 2
|
|
BEND
|
|
IF BLX%(B#) > WALLR# - 22 THEN BEGIN
|
|
BLX%(B#) = WALLR# - 22
|
|
BLVX%(B#) = 0.0 - BLVX%(B#)
|
|
SOUND 1, 3609, 2
|
|
BEND
|
|
IF BLY%(B#) < 72 THEN BEGIN
|
|
BLY%(B#) = 72
|
|
BLVY%(B#) = 0.0 - BLVY%(B#)
|
|
SOUND 1, 3609, 2
|
|
BEND
|
|
IF BLY%(B#) < 596 THEN RETURN
|
|
BLON#(B#) = 0
|
|
BLN# = BLN# - 1
|
|
SOUND 2, 2411, 6
|
|
SPRITE 5 + B#, 0
|
|
RETURN
|
|
|
|
LABEL BALLPADDLE
|
|
IF BLVY%(B#) < 0 THEN RETURN
|
|
HIT# = 0
|
|
T1# = PDX% : T2# = PDY# : T3# = PDW#
|
|
GOSUB HITBAR
|
|
IF CTON# = 0 THEN RETURN
|
|
IF HIT# = 1 THEN RETURN
|
|
T1# = CTX% : T2# = 470 : T3# = 104
|
|
GOSUB HITBAR
|
|
RETURN
|
|
```
|
|
|
|
Balls 0, 1 and 2 live in sprites 5, 6 and 7, which is why `MOVSPR 5 + B#` works.
|
|
`BLST#(B#)` is 1 while a ball is stuck to the paddle. Note the `0.0 -` on every sign flip,
|
|
for the reason in Step 8.
|
|
|
|
Brick collision is in Step 8.
|
|
|
|
### The gem's fall
|
|
|
|
```basic norun
|
|
LABEL MOVEGEM
|
|
IF GMON# = 0 THEN RETURN
|
|
GMY% = GMY% + 3.2
|
|
MOVSPR 8, GMX%, GMY%
|
|
HIT# = 0
|
|
IF GMY% > 596 THEN HIT# = 1
|
|
IF GMY% + 46 > PDY# AND GMY% < PDY# + 24 THEN GOSUB CATCHGEM
|
|
IF HIT# = 0 THEN RETURN
|
|
GMON# = 0
|
|
SPRITE 8, 0
|
|
RETURN
|
|
|
|
LABEL CATCHGEM
|
|
IF GMX% + 48 < PDX% THEN RETURN
|
|
IF GMX% > PDX% + PDW# THEN RETURN
|
|
GOSUB TAKEGEM
|
|
HIT# = 1
|
|
RETURN
|
|
```
|
|
|
|
`TAKEGEM` is in Step 9. `HIT#` is doing double duty here as "this gem is finished with" —
|
|
it is set both by the gem going off the bottom and by it being caught.
|
|
|
|
### Turning text into strokes
|
|
|
|
Step 8 drew a glyph directly. The game builds a list instead, so the drawing can happen on
|
|
a later frame:
|
|
|
|
```basic norun
|
|
LABEL BUILDTEXT
|
|
TLEN# = LEN(TX$)
|
|
IF TLEN# = 0 THEN RETURN
|
|
TXI# = 0
|
|
DO
|
|
GC# = INSTR(ALPHA$, MID(TX$, TXI#, 1))
|
|
IF GC# > 0 - 1 THEN GOSUB BUILDGLYPH
|
|
TXI# = TXI# + 1
|
|
LOOP UNTIL TXI# >= TLEN#
|
|
RETURN
|
|
|
|
LABEL BUILDGLYPH
|
|
GN# = FNC#(GC#)
|
|
IF GN# = 0 THEN RETURN
|
|
GP# = FNI#(GC#)
|
|
GX# = TXX# + TXI# * TXS# * 5
|
|
GK# = 0
|
|
DO
|
|
P1# = FNS#(GP#) : P2# = FNS#(GP# + 1) : GP# = GP# + 2
|
|
IF BUILDH# = 1 AND HN# < 120 THEN BEGIN
|
|
HX1#(HN#) = GX# + (P1# / 10) * TXS#
|
|
HY1#(HN#) = TXY# + MOD(P1#, 10) * TXS#
|
|
HX2#(HN#) = GX# + (P2# / 10) * TXS#
|
|
HY2#(HN#) = TXY# + MOD(P2#, 10) * TXS#
|
|
HC#(HN#) = TXC#
|
|
HN# = HN# + 1
|
|
BEND
|
|
IF BUILDH# = 0 AND PN# < 110 THEN BEGIN
|
|
PX1#(PN#) = GX# + (P1# / 10) * TXS#
|
|
PY1#(PN#) = TXY# + MOD(P1#, 10) * TXS#
|
|
PX2#(PN#) = GX# + (P2# / 10) * TXS#
|
|
PY2#(PN#) = TXY# + MOD(P2#, 10) * TXS#
|
|
PC#(PN#) = TXC#
|
|
PN# = PN# + 1
|
|
BEND
|
|
GK# = GK# + 1
|
|
LOOP UNTIL GK# >= GN#
|
|
RETURN
|
|
```
|
|
|
|
Set `TX$`, `TXX#`, `TXY#`, `TXS#` (the scale) and `TXC#` (the colour source), then set
|
|
`BUILDH#` to 1 for the HUD list or 0 for the field list, and `GOSUB BUILDTEXT`. Both `DO`
|
|
loops test at the bottom, so a one-character string and a one-stroke glyph both work.
|
|
|
|
The bounds tests — `HN# < 120` and `PN# < 110` — are what stops a long string running off
|
|
the end of the arrays.
|
|
|
|
```basic norun
|
|
LABEL HUDTEXT
|
|
HN# = 0
|
|
BUILDH# = 1
|
|
TXS# = 4
|
|
TXY# = 14
|
|
TXC# = 1 : TXX# = 24
|
|
TX$ = "SCORE"
|
|
GOSUB BUILDTEXT
|
|
TXC# = 2 : TXX# = 144
|
|
T4# = SCORE#
|
|
GOSUB FMTNUM
|
|
TX$ = NUM$
|
|
GOSUB BUILDTEXT
|
|
TXC# = 1 : TXX# = 316
|
|
TX$ = "LIVES"
|
|
GOSUB BUILDTEXT
|
|
TXC# = 6 : TXX# = 436
|
|
TX$ = "" + LIVES#
|
|
GOSUB BUILDTEXT
|
|
TXC# = 1 : TXX# = 500
|
|
TX$ = "LEVEL"
|
|
GOSUB BUILDTEXT
|
|
TXC# = 4 : TXX# = 620
|
|
TX$ = "" + LEVEL#
|
|
GOSUB BUILDTEXT
|
|
DHUD# = 1
|
|
RETURN
|
|
|
|
LABEL FMTNUM
|
|
NUM$ = ""
|
|
T2# = T4#
|
|
FOR I# = 1 TO 6
|
|
D# = MOD(T2#, 10)
|
|
NUM$ = MID("0123456789", D#, 1) + NUM$
|
|
T2# = T2# / 10
|
|
NEXT I#
|
|
RETURN
|
|
```
|
|
|
|
`HUDTEXT` rebuilds the whole list from `HN# = 0` and sets `DHUD#`; the frame loop draws it
|
|
whenever it next gets a turn.
|
|
|
|
`SETBANNER` is the same idea for the field list — the message that flashes over the play
|
|
area, in the colour of the gem that put it there:
|
|
|
|
```basic norun
|
|
LABEL SETBANNER
|
|
PN# = 0
|
|
DPLAY# = 1
|
|
IF BAN$ = "" THEN RETURN
|
|
BUILDH# = 0
|
|
TXS# = 8
|
|
TXY# = 430
|
|
TXX# = 400 - LEN(BAN$) * 20
|
|
TXC# = 5
|
|
IF GMTYP# = 1 THEN TXC# = 4
|
|
IF GMTYP# = 2 THEN TXC# = 2
|
|
IF GMTYP# = 3 THEN TXC# = 6
|
|
IF GMTYP# = 4 THEN TXC# = 1
|
|
IF GMTYP# = 5 THEN TXC# = 3
|
|
TX$ = BAN$
|
|
GOSUB BUILDTEXT
|
|
RETURN
|
|
```
|
|
|
|
Setting `BAN$` to `""` and calling it is how a banner is cleared: `PN# = 0` empties the
|
|
field's stroke list and `DPLAY# = 1` asks for a redraw without it. Sources 1 to 6 are cyan,
|
|
yellow, purple, light red, light grey and green, and the five gems are red, yellow, green,
|
|
blue and purple — so the banner is as close to the gem as seven colour sources allow.
|
|
|
|
### The state changes
|
|
|
|
Six routines, each the same shape: set `STATE#`, set `STIMER#` if the state has a duration,
|
|
set the banner, and mark whatever is now out of date.
|
|
|
|
```basic norun
|
|
LABEL BALLGONE
|
|
SOUND 2, 7218, 30, 2, 1804, 220
|
|
LIVES# = LIVES# - 1
|
|
STATE# = 3
|
|
STIMER# = 45
|
|
GMTYP# = 0
|
|
BAN$ = "MISS"
|
|
GOSUB SETBANNER
|
|
GOSUB HUDTEXT
|
|
RETURN
|
|
|
|
LABEL SERVEAGAIN
|
|
BAN$ = ""
|
|
GOSUB SETBANNER
|
|
GOSUB CLEARPOWERS
|
|
GOSUB RESETBALL
|
|
STATE# = 1
|
|
RETURN
|
|
|
|
LABEL LEVELDONE
|
|
PLAY "V3 T2 U8 O4 QC QE QG O5 HC"
|
|
STATE# = 4
|
|
STIMER# = 60
|
|
GMTYP# = 0
|
|
BAN$ = "CLEAR"
|
|
GOSUB SETBANNER
|
|
RETURN
|
|
|
|
LABEL STARTGAME
|
|
PLAY "V3 T2 U8 O4 IC IE IG O5 IC"
|
|
SCORE# = 0
|
|
LIVES# = 3
|
|
LEVEL# = 1
|
|
GOSUB SETUPLEVEL
|
|
RETURN
|
|
```
|
|
|
|
`GMTYP# = 0` before a banner that is not a gem's is what makes `SETBANNER` pick its default
|
|
colour rather than the last gem's.
|
|
|
|
`TITLESCREEN` and `GAMEISOVER` are longer only because both draw a screenful of lettering.
|
|
Both clear the brick array and retire every rectangle with a bare `SOLID`, turn the paddle
|
|
and ball sprites off, reset `PN#`, and then build their text at a large `TXS#`:
|
|
|
|
```basic norun
|
|
LABEL TITLESCREEN
|
|
STATE# = 0
|
|
STIMER# = 0
|
|
GOSUB CLEARPOWERS
|
|
BLN# = 0
|
|
BRN# = 0
|
|
FOR I# = 0 TO 59
|
|
BRK#(I#) = 0
|
|
NEXT I#
|
|
SOLID
|
|
SPRITE 3, 0
|
|
SPRITE 5, 0
|
|
SPRITE 6, 0
|
|
SPRITE 7, 0
|
|
PN# = 0
|
|
TXS# = 12 : TXC# = 2 : TXX# = 120 : TXY# = 170
|
|
TX$ = "BREAKOUT"
|
|
BUILDH# = 0
|
|
GOSUB BUILDTEXT
|
|
TXS# = 4 : TXC# = 1 : TXX# = 220 : TXY# = 370
|
|
TX$ = "SPACE TO START"
|
|
BUILDH# = 0
|
|
GOSUB BUILDTEXT
|
|
HN# = 0
|
|
TXS# = 3 : TXC# = 6 : TXX# = 40 : TXY# = 20
|
|
TX$ = "ARROWS MOVE P PAUSE Q QUIT"
|
|
BUILDH# = 1
|
|
GOSUB BUILDTEXT
|
|
DPLAY# = 1
|
|
DHUD# = 1
|
|
RETURN
|
|
```
|
|
|
|
`GAMEISOVER` is the same routine with `"GAME OVER"`, the final score and the best score,
|
|
and `STATE# = 5` — the state whose only exit is space, back to the title.
|
|
|
|
### Serving, and clearing up
|
|
|
|
```basic norun
|
|
LABEL RESETBALL
|
|
FOR B# = 0 TO 2
|
|
BLON#(B#) = 0
|
|
BLST#(B#) = 0
|
|
NEXT B#
|
|
SPRITE 6, 0
|
|
SPRITE 7, 0
|
|
BLON#(0) = 1
|
|
BLN# = 1
|
|
BSPD% = SPD%
|
|
PDX% = 348
|
|
PDW# = 104
|
|
BLOFF# = 41
|
|
SPRITE 3, 1, 2, 0, 0, 0
|
|
SPRITE 5, 1, 2
|
|
MOVSPR 3, PDX%, PDY#
|
|
BLX%(0) = PDX% + BLOFF#
|
|
BLY%(0) = PDY# - 23
|
|
MOVSPR 5, BLX%(0), BLY%(0)
|
|
RETURN
|
|
|
|
LABEL LAUNCH
|
|
SOUND 1, 6431, 3
|
|
STATE# = 2
|
|
BLST#(0) = 0
|
|
BLVX%(0) = ZVX%(5) * SPD%
|
|
BLVY%(0) = ZVY%(5) * SPD%
|
|
RETURN
|
|
|
|
LABEL CLEARPOWERS
|
|
GOSUB LEVELSPEED
|
|
PDW# = 104
|
|
PDEXP# = 0
|
|
SLOWT# = 0
|
|
STKON# = 0
|
|
STKT# = 0
|
|
CTON# = 0
|
|
CTTM# = 0
|
|
GMON# = 0
|
|
BANT# = 0
|
|
BLOFF# = 41
|
|
SPRITE 4, 0
|
|
SPRITE 8, 0
|
|
SPRITE 3, 1, 2, 0, 0, 0
|
|
RETURN
|
|
|
|
LABEL FILLROW
|
|
FOR C# = 0 TO 9
|
|
BRK#(R# * 10 + C#) = 1
|
|
BRN# = BRN# + 1
|
|
NEXT C#
|
|
RETURN
|
|
|
|
LABEL SHUTDOWN
|
|
FOR I# = 1 TO 8
|
|
SPRITE I#, 0
|
|
NEXT I#
|
|
IF SCORE# > HISCORE# THEN HISCORE# = SCORE#
|
|
PRINT "FINAL SCORE " + SCORE# + " BEST " + HISCORE#
|
|
END
|
|
```
|
|
|
|
The six state changes — `BALLGONE`, `SERVEAGAIN`, `LEVELDONE`, `GAMEISOVER`,
|
|
`TITLESCREEN` and `STARTGAME` — are above, with `SETBANNER`.
|
|
|
|
### The two draw routines, whole
|
|
|
|
Step 4 built the stamps and Step 7 stamped a row; these are the two routines Step 6's queue
|
|
actually calls, with those pieces in place.
|
|
|
|
```basic norun
|
|
LABEL DRAWPROTOS
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
FOR R# = 0 TO 5
|
|
COLOR 1, BRC#(R#)
|
|
T1# = R# * 20
|
|
T2# = T1# + 8
|
|
FOR K# = 0 TO 7
|
|
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
|
|
NEXT K#
|
|
NEXT R#
|
|
SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$
|
|
SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$
|
|
SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$
|
|
SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$
|
|
SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
|
|
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
|
|
```
|
|
|
|
plus Step 6's two erasers, and then `DPLAY# = 1` and `DHUD# = 1` so the first frame draws
|
|
both. Called once from the setup block: nothing captures per frame, so eight of the sixteen
|
|
`SSHAPE` slots are spent here and never again, and `GRAPHIC 5` is never called at all.
|
|
|
|
```basic norun
|
|
LABEL DRAWPLAY
|
|
GRAPHIC 1, 1
|
|
WIDTH 2
|
|
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
|
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
|
BOX 5, 2, 62, 797, 597 : BOX 1, 6, 66, 793, 593
|
|
FOR R# = 0 TO 5
|
|
IF R# = 0 THEN Z$ = S0$
|
|
IF R# = 1 THEN Z$ = S1$
|
|
IF R# = 2 THEN Z$ = S2$
|
|
IF R# = 3 THEN Z$ = S3$
|
|
IF R# = 4 THEN Z$ = S4$
|
|
IF R# = 5 THEN Z$ = S5$
|
|
FOR C# = 0 TO 9
|
|
IF BRK#(R# * 10 + C#) > 0 THEN GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24
|
|
NEXT C#
|
|
NEXT R#
|
|
IF PN# = 1 THEN DRAW PC#(0), PX1#(0), PY1#(0) TO PX2#(0), PY2#(0)
|
|
IF PN# < 2 THEN GOTO PLDONE
|
|
FOR I# = 0 TO PN# - 1
|
|
DRAW PC#(I#), PX1#(I#), PY1#(I#) TO PX2#(I#), PY2#(I#)
|
|
NEXT I#
|
|
LABEL PLDONE
|
|
RETURN
|
|
```
|
|
|
|
Same shape as `DRAWHUD` in Step 7: the walls, then the bricks, then the field's own stroke
|
|
list with its one-item case beside the loop. Nothing at the end — what is drawn is on the
|
|
screen.
|
|
|
|
**A label is one bare word.** `PLDONE` and `HUDONE` have no underscore in them, and cannot:
|
|
underscores are not part of an identifier here.
|
|
|
|
### Game over
|
|
|
|
`TITLESCREEN` is above. `GAMEISOVER` is the same routine with different text, `STATE# = 5`,
|
|
and the high score recorded on the way in:
|
|
|
|
```basic norun
|
|
LABEL GAMEISOVER
|
|
PLAY "V3 T1 U8 O4 QG QE QC O3 HG"
|
|
IF SCORE# > HISCORE# THEN HISCORE# = SCORE#
|
|
STATE# = 5
|
|
GOSUB CLEARPOWERS
|
|
BRN# = 0
|
|
FOR I# = 0 TO 59
|
|
BRK#(I#) = 0
|
|
NEXT I#
|
|
SOLID
|
|
SPRITE 3, 0
|
|
SPRITE 5, 0
|
|
PN# = 0
|
|
TXS# = 9 : TXC# = 4 : TXX# = 148 : TXY# = 230
|
|
TX$ = "GAME OVER"
|
|
BUILDH# = 0
|
|
GOSUB BUILDTEXT
|
|
TXS# = 5 : TXC# = 2 : TXX# = 200 : TXY# = 380
|
|
T4# = SCORE#
|
|
GOSUB FMTNUM
|
|
TX$ = "SCORE " + NUM$
|
|
BUILDH# = 0
|
|
GOSUB BUILDTEXT
|
|
HN# = 0
|
|
TXS# = 3 : TXC# = 5 : TXY# = 20 : TXX# = 40
|
|
T4# = HISCORE#
|
|
GOSUB FMTNUM
|
|
TX$ = "BEST " + NUM$
|
|
BUILDH# = 1
|
|
GOSUB BUILDTEXT
|
|
TXC# = 6 : TXX# = 400
|
|
TX$ = "SPACE FOR TITLE"
|
|
BUILDH# = 1
|
|
GOSUB BUILDTEXT
|
|
DPLAY# = 1
|
|
DHUD# = 1
|
|
RETURN
|
|
```
|
|
|
|
Three sizes of lettering on one screen, from one font table: `TXS# = 9` for the message,
|
|
5 for the score and 3 for the HUD strip. Setting `HN# = 0` partway through is what switches
|
|
from building the field's list to rebuilding the HUD's.
|
|
|
|
### Check it before you have a window
|
|
|
|
Run the assembled file through the plain build first:
|
|
|
|
```sh norun
|
|
$ ./build/basic mybreakout.bas
|
|
```
|
|
|
|
It will stop at the first `SPRSAV` with a message about a missing sprite device, which
|
|
means everything above it parsed. A parse error, a bad `DATA` read or an `OUT OF DATA` at
|
|
this stage is a real problem, and it is much easier to find without a window in the way.
|
|
Then run it properly:
|
|
|
|
```sh norun
|
|
$ ./build-akgl/basic mybreakout.bas
|
|
```
|
|
|
|
## The budgets
|
|
|
|
This program sits close to five ceilings at once, and knowing where they are is what stops
|
|
a feature costing an afternoon before it is abandoned.
|
|
|
|
| Resource | There are | This game uses |
|
|
|---|---|---|
|
|
| Sprites | 8 | 6 |
|
|
| Variables | 128 | 123, plus 4 the interpreter makes |
|
|
| Labels | 64 | 57 |
|
|
| `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 8, spent once at startup |
|
|
| Colour sources | 7 | 7 |
|
|
| Scopes | 32 | 6 deep at most |
|
|
| Tokens on a line | 32, and the 33rd stops the interpreter rather than raising | short lines, temporaries instead of long conditions |
|
|
| Value-pool slots | 4096 for arrays; a plain number costs none | fifteen arrays, all declared once |
|
|
|
|
That is also why several things are **not** in the game, and they are worth naming rather
|
|
than leaving to be discovered: no music under the play, only event sounds and two four-note
|
|
stings; one gem at a time; sticky and multiball share one offset, so two balls stuck to the
|
|
paddle sit on top of each other; and no high score on disk, because there is no disk.
|
|
|
|
## The picture at the top of this chapter
|
|
|
|
Every figure in this guide is generated by running the listing beside it, and the one at
|
|
the top of this chapter is no exception. It is Step 4's field, Step 8's stroke font and
|
|
Step 1's artwork, with the numbers filled in by hand and no frame loop, so it draws one
|
|
frame and stops.
|
|
|
|
The font here carries only the fourteen glyphs the HUD needs, in the order they appear in
|
|
`ALPHA$` — which is the ordering rule from Step 7, shown small enough to check by eye:
|
|
|
|
```basic requires=akgl setup=breakout_art screenshot=breakout-game-artwork size=800x600
|
|
DIM BRC#(6)
|
|
DIM FNC#(14)
|
|
DIM FNI#(14)
|
|
DIM FNS#(100)
|
|
ALPHA$ = "SCOERLIV 01235"
|
|
I# = 0
|
|
R# = 0
|
|
C# = 0
|
|
K# = 0
|
|
N# = 0
|
|
D# = 0
|
|
T1# = 0
|
|
T2# = 0
|
|
GP# = 0
|
|
GX# = 0
|
|
GX2# = 0
|
|
GK# = 0
|
|
TLEN# = 0
|
|
P1# = 0
|
|
P2# = 0
|
|
X1# = 0
|
|
Y1# = 0
|
|
X2# = 0
|
|
Y2# = 0
|
|
SZ# = 0
|
|
TX$ = ""
|
|
TXX# = 0
|
|
TXY# = 0
|
|
TXC# = 1
|
|
TXI# = 0
|
|
GC# = 0
|
|
Z$ = ""
|
|
S0$ = ""
|
|
S1$ = ""
|
|
S2$ = ""
|
|
S3$ = ""
|
|
S4$ = ""
|
|
FOR I# = 0 TO 5
|
|
READ BRC#(I#)
|
|
NEXT I#
|
|
GX# = 0
|
|
FOR I# = 0 TO 13
|
|
READ N#
|
|
FNC#(I#) = N#
|
|
FNI#(I#) = GX#
|
|
IF N# > 0 THEN GOSUB READGLYPH
|
|
NEXT I#
|
|
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
FOR R# = 0 TO 5
|
|
COLOR 1, BRC#(R#)
|
|
T1# = R# * 20
|
|
T2# = T1# + 8
|
|
FOR K# = 0 TO 7
|
|
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
|
|
NEXT K#
|
|
NEXT R#
|
|
SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$
|
|
SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$
|
|
SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$
|
|
SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$
|
|
SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
|
|
|
|
GRAPHIC 1, 1
|
|
WIDTH 2
|
|
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
|
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
|
BOX 5, 2, 62, 797, 597
|
|
BOX 1, 6, 66, 793, 593
|
|
FOR C# = 0 TO 9
|
|
Z$ = S0$ : GSHAPE Z$, 42 + C# * 72, 108
|
|
Z$ = S1$ : GSHAPE Z$, 42 + C# * 72, 132
|
|
Z$ = S2$ : GSHAPE Z$, 42 + C# * 72, 156
|
|
NEXT C#
|
|
FOR C# = 1 TO 8
|
|
Z$ = S3$ : GSHAPE Z$, 42 + C# * 72, 180
|
|
NEXT C#
|
|
FOR C# = 2 TO 6
|
|
Z$ = S4$ : GSHAPE Z$, 42 + C# * 72, 204
|
|
NEXT C#
|
|
SSHAPE Z$, 0, 60, 800, 600
|
|
SPRSAV Z$, 2
|
|
SPRITE 2, 1, 2
|
|
MOVSPR 2, 0, 60
|
|
|
|
GRAPHIC 1, 1
|
|
WIDTH 1
|
|
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
|
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
|
BOX 5, 0, 56, 799, 57
|
|
SZ# = 4
|
|
TXY# = 14
|
|
TXC# = 1 : TXX# = 24 : TX$ = "SCORE" : GOSUB DRAWTEXT
|
|
TXC# = 2 : TXX# = 144 : TX$ = "001250" : GOSUB DRAWTEXT
|
|
TXC# = 1 : TXX# = 316 : TX$ = "LIVES" : GOSUB DRAWTEXT
|
|
TXC# = 6 : TXX# = 436 : TX$ = "3" : GOSUB DRAWTEXT
|
|
TXC# = 1 : TXX# = 500 : TX$ = "LEVEL" : GOSUB DRAWTEXT
|
|
TXC# = 4 : TXX# = 620 : TX$ = "2" : GOSUB DRAWTEXT
|
|
SSHAPE Z$, 0, 0, 800, 60
|
|
SPRSAV Z$, 1
|
|
SPRITE 1, 1, 2
|
|
MOVSPR 1, 0, 0
|
|
|
|
SPRSAV "art/paddleBlu.png", 3
|
|
SPRSAV "art/ballBlue.png", 5
|
|
SPRSAV "art/element_green_polygon_glossy.png", 8
|
|
SPRITE 3, 1, 2
|
|
SPRITE 5, 1, 2
|
|
SPRITE 8, 1, 2
|
|
MOVSPR 3, 300, 540
|
|
MOVSPR 5, 420, 400
|
|
MOVSPR 8, 200, 300
|
|
END
|
|
|
|
LABEL READGLYPH
|
|
FOR K# = 1 TO N# * 2
|
|
READ D#
|
|
FNS#(GX#) = D#
|
|
GX# = GX# + 1
|
|
NEXT K#
|
|
RETURN
|
|
|
|
LABEL DRAWTEXT
|
|
TLEN# = LEN(TX$)
|
|
IF TLEN# = 0 THEN RETURN
|
|
TXI# = 0
|
|
DO
|
|
GC# = INSTR(ALPHA$, MID(TX$, TXI#, 1))
|
|
IF GC# > 0 - 1 THEN GOSUB DRAWGLYPH
|
|
TXI# = TXI# + 1
|
|
LOOP UNTIL TXI# >= TLEN#
|
|
RETURN
|
|
|
|
LABEL DRAWGLYPH
|
|
N# = FNC#(GC#)
|
|
IF N# = 0 THEN RETURN
|
|
GP# = FNI#(GC#)
|
|
GX2# = TXX# + TXI# * SZ# * 5
|
|
GK# = 0
|
|
DO
|
|
P1# = FNS#(GP#)
|
|
P2# = FNS#(GP# + 1)
|
|
GP# = GP# + 2
|
|
X1# = GX2# + (P1# / 10) * SZ#
|
|
Y1# = TXY# + MOD(P1#, 10) * SZ#
|
|
X2# = GX2# + (P2# / 10) * SZ#
|
|
Y2# = TXY# + MOD(P2#, 10) * SZ#
|
|
DRAW TXC#, X1#, Y1# TO X2#, Y2#
|
|
GK# = GK# + 1
|
|
LOOP UNTIL GK# >= N#
|
|
RETURN
|
|
|
|
DATA 3, 9, 8, 6, 4, 5
|
|
|
|
REM S
|
|
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
|
|
REM C
|
|
DATA 3, 0,30, 0,6, 6,36
|
|
REM O
|
|
DATA 4, 0,30, 6,36, 0,6, 30,36
|
|
REM E
|
|
DATA 4, 0,6, 0,30, 3,33, 6,36
|
|
REM R
|
|
DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36
|
|
REM L
|
|
DATA 2, 0,6, 6,36
|
|
REM I
|
|
DATA 1, 10,16
|
|
REM V
|
|
DATA 2, 0,16, 16,30
|
|
REM space
|
|
DATA 0
|
|
REM 0
|
|
DATA 5, 0,30, 6,36, 0,6, 30,36, 30,6
|
|
REM 1
|
|
DATA 2, 10,16, 1,10
|
|
REM 2
|
|
DATA 5, 0,30, 30,33, 3,33, 3,6, 6,36
|
|
REM 3
|
|
DATA 4, 0,30, 30,36, 3,33, 6,36
|
|
REM 5
|
|
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
|
|
```
|
|
|
|

|
|
|
|
`DRAWTEXT` and `DRAWGLYPH` are `DO ... LOOP UNTIL` rather than `FOR`, which is Step 7's
|
|
rule earning its keep twice on one screen: `"3"` is one character long and the `I` glyph is
|
|
one stroke, and a `FOR` would have drawn neither.
|
|
|
|
## Where to go next
|
|
|
|
- **[Chapter 8](08-sprites.md)** is the sprite reference: every form of `SPRSAV`, the
|
|
`MOVSPR` forms, collision and what `RSPPOS` reads back.
|
|
- **[Chapter 6](06-graphics.md)** is the drawing reference, including `SSHAPE`, `GSHAPE`
|
|
and what a shape handle is.
|
|
- **[Chapter 7](07-sound.md)** is `SOUND`, `PLAY`, `ENVELOPE` and `VOL`.
|
|
- **[Chapter 3](03-the-language.md)** has the arithmetic rules from Step 9 in full.
|
|
- **[Chapter 14](14-architecture.md)** explains the step loop and the pools this chapter
|
|
keeps running into, from the interpreter's side.
|