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
136 lines
5.6 KiB
C
136 lines
5.6 KiB
C
/**
|
|
* @file sidescroller.h
|
|
* @brief Shared declarations for the sidescroller tutorial game.
|
|
*
|
|
* The game is four translation units and this is the seam between them:
|
|
*
|
|
* main.c startup, asset loading, the frame loop, teardown
|
|
* player.c the player's hooks and its control bindings
|
|
* actors.c the coins, the patrolling blob, the flying moth
|
|
*
|
|
* Nothing here is prefixed `akgl_`. That prefix belongs to the library; a game
|
|
* built on it takes its own, and this one is `ss_`.
|
|
*/
|
|
|
|
#ifndef _SIDESCROLLER_H_
|
|
#define _SIDESCROLLER_H_
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#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>
|
|
|
|
/*
|
|
* The level's geometry, in the units the map is authored in. level1.tmj is 40x15
|
|
* tiles of 16 pixels, so the world is 640x240 pixels and the view is a 480x240
|
|
* window onto it that scrolls horizontally.
|
|
*/
|
|
#define SS_TILE_SIZE 16 /* Pixels per map cell, from level1.tmj */
|
|
#define SS_VIEW_WIDTH 480 /* Camera width in map pixels */
|
|
#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 */
|
|
|
|
/* 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. */
|
|
#define SS_PLAYER_BOX_X 8.0f
|
|
#define SS_PLAYER_BOX_Y 0.0f
|
|
#define SS_PLAYER_BOX_W 16.0f
|
|
#define SS_PLAYER_BOX_H 32.0f
|
|
|
|
/* Upward velocity a jump installs directly into the actor's environmental term,
|
|
* in pixels per second. Under the map's 900 px/s^2 gravity this peaks a little
|
|
* under 100 px -- six tiles -- which is what the platform heights are cut to. */
|
|
#define SS_JUMP_SPEED 420.0f
|
|
|
|
/* How fast thrust bleeds away when no direction is held, as a fraction per
|
|
* second. The library has no friction at all: see ss_player_friction. */
|
|
#define SS_FRICTION_GROUND 12.0f
|
|
#define SS_FRICTION_AIR 1.5f
|
|
|
|
/* How many of each kind of thing level1.tmj places. */
|
|
#define SS_COIN_COUNT 4
|
|
#define SS_HAZARD_COUNT 2
|
|
|
|
/* How far the autoplay script waits between jumps, in frames. */
|
|
#define SS_AUTOPLAY_JUMP_PERIOD 45
|
|
/* How long it holds the jump key, in frames. A tap jumps lower than a hold. */
|
|
#define SS_AUTOPLAY_JUMP_HOLD 8
|
|
/*
|
|
* The device id the autoplay script stamps on its synthetic key events.
|
|
* Deliberately not 0: the control map binds keyboard 0 meaning "any keyboard",
|
|
* and driving it from a non-zero id is what proves that binding works.
|
|
*/
|
|
#define SS_AUTOPLAY_KBID 11
|
|
|
|
/**
|
|
* @brief Per-actor game data, hung off akgl_Actor::actorData.
|
|
*
|
|
* The library never reads or frees `actorData`, so it is the place a game puts
|
|
* what the library has no field for. These live in a fixed table in actors.c
|
|
* rather than being allocated, which is the same discipline the library's own
|
|
* pools follow.
|
|
*/
|
|
typedef struct {
|
|
float32_t home_x; /**< Where the map placed this actor. The moth orbits it; the player respawns at it. */
|
|
float32_t home_y;
|
|
float32_t phase; /**< Seconds of flight, for the moth's orbit. */
|
|
float32_t facing; /**< -1.0 walking left, +1.0 walking right. The blob's patrol direction. */
|
|
} ss_ActorData;
|
|
|
|
/** @brief The whole game's state. One player, one level, no menus. */
|
|
typedef struct {
|
|
akgl_Actor *player; /**< Borrowed from the actor pool; the map created it. */
|
|
akgl_Actor *coins[SS_COIN_COUNT]; /**< Cleared to NULL as each one is collected. */
|
|
akgl_Actor *hazards[SS_HAZARD_COUNT]; /**< The blob and the moth. Borrowed, never released. */
|
|
int coins_taken;
|
|
int deaths;
|
|
bool jump_requested; /**< Set by the jump binding, consumed by the movement logic. */
|
|
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;
|
|
|
|
/**
|
|
* @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);
|
|
akerr_ErrorContext AKERR_NOIGNORE *ss_player_controls(int controlmapid, char *actorname);
|
|
akerr_ErrorContext AKERR_NOIGNORE *ss_player_autoplay(int frame);
|
|
|
|
/* actors.c */
|
|
akerr_ErrorContext AKERR_NOIGNORE *ss_actors_bind(void);
|
|
|
|
#endif // _SIDESCROLLER_H_
|