From 38eee24832fe6da4f135cae455881ab97953b23a Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sat, 18 Apr 2026 02:47:10 +0100 Subject: Refresh docs and re-indent --- docs/scenes.org | 432 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 docs/scenes.org (limited to 'docs/scenes.org') diff --git a/docs/scenes.org b/docs/scenes.org new file mode 100644 index 0000000..da62291 --- /dev/null +++ b/docs/scenes.org @@ -0,0 +1,432 @@ +#+TITLE: Scenes, Cameras, and Game States +#+AUTHOR: Downstroke Project + +* Overview + +A /scene/ in Downstroke is the immutable record that answers the question "what should the engine simulate and draw right now?". It bundles the entity list, an optional tilemap (for TMX-backed levels), an optional tileset (for sprite-only games that still want tile-sheet rendering), a camera, a background clear color, and a choice of which physics pipeline to run. Each frame the engine reads the current scene from the ~game~ struct, runs an engine-update pass on its entities, lets your ~update:~ hook produce a /new/ scene, snaps the camera to a tagged target if asked, and then hands the new scene to the renderer. + +Everything about scenes is built around swapping one immutable value for another: constructors like ~make-scene~ and ~make-sprite-scene~ build them; accessors like ~scene-entities~ and ~scene-camera~ read them; functional transformers like ~scene-map-entities~ and ~scene-add-entity~ return new scenes; and ~game-scene-set!~ is the single point where the engine sees the new state. If you also want more than one "mode" — a title screen and a play screen, say, or a paused state and a gameplay state — Downstroke layers a lightweight /game state/ system on top, so each state gets its own update and render hooks while sharing the same scene or building its own on demand. + +* The minimum you need + +The smallest scene is a sprite-only one with a single entity, built inside the ~create:~ hook: + +#+begin_src scheme +(import scheme + (chicken base) + (only (list-utils alist) plist->alist) + downstroke-engine + downstroke-world + downstroke-entity + downstroke-scene-loader) + +(define (make-player) + (plist->alist + (list #:type 'player #:x 100 #:y 100 + #:width 32 #:height 32 + #:color '(100 160 255)))) + +(game-run! + (make-game + title: "Minimal Scene" + width: 320 height: 240 + create: (lambda (game) + (game-scene-set! game + (make-sprite-scene + entities: (list (make-player)) + background: '(20 22 30)))))) +#+end_src + +That is all that is required to put a blue square on a dark background. ~make-sprite-scene~ fills in sensible defaults for everything you have not specified — no tilemap, a camera at ~(0, 0)~, no camera target, the default (physics) engine update — and ~game-scene-set!~ installs the result on the game. From here on, every section in this document describes a field you can fill in or a function you can compose to reshape what is on screen. + +* Core concepts + +** The scene record + +~scene~ is a record type defined with ~defstruct~ in ~downstroke-world~. Its fields map one-to-one onto the things the engine needs to know about the current frame: + +| Field | Type | Purpose | +|-------------------+--------------------------------+--------------------------------------------------------------------------------------------------| +| ~entities~ | list of entity alists | Every entity the engine simulates and draws this frame. | +| ~tilemap~ | ~tilemap~ struct or ~#f~ | Optional TMX-loaded tilemap (layers + embedded tileset). ~#f~ for sprite-only scenes. | +| ~tileset~ | ~tileset~ struct or ~#f~ | Optional tileset used for sprite rect math when ~tilemap~ is ~#f~. | +| ~camera~ | ~camera~ struct | The viewport offset (see /Cameras/ below). | +| ~tileset-texture~ | SDL2 texture or ~#f~ | GPU texture used to draw tiles and any entity with a ~#:tile-id~. ~#f~ is allowed (see note). | +| ~camera-target~ | symbol or ~#f~ | A tag the engine will auto-follow with the camera. ~#f~ disables auto-follow. | +| ~background~ | ~#f~ or ~(r g b)~ / ~(r g b a)~ | Framebuffer clear color. ~#f~ means plain black. | +| ~engine-update~ | ~#f~, procedure, or ~'none~ | Per-scene physics pipeline choice (see below). | + +The raw ~make-scene~ constructor is auto-generated by ~defstruct~ and accepts every field as a keyword argument. It does /not/ provide defaults — if you call ~make-scene~ directly you must pass every field, or wrap it with ~make-sprite-scene~ (below) which does the defaulting for you. The paired functional copier ~update-scene~ takes an existing scene and returns a new one with only the named fields replaced: + +#+begin_src scheme +(update-scene scene + entities: (list new-player) + camera-target: 'player) +#+end_src + +*** What each "optional" field actually means + +- ~tilemap: #f~ — sprite-only scenes. No tilemap rendering, no tile collisions (the physics pipeline tile-collision steps no-op when there is no tilemap). Used by all the shmup, topdown, tween, scaling, and sandbox demos when they want full control over layout. +- ~tileset: #f~ — fine when the scene has a ~tilemap~ (the renderer falls back to ~(tilemap-tileset tilemap)~ for rect math) /or/ when no entity has a ~#:tile-id~. Set it explicitly only when you have sprites but no tilemap and still want tile-sheet rendering (the ~animation~ and ~sandbox~ demos do this). +- ~tileset-texture: #f~ — allowed. The renderer's ~draw-entity~ guards on the texture being present and falls back to ~#:color~ rendering when it is missing, so sprite entities are /not/ silently dropped on ~#f~ — they draw as solid colored rectangles. Tilemap layers are simply skipped if either ~tilemap~ or ~tileset-texture~ is ~#f~. +- ~background: #f~ — the renderer clears the framebuffer with opaque black every frame. Any non-~#f~ value must be a list of at least three 0–255 integers; a four-element list includes alpha. +- ~engine-update:~ — three forms are accepted: + - ~#f~ (the default) means "inherit": the engine runs ~default-engine-update~, which is the built-in physics pipeline (tweens → acceleration → gravity → velocity → tile collisions → ground detection → entity collisions → group sync → animation). + - A procedure ~(lambda (game dt) ...)~ replaces the pipeline entirely for this scene. You are fully responsible for whatever transformations of ~(game-scene game)~ should happen each frame. + - The symbol ~'none~ disables engine-update altogether — nothing runs before your ~update:~ hook. Used by the shmup and scaling demos which hand-roll all motion. +- ~camera-target:~ — a symbol that will be looked up via ~scene-find-tagged~ in each entity's ~#:tags~ list. When set, the engine centers the camera on that entity on every frame and clamps the result to ~x ≥ 0~ and ~y ≥ 0~. ~#f~ disables auto-follow (you can still move ~scene-camera~ yourself). + +All eight fields are plain slots: read them with ~scene-entities~, ~scene-tilemap~, ~scene-camera~, ~scene-camera-target~, ~scene-background~, ~scene-engine-update~, and so on. + +** Constructing a scene + +Downstroke offers three escalating ways to build a scene, from full manual control to "load a whole level off disk in one call". + +*** ~make-scene~ — full control + +The auto-generated constructor accepts every slot as a keyword and does no defaulting. You will use it when you want to build a scene by hand with some exotic combination of fields, most often because you are bypassing the physics pipeline or embedding a procedurally built tilemap: + +#+begin_src scheme +(make-scene + entities: (list (make-player)) + tilemap: #f + tileset: #f + camera: (make-camera x: 0 y: 0) + tileset-texture: #f + camera-target: #f + engine-update: 'none) +#+end_src + +This is the form the ~shmup~ and ~scaling~ demos use to turn off the physics pipeline and drive everything from their own ~update:~ hook. If you omit a field it will be unbound on the struct; prefer ~make-sprite-scene~ below unless you actually need that degree of control. + +*** ~make-sprite-scene~ — convenient sprite-only scenes + +~make-sprite-scene~ lives in ~downstroke-scene-loader~ and wraps ~make-scene~ with sensible defaults for sprite-only games: + +#+begin_src scheme +(make-sprite-scene + entities: (list (make-player)) + background: '(20 22 30)) +#+end_src + +It always passes ~tilemap: #f~, and supplies these defaults for anything you leave out: + +| Keyword | Default | +|--------------------+-----------------------------| +| ~entities:~ | ~'()~ | +| ~tileset:~ | ~#f~ | +| ~tileset-texture:~ | ~#f~ | +| ~camera:~ | ~(make-camera x: 0 y: 0)~ | +| ~camera-target:~ | ~#f~ | +| ~background:~ | ~#f~ | +| ~engine-update:~ | ~#f~ (inherit = run physics) | + +Use this whenever your game has no TMX map — see ~demo/getting-started.scm~ for the canonical example. + +*** ~game-load-scene!~ — load a TMX level + +When you have a Tiled (TMX) map on disk, ~game-load-scene!~ wires up the entire pipeline in one call: + +#+begin_src scheme +(game-load-scene! game "demo/assets/level-0.tmx") +#+end_src + +Internally it performs four steps: + +1. Calls ~game-load-tilemap!~ which invokes ~load-tilemap~ to parse the TMX (via expat) and the linked TSX tileset, also loading the tileset's PNG image. The resulting ~tilemap~ struct is stored in the game's asset registry under the key ~'tilemap~. +2. Calls ~create-tileset-texture~ (a thin wrapper around ~create-texture-from-tileset~) to upload the tileset image as an SDL2 texture via the game's renderer. +3. Builds a ~scene~ with ~entities: '()~, the parsed ~tilemap~, the fresh tileset texture, a camera at the origin, and no camera target. +4. Installs the scene on the game with ~game-scene-set!~ and returns it. + +Because it returns the new scene, you can chain additional transformations before the frame begins (see the ~topdown~ and ~platformer~ demos): + +#+begin_src scheme +(let* ((s0 (game-load-scene! game "demo/assets/level-0.tmx")) + (s1 (scene-add-entity s0 (make-player))) + (s2 (update-scene s1 camera-target: 'player))) + (game-scene-set! game s2)) +#+end_src + +Note that ~game-load-scene!~ does /not/ automatically populate ~entities~ from the TMX object layer. That conversion is available as a separate function: + +#+begin_src scheme +(tilemap-objects->entities tilemap registry) +#+end_src + +~tilemap-objects->entities~ walks the parsed ~tilemap-objects~ and, for each TMX ~~ whose ~type~ string matches a prefab in your registry, calls ~instantiate-prefab~ with the object's ~(x, y, width, height)~. Objects with no matching prefab are filtered out. You can call it yourself to build the initial entity list from the TMX objects and then feed the result to ~scene-add-entity~ or a fresh ~update-scene~. + +*** Related asset loaders + +Three smaller helpers register assets into ~(game-assets game)~ keyed by a symbol, so other code can fetch them with ~game-asset~: + +- ~(game-load-tilemap! game key filename)~ — parse a ~.tmx~ and store the ~tilemap~ struct. +- ~(game-load-tileset! game key filename)~ — parse a ~.tsx~ and store the ~tileset~ struct. +- ~(game-load-font! game key filename size)~ — open a TTF font at a given point size and store it. + +Each returns the loaded value so you can bind it directly: + +#+begin_src scheme +(let* ((ts (game-load-tileset! game 'tileset "demo/assets/monochrome_transparent.tsx")) + (tex (create-texture-from-tileset (game-renderer game) ts))) + ...) +#+end_src + +This pattern — load once in ~preload:~ (or early in ~create:~), retrieve many times via ~game-asset~ — is how the ~animation~ and ~sandbox~ demos share one tileset across multiple prefabs and scenes. + +** Manipulating scene entities + +All scene transformers in ~downstroke-world~ are /functional/: they take a scene and return a new one. Nothing is mutated; the idiomatic pattern inside an ~update:~ hook is always + +#+begin_src scheme +(game-scene-set! game ( (game-scene game) ...)) +#+end_src + +or, when composing multiple transformations, a ~chain~ form from ~srfi-197~. + +*** ~scene-add-entity~ + +~(scene-add-entity scene entity)~ appends one entity to the entity list. Use it after ~game-load-scene!~ to drop the player into a freshly loaded TMX level: + +#+begin_src scheme +(chain (game-load-scene! game "demo/assets/level-0.tmx") + (scene-add-entity _ (make-player)) + (update-scene _ camera-target: 'player)) +#+end_src + +*** ~scene-map-entities~ + +~(scene-map-entities scene proc ...)~ applies each ~proc~ in sequence to every entity in the scene. Each ~proc~ has the signature ~(lambda (scene entity) ...) → entity~ — it receives the /current/ scene (read-only, snapshot at the start of the call) and one entity, and must return a replacement entity. Multiple procs are applied like successive ~map~ passes, in argument order. + +This is the workhorse of the physics pipeline; see ~default-engine-update~ in ~downstroke-engine~ for how it is chained to apply acceleration, gravity, velocity, and so on. In game code you use it for per-entity updates that do not need to see each other: + +#+begin_src scheme +(scene-map-entities scene + (lambda (scene_ e) + (if (eq? (entity-type e) 'demo-bot) + (update-demo-bot e dt) + e))) +#+end_src + +*** ~scene-filter-entities~ + +~(scene-filter-entities scene pred)~ keeps only entities for which ~pred~ returns truthy. Use it to remove dead/expired/off-screen entities each frame: + +#+begin_src scheme +(scene-filter-entities scene + (lambda (e) (or (eq? (entity-type e) 'player) (in-bounds? e)))) +#+end_src + +*** ~scene-transform-entities~ + +~(scene-transform-entities scene proc)~ hands /the whole entity list/ to ~proc~ and expects a replacement list back. Use this when an operation needs to see all entities at once — resolving pairwise entity collisions, for instance, or synchronising grouped entities to their origin: + +#+begin_src scheme +(scene-transform-entities scene resolve-entity-collisions) +(scene-transform-entities scene sync-groups) +#+end_src + +The engine's default pipeline uses ~scene-transform-entities~ for exactly these two steps, because they are inherently N-to-N. + +*** Tagged lookups + +Two helpers let you find entities by tag without iterating yourself: + +- ~(scene-find-tagged scene tag)~ returns the /first/ entity whose ~#:tags~ list contains ~tag~, or ~#f~. +- ~(scene-find-all-tagged scene tag)~ returns every such entity as a list. + +Both are O(n) scans and intended for coarse queries — finding the player, all enemies of a type, the camera target — not for every-frame loops over hundreds of entities. The engine itself uses ~scene-find-tagged~ internally to resolve ~camera-target~. + +*** The update idiom + +Putting it together, the typical shape of an ~update:~ hook is either a single transformer: + +#+begin_src scheme +update: (lambda (game dt) + (let* ((input (game-input game)) + (scene (game-scene game)) + (player (update-player (car (scene-entities scene)) input))) + (game-scene-set! game + (update-scene scene entities: (list player))))) +#+end_src + +or a chain of them, when multiple pipelines compose: + +#+begin_src scheme +(game-scene-set! game + (chain (update-scene scene entities: all) + (scene-map-entities _ + (lambda (scene_ e) (if (eq? (entity-type e) 'player) e (move-projectile e)))) + (scene-remove-dead _) + (scene-filter-entities _ in-bounds?))) +#+end_src + +In both shapes, ~game-scene-set!~ is called exactly once per frame with the final scene. That single write is what the next frame's engine-update and renderer read from. + +** Cameras + +A /camera/ is a tiny record with two integer slots, ~x~ and ~y~, holding the top-left pixel coordinate of the viewport in world space. ~make-camera~ is the constructor: + +#+begin_src scheme +(make-camera x: 0 y: 0) +#+end_src + +The renderer subtracts ~camera-x~ and ~camera-y~ from every sprite and tile's world position before drawing, so moving the camera east visually scrolls everything west. + +*** ~camera-follow~ + +~(camera-follow camera entity viewport-w viewport-h)~ returns a /new/ camera struct centered on the entity, clamped so neither ~x~ nor ~y~ goes below zero. It floors both results to integers (SDL2 wants integer rects). You can call it directly from an ~update:~ hook — for example to manually follow an entity without tagging it — but the engine provides an auto-follow mechanism built on top of it. + +*** Auto-follow via ~camera-target:~ + +If a scene has ~camera-target:~ set to a symbol, the engine runs ~update-camera-follow~ /after/ your ~update:~ hook and /before/ rendering. That function looks up the first entity whose ~#:tags~ list contains the target symbol via ~scene-find-tagged~. If found, it replaces the scene's camera with one centered on that entity (using the game's ~width~ and ~height~ as the viewport size); if no tagged entity is found, the camera is left alone. The clamp to ~x ≥ 0~ and ~y ≥ 0~ is inherited from ~camera-follow~, so the camera never reveals negative world coordinates. + +To opt in, just tag the entity you want followed and set ~camera-target:~ to that tag: + +#+begin_src scheme +(define (make-player) + (plist->alist + (list #:type 'player + #:x 100 #:y 50 + #:tags '(player) + ...))) + +(update-scene scene camera-target: 'player) +#+end_src + +Subsequent frames will keep the camera locked to the player. To stop following, set ~camera-target:~ back to ~#f~ (you keep whatever camera position was most recent). + +If you need to animate the camera yourself (shake, parallax, cutscenes), leave ~camera-target:~ at ~#f~ and edit ~scene-camera~ via ~update-scene~ from inside your own update. The engine will not override what you write so long as no target is set. + +** Named game states + +Many games have more than one "mode": a title menu, a playing state, a game-over screen, maybe a pause overlay. Downstroke's game-state system is a lightweight way to switch update and render hooks between modes without having to build a single mega-update. + +A game state is just an alist of lifecycle hooks, built with ~make-game-state~: + +#+begin_src scheme +(make-game-state #:create main-menu-create + #:update main-menu-update + #:render main-menu-render) +#+end_src + +Each of ~#:create~, ~#:update~, and ~#:render~ is optional (~#f~ by default). The engine uses them as follows: + +- ~#:create~ is called by ~game-start-state!~ when the state becomes active. Use it to lazily build the scene for that state (e.g. build the level the first time the player picks "Play"). +- ~#:update~, if present, /replaces/ the game's top-level ~update-hook~ while this state is active. The engine's ~resolve-hooks~ prefers the state hook and falls back to the game-level hook when the state has no ~#:update~. +- ~#:render~ is handled the same way: state-level wins when present, otherwise the game-level ~render-hook~ runs. + +Registration is done by name with ~game-add-state!~: + +#+begin_src scheme +(game-add-state! game 'main-menu + (make-game-state #:update main-menu-update + #:render main-menu-render)) +(game-add-state! game 'playing + (make-game-state #:update playing-update + #:render playing-render)) +#+end_src + +And transitions happen through ~game-start-state!~, which sets the active state and runs the new state's ~#:create~ hook if present: + +#+begin_src scheme +(game-start-state! game 'main-menu) +... +(when (input-pressed? input 'a) + (game-start-state! game 'playing)) +#+end_src + +Only ~update~ and ~render~ are state-scoped; ~preload~ and ~create~ at the ~make-game~ level still fire once each at boot. The engine's built-in engine-update (the physics pipeline) still runs whenever a scene exists, regardless of which state is active — states only swap which user hook drives the frame. + +* Common patterns + +** Sprite-only scene with no tilemap + +The default choice for small demos and menus: one ~make-sprite-scene~ call, no tilemap, a plain background color. + +*Getting started demo* — run with ~bin/demo-getting-started~, source in ~demo/getting-started.scm~. A blue square you push around the screen with the arrow keys; canonical minimal sprite scene. + +*Tweens demo* — run with ~bin/demo-tweens~, source in ~demo/tweens.scm~. A grid of easing curves applied to moving boxes; also sprite-only. + +** TMX-loaded scene with a player prefab + +Load the level off disk, append the player, set up camera follow. One chain per create hook. + +*Platformer demo* — run with ~bin/demo-platformer~, source in ~demo/platformer.scm~. Uses ~game-load-scene!~ for the TMX map and then ~scene-add-entity~ and ~update-scene~ to attach the player and enable camera follow. + +*Topdown demo* — run with ~bin/demo-topdown~, source in ~demo/topdown.scm~. Same pattern, with ~#:gravity? #f~ on the player so the default engine-update gives four-direction motion. + +** Camera following a tagged entity + +Tag the entity you want the viewport to track, set ~camera-target:~ to the tag, let the engine take care of the rest: + +#+begin_src scheme +(define (make-player) + (plist->alist + (list #:type 'player + #:x 100 #:y 50 + #:width 16 #:height 16 + #:tags '(player) + ...))) + +;; In create: +(chain (game-load-scene! game "demo/assets/level-0.tmx") + (scene-add-entity _ (make-player)) + (update-scene _ camera-target: 'player) + (game-scene-set! game _)) +#+end_src + +Any entity carrying the right tag will be followed, and the camera clamps to non-negative world coordinates every frame. The ~platformer~ and ~topdown~ demos both use ~camera-target: 'player~ exactly this way. + +** Switching between a title screen and a play state + +Register two named states in ~create:~, each with its own update and render hooks, and kick off the first one. Transitions happen anywhere you have a ~game~ handle, including from inside another state's update. + +*Menu demo* — run with ~bin/demo-menu~, source in ~demo/menu.scm~. Registers ~main-menu~ and ~playing~ states, switches to ~playing~ when the player selects the menu entry, and switches back on ~Escape~. + +#+begin_src scheme +create: (lambda (game) + (game-add-state! game 'main-menu + (make-game-state #:update main-menu-update + #:render main-menu-render)) + (game-add-state! game 'playing + (make-game-state #:update playing-update + #:render playing-render)) + (game-start-state! game 'main-menu)) +#+end_src + +Each state owns its render surface — in the menu demo both states clear the screen themselves, since there is no scene installed on the game — but nothing stops a state from sharing a scene: the menu state's ~#:create~ could build it, and the play state's ~#:update~ could simply read and transform ~(game-scene game)~. + +** Loading multiple assets up front + +The ~preload:~ hook is the one place guaranteed to run before the first scene is built, after SDL2 has opened a renderer. Use it to warm up the asset registry so ~create:~ and ~update:~ never block on I/O: + +#+begin_src scheme +preload: (lambda (game) + (game-load-tileset! game 'tiles "demo/assets/monochrome_transparent.tsx") + (game-load-font! game 'title "demo/assets/DejaVuSans.ttf" 24) + (game-load-font! game 'body "demo/assets/DejaVuSans.ttf" 14) + (init-audio!) + (load-sounds! '((jump . "demo/assets/jump.wav") + (coin . "demo/assets/coin.wav")))) +#+end_src + +Inside ~create:~ or ~update:~ you read them back with ~game-asset~: + +#+begin_src scheme +(let ((ts (game-asset game 'tiles)) + (fnt (game-asset game 'title))) + ...) +#+end_src + +Because the registry is a plain hash table, you can store anything — parsed JSON, precomputed tables, your own structs — under any symbol key you like. The three ~game-load-*!~ helpers are there for the file formats the engine itself understands; everything else is ~game-asset-set!~ + ~game-asset~. + +* See also + +- [[file:guide.org][guide.org]] — step-by-step construction of the getting-started demo, including the first ~make-sprite-scene~ call. +- [[file:entities.org][entities.org]] — the entity alist, conventional keys like ~#:tags~ and ~#:type~, and how prefabs build entities that the scene-loader instantiates. +- [[file:physics.org][physics.org]] — what the default engine-update actually does, and how to skip or replace individual pipeline steps per entity. +- [[file:rendering.org][rendering.org]] — how ~render-scene!~ walks the scene to draw tiles, sprites, and fallback colored rects. +- [[file:animation.org][animation.org]] — the animation pipeline step, run by ~default-engine-update~ on every entity. +- [[file:tweens.org][tweens.org]] — per-entity declarative tweens, stepped as the first stage of the default pipeline. +- [[file:input.org][input.org]] — action mapping and polling, used inside ~update:~ hooks to drive entity changes. +- [[file:audio.org][audio.org]] — loading and playing sound effects alongside your scene-loading hooks. -- cgit v1.2.3