From b99ada53b715def5492c7d04c0d327fa7048e5d3 Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sun, 5 Apr 2026 23:12:54 +0100 Subject: Complete implementation --- ...-05-milestone-8-game-object-lifecycle-design.md | 234 +++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md (limited to 'docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md') diff --git a/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md b/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md new file mode 100644 index 0000000..d6e71be --- /dev/null +++ b/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md @@ -0,0 +1,234 @@ +# Milestone 8 — The Game Object and Lifecycle API + +**Date:** 2026-04-05 +**Status:** Approved +**Scope:** `engine.scm`, `assets.scm`, macroknight port +**Milestone numbering:** Follows `downstroke.org` / `TODO-engine.org` (Milestone 8). CLAUDE.md refers to the same work as "Milestone 7". + +--- + +## Goal + +Introduce `make-game` and `game-run!` as the public entry point for downstroke. A minimal game becomes ~20 lines of Scheme. This is the "Phaser moment" — the API stabilises around a game struct with lifecycle hooks. + +--- + +## Architecture + +Two new files: + +- **`engine.scm`** — `make-game` struct, `game-run!`, the frame loop, and all public accessors. +- **`assets.scm`** — minimal key→value asset registry. No asset-type-specific logic; that is Milestone 6's domain. + +`engine.scm` imports: `renderer.scm`, `input.scm`, `world.scm`, `assets.scm`, the sdl2 egg (prefix `sdl2:`), `sdl2-ttf` (prefix `ttf:`), and `sdl2-image` (prefix `img:`). + +**Audio / mixer is not initialised by the engine in this milestone.** `mixer.scm` and `sound.scm` remain in macroknight and are not yet extracted. macroknight's `preload:` hook calls `init-audio!` directly from `sound.scm` until those modules are extracted. + +`physics.scm` is **not** imported by the engine. Physics is a library; the user requires it if needed and calls physics functions from their `update:` hook. + +### New function in `renderer.scm` + +`render-scene!` does not yet exist. It must be added to `renderer.scm` as part of this milestone. The module uses `export *` so no explicit export update is needed. + +Existing signatures in `renderer.scm` that `render-scene!` calls: +- `(draw-tilemap renderer camera tileset-texture tilemap)` — 4 args +- `(draw-entities renderer camera tileset tileset-texture entities)` — 5 args +- `tilemap-tileset` is a field accessor on the `tilemap` struct (defined in `tilemap.scm`) + +```scheme +(define (render-scene! renderer scene) + (let ((camera (scene-camera scene)) + (tilemap (scene-tilemap scene)) + (tileset-texture (scene-tileset-texture scene)) + (tileset (tilemap-tileset (scene-tilemap scene))) + (entities (scene-entities scene))) + (draw-tilemap renderer camera tileset-texture tilemap) + (draw-entities renderer camera tileset tileset-texture entities))) +``` + +### Build order (Makefile) + +Only modules that currently exist in downstroke are listed. Modules still in macroknight (`animation`, `ai`, `prefabs`, `mixer`, `sound`) are not prerequisites for this milestone. + +``` +entity → tilemap → world → input → renderer → assets → engine +``` + +`physics` is not a link dependency of `engine` and is omitted from this chain. It is a separate build target. + +--- + +## Data Structures + +### `game` struct (`engine.scm`) + +`defstruct` is called with `constructor: make-game*` so the generated raw constructor does not collide with the public keyword constructor `make-game`. + +```scheme +(defstruct (game constructor: make-game*) + title width height + window renderer + input ;; input-state record (accessor: game-input) + input-config ;; input-config record (accessor: game-input-config) + assets ;; asset registry hash table (from assets.scm) + frame-delay + preload-hook ;; (lambda (game) ...) + create-hook ;; (lambda (game) ...) + update-hook ;; (lambda (game dt) ...) + render-hook ;; (lambda (game) ...) — post-render overlay + scene) ;; current scene struct (accessor: game-scene) +``` + +**Note:** There is no separate `camera` field. The camera lives inside the scene (`scene-camera`). `game-camera` is a convenience accessor (see Accessors section). + +### `make-game` keyword constructor + +```scheme +(define (make-game #!key + (title "Downstroke Game") + (width 640) (height 480) + (frame-delay 16) + (input-config *default-input-config*) + (preload #f) (create #f) (update #f) (render #f)) + (make-game* + title: title width: width height: height + window: #f renderer: #f scene: #f + input: (create-input-state input-config) ;; create-input-state takes one arg: config + input-config: input-config + assets: (make-asset-registry) + frame-delay: frame-delay + preload-hook: preload + create-hook: create + update-hook: update + render-hook: render)) +``` + +### Asset registry (`assets.scm`) + +```scheme +(define (make-asset-registry) (make-hash-table)) +(define (asset-set! reg key val) (hash-table-set! reg key val)) +(define (asset-ref reg key) (hash-table-ref reg key (lambda () #f))) +``` + +--- + +## Lifecycle: `game-run!` + +```scheme +(define (game-run! game) + ;; 1. SDL2 init (no audio — mixer not yet extracted, user calls init-audio! in preload:) + (sdl2:init! '(video joystick game-controller)) + (ttf:init!) + (img:init! '(png)) + + ;; 2. Create window + renderer + (game-window-set! game + (sdl2:create-window! (game-title game) 'centered 'centered + (game-width game) (game-height game) '())) + (game-renderer-set! game + (sdl2:create-renderer! (game-window game) -1 '(accelerated vsync))) + + ;; 3. preload hook + (when (game-preload-hook game) ((game-preload-hook game) game)) + + ;; 4. create hook + (when (game-create-hook game) ((game-create-hook game) game)) + + ;; 5. Frame loop + (let loop ((last-ticks (sdl2:get-ticks))) + (let* ((now (sdl2:get-ticks)) + (dt (- now last-ticks)) + (events (sdl2:collect-events!)) + (input (input-state-update (game-input game) events (game-input-config game)))) + (game-input-set! game input) + (unless (input-held? input 'quit) + (when (game-update-hook game) ((game-update-hook game) game dt)) + (sdl2:render-clear! (game-renderer game)) + (when (game-scene game) + (render-scene! (game-renderer game) (game-scene game))) + (when (game-render-hook game) ((game-render-hook game) game)) + (sdl2:render-present! (game-renderer game)) + (sdl2:delay! (game-frame-delay game)) + (loop now)))) + + ;; 6. Cleanup + (sdl2:destroy-window! (game-window game)) + (sdl2:quit!)) +``` + +**Notes:** +- `sdl2:collect-events!` pumps the SDL2 event queue and returns a list of events (from the sdl2 egg). +- `input-state-update` is the existing function in `input.scm`: `(input-state-update state events config)`. It is purely functional — it returns a new `input-state` record and does not mutate the existing one. +- `create-input-state` in `input.scm` takes one argument: `(create-input-state config)` — it initialises all actions to `#f`. +- Quit is detected via `(input-held? input 'quit)` — the `'quit` action is in `*default-input-config*` mapped to `escape` key: `(escape . quit)` in `keyboard-map`. +- `sdl2:render-clear!` and `sdl2:render-present!` are called directly from the sdl2 egg (no wrapper needed). +- The loop exits when `input-held?` returns true for `'quit`. No user-settable `game-running?` flag in this milestone. + +--- + +## `render:` Hook Semantics + +Following Phaser 2.x: the engine draws the full scene first via `render-scene!` (tilemap layers + entities), then calls the user's `render:` hook. The hook is for post-render overlays only — debug info, HUD elements, custom visuals. It does not replace or suppress the engine's render pass. + +--- + +## Physics + +Physics (`physics.scm`) is a library, not a pipeline. `game-run!` does not call any physics function automatically. Users who want physics `(require-extension downstroke/physics)` and call functions from their `update:` hook. + +--- + +## Public Accessors + +All `game-*` readers and `game-*-set!` mutators are generated by `defstruct` and exported. Additional convenience accessors: + +| Accessor | Description | +|---|---| +| `game-renderer` | SDL2 renderer — available from `preload:` onward | +| `game-window` | SDL2 window | +| `game-scene` / `game-scene-set!` | Current scene struct; user sets this in `create:` | +| `game-camera` | Convenience: `(scene-camera (game-scene game))` — derived, not a struct field. Only valid after `create:` sets a scene; calling before that is an error. | +| `game-assets` | Full asset registry hash table | +| `game-input` | Current `input-state` record | +| `game-input-config` | Current `input-config` record | +| `(game-asset game key)` | Convenience: `(asset-ref (game-assets game) key)` | +| `(game-asset-set! game key val)` | Convenience: `(asset-set! (game-assets game) key val)` | + +`game-camera` is not a struct field — it is a derived accessor: +```scheme +(define (game-camera game) + (scene-camera (game-scene game))) +``` + +--- + +## Scene-Level Preloading + +Out of scope for this milestone. A `preload:` slot will be reserved in the scene struct when `scene-loader.scm` is implemented (Milestone 7), and the two-level asset registry (global + per-scene) will be designed then. + +--- + +## macroknight Port + +macroknight's `game.scm` must be ported to use `make-game` + `game-run!` as part of this milestone. It is the primary integration test. + +**What moves to the engine:** SDL2 init block (lines 87–137), the main loop (line 659+), and all frame-level rendering. + +**What stays in macroknight `game.scm`:** Only the three lifecycle hooks and `game-run!`: +- `preload:` — load fonts, sounds, tilemaps (currently scattered across lines 87–137) +- `create:` — build initial scene (currently `make-initial-game-state`, line 196) +- `update:` — game logic, input dispatch, physics calls (currently the per-frame update functions) + +**Acceptance criterion:** macroknight compiles (`make` succeeds), the game runs and plays correctly (title screen → stage select → gameplay), and `game.scm` is ≤50 lines with no SDL2 init or frame-loop boilerplate. + +--- + +## Out of Scope + +- Scene-level preloading (Milestone 7) +- Asset-type-specific load functions (`game-load-tilemap!`, `game-load-font!`, etc.) — Milestone 6 +- `game-running?` quit flag +- Scene state machine (Milestone 9) +- AI tag lookup (Milestone 10) +- Extraction of `animation`, `ai`, `prefabs`, `mixer`, `sound` modules into downstroke -- cgit v1.2.3