All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m29s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / coverage (push) Successful in 3m56s
akbasic CI Build / akgl_build (push) Successful in 8m34s
akbasic CI Build / mutation_test (push) Successful in 18m50s
1871 lines
50 KiB
Markdown
1871 lines
50 KiB
Markdown
# 17. Tutorial: Breakout
|
|
|
|
This chapter builds a complete game from an empty file: three lives, six rows of bricks,
|
|
three level layouts, a score, a high score, sound, and a title screen that plays itself.
|
|
Nothing is loaded from disk — the ball and the paddle are drawn from numbers in the
|
|
listing, and the wall, the HUD and the messages are characters written into the text grid.
|
|
|
|
This is what you are building:
|
|
|
|

|
|
|
|
The finished listing is
|
|
[`examples/breakout/characters/breakout.bas`](../examples/breakout/characters/breakout.bas).
|
|
You do not need it to follow along, but it is the same program assembled, and it is worth
|
|
opening once you have your own running.
|
|
|
|
Run it 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 |
|
|
|
|
## What you will do
|
|
|
|
- **[Step 1](#step-1-get-a-program-onto-the-screen)** — get a program onto the screen, and
|
|
find out which build you need
|
|
- **[Step 2](#step-2-measure-the-screen)** — measure the screen and work the geometry out
|
|
from what you measured
|
|
- **[Step 3](#step-3-declare-every-name-first)** — declare every name the program will use,
|
|
before anything uses it
|
|
- **[Step 4](#step-4-make-the-ball-and-the-paddle)** — make the ball and the paddle out of
|
|
sprite data
|
|
- **[Step 5](#step-5-build-the-brick-wall)** — build the brick wall out of characters, and
|
|
keep a grid saying which bricks are still there
|
|
- **[Step 6](#step-6-write-the-main-loop)** — write the main loop and the frame pacing
|
|
- **[Step 7](#step-7-read-the-keyboard)** — read the keyboard without blocking
|
|
- **[Step 8](#step-8-move-the-paddle)** — move the paddle and keep it on the screen
|
|
- **[Step 9](#step-9-move-the-ball)** — move the ball and bounce it off the walls
|
|
- **[Step 10](#step-10-break-bricks)** — find out which brick the ball hit, and break it
|
|
- **[Step 11](#step-11-bounce-off-the-paddle)** — bounce off the paddle at an angle the
|
|
player chooses
|
|
- **[Step 12](#step-12-draw-the-hud-and-the-messages)** — draw the HUD and the messages
|
|
- **[Step 13](#step-13-lives-levels-and-game-over)** — handle lives, levels and game over
|
|
- **[Step 14](#step-14-add-sound)** — add sound, on a machine that may not have any
|
|
- **[Step 15](#step-15-add-an-attract-mode)** — add a title screen that plays itself
|
|
- **[Step 16](#step-16-put-the-program-together)** — put the pieces in one file, in the
|
|
right order
|
|
|
|
Each step is a piece you can type in and run. Work through them in order, and Step 16 is
|
|
where they become one program.
|
|
|
|
---
|
|
|
|
## Step 1: Get a program onto the screen
|
|
|
|
**Goal: a program that runs, and a build that can draw.**
|
|
|
|
A BASIC program here is a text file. Line numbers are optional, so a file is just
|
|
statements, one per line:
|
|
|
|
```basic
|
|
PRINT "HELLO"
|
|
END
|
|
```
|
|
|
|
```output
|
|
HELLO
|
|
```
|
|
|
|
Save that as `first.bas` and run it:
|
|
|
|
```sh norun
|
|
$ ./build/basic first.bas
|
|
```
|
|
|
|
There are two builds and the difference matters for this whole chapter. `./build/basic` is
|
|
the plain one: it reads and writes text and has **no graphics device, no sprites and no
|
|
sound**. `./build-akgl/basic` opens a window and has all three. A game needs the second.
|
|
|
|
Ask for something that needs a device and you get a refusal naming what is missing rather
|
|
than a crash:
|
|
|
|
```basic requires=noakgl
|
|
PRINT "BEFORE"
|
|
SPRITE 1, 1, 2
|
|
PRINT "AFTER"
|
|
```
|
|
|
|
```output
|
|
BEFORE
|
|
? 2 : RUNTIME ERROR SPRITE needs a sprite device and this runtime has none
|
|
|
|
```
|
|
|
|
That is worth seeing once, because it is how the finished game behaves if you start it
|
|
with the wrong build: it stops at the first graphics verb and tells you why.
|
|
|
|
For the rest of this chapter, run everything with `./build-akgl/basic`.
|
|
|
|
Two more things to know before writing anything longer.
|
|
|
|
**Every variable carries a type suffix.** `A#` is an integer, `A%` is a float, `A$` is a
|
|
string. There is no such thing as a plain `A` — a bare word is a label. This is different
|
|
from Commodore BASIC, where `A%` is the integer.
|
|
|
|
**`+` joins a string to a number.** `"SCORE " + 40` is `"SCORE 40"`, and `"" + N#` is how
|
|
you turn a number into a string. There is no `STR$`.
|
|
|
|
```basic
|
|
N# = 40
|
|
PRINT "SCORE " + N#
|
|
END
|
|
```
|
|
|
|
```output
|
|
SCORE 40
|
|
```
|
|
|
|
## Step 2: Measure the screen
|
|
|
|
**Goal: every position in the game derived from the window's real size, so the game fits
|
|
whatever window and font the player has.**
|
|
|
|
Do not guess the size of anything. Four functions tell you what you have:
|
|
|
|
| Call | Gives |
|
|
|---|---|
|
|
| `RGR(1)` | the window's width in pixels |
|
|
| `RGR(2)` | the window's height in pixels |
|
|
| `RGR(3)` | the width of one character cell, in pixels |
|
|
| `RGR(4)` | the height of one character cell |
|
|
| `RWINDOW(0)` | the text grid's height, in rows |
|
|
| `RWINDOW(1)` | the text grid's width, in columns |
|
|
|
|
Put them at the very top of the program:
|
|
|
|
```basic norun
|
|
SCW# = RGR(1)
|
|
SCH# = RGR(2)
|
|
CW# = RGR(3)
|
|
CH# = RGR(4)
|
|
COLS# = RWINDOW(1)
|
|
ROWS# = RWINDOW(0)
|
|
```
|
|
|
|
`RGR(1)` is first on purpose. It is the first statement in the program that needs a
|
|
graphics device, so on the wrong build this is the line that refuses, and it refuses
|
|
before anything else has happened.
|
|
|
|
Now derive the layout from those numbers rather than writing pixel positions down. The
|
|
wall is measured in **character cells**, because it is made of characters:
|
|
|
|
```basic norun
|
|
BRW# = 4
|
|
BCOLS# = 10
|
|
BROWS# = 6
|
|
BTOP# = 4
|
|
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
|
|
```
|
|
|
|
A brick is four cells wide and there are ten of them, so the wall is forty cells across.
|
|
`BLEFT#` centres those forty in however many columns there turned out to be. `BTOP#` is
|
|
the row the wall starts on. Integer division truncates, which is exactly what a column
|
|
index wants.
|
|
|
|
The ball and the paddle are sprites, and **sprites are positioned in pixels**, so the play
|
|
area is measured in pixels:
|
|
|
|
```basic norun
|
|
TOPY# = 2 * CH#
|
|
MAXX# = SCW# - 8
|
|
PW# = 144
|
|
PY# = 528
|
|
LOSEY# = PY# + 32
|
|
PSPD# = 10
|
|
MSGROW# = 35
|
|
TITROW# = 20
|
|
```
|
|
|
|
`TOPY#` is the ceiling the ball bounces off, two rows down from the top so it clears the
|
|
HUD line. `MAXX#` is as far right as the ball can go — the window less the ball's own
|
|
eight pixels. `PW#` is the paddle's width and `PY#` the row of pixels it sits on.
|
|
`MSGROW#` and `TITROW#` are text rows, counted from the top, for messages.
|
|
|
|
**Cells and pixels are two different coordinate systems, and they meet in exactly one
|
|
place** — Step 5, where each brick's cell is turned into the rectangle `SOLID` registers.
|
|
Everywhere else, bricks are in cells and everything that moves is in pixels.
|
|
|
|
## Step 3: Declare every name first
|
|
|
|
**Goal: a subroutine that can hand an answer back to its caller.**
|
|
|
|
A `GOSUB` gets its own scope. That has one consequence you must design around:
|
|
|
|
- Assigning to a name that already exists **outside** the subroutine walks up and finds it.
|
|
The change is visible to the caller.
|
|
- Assigning to a name that has **never been seen before** creates it inside the
|
|
subroutine. It disappears at `RETURN`, and the caller reads zero — with no error and no
|
|
warning of any kind.
|
|
|
|
So a subroutine cannot answer through a name it invented itself. Declare the names first,
|
|
at the top of the program, and then any routine can write to them:
|
|
|
|
```basic
|
|
X# = 0
|
|
GOSUB SUBA
|
|
PRINT "THE CALLER SEES " + X#
|
|
END
|
|
|
|
LABEL SUBA
|
|
X# = 99
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
THE CALLER SEES 99
|
|
```
|
|
|
|
Take `X# = 0` off the top of that program and it prints `0`.
|
|
|
|
This is why the game opens with a block that names everything before the first `GOSUB`.
|
|
Arrays go in the same block. Here is the start of it — the names the later steps refer to
|
|
most; [Step 16](#step-16-put-the-program-together) has the finished block, all 66 of them,
|
|
and you can paste that in now if you would rather not come back to it:
|
|
|
|
```basic norun
|
|
DIM BR#(60)
|
|
DIM SB#(63)
|
|
DIM SP#(63)
|
|
DIM LAY$(6)
|
|
DIM BSG$(6)
|
|
|
|
SCORE# = 0
|
|
HIGH# = 0
|
|
LIVES# = 3
|
|
LEVEL# = 1
|
|
LEFTN# = 0
|
|
BSPD# = 4
|
|
STATE# = 0
|
|
HELD# = 0
|
|
PX# = 0
|
|
BX# = 0
|
|
BY# = 0
|
|
BVX# = 0
|
|
BVY# = 0
|
|
HIT# = 0
|
|
BI# = 0
|
|
BR2# = 0
|
|
```
|
|
|
|
`HIT#`, `BI#` and `BR2#` are the three the rule is really about: they are how the
|
|
collision test in Step 10 answers the routine that called it.
|
|
|
|
**`DIM` belongs at the top and nowhere else.** An array declared inside a `GOSUB` or a
|
|
loop takes storage from a pool of 4096 elements that is never given back, so a `DIM` that
|
|
runs repeatedly will eventually end the program:
|
|
|
|
```basic
|
|
X# = 0
|
|
FOR T# = 1 TO 6000
|
|
GOSUB SUBA
|
|
NEXT T#
|
|
PRINT "OK " + X#
|
|
END
|
|
|
|
LABEL SUBA
|
|
DIM LOC#(4)
|
|
LOC#(0) = 1
|
|
X# = X# + LOC#(0)
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
? 8 : RUNTIME ERROR Array of 4 elements does not fit in the 0 remaining value slots
|
|
|
|
```
|
|
|
|
A plain number costs nothing, though, so you can create as many of those inside a routine
|
|
as you like:
|
|
|
|
```basic
|
|
X# = 0
|
|
FOR T# = 1 TO 20000
|
|
GOSUB SUBA
|
|
NEXT T#
|
|
PRINT "OK " + X#
|
|
END
|
|
|
|
LABEL SUBA
|
|
LOC# = 1
|
|
X# = X# + LOC#
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
OK 20000
|
|
```
|
|
|
|
Twenty thousand calls, each creating a local, and the program is fine. The rule that
|
|
survives is the narrow one: **`DIM` at the top, never inside anything.**
|
|
|
|
### Two rules about writing expressions
|
|
|
|
Both apply everywhere in this chapter, so they are worth fixing in your fingers now.
|
|
|
|
**Parenthesise any expression that mixes `+` and `-`.** Write `(A# - B#) + C#`, not
|
|
`A# - B# + C#`:
|
|
|
|
```basic
|
|
A# = 10
|
|
B# = 3
|
|
C# = 2
|
|
PRINT (A# - B#) + C#
|
|
END
|
|
```
|
|
|
|
```output
|
|
9
|
|
```
|
|
|
|
**Parenthesise a condition that uses more than one `AND` or `OR`.** Write
|
|
`IF (A# = 1 AND B# = 2) AND C# = 3 THEN`:
|
|
|
|
```basic
|
|
A# = 1
|
|
B# = 2
|
|
C# = 3
|
|
IF (A# = 1 AND B# = 2) AND C# = 3 THEN PRINT "ALL THREE"
|
|
END
|
|
```
|
|
|
|
```output
|
|
ALL THREE
|
|
```
|
|
|
|
Both habits are free, and both are being made unnecessary: the parser's additive
|
|
precedence is [`TODO.md` §6 item 35](../TODO.md) and the second `AND` is item 36 beside
|
|
it. Once those land, the plain forms will mean what they look like they mean, and
|
|
parentheses you wrote in the meantime will still be correct.
|
|
|
|
## Step 4: Make the ball and the paddle
|
|
|
|
**Goal: two sprites on the screen, in the right places.**
|
|
|
|
A sprite is a 24-by-21 pattern of pixels. `SPRSAV` loads one from an **integer array of 63
|
|
numbers**: three bytes per row, twenty-one rows, most significant bit on the left. A bit
|
|
that is 1 draws; a bit that is 0 is transparent.
|
|
|
|
```text
|
|
byte 0 byte 1 byte 2
|
|
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
|
^ column 0 ^ column 23
|
|
```
|
|
|
|
The ball is an 8-by-8 disc drawn in the **top-left corner** of the pattern. Putting it
|
|
there means the sprite's position and the ball's pixel rectangle are the same thing, so no
|
|
part of the game needs to add an offset to work out where the ball actually is:
|
|
|
|
```text
|
|
. . # # # # . . 60
|
|
. # # # # # # . 126
|
|
# # # # # # # # 255
|
|
```
|
|
|
|
The paddle is a solid bar, 24 wide and 6 deep — the full width of a sprite. `SPRITE`'s
|
|
x-expand flag doubles it to 48 pixels, and three of those laid end to end make one
|
|
144-pixel paddle with no seam.
|
|
|
|
```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
|
|
```
|
|
|
|

|
|
|
|
Read that from the middle. `SPRSAV SB#, 1` installs the ball's pattern into slot 1.
|
|
`SPRITE 1, 1, 2` turns slot 1 on in colour 2. `SPRITE 2, 1, 15, 0, 1, 0` turns slot 2 on
|
|
in colour 15 with the fourth argument after the colour — the x-expand flag — set to 1.
|
|
`MOVSPR` puts a sprite at a pixel position, measured from its top-left corner.
|
|
|
|
The finished game writes the same 63 numbers out as `DATA`, one row per line, with the
|
|
bit pattern drawn in a comment beside each, and loads them in a routine called once at
|
|
startup. [Step 16](#step-16-put-the-program-together) has that routine and both patterns
|
|
in full; the loops above are the same two patterns spelled shorter so the figure fits on a
|
|
page.
|
|
|
|
Moving the four sprites is one routine, and every coordinate is worked out into a variable
|
|
before it is passed:
|
|
|
|
```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*, and an expression beginning with a sign is the relative
|
|
form. Computing into `X2#` first says exactly what you mean. See
|
|
[Chapter 8](08-sprites.md#showing-and-moving).
|
|
|
|
## Step 5: Build the brick wall
|
|
|
|
**Goal: a wall on the screen, and a grid in memory saying which bricks are still standing.**
|
|
|
|
Two things are needed and they are separate: the *state* (which of the sixty bricks are
|
|
alive) and the *picture* (what the player sees). Keep them in step by always redrawing the
|
|
row you just changed.
|
|
|
|
### The layouts
|
|
|
|
A level is six strings of ten characters, `1` for a brick and `0` for a hole, stored as
|
|
`DATA`:
|
|
|
|
```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"
|
|
|
|
LABEL LAY3
|
|
DATA "1010101010"
|
|
DATA "0101010101"
|
|
DATA "1111001111"
|
|
DATA "1100110011"
|
|
DATA "1011111101"
|
|
DATA "0110110110"
|
|
```
|
|
|
|
`READ` takes the next `DATA` item, and `RESTORE` moves that cursor to a given place.
|
|
`RESTORE` accepts a label, so each layout is simply a named position in the `DATA` stream
|
|
and choosing one is three `IF`s:
|
|
|
|
```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
|
|
```
|
|
|
|
Adding a fourth level is a `DATA` block and one more `IF`.
|
|
|
|
**One `READ` cursor walks every `DATA` item in the file, in the order they were written**,
|
|
no matter which routine is doing the reading. If you have two things to load — a font and
|
|
a table, say — the one whose `DATA` comes first in the file must be loaded first, or use
|
|
`RESTORE` to say where to start.
|
|
|
|
### The grid
|
|
|
|
Turn those strings into a flat array of sixty numbers, and count how many bricks there
|
|
are while you are at it:
|
|
|
|
```basic norun
|
|
LABEL BUILDW
|
|
LEFTN# = 0
|
|
FOR R# = 0 TO 5
|
|
S$ = LAY$(R#)
|
|
GOSUB BUILDR
|
|
NEXT R#
|
|
RETURN
|
|
|
|
LABEL BUILDR
|
|
FOR C# = 0 TO 9
|
|
T$ = MID(S$, C#, 1)
|
|
I# = (R# * BCOLS#) + C#
|
|
BR#(I#) = 0
|
|
IF T$ = "1" THEN BR#(I#) = 1
|
|
IF T$ = "1" THEN LEFTN# = LEFTN# + 1
|
|
SX# = (BLEFT# + (C# * BRW#)) * CW#
|
|
SY# = (BTOP# + R#) * CH#
|
|
IF T$ = "1" THEN SOLID I# + 1, SX#, SY#, SX# + (BRW# * CW#), SY# + CH#
|
|
IF T$ = "0" THEN SOLID I# + 1
|
|
NEXT C#
|
|
RETURN
|
|
```
|
|
|
|
Two things happen per cell, and the second is what Step 10 is built on. `BR#()` is the
|
|
program's own record of which bricks are left. **`SOLID` tells the interpreter**, so it can
|
|
answer "did the ball hit a brick, and which one" instead of the program working it out.
|
|
|
|
The id is the array index plus one — `SOLID` numbers from 1 and the array from 0 — so a
|
|
rectangle and its array element are the same brick with no lookup between them. A hole
|
|
retires whatever was there, which matters because a level change reuses the ids.
|
|
|
|
The rectangle is in pixels, and this is the only place the cell grid and the pixel grid meet:
|
|
a brick at column `C#` starts at `(BLEFT# + C# * BRW#) * CW#` and is `BRW# * CW#` wide.
|
|
|
|
`MID` counts from **zero** here, unlike Commodore BASIC. `BR#(row * 10 + col)` is the
|
|
brick at that cell; `LEFTN#` is how many are left, so "is the level finished" is a
|
|
comparison rather than a scan of sixty cells.
|
|
|
|
### Drawing a row
|
|
|
|
`CHAR` writes a string into the text grid at a row and column. It has one property that
|
|
decides how the rest of this section is written: **`CHAR` terminates the row where it
|
|
stops.** Writing at column 12 erases everything from column 13 to the end of the line.
|
|
|
|
So do not write bricks one at a time. Build the whole row as a string and write it once,
|
|
from column 0:
|
|
|
|
```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
|
|
[##][##][##][##][##][##][##][##][##][##]
|
|
[##][##][##][##][##][##][##][##][##][##]
|
|
[==][==] [==][==][==][==] [==][==]
|
|
[==][==][==][==][==][==][==][==][==][==]
|
|
[--] [--][--][--][--] [--][--][--]
|
|
[--][--] [--][--] [--][--][--]
|
|
```
|
|
|
|
A missing brick contributes four spaces, so every row is the same length and the columns
|
|
line up. That block uses `PRINT`, so it runs on either build — which makes it a good way
|
|
to check your layouts before you have a window.
|
|
|
|
In the game, replace the `PRINT S$` with a `CHAR`, and use the real left margin:
|
|
|
|
```basic norun
|
|
LABEL DRAWBR
|
|
S$ = ""
|
|
IF BLEFT# > 0 THEN S$ = " " * BLEFT#
|
|
BSEG$ = BSG$(BR2#)
|
|
FOR CC# = 0 TO 9
|
|
I# = (BR2# * BCOLS#) + CC#
|
|
IF BR#(I#) = 1 THEN S$ = S$ + BSEG$
|
|
IF BR#(I#) = 0 THEN S$ = S$ + " "
|
|
NEXT CC#
|
|
R2# = BTOP# + BR2#
|
|
CHAR 1, 0, R2#, S$
|
|
RETURN
|
|
```
|
|
|
|
`" " * BLEFT#` repeats a string, which is how you get a margin of any width.
|
|
|
|
**Tell the rows apart by shape, not by colour.** `CHAR` accepts a colour argument and
|
|
ignores it — the text layer draws in one colour — so `[##]`, `[==]` and `[--]` are what
|
|
makes a 60-point row look different from a 10-point one.
|
|
|
|
Drawing the whole wall is then six calls:
|
|
|
|
```basic norun
|
|
LABEL DRAWW
|
|
FOR R# = 0 TO 5
|
|
BR2# = R#
|
|
GOSUB DRAWBR
|
|
NEXT R#
|
|
RETURN
|
|
```
|
|
|
|
## Step 6: Write the main loop
|
|
|
|
**Goal: a loop that runs one frame's worth of work and comes back, for as long as the game
|
|
lasts.**
|
|
|
|
Write the main loop with `LABEL` and `GOTO`:
|
|
|
|
```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
|
|
```
|
|
|
|
**A label is one bare word: letters and digits, no underscore.** `TICKEND` is a label;
|
|
`TICK_END` is a parse error, and the message you get is `UNKNOWN TOKEN _` rather than
|
|
anything about naming, so it is worth not writing in the first place. The same goes for
|
|
variable names.
|
|
|
|
`STATE#` is a plain number that the rest of the game writes when something has happened:
|
|
0 is playing, 1 is "the ball was lost", 2 is "the level is clear", 3 is "quit", 5 is
|
|
"start a new game". Nothing acts on it where it is set — the loop notices on the next pass
|
|
and branches. That keeps every state change in one place and out of the middle of the
|
|
physics.
|
|
|
|
The branch targets end in `GOTO TICK` rather than `RETURN`. Nothing `GOSUB`s into a state
|
|
change, so the subroutine stack is empty again by the time the next frame begins.
|
|
|
|
**`SLEEP 0.02` is the frame pacing, and it does not block the host.** It records a
|
|
deadline and the interpreter declines to advance your program until the clock reaches it,
|
|
so the window keeps redrawing and the keyboard keeps being read the whole time. 0.02
|
|
seconds asks for fifty frames a second.
|
|
|
|
### Why `GOTO` rather than `DO ... LOOP`
|
|
|
|
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
|
|
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 12
|
|
scopes, so a game that leaves its main loop once per lost life stops on the
|
|
twelfth one:
|
|
|
|
```basic
|
|
N# = 0
|
|
LABEL TOP
|
|
DO
|
|
N# = N# + 1
|
|
IF N# < 100 THEN GOTO TOP
|
|
LOOP UNTIL N# > 99
|
|
PRINT "SURVIVED " + N#
|
|
```
|
|
|
|
```output
|
|
? 3 : PARSE ERROR Environment pool exhausted at line 3 (12 in use)
|
|
|
|
```
|
|
|
|
A `LABEL`/`GOTO` loop pushes no scope at all, so it can run for as long as the machine is
|
|
on. Use `FOR` and `DO` freely for work that finishes inside a frame — the wall builder in
|
|
Step 5 is a `FOR` inside a `FOR` — and use `GOTO` for anything you will branch out of.
|
|
This is [`TODO.md` §6 item 37](../TODO.md); when it is fixed, `DO ... LOOP` will be
|
|
available for a main loop too, and a `GOTO` loop will still be correct.
|
|
|
|
## Step 7: Read the keyboard
|
|
|
|
**Goal: a paddle that moves smoothly while a key is held, using the only input verb there
|
|
is.**
|
|
|
|
`GET` takes one keystroke from a queue the host fills, and returns 0 if there is nothing
|
|
waiting. It never blocks. There is **no key-up event and no way to ask whether a key is
|
|
currently down**.
|
|
|
|
What you get instead is the operating system's own key repeat, arriving as ordinary
|
|
keypresses about once a frame while a key is held. So: drain the queue every frame, and
|
|
let each 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
|
|
```
|
|
|
|
Eight `GET`s a frame is enough to keep the queue from backing up. `PDIR#` is which way to
|
|
move and `PDEC#` is how many more frames to keep moving; a keypress sets both.
|
|
|
|
The key codes are the host's. 1073741903 and 1073741904 are the right and left arrows, 32
|
|
is space, 112 is `P`, 113 is `Q` and 27 is escape. [Chapter 12](12-function-reference.md)
|
|
says where they come from. A quick way to find any other key is to print what you get:
|
|
|
|
```basic norun
|
|
LABEL SHOWKEY
|
|
GET K#
|
|
IF K# <> 0 THEN PRINT "KEY " + K#
|
|
GOTO SHOWKEY
|
|
```
|
|
|
|
Note `0 - 1` rather than `-1` in `PDIR# = 0 - 1`. Both work; writing the subtraction out
|
|
is the habit that goes with the parenthesising rule from Step 3, and it is what the rest
|
|
of this chapter does.
|
|
|
|
## Step 8: Move the paddle
|
|
|
|
**Goal: the paddle moves while the countdown lasts, and stops at both edges.**
|
|
|
|
```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 countdown is the whole trick. A single tap sets `PDEC#` to 12 and the paddle coasts
|
|
for twelve frames; holding the key down refills it faster than it drains, so the paddle
|
|
keeps going. **Held reads as held and tapped reads as a nudge, with no held-key state
|
|
anywhere.**
|
|
|
|
The clamp is written as a jump to `PADCLMP` rather than wrapped around the movement,
|
|
because the paddle has to be pushed back inside the window whether it moved this frame or
|
|
not.
|
|
|
|
`M# = SCW# - PW#` is computed into a variable rather than written inline. Any expression
|
|
you are about to compare or pass is clearer that way, and it keeps lines short — **a line
|
|
here holds at most 32 tokens**, so long conditions want breaking up into named pieces
|
|
anyway.
|
|
|
|
Going over 32 currently stops the interpreter rather than raising a BASIC error you could
|
|
`TRAP`, which makes a too-long line harder to diagnose than it should be. That is recorded
|
|
in [`TODO.md` §8](../TODO.md), and the fix is to report it as the parse error it is; the
|
|
limit itself stays either way, so short lines are the habit regardless.
|
|
|
|
## Step 9: Move the ball
|
|
|
|
**Goal: a ball that moves, bounces off three walls, and is lost off the bottom.**
|
|
|
|
The ball has a position (`BX#`, `BY#`) and a velocity (`BVX#`, `BVY#`) in pixels per
|
|
frame. Moving it is adding one to the other — but do the two axes **separately**, and test
|
|
each move on its own:
|
|
|
|
```basic norun
|
|
LABEL MOVEBAL
|
|
BX# = BX# + BVX#
|
|
IF BX# < 0 THEN GOSUB WALLL
|
|
IF BX# > MAXX# THEN GOSUB WALLR
|
|
BY# = BY# + BVY#
|
|
IF BY# < TOPY# THEN GOSUB WALLT
|
|
GOSUB PADHIT
|
|
IF BY# > LOSEY# THEN STATE# = 1
|
|
RETURN
|
|
```
|
|
|
|
**Nothing here tests a brick.** Step 10 arms a handler that fires when the ball meets one,
|
|
and it pushes the ball out along the contact rather than restoring a remembered position —
|
|
so there is no "where was it before" to keep.
|
|
|
|
A wall bounce puts the ball on the wall and flips the sign of that axis:
|
|
|
|
```basic norun
|
|
LABEL WALLL
|
|
BX# = 0
|
|
BVX# = 0 - BVX#
|
|
RETURN
|
|
|
|
LABEL WALLR
|
|
BX# = MAXX#
|
|
BVX# = 0 - BVX#
|
|
RETURN
|
|
|
|
LABEL WALLT
|
|
BY# = TOPY#
|
|
BVY# = 0 - BVY#
|
|
RETURN
|
|
```
|
|
|
|
There is no wall at the bottom. Below `LOSEY#` the ball is gone, and the loop's dispatch
|
|
in Step 6 picks that up.
|
|
|
|
Draw a visible ceiling once, at startup, so the ball turns against something the player
|
|
can see:
|
|
|
|
```basic norun
|
|
FRAME$ = "=" * COLS#
|
|
CHAR 1, 0, 1, FRAME$
|
|
```
|
|
|
|
## Step 10: Break bricks
|
|
|
|
**Goal: find out which brick the ball hit, and take it out of the wall.**
|
|
|
|
Step 5 registered every brick with `SOLID`, so this is not arithmetic — it is a question.
|
|
Arm a handler:
|
|
|
|
```basic norun
|
|
COLLISION 2, BRICKHIT
|
|
```
|
|
|
|
That line goes in the setup block, once, before the frame loop starts — arming a handler is
|
|
a standing instruction, not something a frame does.
|
|
|
|
`COLLISION 2` fires when a sprite overlaps one of those rectangles. The handler runs
|
|
**between source lines**, exactly like a `GOSUB` the program did not write, and must end in
|
|
`RETURN`.
|
|
|
|
```basic norun
|
|
LABEL BRICKHIT
|
|
M# = BUMP(2)
|
|
IF (M# AND 1) = 0 THEN RETURN
|
|
T# = RCOLLISION(1, 1)
|
|
IF T# < 1 THEN RETURN
|
|
IF BR#(T# - 1) = 0 THEN RETURN
|
|
D% = RCOLLISION(1, 4)
|
|
BX# = BX# + (RCOLLISION(1, 2) * D%)
|
|
BY# = BY# + (RCOLLISION(1, 3) * D%)
|
|
A# = RCOLLISION(1, 7)
|
|
IF A# = 1 THEN BVX# = 0 - BVX#
|
|
IF A# = 2 THEN BVY# = 0 - BVY#
|
|
BI# = T# - 1
|
|
BR2# = BI# / BCOLS#
|
|
GOSUB KILLBR
|
|
RETURN
|
|
```
|
|
|
|
Read it as four questions and an answer.
|
|
|
|
**`BUMP(2)` is the mask of which sprites met static geometry**, and bit 0 is sprite 1 — the
|
|
ball. Reading it clears it, so the next hit is news again. `BUMP(1)` is a *separate*
|
|
accumulator for sprite-against-sprite, so this handler never sees the paddle.
|
|
|
|
**`RCOLLISION(1, 1)` is which rectangle.** Because Step 5 registered brick `I#` as id
|
|
`I# + 1`, that number is the array index plus one and nothing has to be looked up. The two
|
|
guards after it are worth keeping: a program can be told about a brick it has already
|
|
broken, because the handler runs a line or two after the overlap happened.
|
|
|
|
**Fields 2, 3 and 4 push the ball out.** Field 4 is how deep the overlap is and fields 2 and
|
|
3 are the direction out of the brick, so adding one times the other puts the ball exactly
|
|
clear. That is why `MOVEBAL` in Step 9 keeps no `OX#`/`OY#` backup — there is nothing to
|
|
restore to.
|
|
|
|
`D%` is a float variable, and it has to be: field 4 is a float and a `#` would throw the
|
|
fraction away. The products land in `BX#` and `BY#`, which *are* integers — and that is
|
|
safe here for a reason worth knowing rather than assuming. A box against a box gives a
|
|
normal that is exactly -1, 0 or 1, so the product is a whole number before it is stored.
|
|
Against a circle it would not be, and the push-out would land a pixel short.
|
|
|
|
**Field 7 is which axis to reverse.** It is the one the ball is least far through, which is
|
|
what makes a ball clipping the end of a row go sideways rather than straight back down.
|
|
Working it out yourself means comparing two floats, which is exactly where
|
|
[Chapter 13](13-differences.md)'s left-operand rule catches people, so it is computed for
|
|
you.
|
|
|
|
Breaking the brick clears its cell, scores it, and redraws **just that row**:
|
|
|
|
```basic norun
|
|
LABEL KILLBR
|
|
BR#(BI#) = 0
|
|
SOLID BI# + 1
|
|
LEFTN# = LEFTN# - 1
|
|
PTS# = (BROWS# - BR2#) * 10
|
|
SCORE# = SCORE# + PTS#
|
|
GOSUB DRAWBR
|
|
GOSUB DRAWHUD
|
|
IF LEFTN# < 1 THEN STATE# = 2
|
|
RETURN
|
|
```
|
|
|
|
**`SOLID BI# + 1` retires the rectangle in the same breath as clearing the array.** Miss it
|
|
and the ball goes on bouncing off a brick that is no longer drawn, which is a bewildering
|
|
thing to debug and an easy thing to forget.
|
|
|
|
The top row is worth 60 and the bottom row 10. `LEFTN#` reaching zero sets the state that
|
|
Step 6's dispatch turns into a level change.
|
|
|
|
## Step 11: Bounce off the paddle
|
|
|
|
**Goal: a bounce whose angle is decided by where on the paddle the ball landed.**
|
|
|
|
This is the part that makes it a game rather than a demonstration. Divide the paddle into
|
|
five zones and let the zone choose the horizontal speed:
|
|
|
|
```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
|
|
RETURN
|
|
```
|
|
|
|
The first five lines are the overlap test, each written so it can bail out. `Z#` comes out
|
|
0 to 4, so `(Z# - 2) * 3` gives horizontal speeds of -6, -3, 0, +3 and +6. Catch the ball
|
|
on the left of the paddle and it goes left; catch it in the middle and it goes straight
|
|
up.
|
|
|
|
**Straight up is a problem, so the middle zone does not give it.** A ball with no sideways
|
|
speed rises and falls down the same column for ever and never reaches a brick it has not
|
|
already broken. The last three lines before `PADAIM` catch that case and give the ball a
|
|
shallow angle in the direction it arrived from instead.
|
|
|
|
That handles the obvious case. The subtle one is a ball that has found some other orbit
|
|
missing the wall, and the cure for that is a watchdog. Count frames since the last brick,
|
|
and after ten seconds' worth mark the ball for a new angle. **Add these two lines to the
|
|
end of `MOVEBAL`** from Step 9, just before its `RETURN`:
|
|
|
|
```basic norun
|
|
STALL# = STALL# + 1
|
|
IF STALL# > 500 THEN NUDGE# = 1
|
|
```
|
|
|
|
and **add one line to `KILLBR`** from Step 10, so that hitting a brick resets the count:
|
|
|
|
```basic norun
|
|
STALL# = 0
|
|
```
|
|
|
|
Then apply the nudge at the **next paddle bounce**, never in mid-air — a ball that changes
|
|
direction in open space looks exactly like it hit something invisible. **Add this line to
|
|
`PADHIT` above**, immediately before `LABEL PADAIM`:
|
|
|
|
```basic norun
|
|
IF NUDGE# = 1 THEN GOSUB UNSTICK
|
|
```
|
|
|
|
```basic norun
|
|
LABEL UNSTICK
|
|
NUDGE# = 0
|
|
STALL# = 0
|
|
RMAX# = 4
|
|
GOSUB RANDOM
|
|
BVX# = (RND# * 3) - 6
|
|
IF BVX# = 0 THEN BVX# = 3
|
|
RETURN
|
|
```
|
|
|
|
### You have to write your own random numbers
|
|
|
|
**There is no `RND` in this dialect**, and no `INT`, `SQR`, `ASC` or `TIMER` either. A
|
|
linear congruential generator is nine tokens and does the job. Put the number of possible
|
|
answers in `RMAX#` and read the result from `RND#`:
|
|
|
|
```basic
|
|
SEED# = 12345
|
|
RMAX# = 6
|
|
RND# = 0
|
|
I# = 0
|
|
FOR I# = 1 TO 5
|
|
GOSUB RANDOM
|
|
PRINT "ROLL " + (RND# + 1)
|
|
NEXT I#
|
|
END
|
|
|
|
LABEL RANDOM
|
|
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
|
RND# = MOD((SEED# / 65536), RMAX#)
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
ROLL 1
|
|
ROLL 5
|
|
ROLL 2
|
|
ROLL 1
|
|
ROLL 2
|
|
```
|
|
|
|
The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is
|
|
why the modulus is that number. The answer is taken from the middle bits — `SEED# / 65536`
|
|
— because the low bits of a power-of-two modulus barely change from one call to the next.
|
|
Integer division truncating for free is the `INT` you do not have.
|
|
|
|
Seed it from the clock at startup. `TI#` is the host's uptime in sixtieths of a second,
|
|
which is different every time the game is run:
|
|
|
|
```basic norun
|
|
SEED# = TI#
|
|
```
|
|
|
|
Use `RANDOM` for the serve, too, so the ball does not always leave in the same direction:
|
|
|
|
```basic norun
|
|
LABEL SERVE
|
|
PX# = (SCW# - PW#) / 2
|
|
HELD# = 1
|
|
BX# = PX# + ((PW# / 2) - 4)
|
|
BY# = PY# - 10
|
|
RMAX# = 2
|
|
GOSUB RANDOM
|
|
BVX# = BSPD#
|
|
IF RND# = 0 THEN BVX# = 0 - BSPD#
|
|
BVY# = 0 - BSPD#
|
|
PDEC# = 0
|
|
GOSUB SHOWSPR
|
|
RETURN
|
|
```
|
|
|
|
`HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and
|
|
`HOLDBAL` keeps it there:
|
|
|
|
```basic norun
|
|
LABEL HOLDBAL
|
|
BX# = PX# + ((PW# / 2) - 4)
|
|
BY# = PY# - 10
|
|
RETURN
|
|
```
|
|
|
|
Space clears `HELD#` and the ball launches. That is the `KEYSPC` that Step 7's `HANDKEY`
|
|
calls:
|
|
|
|
```basic norun
|
|
LABEL KEYSPC
|
|
IF HELD# = 0 THEN RETURN
|
|
HELD# = 0
|
|
GOSUB CLRMSG
|
|
RETURN
|
|
```
|
|
|
|
Step 15 adds two lines to the top of it.
|
|
|
|
## Step 12: Draw the HUD and the messages
|
|
|
|
**Goal: a status line and centred messages, both drawn the way `CHAR` wants.**
|
|
|
|
Same rule as the brick rows: build the whole line as one string, then 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
|
|
```
|
|
|
|
In the game that becomes a routine called `DRAWHUD`, whose last line is
|
|
`CHAR 1, 0, 0, H$` instead of `PRINT H$`; everything else is the same, which means you can
|
|
develop the HUD on either build. [Step 16](#step-16-put-the-program-together) has it
|
|
written out.
|
|
|
|
`"" + V#` turns a number into a string, and `PAD6` pads it to six digits so the line does
|
|
not change width as the score grows. Every `+` after the first is parenthesised, for the
|
|
reason in Step 3.
|
|
|
|
A centred message needs no padding on the right, because `CHAR` terminating the row is
|
|
what erases whatever was there before:
|
|
|
|
```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
|
|
```
|
|
|
|
Set `MSG$` and `MROW#`, then `GOSUB SHOWAT`. 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.
|
|
|
|
Two small wrappers save repeating the row number:
|
|
|
|
```basic norun
|
|
LABEL SHOWMSG
|
|
MROW# = MSGROW#
|
|
GOSUB SHOWAT
|
|
RETURN
|
|
|
|
LABEL CLRMSG
|
|
MROW# = MSGROW#
|
|
GOSUB CLRAT
|
|
RETURN
|
|
```
|
|
|
|
## Step 13: Lives, levels and game over
|
|
|
|
**Goal: the states from Step 6, written out.**
|
|
|
|
Each of these is a branch target, not a subroutine. It does its work and jumps back to
|
|
`TICK`.
|
|
|
|
Losing a ball:
|
|
|
|
```basic norun
|
|
LABEL LOSTLIF
|
|
LIVES# = LIVES# - 1
|
|
GOSUB DRAWHUD
|
|
IF LIVES# < 1 THEN GOTO GAMEOVR
|
|
GOSUB SERVE
|
|
MSG$ = "BALL LOST -- PRESS SPACE"
|
|
GOSUB SHOWMSG
|
|
STATE# = 0
|
|
GOTO TICK
|
|
```
|
|
|
|
Clearing a level. The ball speeds up each time, to a ceiling:
|
|
|
|
```basic norun
|
|
LABEL LEVELUP
|
|
MSG$ = "LEVEL CLEARED"
|
|
GOSUB SHOWMSG
|
|
SLEEP 1.5
|
|
LEVEL# = LEVEL# + 1
|
|
IF BSPD# < 7 THEN BSPD# = BSPD# + 1
|
|
GOSUB NEWLEV
|
|
STATE# = 0
|
|
GOTO TICK
|
|
|
|
LABEL NEWLEV
|
|
GOSUB LOADLAY
|
|
GOSUB BUILDW
|
|
GOSUB DRAWW
|
|
GOSUB DRAWHUD
|
|
GOSUB SERVE
|
|
MSG$ = "PRESS SPACE TO LAUNCH"
|
|
GOSUB SHOWMSG
|
|
RETURN
|
|
```
|
|
|
|
Game over waits for a key in a small loop of its own:
|
|
|
|
```basic norun
|
|
LABEL GAMEOVR
|
|
MSG$ = "GAME OVER -- SPACE PLAYS AGAIN, Q QUITS"
|
|
GOSUB SHOWMSG
|
|
LABEL GOWAIT
|
|
GET K#
|
|
IF K# = 32 THEN GOTO NEWGAME
|
|
IF K# = 113 THEN GOTO BYE
|
|
IF K# = 27 THEN GOTO BYE
|
|
SLEEP 0.05
|
|
GOTO GOWAIT
|
|
```
|
|
|
|
And quitting turns the sprites off, so they are not left on the screen behind the final
|
|
message:
|
|
|
|
```basic norun
|
|
LABEL BYE
|
|
SPRITE 1, 0
|
|
SPRITE 2, 0
|
|
SPRITE 3, 0
|
|
SPRITE 4, 0
|
|
SCNCLR
|
|
PRINT "BREAKOUT"
|
|
PRINT "FINAL SCORE " + SCORE#
|
|
PRINT "HIGH SCORE " + HIGH#
|
|
SLEEP 2
|
|
QUIT
|
|
```
|
|
|
|
`SCNCLR` clears the text screen and `QUIT` ends the interpreter; both are in
|
|
[Chapter 11](11-verb-reference.md). Turning the sprites off first matters because a sprite
|
|
is drawn over the text, and a final score behind four sprites is not a final score.
|
|
|
|
The high score is one comparison, called whenever the score changes:
|
|
|
|
```basic norun
|
|
LABEL HISCORE
|
|
IF SCORE# > HIGH# THEN HIGH# = SCORE#
|
|
RETURN
|
|
```
|
|
|
|
**There is nowhere to save it.** This interpreter has no disk — see
|
|
[Chapter 9](09-files-and-disk.md) — so the high score lasts as long as the process does.
|
|
|
|
## Step 14: Add sound
|
|
|
|
**Goal: sound where there is a sound device, silence where there is not, and no crash
|
|
either way.**
|
|
|
|
`SOUND` refuses on a machine with no audio device, and an untrapped refusal ends the
|
|
program. So ask once at startup, remember the answer, and never ask again. Here is the
|
|
idea on its own, as a program you can run to see which kind of machine you are on:
|
|
|
|
```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 from the plain build, which has no devices at all; on the SDL build the
|
|
same program prints the other line.
|
|
|
|
`TRAP NOAUDIO` arms a handler. `RESUME NEXT` returns to the statement after the one that
|
|
failed. **`TRAP` with no argument disarms it again** — leave it armed and the next
|
|
refusal anywhere in your game is silently swallowed. See
|
|
[Chapter 4](04-control-flow.md#trapping-errors).
|
|
|
|
In the game that becomes a routine and its handler, called once from the setup block
|
|
before anything else makes a noise:
|
|
|
|
```basic norun
|
|
LABEL SNDPROBE
|
|
SND# = 1
|
|
TRAP NOAUDIO
|
|
SOUND 1, 2000, 1
|
|
TRAP
|
|
RETURN
|
|
|
|
LABEL NOAUDIO
|
|
SND# = 0
|
|
RESUME NEXT
|
|
```
|
|
|
|
`NOAUDIO` has no `RETURN` of its own, and must not have one: `RESUME NEXT` is what leaves a
|
|
handler, and it goes back into `SNDPROBE` at the `TRAP` after the `SOUND` — so the disarm
|
|
still happens and the `RETURN` after it is the one that runs.
|
|
|
|
After that, every sound is one line guarded by the flag:
|
|
|
|
```basic norun
|
|
LABEL BEEPB
|
|
IF SND# = 1 THEN SOUND 1, 12000, 2
|
|
RETURN
|
|
|
|
LABEL BEEPP
|
|
IF SND# = 1 THEN SOUND 2, 6000, 3
|
|
RETURN
|
|
|
|
LABEL BEEPW
|
|
IF SND# = 1 THEN SOUND 3, 9000, 1
|
|
RETURN
|
|
|
|
LABEL BEEPL
|
|
IF SND# = 1 THEN SOUND 1, 1500, 20
|
|
RETURN
|
|
```
|
|
|
|
Then call them. Each is one `GOSUB` added to a routine you already have:
|
|
|
|
| Add | To | From |
|
|
|---|---|---|
|
|
| `GOSUB BEEPB` | `KILLBR`, before its `RETURN` | [Step 10](#step-10-break-bricks) |
|
|
| `GOSUB BEEPP` | `PADHIT`, at `LABEL PADAIM` | [Step 11](#step-11-bounce-off-the-paddle) |
|
|
| `GOSUB BEEPW` | `WALLL`, `WALLR` and `WALLT`, in all three | [Step 9](#step-9-move-the-ball) |
|
|
| `GOSUB BEEPL` | `LOSTLIF`, as its first line | [Step 13](#step-13-lives-levels-and-game-over) |
|
|
|
|
So `WALLL` from Step 9 becomes:
|
|
|
|
```basic norun
|
|
LABEL WALLL
|
|
BX# = 0
|
|
BVX# = 0 - BVX#
|
|
GOSUB BEEPW
|
|
RETURN
|
|
```
|
|
|
|
and `WALLR` and `WALLT` take the same line in the same place.
|
|
|
|
**`SOUND`'s frequency argument is a SID register value, not hertz.** The pitch you get is
|
|
`register * 1022730 / 16777216`, so 12000 is about 732 Hz. Work the notes you want out
|
|
once and write the numbers down in a comment beside them;
|
|
[Chapter 7](07-sound.md#sound) has the arithmetic.
|
|
|
|
## Step 15: Add an attract mode
|
|
|
|
**Goal: a title screen that plays the game by itself.**
|
|
|
|
This is not decoration. A demo that plays for two minutes exercises the ball, the bricks,
|
|
the level change and the speed-up *together*, for thousands of frames, which is the only
|
|
way some defects show up at all.
|
|
|
|
One flag is the whole difference between the demo and a real game:
|
|
|
|
```basic norun
|
|
LABEL TITLE
|
|
DEMO# = 1
|
|
SCORE# = 0
|
|
LIVES# = 3
|
|
LEVEL# = 1
|
|
BSPD# = 4
|
|
GOSUB LOADLAY
|
|
GOSUB BUILDW
|
|
GOSUB DRAWW
|
|
GOSUB DRAWHUD
|
|
GOSUB TITTEXT
|
|
GOSUB SERVE
|
|
STATE# = 0
|
|
GOTO TICK
|
|
```
|
|
|
|
Everything else is four small edits to routines you already have. Each one says which.
|
|
|
|
**In `MOVEPAD`** from Step 8, add these two lines at the top, immediately after the
|
|
`LABEL`, so that a demo paddle is driven by the machine instead of by `PDEC#`:
|
|
|
|
```basic norun
|
|
IF DEMO# = 1 THEN GOSUB DEMOPAD
|
|
IF DEMO# = 1 THEN GOTO PADCLMP
|
|
```
|
|
|
|
The demo paddle tracks the ball — but **aims off-centre by a random amount**, chosen
|
|
afresh at every bounce:
|
|
|
|
```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
|
|
```
|
|
|
|
`DOFF#` is between -40 and +40 pixels of deliberate error, so the machine plays like
|
|
something with a hand on the paddle and occasionally misses. **A demo that tracks
|
|
perfectly never loses a ball and therefore never tests losing one.**
|
|
|
|
`DEMOAIM` has to be called from somewhere, and the somewhere is the bounce. **In `PADHIT`**
|
|
from Step 11, beside the `NUDGE#` line and immediately before `LABEL PADAIM`:
|
|
|
|
```basic norun
|
|
IF DEMO# = 1 THEN GOSUB DEMOAIM
|
|
```
|
|
|
|
**In `KEYSPC`** from Step 11, add two lines at the top, so that space starts a real game
|
|
rather than launching the demo's ball:
|
|
|
|
```basic norun
|
|
LABEL KEYSPC
|
|
IF DEMO# = 1 THEN STATE# = 5
|
|
IF DEMO# = 1 THEN RETURN
|
|
IF HELD# = 0 THEN RETURN
|
|
HELD# = 0
|
|
GOSUB CLRMSG
|
|
RETURN
|
|
```
|
|
|
|
**In `LOSTLIF`** from Step 13, add one line at the top, so a lost ball re-serves rather
|
|
than costing a life:
|
|
|
|
```basic norun
|
|
LABEL LOSTLIF
|
|
IF DEMO# = 1 THEN GOTO DEMOSRV
|
|
LIVES# = LIVES# - 1
|
|
```
|
|
|
|
with the branch target it needs:
|
|
|
|
```basic norun
|
|
LABEL DEMOSRV
|
|
GOSUB SERVE
|
|
HELD# = 0
|
|
STATE# = 0
|
|
GOTO TICK
|
|
```
|
|
|
|
**In `SERVE`** from Step 11, add one line after `HELD# = 1`, so the demo's ball launches by
|
|
itself:
|
|
|
|
```basic norun
|
|
IF DEMO# = 1 THEN HELD# = 0
|
|
```
|
|
|
|
**In `KILLBR`** from Step 10, guard the scoreboard, because the machine does not get on it:
|
|
|
|
```basic norun
|
|
IF DEMO# = 0 THEN GOSUB HISCORE
|
|
```
|
|
|
|
Leave it running and watch the score climb. That is your test suite for everything the
|
|
unit tests cannot reach.
|
|
|
|
## Step 16: Put the program together
|
|
|
|
**Goal: one file, in an order that runs.**
|
|
|
|
A program runs from the top, so the order of the file matters in three places and nowhere
|
|
else: the setup has to come first, `DATA` has to be in the order it will be read, and every
|
|
`LABEL` has to exist somewhere. Subroutines can go in any order you like.
|
|
|
|
This is the shape of the whole file:
|
|
|
|
```text
|
|
LABEL SETUP the geometry from Step 2
|
|
the declaration block from Step 3
|
|
the brick faces from Step 5
|
|
SEED# = TI#
|
|
the ceiling from Step 9
|
|
GOSUB MKSPR Step 4
|
|
GOSUB SNDPROBE Step 14
|
|
COLLISION 2, BRICKHIT Step 10
|
|
GOTO TITLE
|
|
|
|
the sprite routine, the sound routines, the title screen,
|
|
the game and level setup, the main loop, the input routines,
|
|
the paddle, the ball, the drawing routines, the state changes
|
|
--- in any order ---
|
|
|
|
the sprite patterns as DATA Step 4
|
|
the level layouts as DATA Step 5
|
|
```
|
|
|
|
The two `DATA` blocks come last because nothing else in the program uses `DATA`, and the
|
|
sprite patterns are read before the layouts because `MKSPR` runs before `LOADLAY` does.
|
|
Change that order and each loader gets the other one's numbers.
|
|
|
|
### Declare all of it
|
|
|
|
Step 3 showed the shape of the declaration block. Here it is in full — every name the
|
|
finished game uses, and nothing gets to be created later:
|
|
|
|
```basic norun
|
|
DIM BR#(60)
|
|
DIM SB#(63)
|
|
DIM SP#(63)
|
|
DIM LAY$(6)
|
|
DIM BSG$(6)
|
|
|
|
SCORE# = 0
|
|
HIGH# = 0
|
|
LIVES# = 3
|
|
LEVEL# = 1
|
|
LEFTN# = 0
|
|
BSPD# = 4
|
|
STATE# = 0
|
|
DEMO# = 0
|
|
PAUSED# = 0
|
|
HELD# = 0
|
|
PDIR# = 0
|
|
PDEC# = 0
|
|
PX# = 0
|
|
BX# = 0
|
|
BY# = 0
|
|
BVX# = 0
|
|
BVY# = 0
|
|
K# = 0
|
|
HIT# = 0
|
|
PVX# = 0
|
|
DOFF# = 0
|
|
STALL# = 0
|
|
NUDGE# = 0
|
|
BI# = 0
|
|
BR2# = 0
|
|
PTS# = 0
|
|
SX# = 0
|
|
SY# = 0
|
|
A# = 0
|
|
M# = 0
|
|
D% = 0.0
|
|
V# = 0
|
|
L# = 0
|
|
C2# = 0
|
|
D# = 0
|
|
M# = 0
|
|
X2# = 0
|
|
X3# = 0
|
|
Z# = 0
|
|
BB# = 0
|
|
RX# = 0
|
|
N# = 0
|
|
MROW# = 0
|
|
RMAX# = 2
|
|
RND# = 0
|
|
SND# = 0
|
|
SEED# = 0
|
|
P$ = ""
|
|
H$ = ""
|
|
S$ = ""
|
|
T$ = ""
|
|
MSG$ = ""
|
|
BSEG$ = ""
|
|
FRAME$ = ""
|
|
I# = 0
|
|
R# = 0
|
|
R2# = 0
|
|
C# = 0
|
|
CC# = 0
|
|
KI# = 0
|
|
T# = 0
|
|
```
|
|
|
|
That is 65 names out of the 128 the interpreter has. Loop counters are in there too —
|
|
`I#`, `R#`, `C#`, `CC#`, `KI#` — because a `FOR` creates its counter the same way a
|
|
`GOSUB` creates a local, and declaring them costs nothing.
|
|
|
|
### The routines the earlier steps referred to but did not show
|
|
|
|
Four small ones, for completeness.
|
|
|
|
Loading the sprite patterns at startup, which is Step 4 turned into a routine:
|
|
|
|
```basic norun
|
|
LABEL MKSPR
|
|
FOR I# = 0 TO 62
|
|
READ SB#(I#)
|
|
NEXT I#
|
|
FOR I# = 0 TO 62
|
|
READ SP#(I#)
|
|
NEXT I#
|
|
SPRSAV SB#, 1
|
|
SPRITE 1, 1, 2
|
|
SPRSAV SP#, 2
|
|
SPRSAV SP#, 3
|
|
SPRSAV SP#, 4
|
|
SPRITE 2, 1, 15, 0, 1, 0
|
|
SPRITE 3, 1, 15, 0, 1, 0
|
|
SPRITE 4, 1, 15, 0, 1, 0
|
|
RETURN
|
|
```
|
|
|
|
Note this reads **all 63 numbers** for each pattern, so the `DATA` at the foot of the file
|
|
is 21 lines of three numbers per sprite rather than the shortened form Step 4's figure
|
|
used. Here are both, and the ball's is worth reading against the bit diagram in Step 4:
|
|
|
|
```basic norun
|
|
DATA 60, 0, 0
|
|
DATA 126, 0, 0
|
|
DATA 255, 0, 0
|
|
DATA 255, 0, 0
|
|
DATA 255, 0, 0
|
|
DATA 255, 0, 0
|
|
DATA 126, 0, 0
|
|
DATA 60, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
|
|
DATA 255, 255, 255
|
|
DATA 255, 255, 255
|
|
DATA 255, 255, 255
|
|
DATA 255, 255, 255
|
|
DATA 255, 255, 255
|
|
DATA 255, 255, 255
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
DATA 0, 0, 0
|
|
```
|
|
|
|
Eight rows of ball in the top-left corner and thirteen empty ones; six solid rows of paddle
|
|
and fifteen empty ones. The zeros are not optional — `MKSPR` reads 63 numbers per pattern
|
|
and will take them from the level layouts if they are not there.
|
|
|
|
The HUD, which is Step 12's block with `CHAR` instead of `PRINT`:
|
|
|
|
```basic norun
|
|
LABEL DRAWHUD
|
|
V# = SCORE#
|
|
GOSUB PAD6
|
|
H$ = " SCORE " + P$
|
|
H$ = (H$ + " LIVES ") + LIVES#
|
|
H$ = (H$ + " LEVEL ") + LEVEL#
|
|
V# = HIGH#
|
|
GOSUB PAD6
|
|
H$ = (H$ + " HIGH ") + P$
|
|
CHAR 1, 0, 0, H$
|
|
RETURN
|
|
```
|
|
|
|
Pause, which is the `P` key from Step 7, and which the demo ignores:
|
|
|
|
```basic norun
|
|
LABEL KEYPAU
|
|
IF DEMO# = 1 THEN RETURN
|
|
PAUSED# = 1 - PAUSED#
|
|
IF PAUSED# = 1 THEN MSG$ = "PAUSED"
|
|
IF PAUSED# = 1 THEN GOSUB SHOWMSG
|
|
IF PAUSED# = 0 THEN GOSUB CLRMSG
|
|
RETURN
|
|
```
|
|
|
|
Starting a real game, which is what `STATE# = 5` dispatches to, and the title text and its
|
|
eraser:
|
|
|
|
```basic norun
|
|
LABEL NEWGAME
|
|
DEMO# = 0
|
|
SCORE# = 0
|
|
LIVES# = 3
|
|
LEVEL# = 1
|
|
BSPD# = 4
|
|
PAUSED# = 0
|
|
GOSUB CLRTIT
|
|
GOSUB NEWLEV
|
|
STATE# = 0
|
|
GOTO TICK
|
|
|
|
LABEL TITTEXT
|
|
MROW# = TITROW#
|
|
MSG$ = "B R E A K O U T"
|
|
GOSUB SHOWAT
|
|
MROW# = TITROW# + 2
|
|
MSG$ = "PRESS SPACE TO PLAY"
|
|
GOSUB SHOWAT
|
|
MROW# = TITROW# + 4
|
|
MSG$ = "ATTRACT MODE"
|
|
GOSUB SHOWAT
|
|
RETURN
|
|
|
|
LABEL CLRTIT
|
|
MROW# = TITROW#
|
|
GOSUB CLRAT
|
|
MROW# = TITROW# + 2
|
|
GOSUB CLRAT
|
|
MROW# = TITROW# + 4
|
|
GOSUB CLRAT
|
|
RETURN
|
|
```
|
|
|
|
### 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 `RGR` with a message about a missing graphics device — which
|
|
means everything above that point parsed, and that is the whole of what this check is for.
|
|
A parse error, a bad line number or an `OUT OF DATA` here is a real problem, and it is far
|
|
easier to find without a window in the way. Then run it properly:
|
|
|
|
```sh norun
|
|
$ ./build-akgl/basic mybreakout.bas
|
|
```
|
|
|
|
## The rules this listing never breaks
|
|
|
|
A checklist to write your own game against.
|
|
|
|
| Rule | Because | Where |
|
|
|---|---|---|
|
|
| `DIM` at the top, never inside a loop or a `GOSUB` | An array created inside a scope takes pool storage that is never returned | [Step 3](#step-3-declare-every-name-first) |
|
|
| Declare any name a subroutine has to answer through | A name first seen inside a `GOSUB` dies at `RETURN`, silently | [Step 3](#step-3-declare-every-name-first) |
|
|
| Parenthesise every mixed `+` and `-` | `a - b + c` is currently evaluated as `a - (b + c)` | [Step 3](#step-3-declare-every-name-first) |
|
|
| Parenthesise a second `AND` or `OR` | Only one unparenthesised `AND` or `OR` is matched per expression | [Step 3](#step-3-declare-every-name-first) |
|
|
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | [Step 4](#step-4-make-the-ball-and-the-paddle) |
|
|
| Retire a `SOLID` when the thing it stood for is gone | The ball goes on bouncing off a brick that is no longer drawn | [Step 10](#step-10-break-bricks) |
|
|
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | [Step 5](#step-5-build-the-brick-wall) |
|
|
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | [Step 5](#step-5-build-the-brick-wall) |
|
|
| Loop the game with `GOTO`, and never jump out of a `DO` | A `GOTO` out of a loop does not release the loop's scope, and there are 32 | [Step 6](#step-6-write-the-main-loop) |
|
|
| Name things with letters and digits only | There is no underscore in an identifier, and the error names the character rather than the name | [Step 6](#step-6-write-the-main-loop) |
|
|
| Keep a line under 32 tokens | The 33rd stops the interpreter rather than raising an error you can `TRAP` | [Step 8](#step-8-move-the-paddle) |
|
|
| Span a loop across lines, and never write `FOR I# = 1 TO 1` | A whole loop on one line does not loop; equal bounds run the body zero times | [Chapter 13](13-differences.md) |
|
|
| Probe for a device once and remember the answer | An untrapped refusal ends the program | [Step 14](#step-14-add-sound) |
|
|
|
|
## 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 the game's own drawing code — the row
|
|
builder from Step 5, the sprite patterns from Step 4, the HUD from Step 12 — with the
|
|
numbers filled in by hand and no main loop, so it draws one frame and stops.
|
|
|
|
Type it in and you have a still of the game before you have written any of the game:
|
|
|
|
```basic requires=akgl screenshot=breakout-game size=800x600 text=1
|
|
DIM SB#(63)
|
|
DIM SP#(63)
|
|
DIM BSG$(6)
|
|
DIM LAY$(6)
|
|
I# = 0
|
|
R# = 0
|
|
C# = 0
|
|
S$ = ""
|
|
T$ = ""
|
|
SCNCLR
|
|
COLS# = RWINDOW(1)
|
|
BLEFT# = (COLS# - 40) / 2
|
|
BSG$(0) = "[##]"
|
|
BSG$(1) = "[##]"
|
|
BSG$(2) = "[==]"
|
|
BSG$(3) = "[==]"
|
|
BSG$(4) = "[--]"
|
|
BSG$(5) = "[--]"
|
|
LAY$(0) = "1111111111"
|
|
LAY$(1) = "1111111111"
|
|
LAY$(2) = "1111111111"
|
|
LAY$(3) = "0111111110"
|
|
LAY$(4) = "0110110110"
|
|
LAY$(5) = "0010110100"
|
|
CHAR 1, 0, 0, " SCORE 001250 LIVES 3 LEVEL 2 HIGH 009900"
|
|
CHAR 1, 0, 1, "=" * COLS#
|
|
FOR R# = 0 TO 5
|
|
S$ = " " * BLEFT#
|
|
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#
|
|
I# = 4 + R#
|
|
CHAR 1, 0, I#, S$
|
|
NEXT R#
|
|
CHAR 1, 0, 35, " PRESS SPACE TO LAUNCH"
|
|
FOR I# = 0 TO 62
|
|
SB#(I#) = 0
|
|
SP#(I#) = 0
|
|
NEXT I#
|
|
FOR I# = 0 TO 7
|
|
READ SB#(I# * 3)
|
|
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, 396, 518
|
|
MOVSPR 2, 328, 528
|
|
MOVSPR 3, 376, 528
|
|
MOVSPR 4, 424, 528
|
|
|
|
DATA 60, 126, 255, 255, 255, 255, 126, 60
|
|
```
|
|
|
|

|
|
|
|
## Where to go next
|
|
|
|
- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again out of loaded
|
|
artwork, with powerups and a coloured HUD. It is a bigger program and it teaches the
|
|
drawing verbs, which this chapter never uses.
|
|
- **[Chapter 13](13-differences.md)** is the full list of what this dialect does
|
|
differently from BASIC 7.0.
|
|
- **[Chapter 8](08-sprites.md)** is the sprite reference, and
|
|
**[Chapter 4](04-control-flow.md)** is the whole of `GOSUB`, `TRAP` and the loop forms.
|
|
- **[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.
|