Add two tutorial games that build, run in CI, and cannot drift
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>
2026-08-01 20:59:00 -04:00
|
|
|
# The sidescroller tutorial game.
|
|
|
|
|
#
|
|
|
|
|
# A real target, built by default, so docs/19-tutorial-sidescroller.md cannot
|
|
|
|
|
# quote a program that does not compile -- and registered as a headless smoke
|
|
|
|
|
# test, so it cannot quote one that does not run.
|
|
|
|
|
|
|
|
|
|
get_filename_component(SS_ASSET_DIR
|
|
|
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/../../docs/tutorials/assets/sidescroller" ABSOLUTE)
|
|
|
|
|
|
|
|
|
|
add_executable(sidescroller
|
|
|
|
|
main.c
|
|
|
|
|
player.c
|
|
|
|
|
actors.c
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
target_include_directories(sidescroller PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
|
|
|
|
target_compile_options(sidescroller PRIVATE ${AKGL_WARNING_FLAGS})
|
|
|
|
|
|
|
|
|
|
# Baked in rather than looked up at runtime, so the smoke test can be launched
|
|
|
|
|
# from any working directory. `--assets DIR` overrides it for a reader who has
|
|
|
|
|
# moved the art somewhere else.
|
|
|
|
|
target_compile_definitions(sidescroller PRIVATE "SS_ASSET_DIR=\"${SS_ASSET_DIR}\"")
|
|
|
|
|
|
|
|
|
|
target_link_libraries(sidescroller
|
|
|
|
|
PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf
|
|
|
|
|
SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
|
|
|
|
|
|
|
|
|
# A vendored build leaves the SDL satellite libraries in per-project
|
|
|
|
|
# subdirectories that are not on the loader's default search path, which is the
|
|
|
|
|
# same problem AKGL_VENDORED_RPATH solves for the test executables.
|
|
|
|
|
if(AKGL_VENDORED_DEPENDENCIES)
|
|
|
|
|
set_target_properties(sidescroller PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
# Four seconds of scripted play under the headless drivers: the level loads, the
|
|
|
|
|
# player walks and jumps, the swept collision runs against real terrain, and the
|
|
|
|
|
# program tears down and exits 0. A tutorial that stops working fails here rather
|
|
|
|
|
# than in front of a reader.
|
|
|
|
|
#
|
|
|
|
|
# add_test() rather than _add_test(): the root CMakeLists.txt shadows it only
|
|
|
|
|
# while this project is top-level, and by the time examples/ is added the
|
|
|
|
|
# suppression has already been lifted. An embedded build gets the builtin.
|
|
|
|
|
add_test(NAME example_sidescroller COMMAND sidescroller --frames 240 --autoplay)
|
2026-08-02 14:19:05 -04:00
|
|
|
set_tests_properties(example_sidescroller PROPERTIES TIMEOUT 120)
|
|
|
|
|
# set_property, not set_tests_properties: the latter parses its PROPERTIES
|
|
|
|
|
# arguments as name/value *pairs*, so a semicolon-separated value is split and
|
|
|
|
|
# every element after the first is consumed as a bogus property name. The
|
|
|
|
|
# audio and render entries of this very list were silently lost that way for
|
|
|
|
|
# as long as the list existed -- invisible on any machine whose real audio
|
|
|
|
|
# device works, and found the first time CI ran on a runner without one.
|
|
|
|
|
set_property(TEST example_sidescroller PROPERTY ENVIRONMENT
|
|
|
|
|
"SDL_VIDEODRIVER=dummy"
|
|
|
|
|
"SDL_RENDER_DRIVER=software"
|
|
|
|
|
"SDL_AUDIODRIVER=dummy"
|
Add two tutorial games that build, run in CI, and cannot drift
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>
2026-08-01 20:59:00 -04:00
|
|
|
)
|