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-gametakes 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)—#twhile the player holds the action down.(input-pressed? input 'a)—#tonly on the first frame the action goes down (edge).(input-released? input 'a)—#tonly 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-playerreturns a new entity;update-scenereturns a new scene;game-scene-set!swaps the scene on thegamestruct 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#:vxand#: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#:vxor#:vy, so the only thing moving the square is your ownmove-player. The moment you set#:vxto 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
#:animationsand#: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 viascale:. - 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.