Eleven of the thirteen defects these two games found are fixed, and the chapters that taught around them said things that are no longer true. Chapter 17: the drawing verbs are no longer invisible, they are covered by a text layer that `WINDOW` can now shrink; the cell size is `RGR(3)` and the grid is `RWINDOW`, not a hand-measured constant; a character written past a short row's end lands. Chapter 18: traps 3 and 4 -- the skipped block that broke its caller's `RETURN`, and `SSHAPE` ignoring a subscript -- become history rather than warnings, and Step 3 no longer claims a sprite is the *only* way to put a picture up, only the one that costs nothing. **Neither `.bas` listing changes, and both READMEs now say why.** The character game still writes `CW# = 16` and the artwork game still keeps six brick stamps in six scalars. What those listings are worth is being what a program written against those constraints looks like, with comments explaining what each one was avoiding -- rewriting them to pretend the problems never existed would throw that away. So Chapter 17 Step 2 shows the `RGR(3)`/`RWINDOW` form and says plainly that the listing beside it does not use it. That is the one place in either chapter where a quoted fragment is not verbatim from the game. Also: the closing pointers now say which entries are struck and which stand, and the budgets table in Chapter 18 notes that several of those budgets were tighter when the game was written. TODO.md section 9 item 3 is updated rather than struck: the `WINDOW` half is fixed, the default text area owning the whole window is not, and whether that default is right is a question rather than a defect. Verified against the fixed interpreter: both games run 45 seconds on the SDL frontend with no error line and the score climbing; both suites are green in both configurations; `docs_examples` passes and `docs_screenshots --check` re-renders all thirteen figures byte-identically; coverage is 95.0% of lines against the 90 gate, with src/variable.c at 100%; and every other quoted fragment still appears verbatim in the listing it came from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
817 lines
26 KiB
Markdown
817 lines
26 KiB
Markdown
# 18. Tutorial: Breakout with artwork
|
|
|
|
[Chapter 17](17-tutorial-breakout.md) built Breakout out of text and two `DATA` sprites.
|
|
This chapter builds it again out of **downloaded artwork**, with powerups, a coloured HUD
|
|
and three voices of sound — and almost nothing about the shape of the program survives the
|
|
change. 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 `-` — all still apply here and are not
|
|
repeated.
|
|
|
|
```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 |
|
|
|
|
---
|
|
|
|
## Step 1: Put the artwork on the screen
|
|
|
|
`SPRSAV` loads an image file straight into a sprite slot, and a sprite loaded that way
|
|
keeps **the image's own size** rather than being forced to 24 by 21 — see
|
|
[Chapter 8](08-sprites.md#from-an-image-file).
|
|
|
|
```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. The path is tried against
|
|
the working directory first and then against the directory the program was loaded from,
|
|
so a `.bas` stored beside its `art/` runs from anywhere.
|
|
|
|
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/);
|
|
`examples/breakout/sprites/art/PROVENANCE.md` records which file is used for what.
|
|
Crediting Kenney is not required by CC0 — do it anyway.
|
|
|
|
**`SPRITE n, 1, 2` turns a sprite on in colour 2.** A sprite's colour *multiplies* the
|
|
artwork rather than replacing it, so colour 2 (white) is what leaves the artwork looking
|
|
like itself.
|
|
|
|
## Step 2: Budget the eight sprite slots before you write anything else
|
|
|
|
There are eight sprites. That is not a limit you will design your way around, so decide
|
|
what they are first:
|
|
|
|
| Slot | Is |
|
|
|---|---|
|
|
| 1 | the HUD strip — a captured drawing |
|
|
| 2 | the playing field — a captured drawing |
|
|
| 3 | the paddle |
|
|
| 4 | the catcher bar (the purple gem) |
|
|
| 5, 6, 7 | up to three balls |
|
|
| 8 | the falling gem |
|
|
|
|
Two of the eight are **the screen**, and Step 3 is why. That leaves six for everything
|
|
else, which is the reason **the bricks are drawn rather than made of artwork** — sixty of
|
|
them will not fit in six slots, and there is no way to get artwork onto the screen other
|
|
than a sprite. `GSHAPE` cannot stamp a sprite and `SPRSAV` cannot read one back out.
|
|
|
|
It is also the reason for "one gem at a time": there is one slot for it, so a brick
|
|
broken while a gem is falling drops nothing.
|
|
|
|
## Step 3: Turn what you drew into a sprite
|
|
|
|
In the standalone SDL build the text layer repaints every row of the window, opaque,
|
|
after your program's steps have run and before the frame is presented — so by default
|
|
**anything `DRAW`, `BOX` or `CIRCLE` puts on the screen is painted over before anybody
|
|
sees it**. Sprites are drawn after the text layer, which makes a sprite the one thing a
|
|
program can rely on being visible without doing anything else about it.
|
|
|
|
`WINDOW 0, 0, 49, 1` would hand the rest of the window to the drawing verbs. This program
|
|
does not, and would not want to: a drawing has to be re-issued every frame to survive
|
|
double buffering *and* fit inside one 256-line batch to survive the capture, which is
|
|
Step 4. A sprite has neither constraint — it is drawn from state the interpreter keeps.
|
|
|
|
So: **draw it, capture it with `SSHAPE`, install the capture with `SPRSAV`.** Once it is
|
|
a sprite it stays on the screen for nothing.
|
|
|
|
(The figures in this chapter are rendered by a tool that draws no text layer, which is
|
|
why they can show a bare drawing at all.)
|
|
|
|
```basic norun
|
|
SSHAPE Z$, 0, 60, 800, 600
|
|
SPRSAV Z$, 2
|
|
SPRITE 2, 1, 2
|
|
MOVSPR 2, 0, 60
|
|
```
|
|
|
|
Four lines, and they are the last four of every draw routine in the game. `Z$` holds a
|
|
handle rather than pixels — see [Chapter 6](06-graphics.md#saving-and-stamping-regions) —
|
|
which is all `SPRSAV` needs.
|
|
|
|
### Stamp the bricks; do not paint them
|
|
|
|
Draw one brick per colour, capture the six of them, and stamp them with `GSHAPE`:
|
|
|
|
```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
|
|
```
|
|
|
|

|
|
|
|
Six 68 by 16 bricks, filled **two scan lines at a time** so the set costs about a hundred
|
|
and thirty lines rather than the two hundred and seventy a line-at-a-time loop would take.
|
|
Step 4 explains why that number matters.
|
|
|
|
**`PAINT` would be one statement instead of sixteen and is not an option.** It costs
|
|
nearly four milliseconds a call; sixty of those is seven frames.
|
|
|
|
`SSHAPE` has sixteen slots, nothing gives one back, and `GRAPHIC 5` gives back all of
|
|
them at once. So the game counts what it has spent and rebuilds the stamps from scratch
|
|
whenever the pool runs dry:
|
|
|
|
```basic norun
|
|
LABEL DRAWJOB
|
|
IF SHN# < 14 THEN GOTO DRAWJOB2
|
|
GOSUB DRAWPROTOS
|
|
RETURN
|
|
```
|
|
|
|
### Flatten the field before you draw it
|
|
|
|
The draw routine should not be deciding anything. When a brick breaks, walk the grid and
|
|
write out a **list of the bricks still standing**, row by row — so a row is a contiguous
|
|
run of that list, and drawing it is a stamp and an advance:
|
|
|
|
```basic norun
|
|
LABEL BUILDLIVE
|
|
LN# = 0
|
|
FOR R# = 0 TO 5
|
|
RS#(R#) = LN#
|
|
RC#(R#) = 0
|
|
GOSUB BUILDROW
|
|
NEXT R#
|
|
RETURN
|
|
|
|
LABEL BUILDROW
|
|
FOR C# = 0 TO 9
|
|
IF BRK#(R# * 10 + C#) > 0 THEN BEGIN
|
|
LX#(LN#) = BRKX# + C# * 72
|
|
LY#(LN#) = BRKY# + R# * 24
|
|
LN# = LN# + 1
|
|
RC#(R#) = RC#(R#) + 1
|
|
BEND
|
|
NEXT C#
|
|
RETURN
|
|
```
|
|
|
|
`RS#(R#)` is where row `R#`'s run starts and `RC#(R#)` is how long it is. This costs about
|
|
four lines a brick and has all the time in the world; the draw costs two and has a
|
|
deadline. **That trade is the spine of this program** and it comes back in Step 6 for the
|
|
lettering.
|
|
|
|
Now the whole screen, drawn and captured and dressed with artwork:
|
|
|
|
```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 last four lines is a drawing nobody would ever see. `SSHAPE` and
|
|
`SPRSAV` are what make it the screen.
|
|
|
|
The game keeps its six stamps in **six separate scalars** rather than an array, which it
|
|
had to when it was written — see the fourth trap at the end of this chapter. An array
|
|
works now.
|
|
|
|
## Step 4: Find the frame boundary
|
|
|
|
The host runs **256 source lines and then presents the frame**, and presenting throws the
|
|
drawing buffer away. So everything between a `GRAPHIC 1, 1` and its `SSHAPE` has to
|
|
happen inside one of those batches. Draw more than that and the capture comes back
|
|
holding only the tail of what you drew, over whatever the frame before it left behind —
|
|
which looks exactly like a ghost.
|
|
|
|
`TI#` is refreshed from the host's clock once per batch, so **the step on which `TI#`
|
|
changes is the first step of a batch**. Spinning until it changes is the only way a
|
|
program in this dialect can locate a frame boundary:
|
|
|
|
```basic norun
|
|
LABEL PACE
|
|
LASTT# = TI#
|
|
LABEL PACEEDGE
|
|
IF TI# - LASTT# < 2 THEN GOTO PACEEDGE
|
|
RETURN
|
|
```
|
|
|
|
Two jiffies is thirty frames a second. **`LASTT#` is sampled on entry rather than carried
|
|
over from the last frame, and that is the whole correctness of the routine.** Carried
|
|
over, a frame whose work ran long finds the time already spent, returns immediately from
|
|
somewhere in the middle of a batch, and the capture that follows is ruined. Sampling here
|
|
means the loop always sees `TI#` change under it, and a change is only ever seen on the
|
|
first step of a batch.
|
|
|
|
Measured on one machine: after a jiffy edge, 220 lines of drawing survive the capture
|
|
intact and 250 do not. Every draw routine in the game is written to stay near 200.
|
|
|
|
**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate
|
|
and nothing else. Only drawing has a deadline — which is what makes Step 3's flattening
|
|
and Step 6's stroke lists worth their complexity.
|
|
|
|
## Step 5: Do at most one capture per frame
|
|
|
|
The frame loop paces first, then draws at most one thing, then plays the game:
|
|
|
|
```basic norun
|
|
LABEL FRAME
|
|
GOSUB PACE
|
|
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
|
|
```
|
|
|
|
`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 SHN# < 14 THEN GOTO DRAWJOB2
|
|
GOSUB DRAWPROTOS
|
|
RETURN
|
|
LABEL DRAWJOB2
|
|
IF DPLAY# = 0 THEN GOTO DRAWJOB3
|
|
GOSUB DRAWPLAY
|
|
DPLAY# = 0
|
|
RETURN
|
|
LABEL DRAWJOB3
|
|
IF DHUD# = 0 THEN RETURN
|
|
GOSUB DRAWHUD
|
|
DHUD# = 0
|
|
RETURN
|
|
```
|
|
|
|
The game sets `DPLAY# = 1` or `DHUD# = 1` when something changes and never draws
|
|
directly. One consequence is visible and deliberate: **the score lags the bricks by one
|
|
frame**, because the field goes first. At thirty frames a second nobody can see it.
|
|
|
|
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and when this was written that
|
|
was not a style choice: a loop inside a block that was skipped left the interpreter with
|
|
no `GOSUB` to return from. That is fixed — see trap 3 below — and the shape is kept
|
|
because it costs nothing and reads the same.
|
|
|
|
## Step 6: Draw the lettering, because text has no colour
|
|
|
|
The interpreter's text sink is white and has no verb that changes it; `CHAR` parses a
|
|
colour argument and ignores it. A coloured HUD therefore has to be *drawn*, which means
|
|
carrying a font.
|
|
|
|
The one 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 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. The game reads a character to a glyph number with
|
|
`INSTR(ALPHA$, MID(TX$, TXI#, 1))` over a 41-character alphabet, which is why the order of
|
|
the `DATA` lines matters.
|
|
|
|
**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
|
|
SSHAPE Z$, 0, 0, 800, 60
|
|
SPRSAV Z$, 1
|
|
SPRITE 1, 1, 2
|
|
MOVSPR 1, 0, 0
|
|
RETURN
|
|
```
|
|
|
|
Each stroke carries its own colour source, 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.
|
|
|
|
The one-stroke case is written out beside the loop. That is the second trap below, and
|
|
every loop over a list in this program has it.
|
|
|
|
## Step 7: Bounce the ball without a square root
|
|
|
|
There is no `SQR` in this dialect, so the game never computes a magnitude. Eight landing
|
|
zones across the bar, each holding **very nearly a unit vector**, and a velocity is
|
|
always 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
|
|
```
|
|
|
|
```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
|
|
RETURN
|
|
```
|
|
|
|
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 the SLOW gem a
|
|
**ratio of two speeds** rather than a change of magnitude:
|
|
|
|
```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** variable on purpose: an integer one holds the 0.75 of a slowdown as
|
|
0 and stops the ball dead. That is the first trap below, and it is the expensive one.
|
|
|
|
Scaling by the vertical component instead — which is what this routine did first — is not
|
|
a slowdown at all. A shallow ball's small vertical gets stretched up to the new speed and
|
|
drags the large horizontal with it, so SLOW made the ball *faster*. Sixty degrees of the
|
|
eight zones are shallow enough to do it.
|
|
|
|
Brick collision reflects off whichever face the ball has less of itself past, which is the
|
|
standard box resolution and the reason a ball clipping the end of a row goes sideways
|
|
instead of straight back down:
|
|
|
|
```basic norun
|
|
T1# = RGT# : IF BX1# + 67 < T1# THEN T1# = BX1# + 67
|
|
T2# = LFT# : IF BX1# > T2# THEN T2# = BX1#
|
|
T3# = BOT# : IF BY1# + 15 < T3# THEN T3# = BY1# + 15
|
|
T4# = TOP# : IF BY1# > T4# THEN T4# = BY1#
|
|
IF T1# - T2# < T3# - T4# THEN BLVX%(B#) = 0.0 - BLVX%(B#)
|
|
IF T1# - T2# >= T3# - T4# THEN BLVY%(B#) = 0.0 - BLVY%(B#)
|
|
```
|
|
|
|
`T1#` to `T4#` are the overlapping rectangle; its width against its height is the whole
|
|
test. A ball can only be over four cells at once, so the cells are worked out from its box
|
|
rather than by walking sixty bricks.
|
|
|
|
## Step 8: Gems and powerups
|
|
|
|
A gem is one sprite, one type number and two timers. The type picks the artwork, the
|
|
banner colour and what `TAKEGEM` 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 the artwork; there is no need for a slot per gem, and there was never a slot to
|
|
spare.
|
|
|
|
Every timer is a frame count decremented in one place, so an expiry is where the effect is
|
|
undone:
|
|
|
|
```basic norun
|
|
IF PDEXP# > 0 THEN BEGIN
|
|
PDEXP# = PDEXP# - 1
|
|
IF PDEXP# = 0 THEN BEGIN
|
|
PDW# = 104
|
|
SPRITE 3, 1, 2, 0, 0, 0
|
|
BEND
|
|
BEND
|
|
```
|
|
|
|
EXPAND is `SPRITE 3, 1, 2, 0, 1, 0` — the x-expand bit, which is the only scaling a sprite
|
|
has, and doubling the artwork is exactly what it wants.
|
|
|
|
The CATCH bar **mirrors** the paddle rather than following it:
|
|
|
|
```basic norun
|
|
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
|
|
```
|
|
|
|
That is deliberate. 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` — trap one
|
|
again, and this line was wrong before it was right.
|
|
|
|
## Step 9: Three voices, and a mute that costs nothing
|
|
|
|
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. The brick tone is
|
|
pitched by the row it came from —
|
|
|
|
```basic norun
|
|
SOUND 1, 17175 - R# * 2100, 4
|
|
```
|
|
|
|
— so the top row rings at C6, each row down drops about a third, and a wall coming apart
|
|
plays itself down a scale.
|
|
|
|
Mute with `VOL 0` rather than a flag tested at eleven call sites:
|
|
|
|
```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. One line beats eleven scattered through the game.
|
|
|
|
## Five things in this dialect that do not do what they look like
|
|
|
|
Each of these cost an evening, and each was filed in `TODO.md` §9 with a reduction and
|
|
the file and line of the cause. **Three of the five have since been fixed**, and are kept
|
|
here as the history of why this program is shaped the way it is — a listing whose
|
|
comments explain what it was avoiding is worth more than one quietly rewritten to pretend
|
|
the problem never existed. The two that remain are the first and the last, and the first
|
|
is the dangerous one.
|
|
|
|
### 1. The left operand decides integer or float arithmetic
|
|
|
|
```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%)
|
|
FOR I# = 0 TO 0
|
|
PRINT "THE BODY RAN"
|
|
NEXT I#
|
|
PRINT "AFTER THE LOOP"
|
|
```
|
|
|
|
```output
|
|
INTEGER FIRST 5.600000
|
|
FLOAT FIRST 7.400000
|
|
0 - V% -6
|
|
0.0 - V% -6.400000
|
|
AFTER THE LOOP
|
|
```
|
|
|
|
**This is the dangerous one, because nothing fails.** The program computes something else
|
|
and carries on. Two live bugs in this game came from it: `SPD% = 5.6 + LEVEL# * 0.45` was
|
|
a flat 5.6, so no level ever got faster than level one; and `0 - BLVX%(B#)`, the obvious
|
|
way to reverse a ball, quantised its velocity to whole pixels on every bounce and bled
|
|
speed out of it. Neither produced a diagnostic.
|
|
|
|
**Put the float on the left, and put the answer somewhere with a `%` on it.** A float
|
|
expression landing in a `#` variable truncates.
|
|
|
|
### 2. A `FOR` whose bounds are equal does not run its body
|
|
|
|
The last two lines of that output are the second trap: `FOR I# = 0 TO 0` runs zero times.
|
|
Knowing it and remembering it while writing a loop over "the bricks still standing" are
|
|
different things, and the last brick of a row is exactly that case — which is why every
|
|
loop over a list in this program has its one-item case written out beside it:
|
|
|
|
```basic norun
|
|
LABEL STAMPROW
|
|
IF T2# < 1 THEN RETURN
|
|
IF T2# = 1 THEN GSHAPE Z$, LX#(T1#), LY#(T1#)
|
|
IF T2# < 2 THEN RETURN
|
|
FOR I# = T1# TO T1# + T2# - 1
|
|
GSHAPE Z$, LX#(I#), LY#(I#)
|
|
NEXT I#
|
|
RETURN
|
|
```
|
|
|
|
### 3. A skipped `BEGIN` block containing a loop — *fixed*
|
|
|
|
This one is here as history rather than as a warning. A loop inside a block that was not
|
|
taken used to leave a scope behind, and inside a routine the orphan sat between it and
|
|
its caller — so the `RETURN` after the block reported `RETURN outside the context of
|
|
GOSUB` from a routine that plainly *was* entered by a `GOSUB`. The error named the one
|
|
construct that was not at fault, which is why it cost an evening.
|
|
|
|
```basic
|
|
T# = 0
|
|
GOSUB DOIT
|
|
PRINT "CAME BACK"
|
|
END
|
|
LABEL DOIT
|
|
IF 1 = 0 THEN BEGIN
|
|
FOR I# = 0 TO 2
|
|
T# = T# + 1
|
|
NEXT I#
|
|
BEND
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
CAME BACK
|
|
```
|
|
|
|
`FOR` creates its environment when the line is **parsed** and the skip is decided when it
|
|
is **evaluated**; the skip now releases what parsing pushed. **The listing still guards
|
|
its loops with `GOTO`**, which is where this chapter's own history shows: written that way
|
|
because it had to be, and left that way because it works.
|
|
|
|
### 4. `SSHAPE` and `GSHAPE` ignoring a subscript — *fixed*
|
|
|
|
Also history. The handle used to land in element zero whatever subscript you wrote, and
|
|
`GSHAPE SH$(2)` stamped whatever was in `SH$(0)` — while ordinary assignment and `PRINT`
|
|
honoured the subscript, which is what made it expensive to find. The symptom was that
|
|
every brick came out the colour of the last stamp captured.
|
|
|
|
```basic requires=akgl
|
|
DIM SH$(4)
|
|
SSHAPE SH$(2), 0, 0, 61, 4
|
|
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
|
|
```
|
|
|
|
```output
|
|
[] [SHAPE:0]
|
|
```
|
|
|
|
An array of shapes works now. **The listing in `examples/` still keeps its six brick
|
|
stamps in six scalars** — `S0$` to `S5$` — because that is what it had to do, and the
|
|
field is still drawn as six runs of one colour rather than brick by brick, which turned
|
|
out to be cheaper anyway.
|
|
|
|
### 5. `READ` walks one cursor through every `DATA` item in the file
|
|
|
|
```basic
|
|
DIM T#(3)
|
|
DIM F#(3)
|
|
I# = 0
|
|
GOSUB LOADFONT
|
|
GOSUB LOADTABLE
|
|
PRINT "TABLE " + T#(0) + " " + T#(1) + " " + T#(2)
|
|
PRINT "FONT " + F#(0) + " " + F#(1) + " " + F#(2)
|
|
END
|
|
|
|
LABEL LOADTABLE
|
|
FOR I# = 0 TO 2
|
|
READ T#(I#)
|
|
NEXT I#
|
|
RETURN
|
|
|
|
LABEL LOADFONT
|
|
FOR I# = 0 TO 2
|
|
READ F#(I#)
|
|
NEXT I#
|
|
RETURN
|
|
|
|
DATA 11, 12, 13
|
|
DATA 21, 22, 23
|
|
```
|
|
|
|
```output
|
|
TABLE 21 22 23
|
|
FONT 11 12 13
|
|
```
|
|
|
|
The `DATA` written first went to whichever routine ran first, not to the one it was
|
|
written for. Two loaders means the one whose `DATA` comes first in the file has to be
|
|
called first — obvious in hindsight, and not obvious when the symptom is a font table
|
|
full of brick colours. `RESTORE` to a label is the way out when the order cannot be
|
|
arranged; Chapter 17 uses it to pick a level layout.
|
|
|
|
## The budgets
|
|
|
|
This program sits close to four 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 | 8 |
|
|
| Variables | 128 | 121, plus 4 the interpreter makes |
|
|
| Labels | 64 | 61 |
|
|
| `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 6 stamps plus 1 capture a frame |
|
|
| Value-pool slots | 4096 for arrays and structures; a scalar costs none | six arrays, declared once — see [Chapter 17](17-tutorial-breakout.md#step-3-declare-the-names-a-subroutine-has-to-answer-through) |
|
|
| Scopes | 32 | 6 deep at most |
|
|
| Tokens on a line | 32, and the 33rd kills the interpreter rather than raising | short lines, temporaries instead of long conditions |
|
|
|
|
That is also why several things are **not** in the game, and they are worth naming
|
|
honestly 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.
|
|
|
|
**Several of these budgets were tighter when this was written.** A scalar cost a pool
|
|
slot, a saved shape could not live in an array, and a loop inside a skipped block leaked
|
|
a scope — so a program had to spend variables to avoid all three. The listing has not
|
|
been rewritten to take the room back, because what it does instead is show its working.
|
|
|
|
## 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 14](14-architecture.md)** explains the step loop and the pools this chapter
|
|
keeps running into, from the interpreter's side.
|
|
- `TODO.md` §9 is the eight defects this game found, each with a reduction and the cause.
|
|
Six are fixed and struck; the two that stand are the left-operand rule, which is a
|
|
decision rather than a defect, and the batch tear, which is the host's to own.
|