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) |