aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/specs/2026-04-05-milestone-8-game-object-lifecycle-design.md201
1 files changed, 133 insertions, 68 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
index 2588761..d6e71be 100644
--- 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
@@ -3,6 +3,7 @@
**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".
---
@@ -19,36 +20,67 @@ 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`.
+`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.
+
```
-tilemap → entity → world → animation → physics → ai → input → prefabs → mixer → sound → assets → engine
+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
+(defstruct (game constructor: make-game*)
title width height
window renderer
- input-state
- scene camera
- assets ;; asset registry hash table (from assets.scm)
+ 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
+ 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
@@ -56,20 +88,25 @@ tilemap → entity → world → animation → physics → ai → input → pref
(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
- frame-delay: frame-delay
- assets: (make-asset-registry)
- preload-hook: preload
- create-hook: create
- update-hook: update
- render-hook: render))
+ (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 (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)))
```
@@ -78,54 +115,91 @@ tilemap → entity → world → animation → physics → ai → input → pref
## 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!))
```
-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.
+**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.
+
+---
-`render-scene!` lives in `renderer.scm` and draws tilemap layers then entities — the logic already present in macroknight, extracted into the engine.
+## 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 generated by `defstruct` and exported:
+All `game-*` readers and `game-*-set!` mutators are generated by `defstruct` and exported. Additional convenience accessors:
| Accessor | Description |
|---|---|
-| `game-renderer` | SDL2 renderer — available in `preload:` for texture upload |
+| `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` / `game-camera-set!` | Current camera struct |
+| `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` struct |
+| `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 setter |
-
----
-
-## `render:` Hook Semantics
+| `(game-asset-set! game key val)` | Convenience: `(asset-set! (game-assets game) key val)` |
-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.
+`game-camera` is not a struct field — it is a derived accessor:
+```scheme
+(define (game-camera game)
+ (scene-camera (game-scene game)))
+```
---
@@ -137,26 +211,16 @@ Out of scope for this milestone. A `preload:` slot will be reserved in the scene
## 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.
+macroknight's `game.scm` must be ported to use `make-game` + `game-run!` as part of this milestone. It is the primary integration test.
-Target shape for macroknight `game.scm`:
+**What moves to the engine:** SDL2 init block (lines 87–137), the main loop (line 659+), and all frame-level rendering.
-```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)
-```
+**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.
---
@@ -167,3 +231,4 @@ Target shape for macroknight `game.scm`:
- `game-running?` quit flag
- Scene state machine (Milestone 9)
- AI tag lookup (Milestone 10)
+- Extraction of `animation`, `ai`, `prefabs`, `mixer`, `sound` modules into downstroke