# 13. Tilemaps libakgl does not have a map format. It has a *loader* for somebody else's, and the somebody else is the [Tiled map editor](https://www.mapeditor.org/). Levels are authored in Tiled, saved as JSON (`.tmj`), and read by `akgl_tilemap_load`. That division is deliberate, and it means this chapter owes you almost nothing about the format itself. What a layer is, what a tileset is, how `firstgid` works, how custom properties are stored — all of that is [the Tiled JSON map format reference](https://doc.mapeditor.org/en/stable/reference/json-map-format/), maintained by the people who write the editor. Read it there. What this chapter owes you is the part Tiled cannot tell you: **which subset of the format libakgl actually reads, what it refuses, the three things it adds on top, and how big the resulting object is.** All four have bitten somebody. ## The size of an `akgl_Tilemap` Start here, because it is the one that produces a crash rather than an error message. ```text sizeof(akgl_Tilemap) = 26,388,008 bytes = 25.2 MiB layers[16] 16 x 1,120,296 = 17.1 MiB 512x512 ints of tile data, plus 128 x 560-byte objects, per layer tilesets[16] 16 x 528,944 = 8.1 MiB 65,536 x 2 ints of tile offsets, plus a PATH_MAX image path, each everything else = ~0.0 MiB geometry, perspective, physics ``` **An `akgl_Tilemap` belongs in static storage and nowhere else.** A default thread stack is 8 MiB on Linux; this object is three times that, so a local one blows the stack before the loader writes its first byte, and it does so as a segfault with no error context. That is why the library's own map, `akgl_default_gamemap`, is a file-scope object in `src/game.c`, and why `akgl_gamemap` is a pointer to it rather than a copy. The bounds are fixed at compile time, so the size is fixed too. Every field is present whether the map uses it or not: a 20x15 map with one tileset and two layers costs the same 25.2 MiB as a 511x511 one. Raising any of the constants below raises this number and changes the ABI — see [Chapter 22](22-appendix-limits.md). ## Limits ```c excerpt=include/akgl/tilemap.h /** @brief Widest map, in tiles. Width times height is what is actually bounded. */ #define AKGL_TILEMAP_MAX_WIDTH 512 /** @brief Tallest map, in tiles. */ #define AKGL_TILEMAP_MAX_HEIGHT 512 /** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */ #define AKGL_TILEMAP_MAX_LAYERS 16 /** @brief Tilesets per map. */ #define AKGL_TILEMAP_MAX_TILESETS 16 /** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */ #define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536 /** @brief Longest tileset name. */ #define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512 /** @brief Longest resolved tileset image path. */ #define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX /** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */ #define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512 /** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */ #define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128 ``` Two of these carry a wrinkle worth knowing before you lay out a level. **The width and height bound is on the product, and it is exclusive.** `akgl_tilemap_load` refuses when `width * height >= AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT`, so 262,144 cells is one too many: **a literal 512x512 map is rejected with `AKERR_OUTOFBOUNDS`**, and 512x511 is the largest rectangle that loads. The same off-by-one applies per tile layer in `akgl_tilemap_load_layer_tile`. Nothing is wrong with either check other than the `>=`; it is recorded here because "the maximum is 512x512" is what the constant names say and is not what the code does. **The doc comment on `AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER` is stale.** It says the loader does not enforce it. The loader does — `akgl_tilemap_load_layer_objects` checks `j` at the top of the loop body and raises `AKERR_OUTOFBOUNDS` naming the layer, and `akgl_tilemap_load_tilesets` does the same against `AKGL_TILEMAP_MAX_TILESETS`. Both were genuinely unbounded and both were fixed in 0.5.0 (`TODO.md`, "Found while rewriting the Doxygen comments" item 17), with fixtures in `tests/tilemap.c` at exactly 128, 129 and 17. Trust the code. ## What the loader accepts The loader reads a strict subset of the TMJ format. Everything in this section is a requirement, not a suggestion: a key that is absent raises `AKERR_KEY` and a key with the wrong JSON type raises `AKERR_TYPE`, both naming what was wanted. See [Chapter 4](04-errors.md) for what those statuses mean in libakgl. | Level | Members `akgl_tilemap_load` requires | Notes | |---|---|---| | Map root | `tilewidth`, `tileheight`, `width`, `height`, `layers`, `tilesets` | `orientation` is read from nowhere and forced to 0; isometric is not implemented | | Every layer | `id`, `opacity`, `visible`, `x`, `y`, `type` | A `type` other than the three below keeps these fields and is otherwise skipped | | `tilelayer` | `width`, `height`, `data` | `data` must be a plain array of integers | | `imagelayer` | `image` | Layer `width`/`height` are taken from the loaded texture, not the JSON | | `objectgroup` | `objects` | See below — every object has requirements of its own | | Every object | `name`, `x`, `y`, `visible`, `type` | **Including plain rectangles.** An object with no `type` member fails the whole load | | Every tileset | `columns`, `firstgid`, `imagewidth`, `imageheight`, `margin`, `spacing`, `tilecount`, `tilewidth`, `tileheight`, `name`, `image` | The **embedded** form; see below | Three of these deserve their own paragraph. **Tilesets must be embedded, not external.** Tiled writes a tileset into the map's `tilesets` array in one of two shapes: the whole tileset inline, or a two-member stub `{"firstgid": N, "source": "tiles.tsj"}` pointing at a separate file. `akgl_tilemap_load_tilesets_each` reads `columns`, `image` and the rest directly out of the array element and there is no code anywhere in `src/` that opens a `source` file, so **an external tileset reference fails with `AKERR_KEY` on the missing `columns`.** Turn *Embed tileset* on when you save, or use Tiled's *Map → Embed Tilesets* command. The shipped fixture `tests/assets/testmap.tmj` is embedded, which is what makes it load. **Every object needs a `type` string, and `type` is where the extensions live.** The loader reads it on every object in an object group and compares it against `"actor"` and `"perspective"`; anything else is recorded in the layer's object array and otherwise ignored. An object with the member missing entirely does not reach that comparison — it fails the load. If your editor writes the object's class under a different member name, that is the drift to check first; Tiled's own field naming has changed across releases, and the fixture in this repository is a Tiled 1.8.2 export. **The map file's directory is the root every path resolves against**, so a map and its art move together. The two resolvers are not the same, though, and the difference matters: | Path | Resolved by | Consequence | |---|---|---| | A tileset's `image` | `akgl_path_relative` | Canonicalized against the map's directory; an absolute path works | | An image layer's `image` | `aksl_snprintf("%s/%s", ...)` | A plain join. **Not** canonicalized, so an absolute path becomes `/maps//art/sky.png` and does not open | Both are bounded at `AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE` (`PATH_MAX`). The image-layer join raises `AKERR_OUTOFBOUNDS` naming both lengths when it does not fit; it used to truncate silently, and an over-long path reported itself as a missing file with a name the caller never wrote. ## libakgl's three extensions to Tiled None of these are part of the TMJ format. They are conventions the loader imposes on custom properties and object types, and Tiled will happily author all three without knowing what they mean. ### 1. Object type `actor` spawns a real actor An object whose `type` is the string `"actor"` does not stay a rectangle. The loader claims an actor from the pool, initializes it, binds a character, publishes it in `AKGL_REGISTRY_ACTOR`, and copies the object's position, visibility and layer index onto it. The object's `name` is the registry key. See [Chapter 12](12-actors.md) for what an actor is and [Chapter 11](11-characters.md) for what a character supplies. Two custom properties are required on the object: | Property | Tiled type | Meaning | |---|---|---| | `character` | `string` | Name of a character already in `AKGL_REGISTRY_CHARACTER` | | `state` | `int` | The actor's initial `AKGL_ACTOR_STATE_*` bitmask, **as a number** | **`state` is an integer here, and only here.** Character JSON accepts the string-array form — `["AKGL_ACTOR_STATE_ALIVE", "AKGL_ACTOR_STATE_FACE_DOWN"]` — because `AKGL_ACTOR_STATE_STRING_NAMES` exists to decode it. `akgl_tilemap_load_layer_object_actor` calls `akgl_get_json_properties_integer`, which demands a property Tiled declared as `"int"`, so a map that spells the state as names raises `AKERR_TYPE`. Add up the bit values from [Chapter 12](12-actors.md) and write the sum. Two consequences of the registry being the identity: - **The characters have to be loaded before the map.** An unregistered `character` name comes back as `AKERR_KEY` from `akgl_actor_set_character`, in the middle of a partly loaded map. - **Two objects naming one actor do not make two actors.** The second placement takes another reference on the first and overwrites its position, so the actor ends up wherever the last object put it. That is the mechanism for placing one actor from two layers; it is not a way to spawn a crowd. An `actor` object with an empty `name` is refused with `AKERR_KEY` — an actor has to be addressable. ### 2. A `physics.model` property gives the map its own physics A custom property named `physics.model` on the **map root** hands the map's name to `akgl_physics_factory` and stores the resulting backend in `map->physics`, then sets `map->use_own_physics`. Six more properties configure it, each defaulting to `0.0` when absent: | Property | Tiled type | Sets | |---|---|---| | `physics.model` | `string` | Which backend — `null` or `arcade`. See [Chapter 14](14-physics.md) | | `physics.gravity.x` / `.y` / `.z` | `float` | The backend's gravity constants | | `physics.drag.x` / `.y` / `.z` | `float` | The backend's drag coefficients | A swimming level and a walking level then differ by data rather than by code. **Nothing in the library acts on `use_own_physics`.** `akgl_game_update` steps the global `akgl_physics` and never looks at the map's. The flag is a signal to *you*: read it after loading and point `akgl_physics` at `map->physics` if you want it honoured. The example below does exactly that. A map with no `properties` array at all, or with properties but no `physics.model`, is the normal case: `akgl_tilemap_load_physics` returns success and the map uses the game's global backend. The `AKERR_KEY` handlers in that function are the "map did not ask" path, not error paths. A `physics.model` naming a backend that does not exist *is* an error. ### 3. `p_foreground` and `p_vanishing` define the pseudo-3D band Two objects in an object layer, each with `"type": "perspective"` and a `scale` custom property declared `float`, define a band down the screen over which actors are scaled. That is what makes a character look further away as they walk up a top-down map. ```text y = 0 ----------------------------------------------------- top of the map . . actors above p_vanishing_y stay at . p_vanishing_scale p_vanishing_y ----------- [tiny actor] scale = p_vanishing_scale / \ / \ linear interpolation between the / \ two rows, at p_rate per pixel of y / \ p_foreground_y --------- [ big actor ] scale = p_foreground_scale actors below here stay at p_foreground_scale ``` The names are matched literally: `p_foreground` sets the row where actors are at full size, `p_vanishing` the row where they are smallest. **Both the type and the name have to be right.** The loader checks `type == "perspective"` first and only then compares the name, so an object named `p_vanishing` with type `"object"` is silently just an object — no error, no perspective, and a very confusing afternoon. Each marker contributes its `y` and its `scale`; `p_rate` is derived at load as the scale change per pixel between the two. **A marker at `y = 0` disables perspective**, because the rate is only computed when both `p_foreground_y` and `p_vanishing_y` are non-zero — and a map with no markers has both scales at 1.0 and a rate of 0, so the whole feature is a no-op rather than a special case. Perspective markers are forced invisible: they are geometry, not scenery. `akgl_tilemap_scale_actor` applies the band, writing only the actor's `scale`. It is called per actor by `akgl_game_update` against the **global** `akgl_gamemap` when the frame's iterator carries `AKGL_ITERATOR_OP_TILEMAPSCALE`; see [Chapter 7](07-the-game-and-the-frame.md). The marker heights (`p_foreground_h`, `p_vanishing_h`) are recorded and not used by the current rate calculation. ## A minimal map Two 16-pixel tiles square, one tile layer, one embedded tileset, and one actor object. The state value `17` is `AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN` — 16 plus 1, from the bit table in [Chapter 12](12-actors.md). Add the values you want; there is no symbolic form on this side. ```json kind=tilemap setup=tilemap { "compressionlevel": -1, "height": 2, "infinite": false, "nextlayerid": 3, "nextobjectid": 2, "orientation": "orthogonal", "renderorder": "right-down", "tiledversion": "1.8.2", "tileheight": 16, "tilewidth": 16, "type": "map", "version": "1.8", "width": 2, "layers": [ { "data": [1, 2, 3, 4], "height": 2, "id": 1, "name": "Tile Layer 1", "opacity": 1, "type": "tilelayer", "visible": true, "width": 2, "x": 0, "y": 0 }, { "draworder": "topdown", "id": 2, "name": "Object Layer 1", "opacity": 1, "type": "objectgroup", "visible": true, "x": 0, "y": 0, "objects": [ { "gid": 195, "height": 16, "id": 1, "name": "testactor", "rotation": 0, "type": "actor", "visible": true, "width": 16, "x": 16, "y": 16, "properties": [ { "name": "character", "type": "string", "value": "testcharacter" }, { "name": "state", "type": "int", "value": 17 } ] } ] } ], "tilesets": [ { "columns": 48, "firstgid": 1, "image": "assets/tileset.png", "imageheight": 576, "imagewidth": 768, "margin": 0, "name": "World_A1", "spacing": 0, "tilecount": 1728, "tileheight": 16, "tilewidth": 16 } ] } ``` ## Loading one ```c #include #include #include /* * 25 MiB of tilemap. Static storage, never a local: a stack frame this size * overruns the default 8 MiB thread stack before the loader writes a byte. */ static akgl_Tilemap townmap; akerr_ErrorContext AKERR_NOIGNORE *load_town(void) { PREPARE_ERROR(errctx); /* Reusing a map struct leaks the previous map's textures unless this runs * first -- akgl_tilemap_load zeroes rather than releases. */ PASS(errctx, akgl_tilemap_release(&townmap)); PASS(errctx, akgl_tilemap_load("assets/town.tmj", &townmap)); /* The map may have brought its own physics with it. */ if ( townmap.use_own_physics == true ) { akgl_physics = &townmap.physics; } akgl_gamemap = &townmap; SUCCEED_RETURN(errctx); } ``` Loading a map has side effects well beyond the destination struct: textures are uploaded, actors are created in the pool, and names are published in the registry. So **the renderer, the pools, the registries and every character the map names have to be in place first.** The startup order that produces that is in [Chapter 7](07-the-game-and-the-frame.md). Drawing is per layer. `akgl_tilemap_draw(map, viewport, layeridx)` draws one layer clipped to a viewport in map pixels — usually `akgl_camera` — and draws the tiles at the viewport's edges partially, so scrolling is smooth rather than snapping to the grid. An image layer ignores the viewport and is drawn once at the origin. `akgl_render_2d_draw_world` makes one pass per layer; see [Chapter 8](08-rendering.md). ## Known defects Documented here because this is where a reader hits them. Each is cross-referenced to `TODO.md`. **A failed load releases nothing.** `akgl_tilemap_load` zeroes the destination up front and then loads physics, geometry, layers and tilesets in order. If any of that fails, the textures already uploaded and the actors already created stay exactly where they are, and the destination holds a half-built map. `CLEANUP` gives back the JSON document and one pool string and nothing else. **Treat a failed load as needing `akgl_tilemap_release` and a fresh actor registry** before you try again — otherwise the retry inherits the corpse, and `akgl_tilemap_load` will zero the struct over the previous attempt's texture pointers and leak them. **`akgl_tilemap_release` does not release the actors an object layer created.** It destroys tileset and image-layer textures and clears each pointer as it goes — that half is correct as of 0.5.0, and a second release is safe rather than a use-after-free (`TODO.md`, "Known and still open" item 2). The actors, and their references on characters and sprites, are yours to release. The header's `@warning` describing a tileset double-free is stale; the loop it describes was fixed and `tests/tilemap.c` releases the fixture three times and asserts every texture pointer is `NULL`. **`akgl_tilemap_draw` does not bounds-check `layeridx`.** An index at or past `numlayers` reads a neighbouring layer, and one past 16 reads past the array. `akgl_tilemap_draw_tileset` checks its upper bound but not for a negative index. **A tileset's `margin` is recorded and never applied.** `akgl_tilemap_compute_tileset_offsets` steps by tile size plus `spacing` and ignores `margin` entirely, so a tileset image with a border produces offsets shifted by it — every tile drawn from slightly the wrong place. The FIXME is at `src/tilemap.c:187`. Author tilesets with `margin: 0` until it is fixed. **A second tileset wastes its whole offset table.** The offset table is indexed by local tile id from 0, so tileset B's entries sit at the front of B's own 65,536-entry table while `akgl_tilemap_draw` indexes it by `tilenum - firstgid` — correct, but it means every tileset pays 512 KiB regardless of how many tiles it has. The FIXME on `akgl_Tileset::tile_offsets` explains it. Consequence for you: prefer one large tileset per map to five small ones. **Tileset images do not go through the spritesheet registry**, so an image shared between two maps is loaded twice, into two textures. **`numlayers` is set before the layer bound is checked.** `akgl_tilemap_load_layers` assigns the JSON array's length and *then* refuses a seventeenth layer, so on that failure path the struct briefly claims more layers than it has. It is only reachable on a path that has already failed, but do not read `numlayers` off a map whose load raised.