Say what was hit, which way is out, and how far
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
This commit is contained in:
@@ -1069,6 +1069,99 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_drawing_layer_persists(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The contact says which way to push out and how far.
|
||||
*
|
||||
* The reason for going through libakgl's narrowphase at all. Four comparisons
|
||||
* can say *that* two things overlap; only a narrowphase can say by how much and
|
||||
* in which direction, and that is what a game needs to reverse a ball or stop a
|
||||
* player at a wall.
|
||||
*
|
||||
* **The normal's sign is the assertion worth having.** It points out of the other
|
||||
* shape and toward this one, so a program moves along it by the depth and is
|
||||
* clear. Getting it backwards is the classic mistake and nothing else here would
|
||||
* notice.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_contact_geometry(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Contact contact;
|
||||
uint16_t mask = 0;
|
||||
bool found = false;
|
||||
|
||||
/*
|
||||
* A 24x21 sprite at (100, 100), and a wall whose left edge is at 118. They
|
||||
* overlap by six pixels along x and by far more along y, so the minimum
|
||||
* translation axis is x and the sprite has to move left to clear it.
|
||||
*/
|
||||
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SOLID 3, 118, 0, 300, 300\n"
|
||||
"80 MOVSPR 1, 100, 100\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x01);
|
||||
|
||||
memset(&contact, 0, sizeof(contact));
|
||||
PASS(errctx, SPRITES.contact(&SPRITES, 1, &contact, &found));
|
||||
TEST_REQUIRE(found, "sprite 1 overlapped a wall, so it should have a contact");
|
||||
TEST_REQUIRE_INT(contact.what, 2);
|
||||
TEST_REQUIRE_INT(contact.other, 3);
|
||||
TEST_REQUIRE_INT(contact.axis, 1);
|
||||
/* Six pixels of overlap: 100 + 24 = 124, less the wall's 118. */
|
||||
TEST_REQUIRE(contact.depth > 5.5 && contact.depth < 6.5,
|
||||
"the overlap is six pixels, got %f", contact.depth);
|
||||
/* Out of the wall and toward the sprite is leftward, so negative x. */
|
||||
TEST_REQUIRE(contact.nx < -0.5,
|
||||
"the normal should point away from the wall, got (%f, %f)",
|
||||
contact.nx, contact.ny);
|
||||
stop_runtime();
|
||||
|
||||
/* The same overlap from above: the axis becomes y and the normal flips. */
|
||||
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SOLID 3, 0, 118, 300, 300\n"
|
||||
"80 MOVSPR 1, 100, 100\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x01);
|
||||
memset(&contact, 0, sizeof(contact));
|
||||
PASS(errctx, SPRITES.contact(&SPRITES, 1, &contact, &found));
|
||||
TEST_REQUIRE(found, "sprite 1 should have a contact");
|
||||
TEST_REQUIRE_INT(contact.axis, 2);
|
||||
TEST_REQUIRE(contact.ny < -0.5,
|
||||
"the normal should point up out of the wall, got (%f, %f)",
|
||||
contact.nx, contact.ny);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* Sprite against sprite, and **both** get a contact pointing the way *they*
|
||||
* have to move. Sharing one normal would tell both to go the same direction,
|
||||
* which is how two things end up stuck inside each other.
|
||||
*/
|
||||
PASS(errctx, two_sprites("90 MOVSPR 1, 100, 100\n"
|
||||
"100 MOVSPR 2, 118, 100\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x03);
|
||||
memset(&contact, 0, sizeof(contact));
|
||||
PASS(errctx, SPRITES.contact(&SPRITES, 1, &contact, &found));
|
||||
TEST_REQUIRE(found, "sprite 1 should have a contact");
|
||||
TEST_REQUIRE_INT(contact.what, 1);
|
||||
TEST_REQUIRE_INT(contact.other, 2);
|
||||
TEST_REQUIRE(contact.nx < -0.5, "sprite 1 is on the left, so it moves left");
|
||||
memset(&contact, 0, sizeof(contact));
|
||||
PASS(errctx, SPRITES.contact(&SPRITES, 2, &contact, &found));
|
||||
TEST_REQUIRE(found, "sprite 2 should have a contact too");
|
||||
TEST_REQUIRE_INT(contact.other, 1);
|
||||
TEST_REQUIRE(contact.nx > 0.5, "sprite 2 is on the right, so it moves right");
|
||||
stop_runtime();
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -1139,6 +1232,7 @@ int main(void)
|
||||
CATCH(errctx, test_collision_cross_shape());
|
||||
CATCH(errctx, test_sprite_shapes());
|
||||
CATCH(errctx, test_static_geometry());
|
||||
CATCH(errctx, test_contact_geometry());
|
||||
CATCH(errctx, test_drawing_layer_persists());
|
||||
} CLEANUP {
|
||||
if ( font != NULL ) {
|
||||
|
||||
Reference in New Issue
Block a user