Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m23s
akbasic CI Build / sanitizers (push) Failing after 4m32s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m29s
The assembly step still carried the pre-conversion FILLROW, without the SOLID registration Step 8 spends a section teaching -- a reader following the assembly order got a wall the ball passes straight through. DRAWHUD cleared the graphics layer and never stamped the HBL$ eraser Step 6 tells you to make. Both together took the field down every time a digit in the HUD changed. It is GRAPHIC 1, 0 and a GSHAPE of HBL$ at 0, 0, and the difference between that and the routines that draw the field is now stated rather than left to be discovered. Also say where HUDTEXT is called from, which nothing did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2189 lines
60 KiB
Markdown
2189 lines
60 KiB
Markdown
# 18. Tutorial: Breakout with artwork
|
||
|
||
This chapter builds Breakout a second time, out of loaded PNG artwork, with a drawn
|
||
brick field, five powerups, three voices of sound and a HUD in colour. It is a bigger
|
||
program than [Chapter 17](17-tutorial-breakout.md)'s and it uses a completely different set
|
||
of verbs: everything here is drawn or loaded, and nothing is written into the text grid.
|
||
|
||
This is what you are building:
|
||
|
||

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

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

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

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

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

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