2026-07-31 21:50:50 -04:00
|
|
|
# 8. Sprites
|
|
|
|
|
|
|
|
|
|
Eight sprites, numbered 1 to 8, as on a C128. They need the SDL build.
|
|
|
|
|
|
|
|
|
|
A sprite here is a real object in the host's graphics library, registered alongside
|
|
|
|
|
whatever the host's own game is drawing — so a game that embeds this interpreter can
|
|
|
|
|
see and manipulate a script's sprites.
|
|
|
|
|
|
|
|
|
|
## Giving a sprite a picture
|
|
|
|
|
|
|
|
|
|
`SPRSAV` does it, and it takes three kinds of source.
|
|
|
|
|
|
|
|
|
|
### From an image file
|
|
|
|
|
|
Execute every documented example as a test
docs/ and README.md carry 85 fenced blocks. Every one was checked by hand
exactly once, when it was written, which is not a standard that survives a
changing interpreter -- and four were already wrong: two transcripts showing a
leading space PRINT does not emit, akbasic_TextSink in README.md missing the
two members it had grown hours earlier, and FILTER's refusal quoted with
wording the code does not use.
tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds.
BASIC programs and transcripts run and are byte-compared against an `output`
block; C snippets compile with -fsyntax-only against the real include path,
which CMake writes out because it is transitive through akerror, akstdlib and
akgl; shell blocks run in a sandbox. Anything that would reconfigure the build
tree, hit the network or re-enter the suite is tagged norun with the reason in
MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision.
An untagged block is a failure rather than a default, and the pass line
reports what it executed by kind. Both exist because the way a harness like
this dies is by quietly matching nothing and passing -- which it duly did on
the first CTest run, where a generator expression evaluating to nothing still
contributed an empty argument that the script read as a filename. The count is
what caught it.
The excerpt check earns its own mention: a block tagged
`c excerpt=include/akbasic/sink.h` must still appear in that header, comments
and whitespace ignored. Compiling it would only redefine the type, so a
compile check could not have found the stale struct, and did not.
Registered as the CTest case docs_examples in both configurations. Fixing the
four wrong examples turned up two interpreter defects, fixed in the previous
commit and recorded in TODO.md section 8.
MAINTENANCE.md is new: the fence-tag reference, what to do when the case
fails, and the conventions that until now only existed inside source comments
-- the three test lists and how two of them invert "passed", the sorted verb
table, that a golden file is never edited to suit this interpreter, and that a
fix gets mutation-checked with a file copy rather than git checkout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
|
|
|
```basic requires=akgl setup=ship
|
2026-07-31 21:50:50 -04:00
|
|
|
10 SPRSAV "ship.png", 1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The most useful form, and not one a C128 has. The path is tried against the working
|
|
|
|
|
directory first and then against the directory the running program was loaded from — so
|
|
|
|
|
a `.bas` stored beside its artwork works wherever you launch it from. Anything the
|
|
|
|
|
image library can decode will do.
|
|
|
|
|
|
|
|
|
|
A sprite loaded this way takes **the image's own size**. It is not forced to 24 by 21.
|
|
|
|
|
|
|
|
|
|
### From a saved region
|
|
|
|
|
|
Execute every documented example as a test
docs/ and README.md carry 85 fenced blocks. Every one was checked by hand
exactly once, when it was written, which is not a standard that survives a
changing interpreter -- and four were already wrong: two transcripts showing a
leading space PRINT does not emit, akbasic_TextSink in README.md missing the
two members it had grown hours earlier, and FILTER's refusal quoted with
wording the code does not use.
tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds.
BASIC programs and transcripts run and are byte-compared against an `output`
block; C snippets compile with -fsyntax-only against the real include path,
which CMake writes out because it is transitive through akerror, akstdlib and
akgl; shell blocks run in a sandbox. Anything that would reconfigure the build
tree, hit the network or re-enter the suite is tagged norun with the reason in
MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision.
An untagged block is a failure rather than a default, and the pass line
reports what it executed by kind. Both exist because the way a harness like
this dies is by quietly matching nothing and passing -- which it duly did on
the first CTest run, where a generator expression evaluating to nothing still
contributed an empty argument that the script read as a filename. The count is
what caught it.
The excerpt check earns its own mention: a block tagged
`c excerpt=include/akbasic/sink.h` must still appear in that header, comments
and whitespace ignored. Compiling it would only redefine the type, so a
compile check could not have found the stale struct, and did not.
Registered as the CTest case docs_examples in both configurations. Fixing the
four wrong examples turned up two interpreter defects, fixed in the previous
commit and recorded in TODO.md section 8.
MAINTENANCE.md is new: the fence-tag reference, what to do when the case
fails, and the conventions that until now only existed inside source comments
-- the three test lists and how two of them invert "passed", the sorted verb
table, that a golden file is never edited to suit this interpreter, and that a
fix gets mutation-checked with a file copy rather than git checkout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
|
|
|
```basic requires=akgl
|
2026-07-31 21:50:50 -04:00
|
|
|
10 BOX 1, 0, 0, 24, 21
|
|
|
|
|
20 SSHAPE A$, 0, 0, 24, 21
|
|
|
|
|
30 SPRSAV A$, 1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Draw it with the graphics verbs, capture it, install it. The C128 documents `SPRSAV`'s
|
|
|
|
|
string as the `SSHAPE` data format at a fixed 24 by 21, so sharing the mechanism is
|
|
|
|
|
faithful rather than a shortcut.
|
|
|
|
|
|
|
|
|
|
### From data
|
|
|
|
|
|
Execute every documented example as a test
docs/ and README.md carry 85 fenced blocks. Every one was checked by hand
exactly once, when it was written, which is not a standard that survives a
changing interpreter -- and four were already wrong: two transcripts showing a
leading space PRINT does not emit, akbasic_TextSink in README.md missing the
two members it had grown hours earlier, and FILTER's refusal quoted with
wording the code does not use.
tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds.
BASIC programs and transcripts run and are byte-compared against an `output`
block; C snippets compile with -fsyntax-only against the real include path,
which CMake writes out because it is transitive through akerror, akstdlib and
akgl; shell blocks run in a sandbox. Anything that would reconfigure the build
tree, hit the network or re-enter the suite is tagged norun with the reason in
MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision.
An untagged block is a failure rather than a default, and the pass line
reports what it executed by kind. Both exist because the way a harness like
this dies is by quietly matching nothing and passing -- which it duly did on
the first CTest run, where a generator expression evaluating to nothing still
contributed an empty argument that the script read as a filename. The count is
what caught it.
The excerpt check earns its own mention: a block tagged
`c excerpt=include/akbasic/sink.h` must still appear in that header, comments
and whitespace ignored. Compiling it would only redefine the type, so a
compile check could not have found the stale struct, and did not.
Registered as the CTest case docs_examples in both configurations. Fixing the
four wrong examples turned up two interpreter defects, fixed in the previous
commit and recorded in TODO.md section 8.
MAINTENANCE.md is new: the fence-tag reference, what to do when the case
fails, and the conventions that until now only existed inside source comments
-- the three test lists and how two of them invert "passed", the sorted verb
table, that a golden file is never edited to suit this interpreter, and that a
fix gets mutation-checked with a file copy rather than git checkout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
|
|
|
```basic norun
|
2026-07-31 21:50:50 -04:00
|
|
|
10 DIM P#(63)
|
|
|
|
|
20 FOR I# = 0 TO 62
|
|
|
|
|
30 READ P#(I#)
|
|
|
|
|
40 NEXT I#
|
|
|
|
|
50 SPRSAV P#, 1
|
|
|
|
|
60 DATA 255, 129, 129, ...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
63 bytes: three per row, twenty-one rows, most significant bit leftmost. This is the
|
|
|
|
|
form a type-in listing uses.
|
|
|
|
|
|
|
|
|
|
**It takes an integer array, not a string.** A C128 puts the raw bytes in a string; a
|
|
|
|
|
string here is NUL-terminated, so it cannot hold a zero byte and therefore cannot hold
|
|
|
|
|
a sprite. `DIM P#(63)` is exactly the right size.
|
|
|
|
|
|
|
|
|
|
Writing a sprite back out — `SPRSAV 1, A$` — is refused. That would be a disk
|
|
|
|
|
operation.
|
|
|
|
|
|
|
|
|
|
## Showing and moving
|
|
|
|
|
|
Generate the documentation's figures from the listings they illustrate
Chapters 6 and 8 described what a verb draws in prose. Eight figures now show
it, and each one is produced by running the BASIC listing printed immediately
above it -- so a picture cannot drift away from the code beside it, which is the
way a screenshot goes wrong and the way nothing notices.
tools/screenshot.c is a second SDL host, much smaller than the frontend: dummy
video driver, software renderer, run to completion, read the target back, write
a PNG. It draws no text layer on purpose, so a READY in the corner is not noise
in a figure about BOX and no font has to be resolved.
tools/docs_screenshots.sh reads the new screenshot=NAME fence tag straight out
of the markdown. size=WxH is the second tag, and SCALE's figure uses it: the
point being made is a 320x200 listing filling a larger window, which cannot be
made on a 320x200 surface.
Two gates, answering different questions. docs_examples fails a tagged block
with no image, in both configurations, so a figure cannot be added and
forgotten. docs_screenshots -- a CTest, AKGL build only -- re-renders every
figure and compares byte for byte, so a listing edited without regenerating
fails. Only the second catches a stale picture.
The PNGs are checked in because a reader on the forge has no build tree, and
docs/images/README.md says loudly that they are generated. Regenerating is never
part of a build: the target is run deliberately, so a make cannot put eight
binary diffs in front of whoever ran it.
Drawing the BOX figure caught a defect in TODO.md itself. Deviation 16 claimed
in bold that BOX fills on a negative angle while its own paragraph said the fill
was filed rather than implemented. BOX cannot fill, and filled_rect is reached
by no verb as a result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:37:04 -04:00
|
|
|
```basic requires=akgl screenshot=sprites
|
|
|
|
|
10 COLOR 1, 2
|
|
|
|
|
20 CIRCLE 1, 20, 20, 18, 18
|
|
|
|
|
30 PAINT 1, 20, 20
|
|
|
|
|
40 SSHAPE A$, 0, 0, 40, 40
|
|
|
|
|
50 GRAPHIC 1, 1
|
|
|
|
|
60 FOR N# = 1 TO 3
|
|
|
|
|
70 SPRSAV A$, N#
|
|
|
|
|
80 NEXT N#
|
|
|
|
|
90 SPRITE 1, 1
|
|
|
|
|
100 SPRITE 2, 1, 6
|
|
|
|
|
110 SPRITE 3, 1, 8, 0, 1, 1
|
|
|
|
|
120 MOVSPR 1, 30, 80
|
|
|
|
|
130 MOVSPR 2, 110, 80
|
|
|
|
|
140 MOVSPR 3, 190, 60
|
2026-07-31 21:50:50 -04:00
|
|
|
```
|
|
|
|
|
|
Generate the documentation's figures from the listings they illustrate
Chapters 6 and 8 described what a verb draws in prose. Eight figures now show
it, and each one is produced by running the BASIC listing printed immediately
above it -- so a picture cannot drift away from the code beside it, which is the
way a screenshot goes wrong and the way nothing notices.
tools/screenshot.c is a second SDL host, much smaller than the frontend: dummy
video driver, software renderer, run to completion, read the target back, write
a PNG. It draws no text layer on purpose, so a READY in the corner is not noise
in a figure about BOX and no font has to be resolved.
tools/docs_screenshots.sh reads the new screenshot=NAME fence tag straight out
of the markdown. size=WxH is the second tag, and SCALE's figure uses it: the
point being made is a 320x200 listing filling a larger window, which cannot be
made on a 320x200 surface.
Two gates, answering different questions. docs_examples fails a tagged block
with no image, in both configurations, so a figure cannot be added and
forgotten. docs_screenshots -- a CTest, AKGL build only -- re-renders every
figure and compares byte for byte, so a listing edited without regenerating
fails. Only the second catches a stale picture.
The PNGs are checked in because a reader on the forge has no build tree, and
docs/images/README.md says loudly that they are generated. Regenerating is never
part of a build: the target is run deliberately, so a make cannot put eight
binary diffs in front of whoever ran it.
Drawing the BOX figure caught a defect in TODO.md itself. Deviation 16 claimed
in bold that BOX fills on a negative angle while its own paragraph said the fill
was filed rather than implemented. BOX cannot fill, and filled_rect is reached
by no verb as a result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:37:04 -04:00
|
|
|

