`akbasic_parse_for()` and `akbasic_parse_do()` create their environment while the line is *parsed*; whether to skip it is decided afterwards, when the line is evaluated. So a loop inside a block that was not taken pushed a scope, its body was skipped, and the `NEXT` or `LOOP` that would have popped it was skipped too. Nothing else ever would. At the top level that exhausted the pool after thirty-two skips. Inside a routine it was far more confusing: the orphan sat between the routine 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` -- naming the one construct that was not at fault, which is why it cost an evening to find. The skip now releases what parsing pushed. **Narrower than it first looks.** Releasing on any skip breaks tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas: a zero-iteration `FOR` skips its body by the same mechanism, and there the orphan is load-bearing -- it absorbs the inner `NEXT` so the outer `NEXT` still finds its own `FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the release is conditional on the skip being a *block* skip, which is decidable because nothing inside a skipped block ever runs to arm a `NEXT` wait. Both halves are asserted side by side in tests/structure_verbs.c, the second one citing the golden case that caught it. The forty-skip case names its own step budget: a skipped line is not free, and forty passes over a five-line block cost about 2700 steps against the shared runner's 2000. Chapter 18's trap 3 becomes history rather than a warning, and the note in Step 5 that called `GOTO`-guarded loops "not a style choice" now says why the shape is kept anyway. TODO.md section 9 item 2, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
799 lines
25 KiB
Markdown
799 lines
25 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 **anything
|
|
`DRAW`, `BOX` or `CIRCLE` puts on the screen is painted over before anybody sees it**.
|
|
Sprites are drawn after the text layer. A sprite is the only thing on the screen a
|
|
program can rely on being visible. (`TODO.md` §9 item 3; the figures in this chapter are
|
|
rendered by a tool that omits the text layer, which is why they can show a drawing at
|
|
all.)
|
|
|
|
That leaves exactly one way to put a picture up: **draw it, capture it with `SSHAPE`,
|
|
install the capture with `SPRSAV`.**
|
|
|
|
```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, and that
|
|
is not a style choice — see the fourth trap at the end of this chapter.
|
|
|
|
## 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. All five are filed in `TODO.md` §9 with a reduction and
|
|
the file and line of the cause.
|
|
|
|
### 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` ignore the subscript on a string array
|
|
|
|
```basic requires=akgl
|
|
DIM SH$(4)
|
|
SSHAPE SH$(2), 0, 0, 61, 4
|
|
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
|
|
```
|
|
|
|
```output
|
|
[SHAPE:0] []
|
|
```
|
|
|
|
The handle lands in element zero whatever subscript you write, and `GSHAPE SH$(2)` then
|
|
stamps whatever is in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so
|
|
this is the two verbs reading their leaf directly rather than evaluating it. The symptom
|
|
is that every brick comes out the colour of the last stamp captured.
|
|
|
|
**Keep saved shapes in separate scalars.** The six brick stamps are `S0$` to `S5$` for
|
|
this reason, and the field is 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.
|
|
|
|
## 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, the cause and
|
|
what a fix would touch.
|