From 8da8d717c83622f34a7b27c2c659d4f994136150 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 11:21:32 -0400 Subject: [PATCH] Teach chapter 17 the collision verbs its listing now uses The previous commit converted `examples/breakout/characters/breakout.bas` and left the chapter teaching three routines the listing no longer has. This closes that. Step 5 gains the `SOLID` registration beside the `BR#()` array, and says why there are two records of the same thing: one is the program's, one is the interpreter's, and the id is the array index plus one so neither has to be looked up from the other. It is also now the only place cells and pixels meet, which Step 2 used to promise about Step 10. Step 10 is rewritten. It was `HITTEST` -- fourteen lines dividing pixels by cell sizes to recover a grid index -- plus `XBRICK` and `YBRICK` testing the ball's leading edge, with a paragraph explaining that the cost of testing an edge rather than a box is missing a brick clipped at the corner by up to seven pixels. All of that is gone. What replaces it asks four questions: which rectangle, is it still there, which way out and how far, and which axis to reverse. Three things in it are worth the reader's attention and get it: `BUMP(2)` is a separate accumulator from `BUMP(1)` so the handler never sees the paddle; the guard against being told about an already-broken brick, because a handler runs a line or two after the overlap; and `SOLID BI# + 1` in `KILLBR`, without which the ball goes on bouncing off a brick that is no longer drawn. That last one is now a row in the rules table. The float note is kept and sharpened rather than dropped. `D%` must be a float because field 4 is one, and the products land in integer `BX#`/`BY#` -- which is safe here because a box against a box gives a normal that is exactly -1, 0 or 1, and would *not* be safe against a circle. Stated as a reason rather than left as something that happens to work. Step 9 loses its two brick calls and its `OX#`/`OY#` backup, with a sentence saying why there is nothing to restore to. Step 16's declaration block and name count follow, and the skeleton arms the handler. Every fenced example still runs; both suites green; the game runs clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL --- docs/17-tutorial-breakout.md | 158 +++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 74 deletions(-) diff --git a/docs/17-tutorial-breakout.md b/docs/17-tutorial-breakout.md index 46e911f..9216535 100644 --- a/docs/17-tutorial-breakout.md +++ b/docs/17-tutorial-breakout.md @@ -43,8 +43,7 @@ $ ./build-akgl/basic examples/breakout/characters/breakout.bas - **[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)** — work out which brick the ball is touching, and - break it +- **[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 @@ -190,9 +189,9 @@ HUD line. `MAXX#` is as far right as the ball can go — the window less the bal 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 only one routine in the whole -game converts between them.** That routine is in Step 10. Everywhere else, bricks are in -cells and everything that moves is in pixels. +**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 @@ -521,10 +520,25 @@ FOR C# = 0 TO 9 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. @@ -792,23 +806,19 @@ each move on its own: ```basic norun LABEL MOVEBAL -OX# = BX# BX# = BX# + BVX# IF BX# < 0 THEN GOSUB WALLL IF BX# > MAXX# THEN GOSUB WALLR -GOSUB XBRICK -OY# = BY# BY# = BY# + BVY# IF BY# < TOPY# THEN GOSUB WALLT -GOSUB YBRICK GOSUB PADHIT IF BY# > LOSEY# THEN STATE# = 1 RETURN ``` -Doing X first and then Y means a ball arriving at the corner of a brick reverses one axis -rather than both, which is what looks right. `OX#` and `OY#` remember where the ball came -from, so a collision has somewhere to put it back to. +**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: @@ -842,75 +852,72 @@ CHAR 1, 0, 1, FRAME$ ## Step 10: Break bricks -**Goal: work out which brick a point is inside, and take it out of the wall.** +**Goal: find out which brick the ball hit, and take it out of the wall.** -This is the one routine that converts pixels into cells. Give it a point in `TX#` and -`TY#` and it sets `HIT#` to 1 or 0, and when it is 1 it also sets `BI#` — the index into -the sixty-element grid — and `BR2#`, the row: +Step 5 registered every brick with `SOLID`, so this is not arithmetic — it is a question. +Arm a handler: ```basic norun -LABEL HITTEST -HIT# = 0 -RC# = TY# / CH# -IF RC# < BTOP# THEN RETURN -RB# = RC# - BTOP# -IF RB# > (BROWS# - 1) THEN RETURN -CC2# = TX# / CW# -IF CC2# < BLEFT# THEN RETURN -CB# = (CC2# - BLEFT#) / BRW# -IF CB# > (BCOLS# - 1) THEN RETURN -BI# = (RB# * BCOLS#) + CB# -BR2# = RB# -IF BR#(BI#) = 1 THEN HIT# = 1 -RETURN +COLLISION 2, BRICKHIT ``` -Read it as four questions in a row, each of which can answer "no": is the point below the -top of the wall, above the bottom of it, right of the left edge, left of the right edge. -Only if all four pass is there a cell to look at. Dividing a pixel by `CH#` gives a text -row; dividing by `CW#` gives a text column; dividing the column by `BRW#` gives a brick -column, because a brick is four cells wide. - -`HIT#`, `BI#` and `BR2#` work as answers only because Step 3 created all three outside -this routine. - -**Test the ball's leading edge rather than its centre:** +`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 XBRICK -TX# = BX# -IF BVX# > 0 THEN TX# = BX# + 7 -TY# = BY# + 4 -GOSUB HITTEST -IF HIT# = 0 THEN RETURN -BX# = OX# -BVX# = 0 - BVX# -GOSUB KILLBR -RETURN - -LABEL YBRICK -TX# = BX# + 4 -TY# = BY# -IF BVY# > 0 THEN TY# = BY# + 7 -GOSUB HITTEST -IF HIT# = 0 THEN RETURN -BY# = OY# -BVY# = 0 - BVY# +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 ``` -Moving right, the leading edge is `BX# + 7`; moving left it is `BX#`. A ball tested by its -centre sinks four pixels into a brick before it turns, and it looks like the brick was hit -late. The cost of testing the edge is that a brick clipped at the very corner can be -missed by up to seven pixels' worth of ball. That is the better of the two trades, and it -is worth knowing which one you took. +Read it as four questions and an answer. -Breaking a brick clears its cell, scores it, and redraws **just that row**: +**`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# @@ -920,6 +927,10 @@ 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. @@ -1487,6 +1498,7 @@ LABEL SETUP the geometry from Step 2 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, @@ -1531,8 +1543,6 @@ BX# = 0 BY# = 0 BVX# = 0 BVY# = 0 -OX# = 0 -OY# = 0 K# = 0 HIT# = 0 PVX# = 0 @@ -1541,13 +1551,12 @@ STALL# = 0 NUDGE# = 0 BI# = 0 BR2# = 0 -RB# = 0 -CB# = 0 -RC# = 0 -CC2# = 0 -TX# = 0 -TY# = 0 PTS# = 0 +SX# = 0 +SY# = 0 +A# = 0 +M# = 0 +D% = 0.0 V# = 0 L# = 0 C2# = 0 @@ -1580,7 +1589,7 @@ KI# = 0 T# = 0 ``` -That is 69 names out of the 128 the interpreter has. Loop counters are in there too — +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. @@ -1757,6 +1766,7 @@ A checklist to write your own game against. | 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) |