diff options
| -rw-r--r-- | README.md | 136 | ||||
| -rw-r--r-- | README.org | 138 | ||||
| -rw-r--r-- | docs/animation.org | 2 | ||||
| -rw-r--r-- | docs/guide.org | 22 | ||||
| -rw-r--r-- | docs/input.org | 6 | ||||
| -rw-r--r-- | docs/physics.org | 8 | ||||
| -rw-r--r-- | docs/tweens.org | 10 |
7 files changed, 160 insertions, 162 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..917c4ff --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Downstroke + +A 2D tile-driven game engine for Chicken Scheme, built on SDL2. Targets old-school platformer and arcade games — NES-style and beyond. The API is inspired by Phaser 2: one call to start, lifecycle hooks to fill in. A minimal game is ~20 lines of Scheme. + + (define my-game + (make-game + title: "My Game" width: 320 height: 240 + preload: (lambda (game) ...) ; load assets + create: (lambda (game) ...) ; set up scene + update: (lambda (game dt) ...))) ; per-frame logic (physics runs first) + + (game-run! my-game) + + +## Features + +- Tweens with easing helpers +- Tile-based physics: gravity, velocity, AABB collision against TMX tilemaps +- Built-in update pipeline (before your `update:` hook): tweens → acceleration → gravity → velocity → tile collisions → ground detection → entity collisions → group sync → animation; input is polled each frame and available to hooks and your own logic +- Entities as association lists (alists) — purely functional, no classes; prefab data is often written as plists and converted when loaded +- Data-driven prefab/mixin system for composing entity types +- Configurable input: keyboard, joystick, and controller +- Asset registry with `preload:` lifecycle hook +- Scene and camera management +- Sprite animation with frame/tick tracking +- SDL2<sub>mixer</sub> audio via a thin FFI binding + + +## Status + +The engine lives in this repository as modular Chicken units, with demos, docs, and an egg spec (`downstroke.egg`). It still powers [macroknight](https://genepasquet.itch.io/macroknight) (Spring Lisp Game Jam 2025). The tree is at release candidate `1.0.0rc1` (`VERSION` in the `Makefile`, same string in the egg and the list below). Macroknight is not yet switched to depend on the published egg — that remains the final validation step. + +**Milestones:** + +- [DONE] **1–10** — Core refactoring and API stabilization + - 1: Zero-risk module extraction + - 2: Configurable input system + - 3: Data-driven entity rendering + - 4: Renderer abstraction + - 5: Asset preloading + - 6: Scene loading via `create:` hook + - 7: `make-game` / `game-run!` API + - 8: Camera follow + - 9: Named scene states + - 10: Tag-based entity lookup +- [rc] **11** — Package as Chicken egg (v1.0.0rc1) +- [pending] **12** — Macroknight ported to use the egg + +Milestones 1–6 were pure refactoring. Milestone 7 was the API pivot (`make-game`, `game-run!`, pipeline). Milestones 11–12 are packaging and proving the egg against macroknight. Finer-grained planning may live outside this repo (for example in `downstroke.org` or macroknight's historical `TODO-engine.org`). + + +## Dependencies + +System libraries: `SDL2`, `SDL2_image`, `SDL2_mixer`, `SDL2_ttf` + +Chicken eggs (mirrors `downstroke.egg`; install what you do not already have): + + chicken-install sdl2 sdl2-image sdl2-ttf expat matchable defstruct list-utils \ + srfi-1 srfi-13 srfi-69 srfi-197 simple-logger + +From a git checkout, run tests with the `test` egg (`make test` runs `csi -s` on each suite). + +Games that ship their own FSM logic (for example with the `states` egg) declare that egg themselves; Downstroke does not depend on it. + + +## Building + + make # compile all modules to bin/ + make test # run unit tests (=test= egg) + make demos # build demo games (verifies they compile) + make clean # remove bin/ + +Single test module (from repo root; space after each `-I` is required by `csi`): + + csi -I "$(pwd)" -I "$(pwd)/bin" -s tests/physics-test.scm + +The canonical version string is `VERSION` at the top of the `Makefile`; it must match `((version "…")` in `downstroke.egg` and the `v…` marker in the milestone table. Bump all three with `make bump-version NEW=1.2.3` (POSIX `sed`; `NEW` must not contain `&`, backslash, or `#`). + + +## Architecture + +Modules live at the project root. Each compiles as a Chicken unit (`csc -c -J -unit`). Compile order follows the dependency graph: + + entity → tween → tilemap → world → input → physics → renderer → assets → animation → engine → mixer → sound → prefabs → scene-loader + +**Modules:** + +- `entity` — Alist accessors: `entity-ref`, `entity-set` +- `tilemap` — TMX/TSX parsing (expat), tileset loading +- `world` — Scene struct, entity list ops, camera +- `physics` — Gravity, velocity, AABB tile + entity collisions +- `input` — SDL2 events → action mapping, configurable binds +- `animation` — Frame/tick tracking, sprite ID mapping +- `prefabs` — Mixin composition, entity instantiation +- `renderer` — `draw-sprite`, `draw-tilemap-layer`, `draw-text` +- `assets` — Asset registry for the `preload:` hook +- `scene-loader` — `game-load-scene!`, `instantiate-prefab` +- `mixer` — SDL<sub>mixer</sub> FFI (no Scheme deps) +- `sound` — Sound registry, music playback +- `engine` — `make-game`, `game-run!`, lifecycle orchestration + +Entities are alists at runtime — no classes, purely functional: + + '((#:type . player) (#:x . 100) (#:y . 200) (#:width . 16) (#:height . 16) + (#:vx . 0) (#:vy . 0) (#:gravity? . #t) (#:on-ground? . #f) + (#:tile-id . 29) (#:tags . (player)) + (#:anim-name . walk) + (#:animations . (((#:name . walk) (#:frames . (27 28)) (#:duration . 10)) + ((#:name . idle) (#:frames . (28)) (#:duration . 10))))) + +Entity types are composed from mixins declared in a data file: + + (mixins + (physics-body #:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f) + (has-facing #:facing 1)) + + (prefabs + (player physics-body has-facing #:type player #:tile-id 29)) + + +## LLM (AI) disclosure + +Some engine code was written or refactored with an LLM (aka AI) prior +to the initial release. I don't intend to keep using AI for engine +code after that. + +The `docs/` tree is LLM-generated, and that is expected to continue. + + +## Credits + +Engine extracted from [macroknight](https://genepasquet.itch.io/macroknight) (Spring Lisp Game Jam 2025). + +- Code: Gene Pasquet +- Demo Levels: Owen Pasquet +- Demo Art: [Kenney](https://kenney.nl) (1-bit pack) diff --git a/README.org b/README.org deleted file mode 100644 index a474ff7..0000000 --- a/README.org +++ /dev/null @@ -1,138 +0,0 @@ -* Downstroke - -A 2D tile-driven game engine for Chicken Scheme, built on SDL2. Targets old-school platformer and arcade games — NES-style and beyond. The API is inspired by Phaser 2: one call to start, lifecycle hooks to fill in. A minimal game is ~20 lines of Scheme. - -#+begin_src scheme -(define my-game - (make-game - title: "My Game" width: 320 height: 240 - preload: (lambda (game) ...) ; load assets - create: (lambda (game) ...) ; set up scene - update: (lambda (game dt) ...))) ; per-frame logic (physics runs first) - -(game-run! my-game) -#+end_src - -** Features - -- Tweens with easing helpers -- Tile-based physics: gravity, velocity, AABB collision against TMX tilemaps -- Built-in update pipeline (before your =update:= hook): tweens → acceleration → gravity → velocity → tile collisions → ground detection → entity collisions → group sync → animation; input is polled each frame and available to hooks and your own logic -- Entities as association lists (alists) — purely functional, no classes; prefab data is often written as plists and converted when loaded -- Data-driven prefab/mixin system for composing entity types -- Configurable input: keyboard, joystick, and controller -- Asset registry with =preload:= lifecycle hook -- Scene and camera management -- Sprite animation with frame/tick tracking -- SDL2_mixer audio via a thin FFI binding - -** Status - -The engine lives in this repository as modular Chicken units, with demos, docs, and an egg spec (=downstroke.egg=). It still powers [[https://genepasquet.itch.io/macroknight][macroknight]] (Spring Lisp Game Jam 2025). The tree is at release candidate =1.0.0rc1= (=VERSION= in the =Makefile=, same string in the egg and the table below). Macroknight is not yet switched to depend on the published egg — that remains the final validation step. - -| Milestone | Description | Status | -|-----------+------------------------------------+----------| -| 1 | Zero-risk module extraction | DONE | -| 2 | Configurable input system | DONE | -| 3 | Data-driven entity rendering | DONE | -| 4 | Renderer abstraction | DONE | -| 5 | Asset preloading | DONE | -| 6 | Scene loading via =create:= hook | DONE | -| 7 | =make-game= / =game-run!= API | DONE | -| 8 | Camera follow | DONE | -| 9 | Named scene states | DONE | -| 10 | Tag-based entity lookup | DONE | -| 11 | Package as Chicken egg (v1.0.0rc1) | rc | -| 12 | Macroknight ported to use the egg | pending | - -Milestones 1–6 were pure refactoring. Milestone 7 was the API pivot (=make-game=, =game-run!=, pipeline). Milestones 11–12 are packaging and proving the egg against macroknight. Finer-grained planning may live outside this repo (for example in =downstroke.org= or macroknight's historical =TODO-engine.org=). - -** Dependencies - -System libraries: =SDL2=, =SDL2_image=, =SDL2_mixer=, =SDL2_ttf= - -Chicken eggs (mirrors =downstroke.egg=; install what you do not already have): -#+begin_example -chicken-install sdl2 sdl2-image sdl2-ttf expat matchable defstruct list-utils \ - srfi-1 srfi-13 srfi-69 srfi-197 simple-logger -#+end_example - -From a git checkout, run tests with the =test= egg (=make test= runs =csi -s= on each suite). - -Games that ship their own FSM logic (for example with the =states= egg) declare that egg themselves; Downstroke does not depend on it. - -** Building - -#+begin_src sh -make # compile all modules to bin/ -make test # run unit tests (=test= egg) -make demos # build demo games (verifies they compile) -make clean # remove bin/ -#+end_src - -Single test module (from repo root; space after each =-I= is required by =csi=): -#+begin_src sh -csi -I "$(pwd)" -I "$(pwd)/bin" -s tests/physics-test.scm -#+end_src - -The canonical version string is =VERSION= at the top of the =Makefile=; it must match =((version "…")= in =downstroke.egg= and the =v…= marker in the milestone table. Bump all three with =make bump-version NEW=1.2.3= (POSIX =sed=; =NEW= must not contain =&=, backslash, or =#=). - -** Architecture - -Modules live at the project root. Each compiles as a Chicken unit (=csc -c -J -unit=). Compile order follows the dependency graph: - -: entity → tween → tilemap → world → input → physics → renderer → assets → animation → engine → mixer → sound → prefabs → scene-loader - -| Module | Responsibility | -|----------------+---------------------------------------------------| -| =entity= | Alist accessors: =entity-ref=, =entity-set= | -| =tilemap= | TMX/TSX parsing (expat), tileset loading | -| =world= | Scene struct, entity list ops, camera | -| =physics= | Gravity, velocity, AABB tile + entity collisions | -| =input= | SDL2 events → action mapping, configurable binds | -| =animation= | Frame/tick tracking, sprite ID mapping | -| =prefabs= | Mixin composition, entity instantiation | -| =renderer= | =draw-sprite=, =draw-tilemap-layer=, =draw-text= | -| =assets= | Asset registry for the =preload:= hook | -| =scene-loader= | =game-load-scene!=, =instantiate-prefab= | -| =mixer= | SDL_mixer FFI (no Scheme deps) | -| =sound= | Sound registry, music playback | -| =engine= | =make-game=, =game-run!=, lifecycle orchestration | - -Entities are alists at runtime — no classes, purely functional: - -#+begin_src scheme -'((#:type . player) (#:x . 100) (#:y . 200) (#:width . 16) (#:height . 16) - (#:vx . 0) (#:vy . 0) (#:gravity? . #t) (#:on-ground? . #f) - (#:tile-id . 29) (#:tags . (player)) - (#:anim-name . walk) - (#:animations . (((#:name . walk) (#:frames . (27 28)) (#:duration . 10)) - ((#:name . idle) (#:frames . (28)) (#:duration . 10))))) -#+end_src - -Entity types are composed from mixins declared in a data file: - -#+begin_src scheme -(mixins - (physics-body #:vx 0 #:vy 0 #:gravity? #t #:on-ground? #f) - (has-facing #:facing 1)) - -(prefabs - (player physics-body has-facing #:type player #:tile-id 29)) -#+end_src - -** LLM (AI) disclosure - -Some engine code was written or refactored with an LLM (aka AI) prior -to the initial release. I don't intend to keep using AI for engine -code after that. - -The =docs/= tree is LLM-generated, and that is expected to continue. - -** Credits - -Engine extracted from [[https://genepasquet.itch.io/macroknight][macroknight]] (Spring Lisp Game Jam 2025). - -- Code: Gene Pasquet -- Demo Levels: Owen Pasquet -- Demo Art: [[https://kenney.nl][Kenney]] (1-bit pack) diff --git a/docs/animation.org b/docs/animation.org index 6704034..19458f9 100644 --- a/docs/animation.org +++ b/docs/animation.org @@ -407,4 +407,4 @@ names. pipeline and the =#:skip-pipelines= mechanism. - [[file:tweens.org][tweens.org]] — when you want interpolation rather than discrete frame-swapping. -- [[file:../demo/animation.scm][Animation]] (=bin/demo-animation=) — prefab data in [[file:../demo/assets/animation-prefabs.scm][demo/assets/animation-prefabs.scm]]. +- [[https://git.etenil.net/downstroke/tree/demo/animation.scm][Animation]] (=bin/demo-animation=) — prefab data in [[https://git.etenil.net/downstroke/tree/demo/assets/animation-prefabs.scm][demo/assets/animation-prefabs.scm]]. diff --git a/docs/guide.org b/docs/guide.org index 21a70bc..c1cf35b 100644 --- a/docs/guide.org +++ b/docs/guide.org @@ -266,16 +266,16 @@ Once =getting-started= runs, the rest of Downstroke is a menu of subsystems you And the full set of demos that ship with the repository, each built by ~make demos~: -- [[file:../demo/getting-started.scm][Getting started]] (=bin/demo-getting-started=) — arrow keys move a blue square; the starting point for this guide. -- [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — gravity, jumping, tile collisions, frame animation. -- [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — top-down shooter-style movement and firing. -- [[file:../demo/topdown.scm][Topdown]] (=bin/demo-topdown=) — four-direction movement without gravity. -- [[file:../demo/audio.scm][Audio]] (=bin/demo-audio=) — background music and sound effects. -- [[file:../demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — group prefabs composed from mixins. -- [[file:../demo/spritefont.scm][Sprite font]] (=bin/demo-spritefont=) — bitmap font rendering. -- [[file:../demo/menu.scm][Menu]] (=bin/demo-menu=) — a simple UI menu. -- [[file:../demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — side-by-side comparison of easing functions. -- [[file:../demo/scaling.scm][Scaling]] (=bin/demo-scaling=) — logical-resolution scaling via ~scale:~. -- [[file:../demo/animation.scm][Animation]] (=bin/demo-animation=) — frame-based sprite animation. +- [[https://git.etenil.net/downstroke/tree/demo/getting-started.scm][Getting started]] (=bin/demo-getting-started=) — arrow keys move a blue square; the starting point for this guide. +- [[https://git.etenil.net/downstroke/tree/demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — gravity, jumping, tile collisions, frame animation. +- [[https://git.etenil.net/downstroke/tree/demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — top-down shooter-style movement and firing. +- [[https://git.etenil.net/downstroke/tree/demo/topdown.scm][Topdown]] (=bin/demo-topdown=) — four-direction movement without gravity. +- [[https://git.etenil.net/downstroke/tree/demo/audio.scm][Audio]] (=bin/demo-audio=) — background music and sound effects. +- [[https://git.etenil.net/downstroke/tree/demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — group prefabs composed from mixins. +- [[https://git.etenil.net/downstroke/tree/demo/spritefont.scm][Sprite font]] (=bin/demo-spritefont=) — bitmap font rendering. +- [[https://git.etenil.net/downstroke/tree/demo/menu.scm][Menu]] (=bin/demo-menu=) — a simple UI menu. +- [[https://git.etenil.net/downstroke/tree/demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — side-by-side comparison of easing functions. +- [[https://git.etenil.net/downstroke/tree/demo/scaling.scm][Scaling]] (=bin/demo-scaling=) — logical-resolution scaling via ~scale:~. +- [[https://git.etenil.net/downstroke/tree/demo/animation.scm][Animation]] (=bin/demo-animation=) — frame-based sprite animation. Pick the demo closest to the game you want to build, open its source next to the matching topic doc, and you will have a concrete example of every API call in context. diff --git a/docs/input.org b/docs/input.org index a7f7fc5..809994f 100644 --- a/docs/input.org +++ b/docs/input.org @@ -329,9 +329,9 @@ instead of replacing it. * Demos -- [[file:../demo/getting-started.scm][Getting started]] (=bin/demo-getting-started=) — arrow-key movement with =input-held?=. -- [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — mixes =input-held?= for running with =input-pressed?= for jumping. -- [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — uses =input-pressed?= on =a= to fire bullets exactly once per button press. +- [[https://git.etenil.net/downstroke/tree/demo/getting-started.scm][Getting started]] (=bin/demo-getting-started=) — arrow-key movement with =input-held?=. +- [[https://git.etenil.net/downstroke/tree/demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — mixes =input-held?= for running with =input-pressed?= for jumping. +- [[https://git.etenil.net/downstroke/tree/demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — uses =input-pressed?= on =a= to fire bullets exactly once per button press. * See also diff --git a/docs/physics.org b/docs/physics.org index 9c57cc1..56b2b1f 100644 --- a/docs/physics.org +++ b/docs/physics.org @@ -652,7 +652,7 @@ The jump check in the *Platformer demo*: the pipeline. - [[file:rendering.org][Rendering]] — how =#:tile-id= (written by =apply-animation=) is drawn, and where the camera transform is applied. -- [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — canonical gravity + jump + tile-collide example. -- [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — canonical =engine-update: 'none= example with manual collision checks via =aabb-overlap?=. -- [[file:../demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — multiple movers, entity–entity push-apart, the default pipeline in a busier scene. -- [[file:../demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — =step-tweens= in action; useful when combined with =#:skip-pipelines '(velocity-x velocity-y)=. +- [[https://git.etenil.net/downstroke/tree/demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — canonical gravity + jump + tile-collide example. +- [[https://git.etenil.net/downstroke/tree/demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — canonical =engine-update: 'none= example with manual collision checks via =aabb-overlap?=. +- [[https://git.etenil.net/downstroke/tree/demo/sandbox.scm][Sandbox]] (=bin/demo-sandbox=) — multiple movers, entity–entity push-apart, the default pipeline in a busier scene. +- [[https://git.etenil.net/downstroke/tree/demo/tweens.scm][Tweens]] (=bin/demo-tweens=) — =step-tweens= in action; useful when combined with =#:skip-pipelines '(velocity-x velocity-y)=. diff --git a/docs/tweens.org b/docs/tweens.org index 063a485..f63a9e1 100644 --- a/docs/tweens.org +++ b/docs/tweens.org @@ -36,7 +36,7 @@ key that isn't on the entity yet starts from zero. ** Creating a tween -=make-tween= signature (from [[file:../tween.scm][tween.scm]]): +=make-tween= signature (from [[https://git.etenil.net/downstroke/tree/tween.scm][tween.scm]]): #+BEGIN_SRC scheme (make-tween entity #!key @@ -86,7 +86,7 @@ error immediately from =make-tween=. Attach a tween by storing it under =#:tween= on an entity. The engine's default update pipeline runs =step-tweens= as its first per-entity -step (see [[file:../engine.scm][engine.scm]], =default-engine-update=): +step (see [[https://git.etenil.net/downstroke/tree/engine.scm][engine.scm]], =default-engine-update=): #+BEGIN_SRC scheme (chain scene @@ -115,7 +115,7 @@ step (see [[file:../engine.scm][engine.scm]], =default-engine-update=): backward. The "clear to =#f=" behaviour is hard-coded in =step-tweens= (see -[[file:../tween.scm][tween.scm]]): +[[https://git.etenil.net/downstroke/tree/tween.scm][tween.scm]]): #+BEGIN_SRC scheme (if (tween-finished? tw2) @@ -134,7 +134,7 @@ tween (=repeat: -1=), =on-complete= never fires. The ease symbol you pass to =make-tween= is looked up in =*ease-table*= and resolved to a procedure. The full table, copied from -[[file:../tween.scm][tween.scm]]: +[[https://git.etenil.net/downstroke/tree/tween.scm][tween.scm]]: | symbol | shape | |---------------+---------------------------------------------| @@ -244,7 +244,7 @@ entity. The callback is invoked with the entity in its /final/ interpolated state. The signature is just =(lambda (entity) ...)= and — importantly — its return value is /discarded/. Look at =tween-complete= in -[[file:../tween.scm][tween.scm]]: the callback runs for side-effects +[[https://git.etenil.net/downstroke/tree/tween.scm][tween.scm]]: the callback runs for side-effects only, and the entity returned to the pipeline is the unmodified =final=. So =on-complete= is for triggering world-level actions (playing a sound, logging, enqueueing a command, mutating a global |