|
|
|
|
|
|
|
|
|
|
Three sprites from one drawing: the first in the default colour, the second in colour
|
|
|
|
|
6, and the third in colour 8 with both expansion bits set, which is what makes it twice
|
|
|
|
|
the size. `GRAPHIC 1, 1` on line 50 wipes the drawing the sprites were captured from —
|
|
|
|
|
the picture would otherwise still show the original disc in the top-left corner.
|
|
|
|
|
|
|
|
|
|
**A sprite's colour multiplies the artwork rather than replacing it**, so a white disc
|
|
|
|
|
takes the colour cleanly and a coloured one comes out darker than you asked for.
|
|
|
|
|
|
2026-07-31 21:50:50 -04:00
|
|
|
`SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the
|
|
|
|
|
number is required and **an argument you leave out is left alone**, so `SPRITE 1, 1`
|
|
|
|
|
turns sprite 1 on without disturbing its colour.
|
|
|
|
|
|
|
|
|
|
`MOVSPR` has four forms, and the punctuation is what tells them apart:
|
|
|
|
|
|
|
|
|
|
| Form | What it does |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `MOVSPR 1, 100, 50` | put it at (100, 50) |
|
|
|
|
|
| `MOVSPR 1, +10, -20` | move it *by* that much |
|
|
|
|
|
| `MOVSPR 1, 10 ; 90` | move it 10 pixels along bearing 90 |
|
|
|
|
|
| `MOVSPR 1, 45 # 8` | set it moving along bearing 45 at speed 8 |
|
|
|
|
|
|
|
|
|
|
A bearing is degrees **clockwise from straight up**, so 0 is north and 90 is east. In
|
|
|
|
|
the polar form the distance comes first and the angle second — the opposite order from
|
|
|
|
|
the continuous form, which is a trap worth remembering.
|
|
|
|
|
|
|
|
|
|
The continuous form does not move anything on the statement itself. The sprite moves as
|
|
|
|
|
the program runs, paced by the host's clock. Speed 0 stops it.
|
|
|
|
|
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
Coordinates are the same window pixels the drawing verbs use, not the VIC-II's raster
|
|
|
|
|
coordinates — see Chapter 6, and `RGR(1)` and `RGR(2)` for how big the window is.
|
|
|
|
|
`SCALE` does not apply to them: a sprite is positioned in device pixels whatever the
|
|
|
|
|
drawing verbs are doing.
|
2026-07-31 21:50:50 -04:00
|
|
|
|
|
|
|
|
## Collision
|
|
|
|
|
|
Execute every documented example as a test
docs/ and README.md carry 85 fenced blocks. Every one was checked by hand
exactly once, when it was written, which is not a standard that survives a
changing interpreter -- and four were already wrong: two transcripts showing a
leading space PRINT does not emit, akbasic_TextSink in README.md missing the
two members it had grown hours earlier, and FILTER's refusal quoted with
wording the code does not use.
tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds.
BASIC programs and transcripts run and are byte-compared against an `output`
block; C snippets compile with -fsyntax-only against the real include path,
which CMake writes out because it is transitive through akerror, akstdlib and
akgl; shell blocks run in a sandbox. Anything that would reconfigure the build
tree, hit the network or re-enter the suite is tagged norun with the reason in
MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision.
An untagged block is a failure rather than a default, and the pass line
reports what it executed by kind. Both exist because the way a harness like
this dies is by quietly matching nothing and passing -- which it duly did on
the first CTest run, where a generator expression evaluating to nothing still
contributed an empty argument that the script read as a filename. The count is
what caught it.
The excerpt check earns its own mention: a block tagged
`c excerpt=include/akbasic/sink.h` must still appear in that header, comments
and whitespace ignored. Compiling it would only redefine the type, so a
compile check could not have found the stale struct, and did not.
Registered as the CTest case docs_examples in both configurations. Fixing the
four wrong examples turned up two interpreter defects, fixed in the previous
commit and recorded in TODO.md section 8.
MAINTENANCE.md is new: the fence-tag reference, what to do when the case
fails, and the conventions that until now only existed inside source comments
-- the three test lists and how two of them invert "passed", the sorted verb
table, that a golden file is never edited to suit this interpreter, and that a
fix gets mutation-checked with a file copy rather than git checkout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
|
|
|
```basic norun
|
2026-07-31 21:50:50 -04:00
|
|
|
10 COLLISION 1, BUMPED
|
|
|
|
|
20 REM ... main loop ...
|
|
|
|
|
90 GOTO 20
|
|
|
|
|
100 LABEL BUMPED
|
|
|
|
|
110 PRINT "HIT: " + BUMP(1)
|
|
|
|
|
120 RETURN
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`COLLISION 1, target` calls a subroutine when two sprites overlap. The handler is
|
|
|
|
|
entered between lines and must end in `RETURN`, exactly like a `GOSUB` body. Omitting
|
|
|
|
|
the target disarms it.
|
|
|
|
|
|
|
|
|
|
`BUMP(1)` returns a bitmask of which sprites have collided — bit 0 is sprite 1 — and
|
|
|
|
|
**reading it clears it**, which is what makes "has anything hit me since I last looked"
|
|
|
|
|
answerable.
|
|
|
|
|
|
|
|
|
|
Only type 1, sprite-to-sprite, is implemented. Types 2 and 3 are refused by name.
|
|
|
|
|
|
|
|
|
|
Collision is by bounding box, not by pixel: two sprites whose boxes overlap but whose
|
|
|
|
|
artwork does not are reported as colliding.
|
|
|
|
|
|
Generate the documentation's figures from the listings they illustrate
Chapters 6 and 8 described what a verb draws in prose. Eight figures now show
it, and each one is produced by running the BASIC listing printed immediately
above it -- so a picture cannot drift away from the code beside it, which is the
way a screenshot goes wrong and the way nothing notices.
tools/screenshot.c is a second SDL host, much smaller than the frontend: dummy
video driver, software renderer, run to completion, read the target back, write
a PNG. It draws no text layer on purpose, so a READY in the corner is not noise
in a figure about BOX and no font has to be resolved.
tools/docs_screenshots.sh reads the new screenshot=NAME fence tag straight out
of the markdown. size=WxH is the second tag, and SCALE's figure uses it: the
point being made is a 320x200 listing filling a larger window, which cannot be
made on a 320x200 surface.
Two gates, answering different questions. docs_examples fails a tagged block
with no image, in both configurations, so a figure cannot be added and
forgotten. docs_screenshots -- a CTest, AKGL build only -- re-renders every
figure and compares byte for byte, so a listing edited without regenerating
fails. Only the second catches a stale picture.
The PNGs are checked in because a reader on the forge has no build tree, and
docs/images/README.md says loudly that they are generated. Regenerating is never
part of a build: the target is run deliberately, so a make cannot put eight
binary diffs in front of whoever ran it.
Drawing the BOX figure caught a defect in TODO.md itself. Deviation 16 claimed
in bold that BOX fills on a negative angle while its own paragraph said the fill
was filed rather than implemented. BOX cannot fill, and filled_rect is reached
by no verb as a result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:37:04 -04:00
|
|
|
```basic requires=akgl screenshot=sprite-collision
|
|
|
|
|
10 COLOR 1, 2
|
|
|
|
|
20 CIRCLE 1, 20, 20, 14, 14
|
|
|
|
|
30 PAINT 1, 20, 20
|
|
|
|
|
40 BOX 1, 0, 0, 40, 40
|
|
|
|
|
50 SSHAPE A$, 0, 0, 40, 40
|
|
|
|
|
60 GRAPHIC 1, 1
|
|
|
|
|
70 SPRSAV A$, 1
|
|
|
|
|
80 SPRSAV A$, 2
|
|
|
|
|
90 SPRITE 1, 1, 6
|
|
|
|
|
100 SPRITE 2, 1, 3
|
|
|
|
|
110 MOVSPR 1, 110, 55
|
|
|
|
|
120 MOVSPR 2, 145, 90
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
The artwork here includes a border around the whole 40 by 40 sprite, so each sprite
|
|
|
|
|
draws its own bounding box. Those boxes overlap at one corner and the two discs are
|
|
|
|
|
nowhere near each other — and `BUMP(1)` reports a collision.
|
|
|
|
|
|
Let a program say what part of a sprite collides
`SPRHIT n, kind [,x1, y1, x2, y2]` gives a sprite a collision shape: a box, a
circle inscribed in it, or a capsule. `RSPHIT(n, f)` reads it back in SPRHIT's
own argument order, the way RSPRITE and RSPPOS already do, and needs no device
because it answers from interpreter state.
The rectangle is two corners measured from the sprite's top-left, in device
pixels -- the same `x1, y1, x2, y2` that `BOX` and `SSHAPE` take. A dialect with
two spellings for a rectangle is one nobody can write from memory. Omit it and
the shape fits whatever the picture turned out to be, which is what a sprite
loaded from a file needs: `SPRSAV "ship.png", 1` takes the image's own size and
the program never learns what that was.
**A sprite nobody has shaped collides with its whole frame, expansion bits
included, exactly as before.** That is a promise rather than a convenience, and
it has its own test: the same two sprites in the same two places, once with no
SPRHIT and once with a four-pixel box, reporting a collision and then not.
Named SPRHIT rather than SPRSHAPE because "shape" already means "a region SSHAPE
saved" in this dialect, in this very chapter -- `SPRSAV A$, 1` takes one -- and a
reader who typed `SPRSHAPE A$, 1` would have had every reason to. Both names, and
RSPHIT, were grepped against every label in docs/, examples/ and both corpora
first: a bare word is a label here, so a verb and a label share one namespace and
taking a name a checked-in listing already uses would break it silently.
`SPRHIT n, 0` takes a sprite out of collision while leaving it on the screen --
the ghost, the flashing invulnerable player, the pickup already taken. Hiding it
with `SPRITE n, 0` stops it colliding too, and is what you want when it should
not be seen either.
The circle answers the complaint chapter 8 already ships a figure of. That figure
shows two discs whose *boxes* touch at a corner while the artwork is nowhere
near, and `BUMP(1)` reporting a collision; two `SPRHIT n, 2` and it stops. The
test asserts both halves so the figure's caption stays true.
`tests/verbs_table.c` caught RSPHIT filed after RSPPOS rather than before it,
which is the sorted-table test doing exactly the job it exists for.
Docs: a new section in chapter 8, rows in the verb and function references in
alphabetical order, and chapter 13's "collision is by bounding box" becomes
"collision is by shape" with the addition named. 111 with akgl, 110 without, and
the artwork breakout still runs clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 10:01:33 -04:00
|
|
|
## Giving a sprite a shape
|
|
|
|
|
|
|
|
|
|
That false positive is what `SPRHIT` is for. It says what part of a sprite collides,
|
|
|
|
|
rather than leaving it as the whole picture:
|
|
|
|
|
|
|
|
|
|
```basic norun
|
|
|
|
|
SPRHIT n, kind
|
|
|
|
|
SPRHIT n, kind, x1, y1, x2, y2
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| `kind` | Is |
|
|
|
|
|
|---|---|
|
|
|
|
|
| 0 | nothing — the sprite stays on screen and stops colliding |
|
|
|
|
|
| 1 | a box |
|
|
|
|
|
| 2 | a circle, inscribed in the rectangle |
|
|
|
|
|
| 3 | a capsule, round ends left and right |
|
|
|
|
|
| 4 | a capsule, round ends top and bottom |
|
|
|
|
|
|
|
|
|
|
The rectangle is **two corners measured from the sprite's top-left**, in device pixels —
|
|
|
|
|
the same `x1, y1, x2, y2` that `BOX` and `SSHAPE` take, because a dialect with two
|
|
|
|
|
spellings for a rectangle is one nobody can write from memory. Leave it out and the shape
|
|
|
|
|
fits whatever the picture turned out to be, which is what a sprite loaded from a file
|
|
|
|
|
needs: `SPRSAV "ship.png", 1` takes the image's own size and the program never learns what
|
|
|
|
|
that was.
|
|
|
|
|
|
|
|
|
|
So the two discs above stop colliding as soon as they are discs:
|
|
|
|
|
|
|
|
|
|
```basic norun
|
|
|
|
|
90 SPRHIT 1, 2
|
|
|
|
|
100 SPRHIT 2, 2
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and a ship whose art does not fill its frame can say so:
|
|
|
|
|
|
|
|
|
|
```basic norun
|
|
|
|
|
SPRHIT 1, 1, 4, 2, 20, 19
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**A sprite nobody has shaped collides with its whole frame**, expansion bits included,
|
|
|
|
|
which is what every sprite did before `SPRHIT` existed. Adding the verb changed no existing
|
|
|
|
|
program.
|
|
|
|
|
|
|
|
|
|
`SPRHIT n, 0` is the ghost, the flashing invulnerable player and the pickup that has
|
|
|
|
|
already been taken: still drawn, no longer in the way. Hiding the sprite with `SPRITE n, 0`
|
|
|
|
|
also stops it colliding, and is what you want when it should not be seen either.
|
|
|
|
|
|
|
|
|
|
`RSPHIT(n, f)` reads it back, in `SPRHIT`'s own argument order — 0 the kind, then 1 to 4
|
|
|
|
|
for the two corners. It needs no sprite device, the way `RSPPOS` does not.
|
|
|
|
|
|
Collide sprites with rectangles that are not sprites
`SOLID id, x1, y1, x2, y2` registers static collision geometry; `SOLID id`
retires one and a bare `SOLID` retires them all, the way `TRAP`, `COLLISION` and
`DCLOSE` all read absence. `COLLISION 2` and `BUMP(2)` stop being refused and
mean *sprite met static geometry*.
**This is the thing eight sprite slots made impossible.** A wall of bricks wants
sixty, so until now a program could only collide with one by doing the
arithmetic itself against its own array -- which is exactly what both breakout
listings do, at about two hundred lines between them. A rectangle costs no sprite
slot.
The id is the **program's own number**, 1 to 64, not a minted handle. That is the
whole trick for "which brick did I hit": the id comes back out again, so a wall
built as `SOLID I#, ...` maps onto `B#(I#)` with no lookup, and retiring a broken
brick is `SOLID I#`.
`COLLISION 2` was refused with "sprite-to-background collision needs the screen
read back every frame", which was true of the question a C128 asks -- a sprite
against the bitmap's set pixels. `SOLID` gives this interpreter a background made
of rectangles instead, which is the same question in a form it can answer. Same
move `SPRSAV` made when it learned to take an image path.
`AKBASIC_INTERRUPT_BACKGROUND` has been sitting in the interrupt table commented
"COLLISION 2 -- sprite met background; refused" the whole time. Its accumulator is
separate, so a sprite hitting a wall never sets a bit in `BUMP(1)`.
**There is no `akgl_CollisionWorld` here, and that is deliberate.** libakgl's
uniform grid keeps its cell heads, cell size and origin in file-scope statics, so
it is one index per process -- and `akgl_collision_world_init()` ends in a
`reset()` that memsets those heads *and* calls
`akgl_heap_init_collision_cells()`. An interpreter embedded in a game with its
own collision world would have destroyed every registration that game had made,
on the first `SOLID` a script ran. So the geometry is indexed by an ordinary
array here and pairs go straight to `akgl_collision_test()`, which needs no
world. At sixty-four rectangles that is the right answer anyway; libakgl's own
numbers put a naive sweep at 0.7% of a frame at sixty-four objects.
**The scan now short-circuits when nothing has moved**, and that is what makes
any of it affordable. Its inputs are the sprites' boxes, which slots are
collidable, and the static geometry; if none changed the answer cannot have. A
frame runs one full scan and 255 cached ones. Eight sprites against sixty-four
rectangles is five hundred and twelve tests -- fine once a frame, ruinous 256
times.
The benchmark was rewritten to say which path it is timing, because with the
cache in place a loop that only calls the scan measures the short circuit and
nothing else. Breakout now costs 590.6 ns for its one full scan plus 255 cached
at 40.0, which is 10.8 us against a 1.19 ms frame -- **0.91%, less than the 2.0%
it cost before any of this work**, with static geometry and contacts added on
top.
`NEW` retires the rectangles, where it cannot undefine a sprite pattern: there
*is* an entry point for this one, so leaving them would be a choice, and the
wrong one -- a rectangle is invisible, so one left behind by a deleted program is
an unexplainable collision in the next. `CLR` leaves them alone.
`tests/sprite_verbs.c` gains the whole second path against the mock and its
`COLLISION 2` case is rewritten: it pinned the refusal, and now pins that type 2
arms its own handler without disturbing type 1's. `tests/akgl_backends.c` gains
the end-to-end version, including a full sixty-four-rectangle wall so the proxy
budget is exercised at its ceiling and the pool has to come back intact, and the
sixty-fifth refused by name.
A bare `SOLID` needed `akbasic_parse_optional_arglist` rather than
`akbasic_parse_arglist`, which `DCLOSE` already uses for the same shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 10:25:35 -04:00
|
|
|
## Colliding with things that are not sprites
|
|
|
|
|
|
|
|
|
|
There are eight sprites. A wall of bricks wants sixty, and until now the only way to
|
|
|
|
|
collide with one was to do the arithmetic yourself against your own array.
|
|
|
|
|
|
|
|
|
|
`SOLID` registers a rectangle the interpreter will collide sprites against, and it costs
|
|
|
|
|
no sprite slot:
|
|
|
|
|
|
|
|
|
|
```basic norun
|
|
|
|
|
SOLID id, x1, y1, x2, y2 register or replace rectangle id
|
|
|
|
|
SOLID id retire it
|
|
|
|
|
SOLID retire them all
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`id` is 1 to 64 and it is **your own number**. That is the whole trick for "which brick did
|
|
|
|
|
I hit": the id comes back out again, so a wall built as `SOLID I#, ...` maps straight onto
|
|
|
|
|
`B#(I#)` with no lookup. Retiring a broken brick is the same verb, `SOLID I#`.
|
|
|
|
|
|
|
|
|
|
Rectangles are in window pixels — the coordinates `BOX` draws in and `MOVSPR` positions in,
|
|
|
|
|
with `SCALE` not applying, exactly as for a sprite. So a brick drawn with
|
|
|
|
|
`BOX 1, X#, Y#, X# + 24, Y# + 10` is registered with the same four expressions.
|
|
|
|
|
|
|
|
|
|
The bare form retiring everything is what a level change wants, and it follows `TRAP`,
|
|
|
|
|
`COLLISION` and `DCLOSE`, which all read absence as "off".
|
|
|
|
|
|
|
|
|
|
**`COLLISION 2` is how you hear about it.** On a C128 type 2 is a sprite against the set
|
|
|
|
|
pixels of the bitmap screen; here it is a sprite against the rectangles `SOLID` registered,
|
|
|
|
|
which is the same question in a form this interpreter can answer. `BUMP(2)` is its mask,
|
|
|
|
|
and it is a **separate** accumulator from `BUMP(1)` — a sprite hitting a wall never sets a
|
|
|
|
|
bit in the sprite-to-sprite mask, so a program that only cares about one of them is not
|
|
|
|
|
disturbed by the other.
|
|
|
|
|
|
|
|
|
|
```basic norun
|
|
|
|
|
10 COLLISION 2, WALL
|
|
|
|
|
20 FOR I# = 1 TO 60
|
|
|
|
|
30 SOLID I#, BX#(I#), BY#(I#), BX#(I#) + 24, BY#(I#) + 10
|
|
|
|
|
40 NEXT I#
|
|
|
|
|
...
|
|
|
|
|
100 LABEL WALL
|
|
|
|
|
110 M# = BUMP(2)
|
|
|
|
|
120 IF (M# AND 1) = 0 THEN RETURN
|
|
|
|
|
130 REM ... the ball hit something ...
|
|
|
|
|
140 RETURN
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Sixty-four is the ceiling, and it is a real budget rather than a round number: these come
|
|
|
|
|
out of a pool of collision proxies shared with whatever game this interpreter is embedded
|
|
|
|
|
in, and eight sprites plus sixty-four rectangles is most of akbasic's share of it. The
|
|
|
|
|
sixty-fifth is refused by name.
|
|
|
|
|
|
|
|
|
|
`NEW` retires them all. A rectangle is invisible, so one left behind by a deleted program
|
|
|
|
|
would be an unexplainable collision in the next; `CLR` leaves them alone, because it clears
|
|
|
|
|
variables and a rectangle is not one.
|
|
|
|
|
|
2026-07-31 21:50:50 -04:00
|
|
|
## Reading state back
|
|
|
|
|
|
|
|
|
|
| Function | Gives |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `RSPPOS(n, 0)` | x |
|
|
|
|
|
| `RSPPOS(n, 1)` | y |
|
|
|
|
|
| `RSPPOS(n, 2)` | speed |
|
|
|
|
|
| `RSPRITE(n, f)` | one of `SPRITE`'s settings, in `SPRITE`'s own argument order |
|
|
|
|
|
| `RSPCOLOR(1)` or `RSPCOLOR(2)` | one of the shared multicolour registers |
|
|
|
|
|
|
|
|
|
|
These read the interpreter's own state rather than asking the device, so they work
|
|
|
|
|
even with no device attached.
|
|
|
|
|
|
|
|
|
|
## SPRDEF
|
|
|
|
|
|
|
|
|
|
Not implemented, and deliberately. It is an interactive full-screen sprite editor
|
|
|
|
|
driven by single keystrokes, not something a program can call — and this interpreter
|
|
|
|
|
does not own the screen it would take over. The three `SPRSAV` forms replace it.
|