examples/sidescroller and examples/jrpg are complete programs, built with the library and exercised headless by ctest. The chapters quote them with `c excerpt=examples/...` blocks rather than restating the code, so a chapter cannot drift from a program that compiles -- the excerpt check fails the moment the source moves. 34 excerpts in one chapter, 21 in the other. The two are complementary. The sidescroller is the physics tutorial: gravity, a jump, coins, hazards. The JRPG is the content-pipeline tutorial: a town map, NPCs spawned from map objects, four-way per-facing animation, a text box, a follower. Both smoke tests drive real SDL_Events through akgl_controller_handle_event and step the physics clock at a fixed 1/60s rather than sleeping, so a scripted run is deterministic and finishes in under five seconds. Writing them is what turned up most of the defects recorded in the next commit, because a game exercises paths a unit test does not. Each workaround says in the chapter which library gap forced it: - collision is written in a custom movementlogicfunc, because akgl_physics_arcade_collide raises AKERR_API and akgl_physics_simulate never calls collide at all; - the sidescroller cancels the step's own gravity when it blocks downward, because otherwise a quarter-pixel of penetration makes the *horizontal* sweep report blocked and the character walks backwards a tile at a time; - both clear movement_controls_face on every map-spawned actor, because the default facefunc leaves a stopped actor with no facing bit, no sprite, and no draw; - the JRPG's follower gets a renderfunc that nulls obj->parent for the duration of the draw, because a child's offset is counted twice. Assets are CC0 from three Kenney packs, vendored with per-pack licence text, per-file provenance, and the geometry contract in docs/tutorials/assets/README.md. CC0 specifically rather than merely free: a reader who copies a tutorial into their own game inherits no obligation. scripts/fetch_tutorial_assets.sh refreshes them in the shape mkcontrollermappings.sh was fixed into for 0.5.0 -- it checks curl's status, refuses a pack page that does not say CC0, verifies the archive and the staged dimensions, and leaves the tracked bytes untouched on any failure. Both failure paths were tested, and a no-op refresh is byte-identical. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
15 KiB
Provenance of the tutorial assets
Everything under docs/tutorials/assets/ is CC0 1.0 or was written for this
repository. Nothing here carries an attribution requirement, a share-alike
clause, or a field-of-use restriction, which is the whole point: a reader who
copies a tutorial into their own game inherits whatever obligation these assets
carry, and CC0 carries none. "Free to download" is not the same thing and was
not accepted -- each pack's licence was read on its Kenney page and in the
License.txt it ships, and scripts/fetch_tutorial_assets.sh re-checks both on
every refresh and refuses to vendor a pack that fails either.
LICENSE is the CC0 1.0 legal code. The three LICENSE.kenney_*.txt files are
the licence statements the packs themselves ship, kept beside the art in the
same way tests/assets/akgl_test_mono.LICENSE.txt sits beside its font.
The two tables below account for all 56 asset files: 14 carry upstream bytes and
42 were written here. This file and README.md are the remaining two, and are
documentation of the same kind as the rest of docs/.
Upstream packs
| Pack | URL | Licence | Licence file in this tree |
|---|---|---|---|
| Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | LICENSE.kenney_pixel-line-platformer.txt |
| RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | LICENSE.kenney_rpg-urban-pack.txt |
| Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | LICENSE.kenney_music-jingles.txt |
Kenney asks for credit and does not require it. Credit is given here and in the tutorial chapters.
Files carrying upstream bytes
| File | Pack | Source URL | Licence | Cropped / repacked into |
|---|---|---|---|---|
LICENSE |
creativecommons.org | https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt | CC0 1.0 | The CC0 1.0 Universal legal code, fetched verbatim |
LICENSE.kenney_pixel-line-platformer.txt |
Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | The pack's own License.txt, copied verbatim |
LICENSE.kenney_rpg-urban-pack.txt |
RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | The pack's own License.txt, copied verbatim |
LICENSE.kenney_music-jingles.txt |
Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | The pack's own License.txt, copied verbatim |
sidescroller/tiles.png |
Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tilemap/tilemap_packed.png copied verbatim: 160x96, 10x6 tiles of 16x16, no spacing, no margin |
sidescroller/player.png |
Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tiles 40, 41, 42 and their horizontal mirrors, bottom-centred in six 32x32 cells |
sidescroller/coin.png |
Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tile 44, bottom-centred in one 32x32 cell |
sidescroller/hazard.png |
Pixel Line Platformer | https://kenney.nl/assets/pixel-line-platformer | CC0 1.0 | Tiles 55, 56, 51, 52, bottom-centred in four 32x32 cells |
sidescroller/jingle_start.ogg |
Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | Audio/8-Bit jingles/jingles_NES00.ogg, copied verbatim (1.8 s) |
jrpg/tiles.png |
RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Tilemap/tilemap_packed.png copied verbatim: 432x288, 27x18 tiles of 16x16, no spacing, no margin |
jrpg/player.png |
RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Character block 0 (tileset columns 23-26, rows 0-2) regrouped into twelve 32x32 cells |
jrpg/npc_shopkeeper.png |
RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Character block 3 (tileset columns 23-26, rows 9-11) regrouped into twelve 32x32 cells |
jrpg/npc_elder.png |
RPG Urban Pack | https://kenney.nl/assets/rpg-urban-pack | CC0 1.0 | Character block 2 (tileset columns 23-26, rows 6-8) regrouped into twelve 32x32 cells |
jrpg/jingle_start.ogg |
Music Jingles | https://kenney.nl/assets/music-jingles | CC0 1.0 | Audio/Pizzicato jingles/jingles_PIZZI07.ogg, copied verbatim (1.3 s) |
Files written for this repository
These contain no upstream content. They are libakgl source in JSON form: they name the art above, they do not embed it.
| File | Kind | What it is |
|---|---|---|
jrpg/character_jrpg_elder.json |
character | State-name to sprite-name bindings; contains no upstream content |
jrpg/character_jrpg_player.json |
character | State-name to sprite-name bindings; contains no upstream content |
jrpg/character_jrpg_shopkeeper.json |
character | State-name to sprite-name bindings; contains no upstream content |
jrpg/sprite_jrpg_elder_idle_down.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_idle_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_idle_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_idle_up.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_walk_down.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_walk_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_walk_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_elder_walk_up.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_idle_down.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_idle_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_idle_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_idle_up.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_walk_down.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_walk_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_walk_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_player_walk_up.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_idle_down.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_idle_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_idle_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_idle_up.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_walk_down.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_walk_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_walk_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/sprite_jrpg_shopkeeper_walk_up.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
jrpg/town.tmj |
map | Tiled 1.8 TMJ, tileset embedded. 30x20 cells, ground + decoration + actors |
sidescroller/character_ss_coin.json |
character | State-name to sprite-name bindings; contains no upstream content |
sidescroller/character_ss_hazard_blob.json |
character | State-name to sprite-name bindings; contains no upstream content |
sidescroller/character_ss_hazard_moth.json |
character | State-name to sprite-name bindings; contains no upstream content |
sidescroller/character_ss_player.json |
character | State-name to sprite-name bindings; contains no upstream content |
sidescroller/level1.tmj |
map | Tiled 1.8 TMJ, tileset embedded. 40x15 cells, background + terrain + actors |
sidescroller/sprite_ss_coin.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_hazard_blob.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_hazard_moth.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_player_idle_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_player_idle_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_player_jump_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_player_jump_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_player_run_left.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
sidescroller/sprite_ss_player_run_right.json |
sprite | Frame list into the sheet named by its own spritesheet.filename |
How the repacking works, and why
Tilesets are copied verbatim. Both packs already ship a 16x16 grid with zero
spacing and zero margin, which is the only geometry
akgl_tilemap_compute_tileset_offsets gets right -- it adds spacing to the
tile pitch but sets the first row's y offset to spacing rather than zero,
and it ignores margin completely. A packed sheet with a 1 px gutter, which is
what most of Kenney's older packs ship, would render every tile in row 0 one
pixel low. That ruled several otherwise-suitable packs out.
Character sheets are repacked into one row of 32x32 cells. The asset contract for these tutorials is 16x16 tiles and 32x32 character frames, and neither pack ships 32x32 art, so each 16x16 source tile is composited at (8, 16) inside its cell -- horizontally centred, sitting on the cell's bottom edge. The art is not resampled: a 2x upscale would put 2x2 pixel blocks next to 1x1 tiles, and every sprite in the repository would stop matching the source pixel for pixel. The cost is that three quarters of each cell is empty, and the benefit is one rule the tutorials can rely on: an actor drawn into a 32x32 destination rectangle has its feet on the bottom edge of that rectangle and its body centred, whichever sheet it came from.
Left-facing frames are stored, not mirrored at draw time.
akgl_actor_render calls draw_texture with SDL_FLIP_NONE hard-coded
(src/actor.c), so there is no way to ask for a mirrored blit. The sidescroller
sheet therefore carries frames 0-2 facing right and frames 3-5 as their
horizontal mirrors. The JRPG sheets need no mirroring: the RPG Urban Pack draws
both side views.
One frame is used twice. Pixel Line Platformer has no dedicated jump pose.
Frame 1 is the passing position of the run cycle, which is the airborne one, and
ss_player_jump_right reuses it. Said here rather than left for a reader to
notice.
The coin does not animate. Pixel Line Platformer ships one gold pickup tile
and no rotation frames, so sprite_ss_coin.json is a single frame. Nothing was
invented to pad it out.
Reproducing this
scripts/fetch_tutorial_assets.sh
Fetches all three packs, checks each page and each shipped License.txt says
CC0, verifies the source images are the size the frame arithmetic assumes,
repacks into a temporary directory, checks every staged file's dimensions, and
only then moves anything into place. A failure at any step leaves the tracked
copies untouched and exits non-zero. The output is byte-for-byte reproducible --
ImageMagick's wall-clock date:create and date:modify chunks are stripped, so
a refresh that changes nothing produces no diff.
The script does not touch the JSON or the TMJ maps. Those are hand-maintained.