Delete the sidescroller's collision, and let the library do it

collision.c was 383 lines that re-implemented akgl_physics_simulate's own
arithmetic, because movementlogicfunc runs before gravity, drag and the move
and so had to predict where the actor would end up. The whole file goes. What
replaces it is three lines in main.c, a shape on the player and the blob, and a
`collidable` property on the map's terrain layer -- which also retires
SS_TERRAIN_LAYER_ID, a compiled-in layer id that broke whenever somebody
reordered layers in Tiled.

grounded stays a probe rather than becoming a contact test. A contact says
something pushed back this step; grounded asks whether there is a floor to push
off, and that stays true through the frame after landing. The blob keeps its own
wall and ledge probes for the same reason: the resolver does not say which side
of the blob it was pushed from, and says nothing at all about a floor that is
missing.

The summary line now carries the player's resting position, because exiting 0
was not evidence that collision ran. It reports 136.0,160.0 grounded after 240
autoplay frames -- feet at y=192, flush on the surface of terrain row 12.

The tutorial's collision section is rewritten around the library's, keeping the
quarter-of-a-pixel story as what a predicting resolver costs. The docs harness
learns `json excerpt=`, so the map property the chapter quotes is checked
against the map.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
2026-08-02 07:35:08 -04:00
parent 35a58670d0
commit 490e62dbbf
9 changed files with 232 additions and 596 deletions

View File

@@ -5,7 +5,6 @@
* The game is four translation units and this is the seam between them:
*
* main.c startup, asset loading, the frame loop, teardown
* collision.c the tile queries and the swept resolution libakgl does not have
* player.c the player's hooks and its control bindings
* actors.c the coins, the patrolling blob, the flying moth
*
@@ -23,8 +22,10 @@
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/collision.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/physics.h>
#include <akgl/tilemap.h>
#include <akgl/types.h>
@@ -38,16 +39,6 @@
#define SS_VIEW_HEIGHT 240 /* Camera height; the whole map is this tall */
#define SS_WINDOW_SCALE 2 /* Integer upscale from the view to the window */
/*
* Which layer of the map is solid.
*
* akgl_TilemapLayer records Tiled's numeric layer `id` and does *not* record the
* layer's name -- akgl_tilemap_load_layers reads `id`, `opacity`, `visible`, `x`,
* `y` and `type`, and nothing else. So a game cannot ask for "the layer called
* terrain"; it matches on the id Tiled assigned, which for level1.tmj is 2.
*/
#define SS_TERRAIN_LAYER_ID 2
/* The player's collision box, as an offset into its 32x32 sprite frame. The art
* does not fill the frame edge to edge, and a box the full width of the frame
* catches on doorways the character visibly clears. */
@@ -81,19 +72,6 @@
*/
#define SS_AUTOPLAY_KBID 11
/**
* @brief What one call to ss_collide_resolve found.
*
* `blocked_x` and `blocked_y` say the sweep stopped the actor on that axis this
* step; `grounded` says there is solid terrain one pixel under where the actor
* will be when the step finishes, which is the test a jump is gated on.
*/
typedef struct {
bool blocked_x;
bool blocked_y;
bool grounded;
} ss_Contact;
/**
* @brief Per-actor game data, hung off akgl_Actor::actorData.
*
@@ -120,17 +98,31 @@ typedef struct {
bool grounded; /**< Last step's verdict; what gates the next jump. */
bool autoplay; /**< Drive the player from a script instead of the keyboard. */
int frame; /**< Frames drawn so far. */
float32_t final_x; /**< Where the player finished, stamped before teardown for the summary line. */
float32_t final_y;
} ss_Game;
extern ss_Game ss_game;
/* collision.c -- everything akgl_physics_arcade_collide would have done. */
akerr_ErrorContext AKERR_NOIGNORE *ss_collide_bind(akgl_Tilemap *map);
akerr_ErrorContext AKERR_NOIGNORE *ss_collide_solid_at(float32_t x, float32_t y, bool *dest);
akerr_ErrorContext AKERR_NOIGNORE *ss_collide_box_blocked(SDL_FRect *box, bool *dest);
akerr_ErrorContext AKERR_NOIGNORE *ss_collide_predict(akgl_Actor *obj, float32_t dt, float32_t *dx, float32_t *dy);
akerr_ErrorContext AKERR_NOIGNORE *ss_collide_resolve(akgl_Actor *obj, SDL_FRect *body, float32_t dt, ss_Contact *dest);
akerr_ErrorContext AKERR_NOIGNORE *ss_collide_settle(akgl_Actor *obj, SDL_FRect *body);
/**
* @brief The collision world, owned by main.c.
*
* The physics backend resolves through it after every sub-move; the game only
* touches it to ask questions -- a ledge probe, a wall probe, a spawn point that
* needs lifting clear. Everything that used to be in collision.c is behind it.
*/
extern akgl_CollisionWorld ss_collision;
/**
* @brief Is there solid geometry one pixel under this shape?
*
* The predicate a jump is gated on, and it is deliberately *not* the same
* question as "did the last step's resolution stop me falling". A contact says
* something pushed back this step; this says there is a floor to push off, which
* stays true through the frame after landing and is what makes a jump feel like
* it registered.
*/
akerr_ErrorContext AKERR_NOIGNORE *ss_grounded(akgl_CollisionShape *shape, float32_t x, float32_t y, bool *dest);
/* player.c */
akerr_ErrorContext AKERR_NOIGNORE *ss_player_bind(akgl_Actor *obj);