Delete the JRPG's collision too, and wall the map with proxies
cell_solid and feet_blocked go, and so does the prediction in the player's movementlogicfunc. That prediction was only ever exact because the town has zero gravity and zero drag -- v is t, so `x + tx * dt` is where the step lands. A map with gravity would have had to fold ey in as well, which is the game re-implementing the integrator. Resolution runs after the move now, so there is nothing to predict. The map's decoration layer carries `collidable`, which retires JRPG_LAYER_SOLID: akgl_TilemapLayer has no name member, so the game and the map had to agree on an index out of band and inserting a layer in Tiled broke it. The edge of the world is not on any layer, so it is four static proxies covering the outer ring plus a tile of overhang -- one pool slot per side instead of a hundred solid tiles, and the first use of the static-proxy path in either example. They carry LAYER_STATIC explicitly: shape_box defaults a shape to LAYER_ACTOR, and a wall left on that layer is a wall everything walks through. Only the player gets a shape. NPCs stand still and are spoken to; the follower is a child snapped to its parent every step. Verified against the old implementation with the demo script rewritten to hold one direction: holding left into a building stops the player at x=122 with both, and holding up into the map's edge stops them at y=-4 with both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
@@ -6,7 +6,8 @@
|
||||
* Almost nothing here is game logic. It is the four steps between a directory
|
||||
* of JSON and a world with people standing in it, in the one order that works,
|
||||
* plus the two hooks -- a `movementlogicfunc` and a `renderfunc` -- that a game
|
||||
* has to supply because the library does not.
|
||||
* has to supply because the library does not, and the collision world the
|
||||
* library resolves through.
|
||||
*/
|
||||
|
||||
#include <limits.h>
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/collision.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
@@ -92,10 +94,26 @@ static jrpg_Townsfolk TOWNSFOLK[] = {
|
||||
#define FEET_TOP 20.0f
|
||||
#define FEET_H 10.0f
|
||||
|
||||
/**
|
||||
* @brief How far past the edge of the map the boundary walls extend, in pixels.
|
||||
*
|
||||
* The walls cover the map's outer ring of cells *and* this much beyond it. The
|
||||
* ring is what the old hand-rolled rule made solid and what the art is drawn to
|
||||
* expect; the overhang is what stops an actor arriving fast enough from being
|
||||
* resolved out the far side of a wall exactly one tile thick.
|
||||
*/
|
||||
#define EDGE_OVERHANG 16.0f
|
||||
|
||||
/** @brief Where the party member walks, in pixels relative to the player. */
|
||||
#define FOLLOWER_OFFSET_X (-14.0f)
|
||||
#define FOLLOWER_OFFSET_Y (10.0f)
|
||||
|
||||
akgl_CollisionWorld jrpg_collision;
|
||||
|
||||
/** @brief The four map-edge walls, and the shapes they were built from. */
|
||||
static akgl_CollisionProxy *jrpg_edges[4];
|
||||
static akgl_CollisionShape jrpg_edge_shapes[4];
|
||||
|
||||
/**
|
||||
* @brief Load one sprite definition by its three naming components.
|
||||
*
|
||||
@@ -154,6 +172,62 @@ static akerr_ErrorContext *character_load(char *cast)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wall off the edges of the map with four static proxies.
|
||||
*
|
||||
* The town's decoration layer says what is solid -- a building, a tree, a lamp
|
||||
* post -- and the map's edge is not on it. Nothing else stops an actor walking
|
||||
* off the edge of the world.
|
||||
*
|
||||
* Four proxies rather than a hundred solid tiles, because static geometry that
|
||||
* is not tile-shaped is exactly what a proxy is for: a long thin box costs one
|
||||
* slot whatever its length. `layermask` is set by hand because
|
||||
* akgl_collision_shape_box defaults a shape to LAYER_ACTOR, which is right for
|
||||
* an actor and wrong for scenery -- an actor responds to LAYER_STATIC and would
|
||||
* walk straight through these.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP If the proxy pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *wall_off_the_edges(void)
|
||||
{
|
||||
SDL_FRect sides[4];
|
||||
float32_t ow = 0.0f; /* Map width, plus the overhang at each end. */
|
||||
float32_t oh = 0.0f; /* Map height, the same. */
|
||||
float32_t tw = 0.0f; /* One tile of ring, plus one overhang. */
|
||||
float32_t th = 0.0f;
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ow = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) + (2.0f * EDGE_OVERHANG);
|
||||
oh = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) + (2.0f * EDGE_OVERHANG);
|
||||
tw = (float32_t)akgl_gamemap->tilewidth + EDGE_OVERHANG;
|
||||
th = (float32_t)akgl_gamemap->tileheight + EDGE_OVERHANG;
|
||||
|
||||
/* x y w h */
|
||||
sides[0] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, tw, oh }; /* west */
|
||||
sides[1] = (SDL_FRect){ (ow - tw - EDGE_OVERHANG), -EDGE_OVERHANG, tw, oh }; /* east */
|
||||
sides[2] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, ow, th }; /* north */
|
||||
sides[3] = (SDL_FRect){ -EDGE_OVERHANG, (oh - th - EDGE_OVERHANG), ow, th }; /* south */
|
||||
|
||||
for ( i = 0; i < 4; i++ ) {
|
||||
/*
|
||||
* Acquire and initialize adjacent, with nothing between them: until
|
||||
* akgl_collision_proxy_initialize takes the reference the slot is still
|
||||
* free, and the next acquire hands out the same pointer.
|
||||
*/
|
||||
PASS(errctx, akgl_heap_next_collision_proxy(&jrpg_edges[i]));
|
||||
PASS(errctx, akgl_collision_shape_box(&jrpg_edge_shapes[i], &sides[i], 0.0f));
|
||||
jrpg_edge_shapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
|
||||
jrpg_edge_shapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;
|
||||
jrpg_edge_shapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
|
||||
PASS(errctx, akgl_collision_proxy_initialize(jrpg_edges[i], NULL, &jrpg_edge_shapes[i],
|
||||
0.0f, 0.0f, 0.0f));
|
||||
PASS(errctx, jrpg_collision.partitioner.insert(&jrpg_collision.partitioner, jrpg_edges[i]));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *jrpg_world_load(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -192,6 +266,16 @@ akerr_ErrorContext *jrpg_world_load(void)
|
||||
akgl_physics = &akgl_gamemap->physics;
|
||||
}
|
||||
|
||||
// Collision is opt-in, and this is the whole of turning it on. The cell
|
||||
// arguments are placeholders: akgl_collision_bind_tilemap overwrites them
|
||||
// from the map's own tile size, and reads which layers are solid off each
|
||||
// layer's `collidable` property rather than an index the game and the map
|
||||
// had to agree on out of band.
|
||||
PASS(errctx, akgl_collision_world_init(&jrpg_collision, NULL, 16.0f, 16.0f));
|
||||
PASS(errctx, akgl_collision_bind_tilemap(&jrpg_collision, akgl_gamemap));
|
||||
PASS(errctx, wall_off_the_edges());
|
||||
akgl_physics->collision = &jrpg_collision;
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -200,6 +284,7 @@ akerr_ErrorContext *jrpg_world_populate(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Actor *player = NULL;
|
||||
akgl_Actor *follower = NULL;
|
||||
SDL_FRect body;
|
||||
int i = 0;
|
||||
|
||||
// Every actor the map spawned, including the player.
|
||||
@@ -228,6 +313,15 @@ akerr_ErrorContext *jrpg_world_populate(void)
|
||||
);
|
||||
player->movementlogicfunc = &jrpg_actor_logic_walk;
|
||||
|
||||
// The footprint, and only the player gets one. The NPCs stand still and are
|
||||
// spoken to, not walked into: giving them shapes would make the town a
|
||||
// pinball table and would change what the proximity test in jrpg.c means.
|
||||
// The follower is a child, snapped to the player's position every step, so
|
||||
// a shape on it would fight the snap rather than stop anything.
|
||||
body = (SDL_FRect){ FEET_X, FEET_TOP, FEET_W, FEET_H };
|
||||
PASS(errctx, akgl_collision_shape_box(&player->shape, &body, 0.0f));
|
||||
player->shape_override = true;
|
||||
|
||||
// The party member. Nothing in the map creates it, and nothing has to: an
|
||||
// actor is a pool object with a name, and the character it instantiates is
|
||||
// already registered. This one borrows the elder's template, which is the
|
||||
@@ -252,58 +346,6 @@ akerr_ErrorContext *jrpg_world_populate(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Is the map cell containing this pixel one the player cannot enter?
|
||||
*
|
||||
* Two rules, and both of them are the game's rather than the library's. The
|
||||
* outer ring of the map is solid, because nothing else stops an actor walking
|
||||
* off the edge of the world. Everything else is the decoration layer: a cell
|
||||
* with a tile in it is a building, a tree or a lamp post, and a cell with 0 in
|
||||
* it is open ground.
|
||||
*
|
||||
* @param px Position in map pixels along x.
|
||||
* @param py Position in map pixels along y.
|
||||
* @return True when the cell blocks movement.
|
||||
*/
|
||||
static bool cell_solid(float32_t px, float32_t py)
|
||||
{
|
||||
int tx = 0;
|
||||
int ty = 0;
|
||||
|
||||
tx = (int)(px / (float32_t)akgl_gamemap->tilewidth);
|
||||
ty = (int)(py / (float32_t)akgl_gamemap->tileheight);
|
||||
|
||||
if ( (tx <= 0) || (ty <= 0) ||
|
||||
(tx >= (akgl_gamemap->width - 1)) || (ty >= (akgl_gamemap->height - 1)) ) {
|
||||
return true;
|
||||
}
|
||||
return (akgl_gamemap->layers[JRPG_LAYER_SOLID].data[(ty * akgl_gamemap->width) + tx] != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Would an actor whose frame is at (x, y) have its feet in a wall?
|
||||
*
|
||||
* Tests the four corners of the footprint. Four corners is enough only because
|
||||
* the footprint is smaller than a tile in both directions; a bigger one would
|
||||
* step over a single-cell wall between two of its corners.
|
||||
*
|
||||
* @param x Candidate frame position along x, in map pixels.
|
||||
* @param y Candidate frame position along y, in map pixels.
|
||||
* @return True when any corner of the footprint lands in a solid cell.
|
||||
*/
|
||||
static bool feet_blocked(float32_t x, float32_t y)
|
||||
{
|
||||
float32_t left = x + FEET_X;
|
||||
float32_t right = x + FEET_X + FEET_W;
|
||||
float32_t top = y + FEET_TOP;
|
||||
float32_t bottom = y + FEET_TOP + FEET_H;
|
||||
|
||||
return (cell_solid(left, top) ||
|
||||
cell_solid(right, top) ||
|
||||
cell_solid(left, bottom) ||
|
||||
cell_solid(right, bottom));
|
||||
}
|
||||
|
||||
akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -335,25 +377,13 @@ akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt)
|
||||
// Everything the default hook does -- re-copy the character's speed limits,
|
||||
// turn the movement bits into signed acceleration -- is still wanted. This
|
||||
// hook adds to it rather than replacing it.
|
||||
PASS(errctx, akgl_actor_logic_movement(obj, dt));
|
||||
|
||||
// Where this step would put us. akgl_physics_simulate has already
|
||||
// integrated thrust for this step and capped it against the character's
|
||||
// speed ellipse, and it computes velocity as `e + t` -- and with the town's
|
||||
// zero gravity and zero drag, `e` stays zero, so `v` is `t`. That is what
|
||||
// makes the prediction exact rather than approximate. A map with gravity
|
||||
// would have to account for `ey` here as well.
|
||||
//
|
||||
// Axis at a time, so walking into a wall diagonally slides along it rather
|
||||
// than stopping dead.
|
||||
if ( feet_blocked(obj->x + (obj->tx * dt), obj->y) ) {
|
||||
obj->tx = 0.0f;
|
||||
obj->ax = 0.0f;
|
||||
}
|
||||
if ( feet_blocked(obj->x, obj->y + (obj->ty * dt)) ) {
|
||||
obj->ty = 0.0f;
|
||||
obj->ay = 0.0f;
|
||||
}
|
||||
// Walls used to be here too: a prediction of where this step would land,
|
||||
// tested axis at a time. It was only ever correct because the town has zero
|
||||
// gravity and zero drag, so `v` is `t` and the prediction is exact. The
|
||||
// library resolves after the move instead, on a position that already
|
||||
// happened, and that qualifier goes away with it.
|
||||
PASS(errctx, akgl_actor_logic_movement(obj, dt));
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user