Let a map say which layers are solid, and answer questions about them
A tile layer with a `collidable` boolean custom property is collision geometry. Before this a game had to hard-code a layer index -- both examples do, because akgl_TilemapLayer does not retain the name Tiled wrote -- and that index changes the moment somebody reorders layers in the editor. Solid tiles are **not** given proxies. At the maximum map size that is a quarter of a million per layer, tens of megabytes of index to describe data that is already a dense grid sitting in the tilemap. The world keeps a borrowed pointer and reads layers[i].data[] over whatever cell range a query covers: nine array reads for a 32-pixel actor on 16-pixel tiles, nothing to build at level load, and nothing to maintain per frame. Static geometry that is not tile-aligned is still an ordinary proxy with AKGL_COLLISION_FLAG_STATIC; both mechanisms exist and tiles use the free one because there are a hundred thousand of them. Four public queries come with it, all of which answer without resolving: solid_at, box_blocked, query_box and settle. They are what a game reaches for when it wants to know rather than to be pushed -- a ledge probe ahead of a walking enemy, a check that a doorway is clear -- and the sidescroller cannot drop its hand-rolled collision without them. akgl_collision_settle is the one worth naming. Resolution stops a shape entering geometry and has nothing to say about one that began inside it: what it does instead is refuse every move, so an actor spawned in a wall is simply stuck. Level authors produce that constantly, so settling walks a shape up a tile at a time and refuses loudly rather than searching forever. Two defects found by writing the tests: - The fixture put an akgl_Tilemap on the stack and segfaulted before the first assertion. It is about 26 MB -- the layer and tileset arrays are sized for the worst case the format allows -- and tilemap.h says so. It is static now, with a comment saying why, because the next person to write a map fixture will reach for a local first as well. - The far-edge nudge used AKGL_COLLISION_EPSILON, which is 1e-6. That is a sensible tolerance on a unit vector and a meaningless one on a map coordinate: `float` carries about seven significant digits, so at a coordinate of 144 the smallest representable step is around 1.5e-5 and `144.0f - 1e-6f` is exactly 144.0f. The nudge did nothing, a box resting flush on the floor read as inside it, and every move it tried looked blocked -- an actor standing on the ground unable to walk. Two different quantities were sharing one constant; the tile one is now its own, at a thousandth of a pixel, which is what the sidescroller example independently arrived at. Both breaks verified: removing the nudge and ignoring the `collidable` bit each turn the suite red with the symptom named. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,7 @@ typedef struct akgl_Tilemap {
|
||||
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */
|
||||
|
||||
// Different levels may have different physics.
|
||||
uint32_t collidablelayers; /**< Bit `i` set means `layers[i]` is solid geometry for collision, from that layer's `collidable` custom property in Tiled. 0 means nothing on this map collides, which is what a map authored before collision existed gets. */
|
||||
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `akgl_physics` instead of the global backend. */
|
||||
akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */
|
||||
} akgl_Tilemap;
|
||||
@@ -301,6 +302,25 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj,
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
|
||||
|
||||
/**
|
||||
* @brief Read a boolean Tiled custom property.
|
||||
*
|
||||
* Tiled writes these with a declared type of `"bool"`, and the accessor matches
|
||||
* on that type -- a property written as an `int` named the same thing raises
|
||||
* `AKERR_TYPE` rather than being coerced, because a map that says `1` where it
|
||||
* means `true` is a map that will say something else next time.
|
||||
*
|
||||
* @param obj The object carrying a `properties` array. Required.
|
||||
* @param key Property name. Required.
|
||||
* @param dest Receives the value. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY If no property of that name exists. **Absent is not an
|
||||
* error to the loader** -- a layer with no `collidable` property is not
|
||||
* collidable, which is the common case.
|
||||
* @throws AKERR_TYPE If the property exists but was not written as a bool.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_boolean(json_t *obj, char *key, bool *dest);
|
||||
/**
|
||||
* @brief Build a tileset's local-id-to-pixel-offset table.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user