diff options
| -rw-r--r-- | docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md | 169 |
1 files changed, 169 insertions, 0 deletions
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..2588761 --- /dev/null +++ b/docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md @@ -0,0 +1,169 @@ +# Milestone 8 — The Game Object and Lifecycle API + +**Date:** 2026-04-05 +**Status:** Approved +**Scope:** `engine.scm`, `assets.scm`, macroknight port + +--- + +## 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`. + +`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. + +### Build order (Makefile) + +``` +tilemap → entity → world → animation → physics → ai → input → prefabs → mixer → sound → assets → engine +``` + +--- + +## Data Structures + +### `game` struct (`engine.scm`) + +```scheme +(defstruct game + title width height + window renderer + input-state + scene camera + 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 +``` + +### `make-game` keyword constructor + +```scheme +(define (make-game #!key + (title "Downstroke Game") + (width 640) (height 480) + (frame-delay 16) + (preload #f) (create #f) (update #f) (render #f)) + (make-game* title: title width: width height: height + frame-delay: frame-delay + assets: (make-asset-registry) + 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!` + +``` +SDL2 init + └─ create window + renderer → stored in game struct + └─ call preload-hook (user loads assets) + └─ call create-hook (user builds initial scene) + └─ frame loop: + input-handle-events! → update input-state + call update-hook game dt + render-clear! + render-scene! renderer scene camera ;; engine draws world + call render-hook game ;; user overlay + render-present! + sdl2:delay! frame-delay + └─ cleanup: destroy window, sdl2:quit! +``` + +The loop exits when `input-quit?` is true (SDL_QUIT event). There is no user-settable `game-running?` flag in this milestone. + +`render-scene!` lives in `renderer.scm` and draws tilemap layers then entities — the logic already present in macroknight, extracted into the engine. + +--- + +## Public Accessors + +All generated by `defstruct` and exported: + +| Accessor | Description | +|---|---| +| `game-renderer` | SDL2 renderer — available in `preload:` for texture upload | +| `game-window` | SDL2 window | +| `game-scene` / `game-scene-set!` | Current scene struct; user sets this in `create:` | +| `game-camera` / `game-camera-set!` | Current camera struct | +| `game-assets` | Full asset registry hash table | +| `game-input` | Current `input-state` struct | +| `(game-asset game key)` | Convenience: `(asset-ref (game-assets game) key)` | +| `(game-asset-set! game key val)` | Convenience setter | + +--- + +## `render:` Hook Semantics + +Following Phaser 2.x: the engine draws the full scene first (tilemap + 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. + +--- + +## 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` is ported to use `make-game` + `game-run!` as part of this milestone. macroknight is the primary integration test; any engine change that requires a macroknight update must update macroknight in the same session. + +Target shape for macroknight `game.scm`: + +```scheme +(define my-game + (make-game + title: "MacroKnight" width: 600 height: 400 + preload: (lambda (game) + ;; load fonts, sounds, tilemaps + ...) + create: (lambda (game) + ;; build initial scene + ...) + update: (lambda (game dt) + ;; game logic, input handling, physics calls + ...))) + +(game-run! my-game) +``` + +--- + +## 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) |
