Getting Started with Downstroke

Table of Contents

1. Welcome

Downstroke is a 2D tile-driven game engine for CHICKEN Scheme, built on SDL2. Its API is inspired by Phaser 2: a minimal game is about twenty lines of Scheme, with the engine taking care of window creation, the main loop, input, and rendering. This guide walks you through building your very first Downstroke game — a blue square you can push around the screen with the arrow keys. By the end you will have the complete source of the getting-started demo and a clear map of where to go next.

You write a Downstroke game by calling make-game with a few keyword arguments (a title, a size, and one or two lifecycle hooks), and then handing the result to game-run!. Everything else — opening the window, polling input, clearing the framebuffer, presenting the frame — is the engine's job. You only describe what the game is: what the scene looks like, what the entities are, and how they change.

2. The minimum you need

The smallest Downstroke program you can write opens a window, runs the main loop, and quits when you press Escape. No scene, no entities, no update logic — just the lifecycle shell:

(import scheme
        (chicken base)
        (downstroke engine))

(game-run!
 (make-game
  title:  "Hello Downstroke"
  width:  320
  height: 240))

Save that as hello.scm, compile, and run. You should get a 320×240 window with a black background. Press Escape to quit.

Two things are worth noticing:

  • make-game takes keyword arguments (note the trailing colon: title:, width:, height:). They all have defaults, so you can leave any of them off. The window defaults to 640×480 and titled "Downstroke Game".
  • game-run! is what actually starts SDL2 and enters the main loop. It blocks until the player quits.

There are four lifecycle hooks you can attach to make-game:

Keyword When it runs Signature
preload: once, before create: (lambda (game) ...)
create: once, after preload: (lambda (game) ...)
update: every frame (lambda (game dt) ...)
render: every frame, after scene draw (lambda (game) ...)

In the rest of this guide you will fill in create: (to build the scene) and update: (to move the player).

3. Core concepts

3.1. Creating a scene

A scene is the container for everything the engine draws and simulates: a list of entities, an optional tilemap, a camera, and a background color. You build one with make-scene or — for sprite-only games like ours — with the simpler make-sprite-scene, which skips the tilemap fields.

In the create: hook you typically build a scene and hand it to the game with game-scene-set!:

(import scheme
        (chicken base)
        (downstroke engine)
        (downstroke world)
        (downstroke scene-loader))

