aboutsummaryrefslogtreecommitdiff

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)

šŸ“– Full Documentation

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
  • SDL2mixer audio via a thin FFI binding

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 — SDLmixer 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 (Spring Lisp Game Jam 2025).

  • Code: Gene Pasquet
  • Demo Levels: Owen Pasquet
  • Demo Art: Kenney (1-bit pack)