diff --git a/docs/18-tutorial-breakout-artwork.md b/docs/18-tutorial-breakout-artwork.md index 15aca20..22b34ff 100644 --- a/docs/18-tutorial-breakout-artwork.md +++ b/docs/18-tutorial-breakout-artwork.md @@ -54,26 +54,25 @@ tells you which it is. - **[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-turn-a-drawing-into-a-sprite)** — draw something and turn it into a - sprite so it stays on the screen +- **[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-find-the-frame-boundary)** — find the host's frame boundary, so a - drawing survives being captured -- **[Step 6](#step-6-draw-at-most-one-thing-per-frame)** — draw at most one thing per +- **[Step 5](#step-5-draw-at-most-one-thing-per-frame)** — draw at most one thing per frame, chosen by dirty flags -- **[Step 7](#step-7-keep-a-flattened-list-of-live-bricks)** — keep a flattened list of the - bricks still standing, so the draw routine decides nothing -- **[Step 8](#step-8-draw-lettering-with-a-stroke-font)** — draw lettering with a stroke +- **[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 9](#step-9-move-and-bounce-the-ball)** — move the ball and bounce it off bars and - bricks without a square root -- **[Step 10](#step-10-gems-and-powerups)** — drop gems and apply what catching one does -- **[Step 11](#step-11-three-voices-and-a-mute)** — three voices of sound and a mute that +- **[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 12](#step-12-the-states)** — the frame loop's states: title, serve, play, lost, +- **[Step 11](#step-11-the-states)** — the frame loop's states: title, serve, play, lost, cleared -- **[Step 13](#step-13-put-the-program-together)** — put the pieces in one file, in the +- **[Step 12](#step-12-put-the-program-together)** — put the pieces in one file, in the right order --- @@ -143,15 +142,15 @@ around later, so spend them on paper first. This game spends them like this: | Slot | Is | |---|---| -| 1 | the HUD strip — a captured drawing | -| 2 | the playing field — a captured drawing | +| 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 | -Two of the eight are **the screen itself**, and Step 3 explains why. That leaves six for -everything that moves, and two consequences fall straight out of it: +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` @@ -161,44 +160,41 @@ everything that moves, and two consequences fall straight out of it: Deciding this first is what stops a feature costing an afternoon before it is abandoned. -## Step 3: Turn a drawing into a sprite +## Step 3: Make room for what you draw **Goal: something you drew, still on the screen on the next frame.** -Two layers are redrawn for you every frame from state the interpreter keeps: the text grid -and the sprites. **The drawing verbs are not one of them.** `DRAW`, `BOX`, `CIRCLE` and -`PAINT` write straight into the renderer's buffer, and by default the text layer owns -every row of the window and repaints over them before the frame is shown. +Two things have to be true before a drawing is any use, and only one of them is automatic. -So a drawing has to be turned into something that persists. The sequence is: draw it, -capture it with `SSHAPE`, install the capture into a sprite with `SPRSAV`. +**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 -SSHAPE Z$, 0, 60, 800, 600 -SPRSAV Z$, 2 -SPRITE 2, 1, 2 -MOVSPR 2, 0, 60 +WINDOW 0, 35, 49, 36 ``` -Four lines, and they are the last four lines of every draw routine in this game. `SSHAPE` -captures the rectangle from (0, 60) to (800, 600) into `Z$`; `SPRSAV` installs it into -slot 2; `SPRITE` turns the slot on; `MOVSPR` puts it back where it was drawn. +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. -`Z$` holds a **handle**, not pixels — see -[Chapter 6](06-graphics.md#saving-and-stamping-regions). You can pass it to `GSHAPE` and -`SPRSAV`. You cannot print it, save it or take its `LEN`. +That is the whole of it. Draw your field once and it stays; draw your HUD once and it +stays. -Once it is a sprite it stays on the screen for nothing, and that is the point: a sprite is -drawn from state the interpreter keeps, so it costs the program no work per frame at all. +**Two consequences shape the rest of this chapter, and both are things you no longer have +to do.** -`WINDOW 0, 0, 49, 1` would shrink the text area and hand the rest of the window to the -drawing verbs, which is the other way to make a drawing visible. This program does not use -it, because a drawing kept that way still has to be re-issued every frame *and* fit inside -one batch (Step 5), and a sprite has neither constraint. Making the drawing verbs visible -without a `WINDOW` call is [`TODO.md` §9 item 3](../TODO.md). +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. -The figures in this chapter are rendered by a tool that draws no text layer, which is why -they can show a bare drawing at all. +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 @@ -242,7 +238,7 @@ 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, and Step 5 explains why that number matters. +seventy a line-at-a-time loop would take. Capturing the six is six `SSHAPE`s: @@ -255,19 +251,18 @@ 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.** `GRAPHIC 5` gives back all of -them at once, so count what you have spent and rebuild the stamps when the pool runs dry: +**`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 5 GRAPHIC 1, 1 WIDTH 1 -SHN# = 99 ``` -with the six `DRAW` loops and six `SSHAPE`s after it, and `SHN# = 6` at the end. Every -routine that captures adds one to `SHN#`, and the frame loop rebuilds when it reaches 14. +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. @@ -342,55 +337,12 @@ 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: Find the frame boundary - -**Goal: a drawing that is not cut in half by the host presenting the frame.** - -The host runs **256 source lines and then presents the frame**, and presenting throws the -drawing buffer away. So all the drawing between a `GRAPHIC 1, 1` and its `SSHAPE` has to -happen inside one of those batches. Draw more than fits and the capture comes back holding -only what was issued since the present, over whatever the frame before it left behind — -which looks exactly like a ghost. - -`TI#` is the host's clock in sixtieths of a second, and it is refreshed **once per batch**. -So the step on which `TI#` changes is the first step of a batch, and spinning until it -changes is how a program finds a frame boundary: - -```basic norun -LABEL PACE -LASTT# = TI# -LABEL PACEEDGE -IF TI# - LASTT# < 2 THEN GOTO PACEEDGE -RETURN -``` - -Two jiffies is thirty frames a second. - -**`LASTT#` is sampled on entry rather than carried over from the last frame, and that is -the whole correctness of the routine.** Carried over, a frame whose work ran long would -find the time already spent, return immediately from somewhere in the middle of a batch, -and ruin the capture that followed. Sampling here means the loop always sees `TI#` change -under it, and a change is only ever seen on the first step of a batch. - -Measured on one machine: after a jiffy edge, 220 lines of drawing survive the capture -intact and 250 do not. Write every draw routine to stay near 200 and you have margin. - -**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate and -nothing else — only *drawing* has a deadline. That single fact is what Steps 7 and 8 are -built on: move everything that is not a drawing verb out of the routine that draws. - -This is the host's budget rather than the interpreter's, so the pacing routine is the -answer rather than a workaround for something being fixed; -[`TODO.md` §9 item 5](../TODO.md) records the measurements and why no fix belongs in the -library. - -## Step 6: Draw at most one thing per frame +## 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 PACE GOSUB DRAWJOB GOSUB READKEYS IF STATE# = 0 THEN GOSUB TITLETICK @@ -402,19 +354,16 @@ IF RUNNING# = 0 THEN GOTO SHUTDOWN GOTO FRAME ``` -Pace first, because that is what puts the drawing at the top of a batch. Then at most one -capture, because a capture is the only thing with a deadline. Then the game, which may -take as long as it likes. +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 SHN# < 14 THEN GOTO DRAWJOB2 -GOSUB DRAWPROTOS -RETURN -LABEL DRAWJOB2 IF DPLAY# = 0 THEN GOTO DRAWJOB3 GOSUB DRAWPLAY DPLAY# = 0 @@ -434,105 +383,43 @@ 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 7: Keep a flattened list of live bricks +## Step 6: Erase one brick, not the whole field -**Goal: a draw routine that decides nothing.** +**Goal: take a broken brick off the screen without redrawing the other fifty-nine.** -The draw routine has a deadline and the rest of the program does not, so any decision that -can be made earlier should be. When a brick breaks, walk the grid once and write out a -**list of the bricks still standing**, row by row — so a row becomes a contiguous run of -that list, and drawing it is a stamp and an advance: +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 -LABEL BUILDLIVE -LN# = 0 -FOR R# = 0 TO 5 - RS#(R#) = LN# - RC#(R#) = 0 - GOSUB BUILDROW -NEXT R# -RETURN +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$ +``` -LABEL BUILDROW -FOR C# = 0 TO 9 - IF BRK#(R# * 10 + C#) > 0 THEN BEGIN - LX#(LN#) = BRKX# + C# * 72 - LY#(LN#) = BRKY# + R# * 24 - LN# = LN# + 1 - RC#(R#) = RC#(R#) + 1 - BEND -NEXT C# +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 ``` -`LX#()` and `LY#()` are the pixel positions of the bricks still alive. `RS#(R#)` is where -row `R#`'s run starts in that list and `RC#(R#)` is how long it is. +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. -Drawing a row is then one loop with no tests in it: +**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. -```basic norun -LABEL STAMPROW -IF T2# < 1 THEN RETURN -IF T2# = 1 THEN GSHAPE Z$, LX#(T1#), LY#(T1#) -IF T2# < 2 THEN RETURN -FOR I# = T1# TO T1# + T2# - 1 - GSHAPE Z$, LX#(I#), LY#(I#) -NEXT I# -RETURN -``` - -and `DRAWPLAY` calls it six times, once per stamp: - -```basic norun -Z$ = S0$ : T1# = RS#(0) : T2# = RC#(0) : GOSUB STAMPROW -Z$ = S1$ : T1# = RS#(1) : T2# = RC#(1) : GOSUB STAMPROW -Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW -Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW -Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW -Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW -``` - -Building the list costs about four lines a brick and has all the time in the world. -Drawing costs two and has a deadline. **That trade is the spine of this program**, and it -comes back in Step 8 for the lettering. - -### Write the one-item case out beside every loop - -Notice the two lines before the `FOR` in `STAMPROW`. **A `FOR` whose bounds are equal does -not run its body**, and the last brick left in a row is exactly that case: - -```basic -FOR I# = 0 TO 0 - PRINT "THE BODY RAN" -NEXT I# -PRINT "AFTER THE LOOP" -``` - -```output -AFTER THE LOOP -``` - -So a loop over a list of unknown length needs its count of one written out beside it, as -`STAMPROW` does. The alternative is a `DO ... LOOP UNTIL`, which tests at the bottom and -therefore always runs once: - -```basic -I# = 0 -DO - PRINT "PASS " + I# - I# = I# + 1 -LOOP UNTIL I# >= 1 -``` - -```output -PASS 0 -``` - -Use whichever reads better. This is [`TODO.md` §6 item 19](../TODO.md), which is waiting on -a decision about a checked-in acceptance file rather than on the work; when it lands, -`FOR I# = 0 TO 0` will run once and both shapes above will still be correct. - -## Step 8: Draw lettering with a stroke font +## Step 7: Draw lettering with a stroke font **Goal: a HUD in more than one colour.** @@ -640,11 +527,6 @@ FOR I# = 0 TO HN# - 1 DRAW HC#(I#), HX1#(I#), HY1#(I#) TO HX2#(I#), HY2#(I#) NEXT I# LABEL HUDONE -SSHAPE Z$, 0, 0, 800, 60 -SPRSAV Z$, 1 -SPRITE 1, 1, 2 -MOVSPR 1, 0, 0 -SHN# = SHN# + 1 RETURN ``` @@ -654,11 +536,14 @@ score yellow, the lives green, the level red. **Seven colour sources is the ceil 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, for the reason in Step 7. And note -`SHN# = SHN# + 1`: every routine that captures counts the slot it spent, which is what -Step 4's rebuild-when-dry test reads. +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 9: Move and bounce the ball +## Step 8: Move and bounce the ball **Goal: bounces at sensible angles, computed without a square root.** @@ -692,12 +577,12 @@ 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 13 calls it twice with different +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 10 adds two more lines before that `RETURN`, for the sticky gem. +Step 9 adds two more lines before that `RETURN`, for the sticky gem. ### Which type a variable is decides the arithmetic @@ -768,92 +653,97 @@ scaling that up to the new speed multiplies everything by 1/0.53, and the ball e nearly twice as fast as intended. Every zone inflates; only the middle two are close enough to vertical for it not to show. -A ball can only be over four cells at once, so work out **which** cells from its bounding -box rather than walking all sixty bricks: +### 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 BALLBRICKS -IF BRN# = 0 THEN RETURN -LFT# = BLX%(B#) -TOP# = BLY%(B#) -RGT# = LFT# + 21 -BOT# = TOP# + 21 -IF BOT# < BRKY# THEN RETURN -IF TOP# > BRKY# + 6 * 24 THEN RETURN -CC1# = (LFT# - BRKX#) / 72 -CC2# = (RGT# - BRKX#) / 72 -RR1# = (TOP# - BRKY#) / 24 -RR2# = (BOT# - BRKY#) / 24 -IF CC1# < 0 THEN CC1# = 0 -IF CC2# > 9 THEN CC2# = 9 -IF RR1# < 0 THEN RR1# = 0 -IF RR2# > 5 THEN RR2# = 5 -IF CC2# < CC1# THEN RETURN -IF RR2# < RR1# THEN RETURN -HIT# = 0 -R# = RR1# -DO - C# = CC1# - DO - IF HIT# = 0 THEN GOSUB TESTCELL - C# = C# + 1 - LOOP UNTIL C# > CC2# OR HIT# = 1 - R# = R# + 1 -LOOP UNTIL R# > RR2# OR HIT# = 1 +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 ``` -`LFT#`, `TOP#`, `RGT#` and `BOT#` are the ball's own box — the artwork is 22 pixels across, -so the ball's right edge is its left plus 21. `CC1#` to `RR2#` are the range of cells that -box can touch, clamped to the field. Both loops are `DO ... LOOP UNTIL` rather than `FOR`, -because a ball entirely inside one column gives `CC1#` and `CC2#` the same value and a -`FOR` would not run at all. +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. -One brick at a time, then. A collision reflects off whichever face the ball has less of -itself past. `T1#` to `T4#` are the **overlapping** rectangle, and its width against its -height is the whole test: +**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 -LABEL TESTCELL -N# = R# * 10 + C# +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. 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 -BX1# = BRKX# + C# * 72 -BY1# = BRKY# + R# * 24 -IF RGT# < BX1# THEN RETURN -IF LFT# > BX1# + 67 THEN RETURN -IF BOT# < BY1# THEN RETURN -IF TOP# > BY1# + 15 THEN RETURN -T1# = RGT# : IF BX1# + 67 < T1# THEN T1# = BX1# + 67 -T2# = LFT# : IF BX1# > T2# THEN T2# = BX1# -T3# = BOT# : IF BY1# + 15 < T3# THEN T3# = BY1# + 15 -T4# = TOP# : IF BY1# > T4# THEN T4# = BY1# -IF T1# - T2# < T3# - T4# THEN BLVX%(B#) = 0.0 - BLVX%(B#) -IF T1# - T2# >= T3# - T4# THEN BLVY%(B#) = 0.0 - BLVY%(B#) +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 -HIT# = 1 -GOSUB BUILDLIVE +GOSUB ERASEBRICK GOSUB HUDTEXT -DPLAY# = 1 -IF GMON# = 1 THEN RETURN -RNMAX# = 7 -GOSUB NEXTRAND -IF RNVAL# = 0 THEN GOSUB SPAWNGEM RETURN ``` -That is the standard box resolution, and it is the reason a ball clipping the end of a row -goes sideways instead of straight back down. +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. -Everything after `BRK#(N#) = 0` is the consequences of the hit, in one place: score it, -sound it, rebuild the live list from Step 7, rebuild the HUD text, mark the field dirty so -Step 6's queue redraws it, and roll one chance in seven for a gem — but only if there is not -one already falling, because Step 2 left exactly one slot for it. `BX1#` and `BY1#` are the -broken brick's own corner, which is where the gem will appear. +`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. -## Step 10: Gems and powerups +**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.** @@ -948,7 +838,7 @@ NEXT B# RETURN ``` -STICKY is three lines at the end of `HITBAR` in Step 9, replacing its final `RETURN` — the +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 @@ -973,7 +863,7 @@ RETURN ``` The CATCH bar **mirrors** the paddle rather than following it — the line is in `MOVEPADDLE` -in Step 13: +in Step 12: ```basic norun CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104 @@ -981,7 +871,7 @@ 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 9. +in Step 8. ### Every timer in one place @@ -1032,7 +922,7 @@ Each arm is the same shape: count down, and on the frame it reaches zero, put ba 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 11: Three voices and a mute +## Step 10: Three voices and a mute **Goal: sound that does not cut itself off, and a mute that costs one line.** @@ -1078,9 +968,9 @@ TEMPO 12 PLAY "V3 T2 U8 O4 QC QE QG O5 HC" ``` -## Step 12: The states +## Step 11: The states -**Goal: the five states the frame loop in Step 6 dispatches to.** +**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 @@ -1139,10 +1029,10 @@ 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 BUILDLIVE GOSUB RESETBALL GOSUB HUDTEXT STATE# = 1 @@ -1159,7 +1049,7 @@ RETURN 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 13: Put the program together +## Step 12: Put the program together **Goal: one file, in an order that runs.** @@ -1167,18 +1057,21 @@ The file runs from the top, so three things about the order matter and nothing e 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 11 + 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 6 +LABEL FRAME the frame loop from Step 5 - the pacing, the draw jobs, the input, the states, the ball, - the gems, the text builders, the generator + 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 @@ -1241,19 +1134,14 @@ GMX% = 0 GMY% = 0 DIM BRK#(60) -DIM LX#(60) -DIM LY#(60) -DIM RS#(6) -DIM RC#(6) DIM BRC#(6) DIM BRV#(6) -LN# = 0 BRN# = 0 S0$ = "" : S1$ = "" : S2$ = "" S3$ = "" : S4$ = "" : S5$ = "" +BL$ = "" : HBL$ = "" Z$ = "" -SHN# = 99 DHUD# = 1 DPLAY# = 1 BAN$ = "" @@ -1294,10 +1182,18 @@ 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 ``` -`SHN# = 99` means "the brick stamps do not exist", so the frame loop's first job is to -build them. `DPLAY#` and `DHUD#` start at 1 so the first frame draws both. +**`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 @@ -1334,7 +1230,7 @@ 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 9. +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#`. @@ -1527,7 +1423,6 @@ IF BLST#(B#) = 0 THEN BEGIN BEND IF BLON#(B#) = 0 THEN RETURN IF BLST#(B#) = 0 THEN BEGIN - GOSUB BALLBRICKS GOSUB BALLPADDLE BEND MOVSPR 5 + B#, BLX%(B#), BLY%(B#) @@ -1570,9 +1465,9 @@ 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 9. +for the reason in Step 8. -`BALLBRICKS` and `TESTCELL` are in Step 9. +Brick collision is in Step 8. ### The gem's fall @@ -1597,7 +1492,7 @@ HIT# = 1 RETURN ``` -`TAKEGEM` is in Step 10. `HIT#` is doing double duty here as "this gem is finished with" — +`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 @@ -1771,7 +1666,7 @@ RETURN 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 call `BUILDLIVE` so the field draws empty, turn the paddle +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 @@ -1784,7 +1679,7 @@ BRN# = 0 FOR I# = 0 TO 59 BRK#(I#) = 0 NEXT I# -GOSUB BUILDLIVE +SOLID SPRITE 3, 0 SPRITE 5, 0 SPRITE 6, 0 @@ -1886,10 +1781,8 @@ actually calls, with those pieces in place. ```basic norun LABEL DRAWPROTOS -GRAPHIC 5 GRAPHIC 1, 1 WIDTH 1 -SHN# = 99 FOR R# = 0 TO 5 COLOR 1, BRC#(R#) T1# = R# * 20 @@ -1904,15 +1797,11 @@ 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$ -SHN# = 6 -DPLAY# = 1 -DHUD# = 1 -RETURN ``` -`GRAPHIC 5` hands all sixteen `SSHAPE` slots back, which is the only way to get any of them -back — so `SHN#` goes to 99 first and to 6 at the end, and both flags are set because the -field and the HUD were drawn with stamps that no longer exist. +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 @@ -1921,29 +1810,29 @@ 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 -Z$ = S0$ : T1# = RS#(0) : T2# = RC#(0) : GOSUB STAMPROW -Z$ = S1$ : T1# = RS#(1) : T2# = RC#(1) : GOSUB STAMPROW -Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW -Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW -Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW -Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW +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 -SSHAPE Z$, 0, 60, 800, 600 -SPRSAV Z$, 2 -SPRITE 2, 1, 2 -MOVSPR 2, 0, 60 -SHN# = SHN# + 1 RETURN ``` -Same shape as `DRAWHUD` in Step 8: the walls, then the bricks, then the field's own stroke -list with its one-item case beside the loop, then Step 3's four lines that turn all of it -into sprite 2. +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. @@ -1963,7 +1852,7 @@ BRN# = 0 FOR I# = 0 TO 59 BRK#(I#) = 0 NEXT I# -GOSUB BUILDLIVE +SOLID SPRITE 3, 0 SPRITE 5, 0 PN# = 0 @@ -2021,10 +1910,10 @@ a feature costing an afternoon before it is abandoned. | Resource | There are | This game uses | |---|---|---| -| Sprites | 8 | 8 | -| Variables | 128 | 121, plus 4 the interpreter makes | -| Labels | 64 | 61 | -| `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 6 stamps plus 1 capture a frame | +| 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 | @@ -2043,7 +1932,7 @@ Step 1's artwork, with the numbers filled in by hand and no frame loop, so it dr 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 8, shown small enough to check by eye: +`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) diff --git a/examples/breakout/sprites/README.md b/examples/breakout/sprites/README.md index 0d9973d..3b93169 100644 --- a/examples/breakout/sprites/README.md +++ b/examples/breakout/sprites/README.md @@ -48,10 +48,10 @@ dive across the field into a save at both ends. ## How it works, and how to write your own **[Chapter 18 of the guide](../../../docs/18-tutorial-breakout-artwork.md)** builds this -program in twelve steps: loading artwork, budgeting the eight sprite slots, capturing a -drawing into one, stamping the brick field, finding the host's frame boundary with `TI#`, -moving everything that is not drawing out of the deadline, the stroke font, the bounce -without a square root, and the gems. +program step by step: loading artwork, budgeting the eight sprite slots, taking the text +layer out of the way so a drawing can be seen, stamping the brick field, registering the +bricks as collision geometry, the stroke font, the bounce without a square root, and the +gems. [Chapter 17](../../../docs/17-tutorial-breakout.md) does the same for [`../characters`](../characters), which builds the same game out of the text grid and two @@ -71,18 +71,16 @@ Crediting Kenney is not required by CC0. It is here because it should be. Named honestly rather than left to be discovered: - **No music under the game**, only event sounds and two four-note stings. There is room in - the third voice for it; there was not much room anywhere else when it was written. 121 of - the interpreter's 128 variables are spoken for and the label table is at 61 of 64 — - several of them spent working around defects that have since been fixed. + the third voice for it; there was not much room anywhere else when it was written. The + conversion gave some of that room back — the variable table is under the 128 ceiling with + a handful to spare and the label table is at 57 of 64, where it was at 61. - **One gem at a time.** There is one sprite slot for it. A brick broken while a gem is falling drops nothing. - **Sticky and multiball share one offset.** Two balls stuck to the paddle at once sit on top of each other. It is rare enough that fixing it would cost a variable I do not have. -- **The score lags a frame behind the bricks.** Only one capture happens per frame and the - field goes first, so a brick disappears one frame before the score that counts it. At - thirty frames a second nobody can see it, and it is a deliberate trade rather than an - oversight. - **No high score on disk.** There is no disk. +- **No attract mode**, unlike `../characters`. It sits on the title screen until somebody + presses space, which also means it cannot test itself unattended the way that one can. The eight interpreter defects this game turned up are filed in `TODO.md` §9, each with a reduction that fits on a screen and the file and line of the cause. **Six are now fixed** @@ -92,8 +90,14 @@ drawing. The two that stand are the left-operand arithmetic rule, which turned o decision rather than a defect and is now documented as one, and the 256-line batch tear, which belongs to the host. -**The listing's code is unchanged**, and deliberately: it is what a program written -against those constraints looks like, and none of the workarounds costs anything now that -they are not needed. Six separate scalars for six brick stamps, and loops guarded by -`GOTO` rather than wrapped in a block, are both still what the file does. Its comments say -which of the five traps stand and which are marked FIXED. +**The listing has since been converted**, and its header records what went. It used to +spend two of its eight sprites on the screen itself — an `SSHAPE` of the HUD strip and one +of the whole play field, because a drawing lasted a single frame and a sprite was the only +thing redrawn for nothing. A drawing persists now, so both slots are free, the `PACE` +routine that hunted for a frame boundary is gone with the captures it protected, and the +flattened live list that made a full-field redraw cheap is gone with the redraw itself: a +broken brick is erased in place. + +What has *not* changed is the shape of the workarounds that no longer cost anything — six +separate scalars for six brick stamps, loops guarded by `GOTO` rather than wrapped in a +block. Its comments say which of the five traps still stand and which are marked FIXED. diff --git a/examples/breakout/sprites/breakout.bas b/examples/breakout/sprites/breakout.bas index bc6d1fa..7db98da 100644 --- a/examples/breakout/sprites/breakout.bas +++ b/examples/breakout/sprites/breakout.bas @@ -145,9 +145,9 @@ DIM BRV#(6) BRN# = 0 REM ------------------------------------------------------- shapes and dirt -REM SHN# counts the SSHAPE slots spent. There are sixteen, nothing gives -REM one back, and GRAPHIC 5 gives back all of them at once -- so the brick -REM stamps are thrown away and rebuilt every time the pool runs dry. +REM Eight of the sixteen SSHAPE slots, spent once in DRAWPROTOS and never +REM again: six brick stamps and the two erasers. Nothing captures per frame +REM any more, so the pool never runs dry and GRAPHIC 5 is never called. REM One scalar per row colour, and scalars rather than an array because REM SSHAPE and GSHAPE used to read the leaf they were handed without REM applying its subscript -- a string array element always resolved to @@ -160,8 +160,6 @@ S3$ = "" S4$ = "" S5$ = "" Z$ = "" -REM 99 means the stamps are gone and have to be rebuilt before anything -REM can be drawn with them. DHUD# = 1 DPLAY# = 1 BAN$ = "" @@ -228,12 +226,6 @@ 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 @@ -310,14 +302,10 @@ GOSUB TITLESCREEN REM ===================================================================== REM The frame loop REM -REM PACE first, because it is what puts the drawing at the top of a host -REM batch; then at most one capture, because a capture is the only thing -REM with a deadline; then the game, which may take as long as it likes. +REM At most one drawing job, then the game, which may take as long as it +REM likes. Nothing here has a deadline: a drawing that spans two host +REM batches simply arrives over two frames and the layer keeps both halves. REM ===================================================================== -REM No pacing. PACE spun on TI# until a batch boundary so that an SSHAPE -REM capture would not be cut in half by a present; nothing is captured now, so -REM a drawing that spans two batches simply arrives over two frames and the -REM layer keeps both halves. LABEL FRAME GOSUB DRAWJOB GOSUB READKEYS @@ -338,18 +326,17 @@ PRINT "FINAL SCORE " + SCORE# + " BEST " + HISCORE# END REM ===================================================================== -REM One capture per frame, and only one +REM One drawing job per frame, and only one REM -REM Rebuilding the brick stamps comes first, because everything else -REM draws with them and GRAPHIC 5 has just thrown them away. +REM The field is the expensive one -- sixty stamps and a border -- so it +REM never shares a frame with the HUD. Both are flagged rather than drawn +REM where the change happens, so a routine that changes three things costs +REM one drawing. REM ===================================================================== REM Labels rather than BEGIN blocks here. When this was written it was not REM a style choice -- a loop inside a block that was skipped left the REM interpreter with no GOSUB to return from and stopped the program. That REM is fixed; the shape is kept because it reads the same either way. -REM The stamps are built once now rather than counted and rebuilt. Nothing -REM captures per frame any more, so the sixteen SSHAPE slots are spent once and -REM stay spent -- eight of them -- and GRAPHIC 5 is never called. LABEL DRAWJOB LABEL DRAWJOB2 IF DPLAY# = 0 THEN GOTO DRAWJOB3 @@ -1002,10 +989,9 @@ FOR I# = 0 TO 59 BRK#(I#) = 0 NEXT I# REM Retire the previous level's rectangles *before* laying this one out, not -REM after. This is where GOSUB BUILDLIVE used to sit, and it meant something -REM different: rebuild the list of what is standing. Left in place, the clear -REM ran after FILLROW had registered all sixty and threw them all away again -- -REM which cost nothing visible and simply stopped the ball hitting anything. +REM after. Put after FILLROW it throws away all sixty rectangles that were +REM just registered, which costs nothing visible and simply stops the ball +REM hitting anything. SOLID FOR R# = 0 TO 5 IF R# < T3# THEN GOSUB FILLROW