The narrowphase has been producing a contact since it went in, and the interpreter was throwing it away. `RCOLLISION(n, f)` reports it: what was hit (a sprite or a `SOLID` rectangle), which one, the contact normal, the penetration depth, the contact point, and which axis to reverse. **The normal points out of the other thing and toward this one**, so a program moves along it by the depth and is exactly clear. That sign is the one assertion in the new test that could not be caught any other way -- both parties of a sprite-against-sprite hit get their own record, each pointing the way *that* sprite has to move, and sharing one would tell both to go the same direction, which is how two things end up stuck inside each other. **Field 7 is the one that deletes the most BASIC.** It is the minimum translation axis, computed from the normal in C, and it is there because doing it in BASIC means comparing two floats -- which is exactly where this dialect's left-operand rule catches people. `BALLBRICKS`/`TESTCELL` in the artwork breakout spend six lines computing an overlap rectangle and comparing its width to its height to get this number. The record is **sticky and deepest-wins**: replaced whenever that sprite is in a contact and otherwise left alone, so `BUMP` stays the event and this stays the detail of it. Making it clear itself when nothing touches would break the pairing, because `BUMP` accumulates across steps and a once-a-frame poll would find the detail already gone. Reading `BUMP` clears both, so they cannot disagree. Deliberately narrower than `akgl_Contact`: no actor pointers, because BASIC has no actor; no tile fields, because there is no tilemap; no z, because every test is planar; and **no `dt` and no `sensor`**, which libakgl documents as filled in by the resolver. This interpreter never resolves anything, so those two come back zero and mean nothing, and an always-zero field in a reference table is a lie. Documented with the two caveats that matter: fields 2, 3 and 4 are floats and want a `%` variable, and the contact *point* is exact only for two boxes -- libakgl's solver returns a point on the portal it converged to, while the normal and depth are exact for every pair. Chapter 8's collision section stops claiming only type 1 exists, which has been false since the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
400 lines
18 KiB
C
400 lines
18 KiB
C
/**
|
|
* @file sprite.h
|
|
* @brief Declares the sprite backend: where SPRITE, MOVSPR and SPRSAV land.
|
|
*
|
|
* The same shape as akbasic/graphics.h and for the same reason -- the core
|
|
* library builds with no SDL and no libakgl present, so a sprite verb calls
|
|
* through a record of function pointers rather than reaching for akgl_actor_*.
|
|
* akbasic_sprite_init_akgl() lives in the separate akbasic_akgl target.
|
|
*
|
|
* **A sprite is a Commodore sprite, not a libakgl sprite.** libakgl's has a
|
|
* sheet, a frame list, an animation speed and a state-to-sprite map, all of
|
|
* which arrive from a JSON document; BASIC 7.0 has a word for none of it. What
|
|
* BASIC has is eight numbered slots, each holding one still image with a
|
|
* position, a colour, a visibility flag and two expansion bits. So the JSON
|
|
* layer is bypassed rather than reached through: the adaptor builds the same
|
|
* structures directly and hands the interpreter this much smaller surface.
|
|
*
|
|
* The one place the two overlap is loading an image from a file, which is why
|
|
* `define_file` is here: a program that has art on disk should be able to say so,
|
|
* and libakgl already resolves a relative asset path and decodes the image. That
|
|
* path is not bypassed -- it is exactly akgl_spritesheet_initialize().
|
|
*
|
|
* A runtime with no sprite backend is the normal case, so every verb that needs
|
|
* one refuses with AKBASIC_ERR_DEVICE. The verbs that only touch interpreter
|
|
* state -- SPRCOLOR, and the RSP* readbacks -- work regardless, which is what
|
|
* makes them testable without a device.
|
|
*/
|
|
|
|
#ifndef _AKBASIC_SPRITE_H_
|
|
#define _AKBASIC_SPRITE_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/graphics.h>
|
|
#include <akbasic/types.h>
|
|
|
|
/** @brief Sprites a program has. Eight, as on a C128; the verbs number them 1-8. */
|
|
#define AKBASIC_MAX_SPRITES 8
|
|
|
|
/**
|
|
* @brief The collision shapes SPRHIT understands.
|
|
*
|
|
* **Deliberately libakgl's own numbers**, so the two documents agree and nobody
|
|
* has to maintain a mapping between them. A kind this interpreter has no use for
|
|
* is still refused by name rather than silently accepted.
|
|
*/
|
|
#define AKBASIC_SHAPE_NONE 0
|
|
#define AKBASIC_SHAPE_BOX 1
|
|
#define AKBASIC_SHAPE_CIRCLE 2
|
|
#define AKBASIC_SHAPE_CAPSULE_X 3
|
|
#define AKBASIC_SHAPE_CAPSULE_Y 4
|
|
/** @brief One past the last kind, for the range check. */
|
|
#define AKBASIC_SHAPE_LAST 5
|
|
|
|
/**
|
|
* @brief Static collision rectangles a program can register with SOLID.
|
|
*
|
|
* Sixty-four, which is the smallest power of two that holds a sixty-brick wall
|
|
* with headroom. The ceiling matters because these come out of libakgl's
|
|
* collision proxy pool, which is shared with whatever host this interpreter is
|
|
* embedded in: eight sprites plus sixty-four solids is seventy-two of
|
|
* #AKGL_MAX_HEAP_COLLISION_PROXY, and the rest is the host's.
|
|
*/
|
|
#ifndef AKBASIC_MAX_SOLIDS
|
|
#define AKBASIC_MAX_SOLIDS 64
|
|
#endif
|
|
|
|
/** @brief Width of a Commodore sprite in pixels. */
|
|
#define AKBASIC_SPRITE_WIDTH 24
|
|
/** @brief Height of a Commodore sprite in pixels. */
|
|
#define AKBASIC_SPRITE_HEIGHT 21
|
|
/**
|
|
* @brief Bytes in a sprite pattern: three per row, twenty-one rows.
|
|
*
|
|
* The C128 documents SPRSAV's string as the SSHAPE data format at a fixed 24x21,
|
|
* which is 63 bytes of bitmap plus a four-byte header. Only the bitmap is
|
|
* carried here -- the header records a width and height that are constants for a
|
|
* sprite -- so a DIMmed integer array of this many elements is a whole pattern.
|
|
*/
|
|
#define AKBASIC_SPRITE_PATTERN_BYTES 63
|
|
|
|
/**
|
|
* @brief Fastest MOVSPR speed, and how far one unit of it travels.
|
|
*
|
|
* `MOVSPR n, angle # speed` takes 0-15 on a C128 and the reference manual does
|
|
* not say what a unit is in pixels; it says 15 is fastest and 0 stops. So the
|
|
* mapping is ours: one unit is #AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND device
|
|
* pixels per second, which puts speed 15 at four seconds to cross a 320-pixel
|
|
* screen and proportionally longer across a wider one. Recorded as a deviation
|
|
* in TODO.md section 5 rather than presented as fidelity.
|
|
*/
|
|
#define AKBASIC_SPRITE_MAX_SPEED 15
|
|
/** @brief Pixels per second one unit of MOVSPR speed is worth. See #AKBASIC_SPRITE_MAX_SPEED. */
|
|
#define AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND 5.0
|
|
|
|
/**
|
|
* @brief One sprite, as BASIC sees it.
|
|
*
|
|
* Every field here is something a BASIC verb sets and an RSP* function reads
|
|
* back, which is why the whole thing lives on the runtime rather than on the
|
|
* device: a host that swaps one renderer for another does not expect the
|
|
* program's sprite positions to go with it. The device is told about changes; it
|
|
* is never asked what they were.
|
|
*/
|
|
typedef struct
|
|
{
|
|
bool defined; /* SPRSAV has given this slot an image */
|
|
bool enabled; /* SPRITE n, 1 */
|
|
bool behind; /* priority: drawn behind the drawing surface */
|
|
bool xexpand; /* SPRITE's x-expand bit */
|
|
bool yexpand;
|
|
bool multicolour;
|
|
int colorindex; /* 1-16, the sprite's own colour */
|
|
double x; /* position in device pixels; see graphics.h */
|
|
double y;
|
|
double angle; /* MOVSPR's continuous motion: degrees */
|
|
int speed; /* clockwise from vertical, and 0-15 */
|
|
|
|
/**
|
|
* SPRHIT's collision shape: one of the #AKBASIC_SHAPE_* kinds, and a
|
|
* rectangle measured from the sprite's top-left corner.
|
|
*
|
|
* `shapeexplicit` is what separates "the program asked for the whole frame"
|
|
* from "the program has not said". They are the same shape today and will
|
|
* not stay that way: a frame-fitting shape has to be rebuilt when the
|
|
* picture or an expansion bit changes, and one the program gave in pixels
|
|
* must not be.
|
|
*/
|
|
int shapekind;
|
|
double shapex1;
|
|
double shapey1;
|
|
double shapex2;
|
|
double shapey2;
|
|
bool shapeexplicit;
|
|
} akbasic_Sprite;
|
|
|
|
/**
|
|
* @brief One rectangle registered by SOLID, as BASIC sees it.
|
|
*
|
|
* The id is the array index plus one and is the program's own number, not a
|
|
* minted handle. A brick in a sixty-brick wall has a natural index; making the
|
|
* program carry one back out of the interpreter would be inventing a second
|
|
* numbering for something that already has one.
|
|
*/
|
|
typedef struct
|
|
{
|
|
bool active;
|
|
double x1;
|
|
double y1;
|
|
double x2;
|
|
double y2;
|
|
} akbasic_Solid;
|
|
|
|
/**
|
|
* @brief What sprite n last collided with, and how.
|
|
*
|
|
* Narrower than libakgl's `akgl_Contact` on purpose. No actor pointers, because
|
|
* BASIC has no actor; no tile fields, because there is no tilemap; no z, because
|
|
* every test is planar. And **no `dt` and no `sensor`**, which libakgl documents
|
|
* as filled in by the resolver -- this interpreter never resolves anything, so
|
|
* they would come back zero and mean nothing. An always-zero field in a reference
|
|
* table is a lie.
|
|
*
|
|
* `axis` is the one field that is not the library's. It is the minimum
|
|
* translation axis -- which way to reverse -- and it is here because working it
|
|
* out from the normal is exactly where this dialect's left-operand arithmetic
|
|
* rule catches people, and because it is the single field that deletes the most
|
|
* BASIC.
|
|
*/
|
|
typedef struct
|
|
{
|
|
int what; /* 0 nothing yet, 1 a sprite, 2 static geometry */
|
|
int other; /* the other sprite's number, or the SOLID id */
|
|
double nx; /* unit normal, out of the other and toward this */
|
|
double ny;
|
|
double depth; /* how far in, in pixels */
|
|
double px; /* where they touched */
|
|
double py;
|
|
int axis; /* 1 reverse x, 2 reverse y */
|
|
} akbasic_Contact;
|
|
|
|
/**
|
|
* @brief The sprite verbs' own state, which lives on the runtime.
|
|
*
|
|
* `bumped` accumulates rather than reporting an instant. A collision that
|
|
* happens between two BUMP() calls is still news when the second one asks, and a
|
|
* program polling once a frame would otherwise miss anything that started and
|
|
* ended inside a frame. BUMP() clears what it reports, which is what a C128 does
|
|
* and what makes "has this sprite hit anything since I last looked" answerable.
|
|
*/
|
|
typedef struct
|
|
{
|
|
akbasic_Sprite sprites[AKBASIC_MAX_SPRITES];
|
|
akbasic_Solid solids[AKBASIC_MAX_SOLIDS];
|
|
akbasic_Contact contacts[AKBASIC_MAX_SPRITES];
|
|
int sharedcolor1; /* SPRCOLOR's two multicolour registers, 1-16 */
|
|
int sharedcolor2;
|
|
uint16_t bumped; /* bit n-1 set when sprite n has collided */
|
|
uint16_t bumpedsolid; /* and when it has met static geometry */
|
|
int64_t lastservicems; /* when MOVSPR's continuous motion last moved */
|
|
} akbasic_SpriteState;
|
|
|
|
/**
|
|
* @brief Where the sprite verbs land.
|
|
*
|
|
* Sprites are numbered 1 through #AKBASIC_MAX_SPRITES at this boundary, the same
|
|
* way a BASIC program numbers them. Translating to a zero-based slot is the
|
|
* backend's business, and doing it here would mean two numbering schemes in one
|
|
* file for no gain.
|
|
*
|
|
* There is deliberately no entry point for the RSP* readbacks. Everything they
|
|
* report is in #akbasic_SpriteState, which the interpreter owns, so asking the
|
|
* device would be asking it to repeat what it was told.
|
|
*/
|
|
typedef struct akbasic_SpriteBackend
|
|
{
|
|
void *self;
|
|
|
|
/**
|
|
* Install a pattern into sprite @p n from packed bitmap bytes.
|
|
*
|
|
* Three bytes per row, most significant bit leftmost, twenty-one rows --
|
|
* the layout a C128 keeps in its sprite block, and what a type-in listing's
|
|
* DATA statements hold. @p fg is what a set bit draws and @p bg what a clear
|
|
* one does; a clear bit is normally transparent, which is @p bg with a zero
|
|
* alpha.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*define)(struct akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg);
|
|
/**
|
|
* Install a pattern into sprite @p n from a region SSHAPE saved.
|
|
*
|
|
* The handle is the graphics backend's, not this one's -- SSHAPE puts it in
|
|
* a BASIC string and SPRSAV hands it straight back. An adaptor that serves
|
|
* both devices resolves it; one that does not may refuse with
|
|
* AKBASIC_ERR_DEVICE.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*define_shape)(struct akbasic_SpriteBackend *self, int n, int handle);
|
|
/**
|
|
* Install a pattern into sprite @p n by loading an image file.
|
|
*
|
|
* @p path is what the program wrote, resolved by the backend rather than
|
|
* here: the interpreter has no idea what a path means to the host, and the
|
|
* akgl adaptor resolves it exactly as libakgl resolves a spritesheet named
|
|
* by a sprite document. @p root is the directory to fall back to when the
|
|
* path does not resolve against the working directory, normally the
|
|
* directory the running program was loaded from; it may be NULL.
|
|
*
|
|
* Unlike the other two, this one does not force 24x21: the image's own size
|
|
* is the sprite's size. A modern PC has no reason to throw away art that
|
|
* does not happen to be sprite-shaped.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*define_file)(struct akbasic_SpriteBackend *self, int n, const char *path, const char *root);
|
|
/** Show or hide sprite @p n. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*show)(struct akbasic_SpriteBackend *self, int n, bool visible);
|
|
/** Put sprite @p n at (@p x, @p y), in device pixels. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akbasic_SpriteBackend *self, int n, double x, double y);
|
|
/**
|
|
* Set sprite @p n's own colour, its expansion bits and its priority.
|
|
*
|
|
* One call rather than four because `SPRITE` sets them together and a
|
|
* backend that has to rebuild a texture to change a colour would otherwise
|
|
* do it four times for one statement.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*configure)(struct akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind);
|
|
/** Set the two multicolour registers every multicolour sprite shares. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*shared_colors)(struct akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2);
|
|
/**
|
|
* Report which sprites are currently overlapping another sprite.
|
|
*
|
|
* Bit n-1 of @p mask is set when sprite n overlaps at least one other. The
|
|
* interpreter calls this once per step, ORs the result into
|
|
* akbasic_SpriteState::bumped, and raises the collision interrupt when
|
|
* anything is set -- so a backend answers about *now* and does not have to
|
|
* remember anything.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*collisions)(struct akbasic_SpriteBackend *self, uint16_t *mask);
|
|
/**
|
|
* Give sprite @p n a collision shape.
|
|
*
|
|
* @p kind is an #AKBASIC_SHAPE_NONE .. #AKBASIC_SHAPE_CAPSULE_Y, and the
|
|
* rectangle is two corners measured from the sprite's top-left, in device
|
|
* pixels -- the same form `BOX` and `SSHAPE` take, because a dialect with two
|
|
* spellings for a rectangle is a dialect nobody can write from memory.
|
|
*
|
|
* Optional. A backend that withholds it makes `SPRHIT` refuse by name, which
|
|
* is what a device that cannot do what was asked is supposed to do.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*shape)(struct akbasic_SpriteBackend *self, int n, int kind,
|
|
double x1, double y1, double x2, double y2);
|
|
/**
|
|
* Register, replace or retire static collision geometry @p id.
|
|
*
|
|
* @p define false retires it and the rectangle is ignored. Rectangles are in
|
|
* window pixels -- the coordinates `BOX` draws in and `MOVSPR` positions in,
|
|
* with `SCALE` not applying, exactly as for a sprite.
|
|
*
|
|
* Optional, like `shape`. A backend that withholds it makes `SOLID` refuse
|
|
* by name.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*solid)(struct akbasic_SpriteBackend *self, int id, bool define,
|
|
double x1, double y1, double x2, double y2);
|
|
/**
|
|
* Report which sprites are overlapping static geometry.
|
|
*
|
|
* Bit n-1 is set when sprite n overlaps at least one rectangle `solid`
|
|
* registered. Answered from the same pass as `collisions`, so the two are
|
|
* always about the same instant whichever order they are called in.
|
|
*
|
|
* Optional. A runtime whose backend withholds it reports no static
|
|
* collisions rather than refusing, exactly as one with no backend at all
|
|
* reports none -- a step is not a statement and has no line to blame.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*solids)(struct akbasic_SpriteBackend *self, uint16_t *mask);
|
|
/**
|
|
* Describe sprite @p n's deepest collision from the scan just run.
|
|
*
|
|
* @p found comes back false when that sprite hit nothing, and @p dest is
|
|
* then untouched. Deepest rather than first, because the deepest is the one
|
|
* a program has to undo.
|
|
*
|
|
* A pure accessor: it reads what the same pass that answered `collisions`
|
|
* and `solids` already worked out. That is why the contract on `collisions`
|
|
* says the two are about the same instant -- this depends on it.
|
|
*
|
|
* Optional. Withholding it makes `RCOLLISION` report nothing rather than
|
|
* refuse, the way a runtime with no backend at all does.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*contact)(struct akbasic_SpriteBackend *self, int n,
|
|
akbasic_Contact *dest, bool *found);
|
|
} akbasic_SpriteBackend;
|
|
|
|
/**
|
|
* @brief Reset the sprite state to its power-on values.
|
|
*
|
|
* Eight undefined, hidden sprites at the origin in white, the two shared
|
|
* multicolour registers at their C128 defaults, and nothing bumped. NEW comes
|
|
* back through here.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_state_init(akbasic_SpriteState *obj);
|
|
|
|
/**
|
|
* @brief Unpack 63 bytes of sprite bitmap into one pixel per byte.
|
|
*
|
|
* Three bytes per row, most significant bit leftmost. @p dest receives
|
|
* #AKBASIC_SPRITE_WIDTH * #AKBASIC_SPRITE_HEIGHT entries, each 0 or 1. Shared by
|
|
* every backend that has to turn a pattern into pixels, and separate from them
|
|
* so the bit order is asserted once rather than trusted eight times.
|
|
*
|
|
* @param pattern Packed bitmap; at least @p bytes readable.
|
|
* @param bytes How many bytes @p pattern holds; must be #AKBASIC_SPRITE_PATTERN_BYTES.
|
|
* @param dest Output destination populated by the function; 504 entries.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When either pointer is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When `bytes` is not #AKBASIC_SPRITE_PATTERN_BYTES.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_unpack(const uint8_t *pattern, int bytes, uint8_t *dest);
|
|
|
|
struct akbasic_Runtime;
|
|
|
|
/**
|
|
* @brief Move every sprite MOVSPR set in continuous motion.
|
|
*
|
|
* Called once per akbasic_runtime_step(), before the line runs, and paced off
|
|
* akbasic_runtime_settime() -- exactly the way the PLAY queue is paced, and for
|
|
* the same reason: this library owns no loop and must not block, so the caller
|
|
* that owns the frame owns the clock.
|
|
*
|
|
* A host that never sets the time leaves it at zero and nothing moves, which is
|
|
* a still picture rather than a hang.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_service(struct akbasic_Runtime *obj);
|
|
|
|
/**
|
|
* @brief Ask the device what is overlapping, and raise the collision interrupt.
|
|
*
|
|
* Also called once per step. What it finds is ORed into
|
|
* akbasic_SpriteState::bumped for BUMP() to report, and raised as
|
|
* #AKBASIC_INTERRUPT_SPRITE -- unconditionally, because an unarmed interrupt
|
|
* records nothing and there is therefore nothing to ask first.
|
|
*
|
|
* A runtime with no sprite device, or one whose backend withholds `collisions`,
|
|
* does nothing here rather than refusing: a step is not a statement and has no
|
|
* program line to blame.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_collision_service(struct akbasic_Runtime *obj);
|
|
|
|
#endif // _AKBASIC_SPRITE_H_
|