Add two Breakout examples and the tutorials that build them
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Two complete games in `examples/breakout/`, both 100% BASIC: `characters/` draws its wall in the text grid with two `DATA` sprites for the ball and paddle, and `sprites/` loads CC0 artwork and captures its whole screen with `SSHAPE`/`SPRSAV`. They take opposite shapes for reasons that are entirely this interpreter's, which is what the chapters are for. `docs/17-tutorial-breakout.md` and `docs/18-tutorial-breakout-artwork.md` build each one a step at a time, and end in a checklist of the rules a real program runs into: create every name before the loop starts, write a text row whole, loop with `GOTO` rather than `DO`, put the float on the left. Every trap is a runnable block with its own output rather than a claim -- the value pool dying at four thousand names, the skipped `BEGIN` block that breaks its caller's `RETURN`, `SSHAPE` ignoring a subscript, `READ`'s single cursor. Five figures, generated from the listings beside them by `docs_screenshots`, and a `breakout_art` setup so the ones that load artwork load the example's own. The character game's wall cannot be photographed -- the screenshot host omits the text layer on purpose -- so it is shown as compared output instead. `docs/07-sound.md` never said `SOUND`'s frequency is a SID register value rather than hertz, which both games depend on. It says so now, with the conversion from `src/audio_tables.c:84`. `TODO.md` gains the thirteen defects the two games turned up -- §6 items 30 to 33 and all of §9 -- each with a reduction that fits on a screen, the file and line of the cause, and what a fix would touch. Verified: `docs_examples` passes in both build configurations, `docs_screenshots --check` re-renders all thirteen figures and byte-compares them, the full 109-test suite passes in both builds, every quoted fragment was checked to appear verbatim in the listing it came from, and every relative link and anchor resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,12 @@ name, and a machine with no sound card still runs the interpreter — only `SOUN
|
||||
Voice, frequency, duration. Voices are numbered from 1. The duration is in *jiffies* —
|
||||
sixtieths of a second — as on a C128, so 60 is one second.
|
||||
|
||||
**The frequency is a SID register value, not hertz.** As on a C128 the pitch is
|
||||
`frequency * 1022730 / 16777216` — the NTSC system clock over the oscillator's 24-bit
|
||||
accumulator — so the 4000 above is about 244 Hz, and the argument ranges from 0 to
|
||||
65535. Work the notes you need out once and write the arithmetic down beside them: 4298
|
||||
is C4, 8579 is C5, 17175 is C6. `PLAY` takes note names and does the conversion for you.
|
||||
|
||||
Further arguments give a frequency sweep: a step, a direction and a range, which is
|
||||
what makes a siren or a laser.
|
||||
|
||||
|
||||
763
docs/17-tutorial-breakout.md
Normal file
763
docs/17-tutorial-breakout.md
Normal file
@@ -0,0 +1,763 @@
|
||||
# 17. Tutorial: Breakout
|
||||
|
||||
This chapter builds a complete game — three lives, six rows of bricks, three level
|
||||
layouts, a high score and an attract mode that plays itself — out of nothing but the
|
||||
verbs in the earlier chapters. The finished listing is
|
||||
[`examples/breakout/characters/breakout.bas`](../examples/breakout/characters/breakout.bas),
|
||||
and it is worth having open beside this.
|
||||
|
||||
**The ball and the paddle are sprites; the bricks, the HUD and the messages are
|
||||
characters in the text grid.** That split is the first decision and everything else
|
||||
follows from it, so it is Step 1.
|
||||
|
||||
Run the finished game with the SDL build:
|
||||
|
||||
```sh norun
|
||||
$ ./build-akgl/basic examples/breakout/characters/breakout.bas
|
||||
```
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| left / right | move the paddle |
|
||||
| space | start a game, then launch the ball |
|
||||
| P | pause |
|
||||
| Q or escape | quit |
|
||||
|
||||
Chapter 18 builds the same game again out of sprite artwork, and takes a completely
|
||||
different shape. Read this one first: it is the smaller of the two and it is where the
|
||||
rules that constrain both are explained.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Decide what is a sprite and what is a character
|
||||
|
||||
Two layers of this interpreter are redrawn from state it keeps for you, every frame,
|
||||
without your program doing anything:
|
||||
|
||||
- the **text grid**, repainted row by row by the akgl sink, and
|
||||
- the **sprites**, walked slot by slot after it.
|
||||
|
||||
The drawing verbs are not one of them. `DRAW`, `BOX`, `CIRCLE` and `PAINT` go straight
|
||||
into the renderer's back buffer, and the text layer paints over the whole window on its
|
||||
way past — so in the standalone SDL build **nothing you draw with them is ever visible**.
|
||||
That is recorded as a defect (`TODO.md` §9 item 3) rather than a design, but it is the
|
||||
interpreter you have.
|
||||
|
||||
So this game uses no drawing verb at all. Anything that has to move smoothly is a sprite;
|
||||
anything that can live on a 16-pixel grid is text. Ball and paddle move; bricks, score
|
||||
and messages do not.
|
||||
|
||||
**If you want artwork on the screen, that rule flips** and the whole architecture changes
|
||||
with it. That is Chapter 18.
|
||||
|
||||
## Step 2: Measure the screen once, at the top
|
||||
|
||||
```basic norun
|
||||
CW# = 16
|
||||
CH# = 16
|
||||
SCW# = RGR(1)
|
||||
SCH# = RGR(2)
|
||||
COLS# = SCW# / CW#
|
||||
ROWS# = SCH# / CH#
|
||||
```
|
||||
|
||||
`RGR(1)` and `RGR(2)` are the window in pixels — see
|
||||
[Chapter 6](06-graphics.md#rgr) — and the game asks for them rather than assuming 800 by
|
||||
600, so it survives a host that opens a different window.
|
||||
|
||||
**The cell size is a constant and has to be**, which is the one thing here you cannot
|
||||
derive. `WINDOW` knows the grid, but the standalone frontend's tee sink never offers it
|
||||
(`TODO.md` §9 item 3 again), so a program has no way to ask. 16 by 16 is the bundled
|
||||
C64_Pro_Mono at 16 points. Change the font or the point size and change these two with
|
||||
it; everything below is derived from them, including the ball's speed relative to the
|
||||
wall.
|
||||
|
||||
Lay the rest of the geometry out in the same block — the wall in cells, the play area in
|
||||
pixels:
|
||||
|
||||
```basic norun
|
||||
BRW# = 4
|
||||
BCOLS# = 10
|
||||
BROWS# = 6
|
||||
BTOP# = 4
|
||||
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
|
||||
TOPY# = 2 * CH#
|
||||
MAXX# = SCW# - 8
|
||||
PW# = 144
|
||||
PY# = 528
|
||||
```
|
||||
|
||||
A brick is four cells wide, so ten of them are forty cells, and `BLEFT#` centres that in
|
||||
whatever `COLS#` turned out to be. Integer division truncates here, which is exactly what
|
||||
a cell index wants.
|
||||
|
||||
## Step 3: Create every name before the game starts
|
||||
|
||||
This is the rule that decides whether your game runs for four minutes or for ever, and it
|
||||
is worth getting straight before you write a single subroutine.
|
||||
|
||||
**A name that is created costs value-pool slots that are never handed back.** The pool is
|
||||
a bump allocator with 4096 slots for the life of the run. Scope exit *does* destroy
|
||||
variables — a `GOSUB`'s locals go when it returns — but their slots are not reclaimed, so
|
||||
recreating the same local on the next call takes fresh ones.
|
||||
|
||||
Here is the whole defect on one screen:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
? 8 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
|
||||
|
||||
```
|
||||
|
||||
Six thousand calls, one new name each, and it dies on the four-thousand-and-somethingth
|
||||
— naming the line that was unlucky rather than the line that was wrong. A game loop that
|
||||
creates one name per tick is dead in half a minute. This one was, and it took twenty-five
|
||||
seconds to do it.
|
||||
|
||||
The fix is one line, moved:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
LOC# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
OK 6000
|
||||
```
|
||||
|
||||
So **declare everything at the top** — variables, scratch temporaries, loop counters, all
|
||||
of it — and after that the program only ever stores into names that already exist:
|
||||
|
||||
```basic norun
|
||||
DIM BR#(60)
|
||||
DIM LAY$(6)
|
||||
DIM BSG$(6)
|
||||
SCORE# = 0
|
||||
LIVES# = 3
|
||||
BX# = 0
|
||||
BY# = 0
|
||||
HIT# = 0
|
||||
BI# = 0
|
||||
I# = 0
|
||||
R# = 0
|
||||
C# = 0
|
||||
KI# = 0
|
||||
```
|
||||
|
||||
`I#`, `R#`, `C#` and `KI#` are in that list because a `FOR` counter is a name like any
|
||||
other: name one that does not exist yet and every entry to the loop spends slots.
|
||||
|
||||
The same block does a second job. A `GOSUB` gets its own scope, and assignment walks up
|
||||
the chain to find an outer name — but a name *first seen* inside a subroutine is created
|
||||
in that subroutine and dies at `RETURN`. `HIT#` and `BI#` are how `HITTEST` answers its
|
||||
caller, so they have to exist before anybody calls it. See
|
||||
[Chapter 4](04-control-flow.md#goto-and-gosub) for the scope rules.
|
||||
|
||||
This is filed as `TODO.md` §6 item 30. Until it is fixed, the declaration block is not a
|
||||
style preference — it is the price of a program that runs.
|
||||
|
||||
## Step 4: Build the wall, and write a row whole
|
||||
|
||||
Levels are `DATA`, one string per row, `1` for a brick and `0` for a hole:
|
||||
|
||||
```basic norun
|
||||
LABEL LAY1
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
|
||||
LABEL LAY2
|
||||
DATA "0011111100"
|
||||
DATA "0111111110"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "0111111110"
|
||||
DATA "0011111100"
|
||||
```
|
||||
|
||||
`RESTORE` takes a label — it wants a line number and a label evaluates to one — so a
|
||||
layout is just a named place in the `DATA` stream, and adding a fourth level is a block
|
||||
and one `IF`:
|
||||
|
||||
```basic norun
|
||||
LABEL LOADLAY
|
||||
N# = MOD((LEVEL# - 1), 3)
|
||||
IF N# = 0 THEN RESTORE LAY1
|
||||
IF N# = 1 THEN RESTORE LAY2
|
||||
IF N# = 2 THEN RESTORE LAY3
|
||||
FOR R# = 0 TO 5
|
||||
READ LAY$(R#)
|
||||
NEXT R#
|
||||
RETURN
|
||||
```
|
||||
|
||||
Now draw it — and here is the second rule that shapes this listing:
|
||||
|
||||
**`CHAR` terminates the row where it stops.** A row of the grid is a C string, so writing
|
||||
at column N erases everything from N+1 on, and writing *past* the terminator of a short
|
||||
row draws nothing at all. There is no way to poke one character into the middle of a row
|
||||
and leave the rest alone.
|
||||
|
||||
So a row is rebuilt whole and written from column 0, in one `CHAR`:
|
||||
|
||||
```basic
|
||||
DIM LAY$(6)
|
||||
DIM BSG$(6)
|
||||
BSG$(0) = "[##]"
|
||||
BSG$(1) = "[##]"
|
||||
BSG$(2) = "[==]"
|
||||
BSG$(3) = "[==]"
|
||||
BSG$(4) = "[--]"
|
||||
BSG$(5) = "[--]"
|
||||
LAY$(0) = "1111111111"
|
||||
LAY$(1) = "1111111111"
|
||||
LAY$(2) = "1101111011"
|
||||
LAY$(3) = "1111111111"
|
||||
LAY$(4) = "1011110111"
|
||||
LAY$(5) = "0110110111"
|
||||
S$ = ""
|
||||
T$ = ""
|
||||
R# = 0
|
||||
C# = 0
|
||||
FOR R# = 0 TO 5
|
||||
GOSUB BUILDROW
|
||||
PRINT S$
|
||||
NEXT R#
|
||||
END
|
||||
|
||||
LABEL BUILDROW
|
||||
S$ = " "
|
||||
FOR C# = 0 TO 9
|
||||
T$ = MID(LAY$(R#), C#, 1)
|
||||
IF T$ = "1" THEN S$ = S$ + BSG$(R#)
|
||||
IF T$ = "0" THEN S$ = S$ + " "
|
||||
NEXT C#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
[##][##][##][##][##][##][##][##][##][##]
|
||||
[##][##][##][##][##][##][##][##][##][##]
|
||||
[==][==] [==][==][==][==] [==][==]
|
||||
[==][==][==][==][==][==][==][==][==][==]
|
||||
[--] [--][--][--][--] [--][--][--]
|
||||
[--][--] [--][--] [--][--][--]
|
||||
```
|
||||
|
||||
That block runs anywhere, with or without a graphics device, because `PRINT` and `CHAR`
|
||||
are building the same string. In the game the last line is `CHAR 1, 0, R2#, S$` instead,
|
||||
and the leading spaces are `" " * BLEFT#`.
|
||||
|
||||
**The rows are told apart by shape, not by colour.** `CHAR` parses a colour argument and
|
||||
ignores it; the sink draws in one colour. `[##]`, `[==]` and `[--]` are what makes a
|
||||
60-point row look different from a 10-point one.
|
||||
|
||||
Killing a brick clears its cell and redraws that one row:
|
||||
|
||||
```basic norun
|
||||
LABEL KILLBR
|
||||
BR#(BI#) = 0
|
||||
LEFTN# = LEFTN# - 1
|
||||
PTS# = (BROWS# - BR2#) * 10
|
||||
SCORE# = SCORE# + PTS#
|
||||
GOSUB DRAWBR
|
||||
GOSUB DRAWHUD
|
||||
IF LEFTN# < 1 THEN STATE# = 2
|
||||
RETURN
|
||||
```
|
||||
|
||||
`LEFTN#` is counted up when the wall is built and down when a brick goes, so "is the
|
||||
level clear" is a comparison rather than a scan of sixty cells.
|
||||
|
||||
## Step 5: Make the ball and the paddle
|
||||
|
||||
`SPRSAV` takes an **integer array** of 63 bytes: three per row, twenty-one rows, most
|
||||
significant bit leftmost. A clear bit is transparent — see
|
||||
[Chapter 8](08-sprites.md#from-data).
|
||||
|
||||
Two patterns are all this game needs. The ball is an 8 by 8 disc in the *top-left corner*
|
||||
of the pattern, so the sprite's position and its pixel rectangle are the same thing and
|
||||
no offset arithmetic is needed anywhere. The paddle is a solid 24 by 6 bar with `SPRITE`'s
|
||||
x-expand bit set, which makes it 48 wide; three of them laid end to end are one 144-pixel
|
||||
paddle.
|
||||
|
||||
```basic requires=akgl screenshot=breakout-paddle size=240x120
|
||||
DIM SB#(63)
|
||||
DIM SP#(63)
|
||||
I# = 0
|
||||
D# = 0
|
||||
FOR I# = 0 TO 62
|
||||
SB#(I#) = 0
|
||||
SP#(I#) = 0
|
||||
NEXT I#
|
||||
FOR I# = 0 TO 7
|
||||
READ D#
|
||||
SB#(I# * 3) = D#
|
||||
NEXT I#
|
||||
FOR I# = 0 TO 17
|
||||
SP#(I#) = 255
|
||||
NEXT I#
|
||||
SPRSAV SB#, 1
|
||||
SPRSAV SP#, 2
|
||||
SPRSAV SP#, 3
|
||||
SPRSAV SP#, 4
|
||||
SPRITE 1, 1, 2
|
||||
SPRITE 2, 1, 15, 0, 1, 0
|
||||
SPRITE 3, 1, 15, 0, 1, 0
|
||||
SPRITE 4, 1, 15, 0, 1, 0
|
||||
MOVSPR 1, 108, 40
|
||||
MOVSPR 2, 48, 80
|
||||
MOVSPR 3, 96, 80
|
||||
MOVSPR 4, 144, 80
|
||||
|
||||
DATA 60, 126, 255, 255, 255, 255, 126, 60
|
||||
```
|
||||
|
||||

|
||||
|
||||
The three segments meet with no seam, which is the whole point of the arrangement. The
|
||||
game writes the same 63 bytes out as `DATA` a row at a time, with the bit pattern drawn
|
||||
in a comment beside it; the loops above are the same two patterns spelled shorter so the
|
||||
figure fits on a page.
|
||||
|
||||
Moving them is four `MOVSPR`s, and each coordinate is worked out into a variable first:
|
||||
|
||||
```basic norun
|
||||
LABEL SHOWSPR
|
||||
MOVSPR 1, BX#, BY#
|
||||
MOVSPR 2, PX#, PY#
|
||||
X2# = PX# + 48
|
||||
MOVSPR 3, X2#, PY#
|
||||
X3# = PX# + 96
|
||||
MOVSPR 4, X3#, PY#
|
||||
RETURN
|
||||
```
|
||||
|
||||
**Do not write `MOVSPR 3, PX# + 48, PY#`.** `MOVSPR` reads a leading sign as *move by this
|
||||
much* rather than *move to here* — see [Chapter 8](08-sprites.md#showing-and-moving) — and
|
||||
an expression that starts with one is the relative form. Computing into `X2#` first is
|
||||
unambiguous.
|
||||
|
||||
Sprites are positioned by their top-left corner in **device pixels**, so ball, paddle and
|
||||
walls are all plain pixel arithmetic. Only the bricks are in cells, and exactly one
|
||||
routine converts between the two (Step 8).
|
||||
|
||||
## Step 6: Write the main loop as a `GOTO` loop
|
||||
|
||||
```basic norun
|
||||
LABEL TICK
|
||||
GOSUB READKEY
|
||||
IF STATE# <> 0 THEN GOTO DISPATCH
|
||||
IF PAUSED# = 1 THEN GOTO TICKEND
|
||||
GOSUB MOVEPAD
|
||||
IF HELD# = 1 THEN GOSUB HOLDBAL
|
||||
IF HELD# = 0 THEN GOSUB MOVEBAL
|
||||
GOSUB SHOWSPR
|
||||
LABEL TICKEND
|
||||
SLEEP 0.02
|
||||
GOTO TICK
|
||||
|
||||
LABEL DISPATCH
|
||||
IF STATE# = 1 THEN GOTO LOSTLIF
|
||||
IF STATE# = 2 THEN GOTO LEVELUP
|
||||
IF STATE# = 3 THEN GOTO BYE
|
||||
IF STATE# = 5 THEN GOTO NEWGAME
|
||||
STATE# = 0
|
||||
GOTO TICK
|
||||
```
|
||||
|
||||
**`DO ... LOOP` would be the natural way to write this and it is the wrong one.** Every
|
||||
loop and every `GOSUB` pushes one of 32 scopes, and a state change that jumped out of a
|
||||
`DO` — losing a life, clearing a level — would leak its scope every single time. Thirty-two
|
||||
lives later the program stops. A `GOTO` loop pushes nothing at all, so the deepest chain
|
||||
in play is the six `GOSUB`s of a brick hit.
|
||||
|
||||
Nothing here `GOSUB`s into the state changes either. They are branch targets that run and
|
||||
then `GOTO TICK`, so the stack is empty again by the time the next frame starts.
|
||||
|
||||
`SLEEP 0.02` is the frame pacing, and **it does not block**: it records a deadline and
|
||||
the step loop declines to advance the program until the clock reaches it, so the host
|
||||
goes on drawing frames and pumping the keyboard the whole time. Fifty ticks a second is
|
||||
what that asks for. Chapter 18 needs a much sharper instrument than this one and builds
|
||||
it out of `TI#`.
|
||||
|
||||
## Step 7: Read the keyboard through a countdown
|
||||
|
||||
`GET` is the only non-blocking read there is. There is **no key-up event and no held-key
|
||||
state** — what you get instead is the host's own key repeat arriving as ordinary key-down
|
||||
events, about one a tick while a key is held down.
|
||||
|
||||
So drain the queue every tick, and let a keystroke refill a countdown:
|
||||
|
||||
```basic norun
|
||||
LABEL READKEY
|
||||
FOR KI# = 1 TO 8
|
||||
GET K#
|
||||
IF K# <> 0 THEN GOSUB HANDKEY
|
||||
NEXT KI#
|
||||
RETURN
|
||||
|
||||
LABEL HANDKEY
|
||||
IF K# = 1073741904 THEN PDIR# = 0 - 1
|
||||
IF K# = 1073741904 THEN PDEC# = 12
|
||||
IF K# = 1073741903 THEN PDIR# = 1
|
||||
IF K# = 1073741903 THEN PDEC# = 12
|
||||
IF K# = 32 THEN GOSUB KEYSPC
|
||||
IF K# = 112 THEN GOSUB KEYPAU
|
||||
IF K# = 113 THEN STATE# = 3
|
||||
IF K# = 27 THEN STATE# = 3
|
||||
RETURN
|
||||
```
|
||||
|
||||
```basic norun
|
||||
LABEL MOVEPAD
|
||||
IF PDEC# < 1 THEN GOTO PADCLMP
|
||||
PDEC# = PDEC# - 1
|
||||
PX# = PX# + (PDIR# * PSPD#)
|
||||
LABEL PADCLMP
|
||||
IF PX# < 0 THEN PX# = 0
|
||||
M# = SCW# - PW#
|
||||
IF PX# > M# THEN PX# = M#
|
||||
RETURN
|
||||
```
|
||||
|
||||
The paddle coasts to a stop over twelve ticks when the repeats stop, so **held reads as
|
||||
held and tapped reads as a nudge** — with no held-key state anywhere. 1073741903 and
|
||||
1073741904 are the right and left arrows; the codes are the host's, and Chapter 12's
|
||||
`GET` entry says where they come from.
|
||||
|
||||
Note `0 - 1` rather than `-1`. Unary minus on a literal is fine, but the habit of writing
|
||||
the subtraction out is worth forming here: this parser reads `a - b + c` as `a - (b + c)`,
|
||||
so every mixed `+` and `-` in the game is parenthesised as a rule rather than where it
|
||||
happens to matter. It also matches only **one** unparenthesised `AND` or `OR` per
|
||||
expression. Both are in [Chapter 13](13-differences.md).
|
||||
|
||||
## Step 8: Move the ball one axis at a time
|
||||
|
||||
```basic norun
|
||||
LABEL MOVEBAL
|
||||
OX# = BX#
|
||||
BX# = BX# + BVX#
|
||||
IF BX# < 0 THEN GOSUB WALLL
|
||||
IF BX# > MAXX# THEN GOSUB WALLR
|
||||
GOSUB XBRICK
|
||||
OY# = BY#
|
||||
BY# = BY# + BVY#
|
||||
IF BY# < TOPY# THEN GOSUB WALLT
|
||||
GOSUB YBRICK
|
||||
GOSUB PADHIT
|
||||
IF BY# > LOSEY# THEN STATE# = 1
|
||||
RETURN
|
||||
```
|
||||
|
||||
X first and then Y, **each move tested on its own**, so a ball arriving at the corner of
|
||||
a brick reverses one axis rather than both. `OX#` and `OY#` remember where it came from,
|
||||
which is what a collision backs it out to.
|
||||
|
||||
The brick test converts a pixel to a cell, and it is the only place in the program that
|
||||
does:
|
||||
|
||||
```basic norun
|
||||
LABEL HITTEST
|
||||
HIT# = 0
|
||||
RC# = TY# / CH#
|
||||
IF RC# < BTOP# THEN RETURN
|
||||
RB# = RC# - BTOP#
|
||||
IF RB# > (BROWS# - 1) THEN RETURN
|
||||
CC2# = TX# / CW#
|
||||
IF CC2# < BLEFT# THEN RETURN
|
||||
CB# = (CC2# - BLEFT#) / BRW#
|
||||
IF CB# > (BCOLS# - 1) THEN RETURN
|
||||
BI# = (RB# * BCOLS#) + CB#
|
||||
BR2# = RB#
|
||||
IF BR#(BI#) = 1 THEN HIT# = 1
|
||||
RETURN
|
||||
```
|
||||
|
||||
Callers hand it a point in `TX#`/`TY#` and read `HIT#`, `BI#` and `BR2#` back out — which
|
||||
works only because Step 3 created all three outside the routine.
|
||||
|
||||
**Test the ball's leading edge, not its centre:**
|
||||
|
||||
```basic norun
|
||||
LABEL XBRICK
|
||||
TX# = BX#
|
||||
IF BVX# > 0 THEN TX# = BX# + 7
|
||||
TY# = BY# + 4
|
||||
GOSUB HITTEST
|
||||
IF HIT# = 0 THEN RETURN
|
||||
BX# = OX#
|
||||
BVX# = 0 - BVX#
|
||||
GOSUB KILLBR
|
||||
RETURN
|
||||
```
|
||||
|
||||
A ball tested by its centre sinks four pixels into a brick before it turns, and it looks
|
||||
like the brick was hit late. The cost of the leading edge is that a brick clipped at the
|
||||
very corner can be missed by up to seven pixels' worth of ball; that is the better trade
|
||||
of the two, and it is worth knowing which one you took.
|
||||
|
||||
## Step 9: Make the paddle bounce mean something
|
||||
|
||||
```basic norun
|
||||
LABEL PADHIT
|
||||
IF BVY# < 1 THEN RETURN
|
||||
BB# = BY# + 8
|
||||
IF BB# < PY# THEN RETURN
|
||||
IF BY# > (PY# + 10) THEN RETURN
|
||||
RX# = PX# + PW#
|
||||
IF (BX# + 8) < PX# THEN RETURN
|
||||
IF BX# > RX# THEN RETURN
|
||||
BY# = PY# - 8
|
||||
BVY# = 0 - BSPD#
|
||||
Z# = ((BX# + 4) - PX#) / (PW# / 5)
|
||||
IF Z# < 0 THEN Z# = 0
|
||||
IF Z# > 4 THEN Z# = 4
|
||||
PVX# = BVX#
|
||||
BVX# = (Z# - 2) * 3
|
||||
IF BVX# <> 0 THEN GOTO PADAIM
|
||||
BVX# = 2
|
||||
IF PVX# < 0 THEN BVX# = 0 - 2
|
||||
LABEL PADAIM
|
||||
GOSUB BEEPP
|
||||
RETURN
|
||||
```
|
||||
|
||||
Five zones across the face and the zone decides the angle: `Z#` is 0 to 4, so the new
|
||||
horizontal speed is -6, -3, 0, +3, +6. Where you catch it decides where it goes, which is
|
||||
the entire game.
|
||||
|
||||
The dead-centre case needs its own handling. A ball with **no** sideways speed rises and
|
||||
falls down one column for ever, which is how attract mode used to hang itself — so the
|
||||
middle zone keeps the direction the ball came in on and gives it a shallow angle instead
|
||||
of zero.
|
||||
|
||||
There is a watchdog for the same failure in a subtler form. Ten seconds without touching
|
||||
a brick means the ball has found an orbit that misses the wall, so it gets a new angle —
|
||||
**at the next paddle bounce, never in mid-air.** Changing it in flight looks exactly like
|
||||
the ball hitting something that is not there:
|
||||
|
||||
```basic norun
|
||||
LABEL UNSTICK
|
||||
NUDGE# = 0
|
||||
STALL# = 0
|
||||
RMAX# = 4
|
||||
GOSUB RANDOM
|
||||
BVX# = (RND# * 3) - 6
|
||||
IF BVX# = 0 THEN BVX# = 3
|
||||
RETURN
|
||||
```
|
||||
|
||||
`RANDOM` is a linear congruential generator, because **this dialect has no `RND`** — and
|
||||
no `INT`, `SQR`, `ASC` or `TIMER` either. `TI#` is jiffies off the host's clock and is
|
||||
uptime rather than zero-based, which makes it a fine seed:
|
||||
|
||||
```basic norun
|
||||
LABEL RANDOM
|
||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||
RND# = MOD((SEED# / 65536), RMAX#)
|
||||
RETURN
|
||||
```
|
||||
|
||||
The multiply stays inside a 64-bit integer for any seed under 2^31, and the answer comes
|
||||
from the middle bits because the low bits of a power-of-two modulus barely move.
|
||||
Integer division truncates for free, which is the `INT` you do not have.
|
||||
|
||||
## Step 10: The HUD and the messages
|
||||
|
||||
Same rule as the wall: build the whole line, write it once.
|
||||
|
||||
```basic
|
||||
V# = 0
|
||||
P$ = ""
|
||||
H$ = ""
|
||||
SCORE# = 1250
|
||||
LIVES# = 3
|
||||
LEVEL# = 2
|
||||
HIGH# = 9900
|
||||
V# = SCORE#
|
||||
GOSUB PAD6
|
||||
H$ = " SCORE " + P$
|
||||
H$ = (H$ + " LIVES ") + LIVES#
|
||||
H$ = (H$ + " LEVEL ") + LEVEL#
|
||||
V# = HIGH#
|
||||
GOSUB PAD6
|
||||
H$ = (H$ + " HIGH ") + P$
|
||||
PRINT H$
|
||||
END
|
||||
|
||||
LABEL PAD6
|
||||
P$ = "" + V#
|
||||
DO WHILE LEN(P$) < 6
|
||||
P$ = "0" + P$
|
||||
LOOP
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
SCORE 001250 LIVES 3 LEVEL 2 HIGH 009900
|
||||
```
|
||||
|
||||
`"" + V#` is how a number becomes a string: `+` concatenates a string with a number, and
|
||||
that is the conversion — see [Chapter 5](05-strings-and-formatting.md). Every `+` after
|
||||
the first is parenthesised, for the reason in Step 7.
|
||||
|
||||
A centred message is the same idea, and needs no padding on the right because `CHAR`
|
||||
terminating the row is what erases the rest of it:
|
||||
|
||||
```basic norun
|
||||
LABEL SHOWAT
|
||||
L# = LEN(MSG$)
|
||||
C2# = (COLS# - L#) / 2
|
||||
S$ = MSG$
|
||||
IF C2# > 0 THEN S$ = (" " * C2#) + MSG$
|
||||
CHAR 1, 0, MROW#, S$
|
||||
RETURN
|
||||
|
||||
LABEL CLRAT
|
||||
CHAR 1, 0, MROW#, " "
|
||||
RETURN
|
||||
```
|
||||
|
||||
Clearing a message is a single space written at column 0 — one character, and the
|
||||
terminator behind it takes the rest of the row with it.
|
||||
|
||||
## Step 11: Ask for sound once, then believe the answer
|
||||
|
||||
`SOUND` refuses on a machine with no audio device, and an untrapped refusal ends the
|
||||
game. Ask once at startup, record the answer, and never ask again:
|
||||
|
||||
```basic requires=noakgl
|
||||
SND# = 1
|
||||
TRAP NOAUDIO
|
||||
SOUND 1, 2000, 1
|
||||
TRAP
|
||||
IF SND# = 1 THEN PRINT "SOUND IS AVAILABLE"
|
||||
IF SND# = 0 THEN PRINT "NO AUDIO DEVICE, PLAYING SILENTLY"
|
||||
END
|
||||
|
||||
LABEL NOAUDIO
|
||||
SND# = 0
|
||||
RESUME NEXT
|
||||
```
|
||||
|
||||
```output
|
||||
NO AUDIO DEVICE, PLAYING SILENTLY
|
||||
```
|
||||
|
||||
That output is the stdio build, which has no devices at all. On the SDL build the same
|
||||
program prints the other line. `TRAP` with no argument disarms the handler again — leave
|
||||
it armed and the next refusal anywhere in the game is silently swallowed. See
|
||||
[Chapter 4](04-control-flow.md#trapping-errors).
|
||||
|
||||
After that every sound is guarded by the flag it recorded:
|
||||
|
||||
```basic norun
|
||||
LABEL BEEPB
|
||||
IF SND# = 1 THEN SOUND 1, 12000, 2
|
||||
RETURN
|
||||
```
|
||||
|
||||
**`SOUND`'s frequency argument is a SID register value, not hertz** — the pitch is
|
||||
`register * 1022730 / 16777216`, and [Chapter 7](07-sound.md#sound) has the arithmetic.
|
||||
Work the notes out once and write them down in a comment beside the numbers.
|
||||
|
||||
## Step 12: Give it an attract mode, and test with it
|
||||
|
||||
The title screen plays itself, and this is not decoration — it is how the game gets
|
||||
tested without a hand on the keyboard. `DEMO#` is the only difference between the demo
|
||||
and a real game:
|
||||
|
||||
```basic norun
|
||||
LABEL DEMOPAD
|
||||
TX# = (BX# + 4) - (PW# / 2)
|
||||
TX# = TX# + DOFF#
|
||||
D# = TX# - PX#
|
||||
IF D# > PSPD# THEN D# = PSPD#
|
||||
M# = 0 - PSPD#
|
||||
IF D# < M# THEN D# = M#
|
||||
PX# = PX# + D#
|
||||
RETURN
|
||||
|
||||
LABEL DEMOAIM
|
||||
RMAX# = 81
|
||||
GOSUB RANDOM
|
||||
DOFF# = RND# - 40
|
||||
RETURN
|
||||
```
|
||||
|
||||
The demo paddle tracks the ball but **aims off-centre by a random amount**, refreshed at
|
||||
every bounce, so it plays like something with a hand on it and occasionally misses. A
|
||||
demo that tracks perfectly never loses a ball and therefore never tests losing one.
|
||||
|
||||
Leave it running for two minutes and watch the score climb: that exercises the ball, the
|
||||
bricks, the level change, the speed-up and — most usefully — the value pool from Step 3,
|
||||
which is the failure that only shows up after thousands of ticks.
|
||||
|
||||
The machine does not get on the scoreboard. Attract mode keeps score so the wall behaves,
|
||||
but only a player's score is ever the high one:
|
||||
|
||||
```basic norun
|
||||
LABEL HISCORE
|
||||
IF SCORE# > HIGH# THEN HIGH# = SCORE#
|
||||
RETURN
|
||||
```
|
||||
|
||||
There is nowhere to save it. There is no disk in this interpreter — see
|
||||
[Chapter 9](09-files-and-disk.md) — so the high score lasts as long as the process does.
|
||||
|
||||
## The rules this listing never breaks
|
||||
|
||||
Every one of these was learned by breaking it. They are the checklist to write your own
|
||||
game against:
|
||||
|
||||
| Rule | Because | Where |
|
||||
|---|---|---|
|
||||
| Create every name at the top, loop counters included | A name created inside a scope costs pool slots for good; 4096 ends the run | Step 3 |
|
||||
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | Step 4 |
|
||||
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | Step 4 |
|
||||
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | Step 5 |
|
||||
| Loop the game with `GOTO`, not `DO` | 32 scopes, and jumping out of a loop leaks one | Step 6 |
|
||||
| Parenthesise every mixed `+` and `-` | `a - b + c` parses as `a - (b + c)` | Step 7 |
|
||||
| One `AND` or `OR` per expression unless parenthesised | The parser matches one | Step 7 |
|
||||
| Keep a line under 32 tokens | The 33rd kills the interpreter with a stack trace, not a BASIC error you can `TRAP` | — |
|
||||
| Span a loop across lines, and never `FOR I = 1 TO 1` | A whole loop on one line does not loop; equal bounds run zero times | [Ch 13](13-differences.md) |
|
||||
| Probe for a device once and remember | An untrapped refusal ends the program | Step 11 |
|
||||
|
||||
## Where to go next
|
||||
|
||||
- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again with sprite
|
||||
artwork, powerups and a coloured HUD. Every architectural decision comes out different,
|
||||
and the chapter is about why.
|
||||
- **[Chapter 13](13-differences.md)** is the full list of what this dialect does
|
||||
differently from BASIC 7.0.
|
||||
- **[Chapter 14](14-architecture.md)** is the interpreter itself — the step loop, the
|
||||
pools and how to debug a program that stops for no visible reason.
|
||||
- `TODO.md` §6 and §9 carry the defects this game found, each with a reduction that fits
|
||||
on a screen and what a fix would touch.
|
||||
796
docs/18-tutorial-breakout-artwork.md
Normal file
796
docs/18-tutorial-breakout-artwork.md
Normal file
@@ -0,0 +1,796 @@
|
||||
# 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 again that is not a style
|
||||
choice — a `RETURN` inside a block leaves the interpreter with no `GOSUB` to return from.
|
||||
The last trap in this chapter is exactly that.
|
||||
|
||||
## 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 breaks the enclosing `RETURN`
|
||||
|
||||
```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
|
||||
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
|
||||
|
||||
```
|
||||
|
||||
The routine plainly *was* called by a `GOSUB`. `FOR` creates its environment when the
|
||||
line is **parsed**, and the block skip is decided when it is **evaluated**, so a skipped
|
||||
loop pushes a scope that its skipped `NEXT` never pops — and the orphan sits between the
|
||||
routine and its caller. Outside a routine the same thing exhausts the pool of 32 after
|
||||
thirty-two skips instead.
|
||||
|
||||
Guard loops with `GOTO` rather than wrapping them in a block, which is what every draw
|
||||
routine in this game does.
|
||||
|
||||
### 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, none reclaimed | none after startup — see [Chapter 17](17-tutorial-breakout.md#step-3-create-every-name-before-the-game-starts) |
|
||||
| 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.
|
||||
@@ -13,6 +13,10 @@ everything that behaves differently here and why. **[Chapter 14](14-architecture
|
||||
the odd one out: it is about the interpreter rather than the language, for anyone
|
||||
embedding it, debugging it or changing it.
|
||||
|
||||
**[Chapters 17](17-tutorial-breakout.md)** and **[18](18-tutorial-breakout-artwork.md)**
|
||||
are tutorials rather than reference: they build one complete game twice, two different
|
||||
ways, and are where the rules that a real program runs into are collected.
|
||||
|
||||
## Chapters
|
||||
|
||||
| | |
|
||||
@@ -33,6 +37,8 @@ embedding it, debugging it or changing it.
|
||||
| **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it |
|
||||
| **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints |
|
||||
| **[16. Structures](16-structures.md)** | `TYPE`, records, strict pointers, and sharing a C struct with an embedding host |
|
||||
| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, a step at a time |
|
||||
| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn HUD |
|
||||
|
||||
## The shortest possible start
|
||||
|
||||
|
||||
BIN
docs/images/breakout-artwork.png
Normal file
BIN
docs/images/breakout-artwork.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/images/breakout-lettering.png
Normal file
BIN
docs/images/breakout-lettering.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 451 B |
BIN
docs/images/breakout-paddle.png
Normal file
BIN
docs/images/breakout-paddle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 262 B |
BIN
docs/images/breakout-screen.png
Normal file
BIN
docs/images/breakout-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/images/breakout-stamps.png
Normal file
BIN
docs/images/breakout-stamps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 388 B |
Reference in New Issue
Block a user