(game-run!
 (make-game
  title:  "Blue Window"
  width:  320 height: 240
  create: (lambda (game)
            (game-scene-set! game
              (make-sprite-scene
                entities:   '()
                background: '(20 22 30))))))

The background: argument is a list of three or four integers (r g b) or (r g b a) in the 0–255 range. The engine clears the framebuffer with this color at the top of every frame. If you omit it, the background is plain black.

make-sprite-scene also accepts camera:, camera-target:, tileset:, tileset-texture:, and engine-update:, but you do not need any of them for the guide.

3.2. Adding an entity

In Downstroke an entity is just an alist — a list of (key . value) pairs — with a small set of conventional keyword keys. There are no classes and no inheritance; an entity is data you can read with entity-ref and transform with entity-set.

It is common to write entities in plist form (alternating keys and values) for readability and then convert them with plist->alist from the list-utils egg:

(import (only (list-utils alist) plist->alist))

(define (make-player)
  (plist->alist
   (list #:type   'player
         #:x      150 #:y 100
         #:width  32  #:height 32
         #:color  '(100 160 255))))

The keys in use here are the conventional ones the engine understands:

  • #:type — a symbol you use to distinguish entities; purely for your own bookkeeping.
  • #:x, #:y — pixel position of the entity's top-left corner.
  • #:width, #:height — pixel size of the entity's bounding box.
  • #:color — an (r g b) or (r g b a) list. When an entity has no #:tile-id (or the scene has no tileset texture), the renderer fills the entity's rectangle with this color. That is exactly what we want for our guide: no sprite sheet, just a colored box.

Once you have a player factory, drop one into the scene's entities: list:

(make-sprite-scene
  entities:   (list (make-player))
  background: '(20 22 30))

Compile and run, and you should see a light-blue 32×32 square on a dark background. It does not move yet — that is the next step.

3.3. Reading input

Input in Downstroke is organised around actions rather than raw keys. The engine ships with a default input config (*default-input-config* in (downstroke input)) that maps the arrow keys, WASD, a couple of action buttons, and game-controller buttons to a small set of action symbols:

Action Default keys
up Up, W
down Down, S
left Left, A
right Right, D
a J, Z
b K, X
start Return
select (controller only)
quit Escape

Inside update: you reach the input state with (game-input game), and then query it with three predicates from (downstroke input):

  • (input-held? input 'left)#t while the player holds the action down.
  • (input-pressed? input 'a)#t only on the first frame the action goes down (edge).
  • (input-released? input 'a)#t only on the frame the action goes up.

input-held? is what we want for continuous movement:

(define +speed+ 2)

(define (input-dx input)
  (cond ((input-held? input 'left)  (- +speed+))
        ((input-held? input 'right)    +speed+)
        (else 0)))

(define (input-dy input)
  (cond ((input-held? input 'up)    (- +speed+))
        ((input-held? input 'down)     +speed+)
        (else 0)))

+speed+ is pixels per frame. At the engine's default 16 ms frame delay that works out to roughly 120 pixels per second; feel free to adjust.

The quit action is already wired into the main loop: pressing Escape ends the game, so you do not need to handle it yourself.

3.4. Updating entity state

Entities are immutable. entity-set does not mutate; it returns a new alist with the key updated. To move the player, read its current position, compute the new position, and produce a new entity:

(define (move-player player input)
  (let* ((x  (entity-ref player #:x 0))
         (y  (entity-ref player #:y 0))
         (dx (input-dx input))
         (dy (input-dy input)))
    (entity-set (entity-set player #:x (+ x dx)) #:y (+ y dy))))

entity-ref takes an optional default (0 above), returned if the key is absent.

Once you have a new entity, you also need a new scene that contains it, because scenes are immutable too. update-scene is the copier: pass it an existing scene and any fields you want to change.

(update-scene scene
  entities: (list (move-player player input)))

Finally, install the new scene on the game with game-scene-set!. Putting it all together in the update: hook:

update: (lambda (game dt)
          (let* ((scene  (game-scene game))
                 (input  (game-input game))
                 (player (car (scene-entities scene))))
            (game-scene-set! game
              (update-scene scene
                entities: (list (move-player player input))))))

A note about dt: the update hook is called with the number of milliseconds elapsed since the previous frame. We ignore it here because we move by a fixed number of pixels per frame, but most real games use dt to make motion frame-rate independent — see physics.org for the engine's built-in pipeline which does this for you.

4. Putting it together

Here is the full demo/getting-started.scm source. Read it top to bottom — each piece should now look familiar.

(import scheme
        (chicken base)
        (only (list-utils alist) plist->alist)
        (downstroke engine)
        (downstroke world)
        (downstroke entity)
        (downstroke input)
        (downstroke scene-loader))

(define +speed+ 2)

(define (make-player)
  (plist->alist
   (list #:type   'player
         #:x      150 #:y 100
         #:width  32  #:height 32
         #:color  '(100 160 255))))

(define (move-player player input)
  (let* ((x  (entity-ref player #:x 0))
         (y  (entity-ref player #:y 0))
         (dx (cond ((input-held? input 'left)  (- +speed+))
                   ((input-held? input 'right)    +speed+)
                   (else 0)))
         (dy (cond ((input-held? input 'up)    (- +speed+))
                   ((input-held? input 'down)     +speed+)
                   (else 0))))
    (entity-set (entity-set player #:x (+ x dx)) #:y (+ y dy))))

(game-run!
 (make-game
  title:  "Getting Started"
  width:  320 height: 240

  create: (lambda (game)
            (game-scene-set! game
              (make-sprite-scene
                entities:   (list (make-player))
                background: '(20 22 30))))

  update: (lambda (game dt)
            (let* ((scene  (game-scene game))
                   (input  (game-input game))
                   (player (car (scene-entities scene))))
              (game-scene-set! game
                (update-scene scene
                  entities: (list (move-player player input))))))))

This file ships with the engine. To run it, build the project and launch the demo binary:

Getting started demo — run with bin/demo-getting-started, source in demo/getting-started.scm.

From the Downstroke source tree, make will compile the engine and make demos will build every demo executable under bin/. When the binary starts you should see a 320×240 window with a blue square you can push around with the arrow keys (or WASD). Escape quits.

A few things to notice in the final code:

  • We never mutate anything. move-player returns a new entity; update-scene returns a new scene; game-scene-set! swaps the scene on the game struct for the next frame.
  • Every frame, update: rebuilds the entire entities list from scratch. That is fine for a one-entity demo and scales happily into the dozens; for larger games the engine's built-in physics pipeline (see physics.org) does the same work for you using keyword conventions like #:vx and #:vy.
  • The engine's default engine update — the physics pipeline — still runs before your update: hook. It is a no-op for our player because we never set velocity keys like #:vx or #:vy, so the only thing moving the square is your own move-player. The moment you set #:vx to a non-zero number, the engine will start applying it for you.

5. Where to next

Once getting-started runs, the rest of Downstroke is a menu of subsystems you can opt into piece by piece. Every topic below has its own doc file and at least one matching demo:

  • entities.org — the entity model, the list of conventional keys, and how to build reusable prefabs.
  • physics.org — the built-in frame pipeline: gravity, velocity, tile collisions, ground detection, and entity-vs-entity collisions.
  • scenes.org — scenes, cameras, camera follow, and switching between named game states.
  • rendering.org — how the renderer draws sprites, solid-color rects, and bitmap sprite fonts.
  • animation.org — frame-based sprite animation driven by #:animations and #:anim-name.
  • input.org — customising the action list, keyboard map, and game-controller bindings.
  • tweens.org — declarative easing for position, scale, color, and other numeric keys.
  • audio.org — loading and playing sound effects and music.

And the full set of demos that ship with the repository, each built by make demos:

  • Getting started (bin/demo-getting-started) — arrow keys move a blue square; the starting point for this guide.
  • Platformer (bin/demo-platformer) — gravity, jumping, tile collisions, frame animation.
  • Shmup (bin/demo-shmup) — top-down shooter-style movement and firing.
  • Topdown (bin/demo-topdown) — four-direction movement without gravity.
  • Audio (bin/demo-audio) — background music and sound effects.
  • Sandbox (bin/demo-sandbox) — group prefabs composed from mixins.
  • Sprite font (bin/demo-spritefont) — bitmap font rendering.
  • Menu (bin/demo-menu) — a simple UI menu.
  • Tweens (bin/demo-tweens) — side-by-side comparison of easing functions.
  • Scaling (bin/demo-scaling) — logical-resolution scaling via scale:.
  • 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.

Author: Downstroke Project

Created: 2026-04-20 Mon 15:22

Validate