1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
#+TITLE: Downstroke — Getting Started
#+AUTHOR: Downstroke Project
* Introduction
Downstroke is a 2D tile-driven game engine for Chicken Scheme, built on SDL2. It is inspired by Phaser 2: a minimal game is about 20 lines of Scheme.
The engine handles SDL2 initialization, the event loop, input, rendering, and physics. You provide lifecycle hooks to customize behavior. This guide walks you through building your first game with Downstroke.
* Installation
** System Dependencies
Downstroke requires the following system libraries:
- =SDL2=
- =SDL2_mixer=
- =SDL2_ttf=
- =SDL2_image=
- =expat=
On Debian/Ubuntu:
#+begin_src bash
sudo apt-get install libsdl2-dev libsdl2-mixer-dev libsdl2-ttf-dev libsdl2-image-dev libexpat1-dev
#+end_src
On macOS with Homebrew:
#+begin_src bash
brew install sdl2 sdl2_mixer sdl2_ttf sdl2_image expat
#+end_src
** Chicken Eggs
Install the Downstroke egg along with its dependencies:
#+begin_src bash
chicken-install downstroke sdl2 sdl2-image defstruct matchable states
#+end_src
* Hello World — Your First Game
Create a file called =mygame.scm=:
#+begin_src scheme
(import downstroke-engine)
(define *game*
(make-game title: "Hello World" width: 640 height: 480))
(game-run! *game*)
#+end_src
Build and run:
#+begin_src bash
csc mygame.scm -o mygame
./mygame
#+end_src
You should see a black window titled "Hello World". Press =Escape= or close the window to quit. The =game-run!= function handles SDL2 initialization, window creation, the event loop, and cleanup automatically. The engine also provides a default =quit= action (Escape key and window close button).
* Moving Square — First Entity
Now let's add an entity you can move with the keyboard. Create =square.scm=:
#+begin_src scheme
(import scheme
(chicken base)
(prefix sdl2 "sdl2:")
downstroke-engine
downstroke-world
downstroke-entity
downstroke-input)
(define *game*
(make-game
title: "Moving Square" width: 640 height: 480
create: (lambda (game)
(game-scene-set! game
(make-scene
entities: (list (list #:type 'box #:x 300 #:y 200
#:width 32 #:height 32
#:vx 0 #:vy 0))
tilemap: #f
camera: (make-camera x: 0 y: 0)
tileset-texture: #f)))
update: (lambda (game dt)
(let* ((input (game-input game))
(scene (game-scene game))
(box (car (scene-entities scene)))
;; Read input and update velocity
(box (entity-set box #:vx (cond ((input-held? input 'left) -3)
((input-held? input 'right) 3)
(else 0))))
(box (entity-set box #:vy (cond ((input-held? input 'up) -3)
((input-held? input 'down) 3)
(else 0))))
;; Apply velocity to position
(box (entity-set box #:x (+ (entity-ref box #:x 0)
(entity-ref box #:vx 0))))
(box (entity-set box #:y (+ (entity-ref box #:y 0)
(entity-ref box #:vy 0)))))
(scene-entities-set! scene (list box))))
render: (lambda (game)
(let* ((scene (game-scene game))
(box (car (scene-entities scene)))
(x (inexact->exact (floor (entity-ref box #:x 0))))
(y (inexact->exact (floor (entity-ref box #:y 0))))
(w (entity-ref box #:width 32))
(h (entity-ref box #:height 32)))
(sdl2:set-render-draw-color! (game-renderer game) 255 200 0 255)
(sdl2:render-fill-rect! (game-renderer game)
(sdl2:make-rect x y w h))))))
(game-run! *game*)
#+end_src
Run it:
#+begin_src bash
csc square.scm -o square
./square
#+end_src
Press arrow keys to move the yellow square around. Here are the key ideas:
- **Scenes**: =make-scene= creates a container for entities, tilemaps, and the camera. It holds the game state each frame.
- **Entities**: Entities are plists (property lists). They have no class; they're pure data. Access properties with =entity-ref=, and update with =entity-set= (which returns a *new* plist — always bind the result).
- **Input**: =input-held?= returns =#t= if an action is currently pressed. Actions are symbols like ='left=, ='right=, ='up=, ='down= (from the default input config).
- **Update & Render**: The =update:= hook runs first and updates entities. The =render:= hook runs after the default rendering pipeline and is used for custom drawing like this colored rectangle.
- **Rendering**: Since our tilemap is =#f=, the default renderer draws nothing; all drawing happens in the =render:= hook using SDL2 functions.
* Adding a Tilemap and Physics
For a real game, you probably want tilemaps, gravity, and collision detection. Downstroke includes a full physics pipeline. Here's the pattern:
#+begin_src scheme
(import scheme
(chicken base)
downstroke-engine
downstroke-world
downstroke-entity
downstroke-input
downstroke-physics
downstroke-scene-loader
downstroke-sound)
(define *game*
(make-game
title: "Platformer Demo" width: 600 height: 400
preload: (lambda (game)
;; Initialize audio and load sounds (optional)
(init-audio!)
(load-sounds! '((jump . "assets/jump.wav"))))
create: (lambda (game)
;; Load the tilemap from a TMX file (made with Tiled editor)
(let* ((scene (game-load-scene! game "assets/level.tmx"))
;; Create a player entity
(player (list #:type 'player
#:x 100 #:y 50
#:width 16 #:height 16
#:vx 0 #:vy 0
#:gravity? #t
#:on-ground? #f
#:tile-id 1)))
;; Add player to the scene
(scene-add-entity scene player)))
update: (lambda (game dt)
;; Typical pattern for platformer physics
(let* ((input (game-input game))
(scene (game-scene game))
(tm (scene-tilemap scene))
(player (car (scene-entities scene)))
;; Set horizontal velocity from input
(player (entity-set player #:vx
(cond
((input-held? input 'left) -3)
((input-held? input 'right) 3)
(else 0))))
;; Jump on button press if on ground
(_ (when (and (input-pressed? input 'a)
(entity-ref player #:on-ground? #f))
(play-sound 'jump)))
(player (apply-jump player (input-pressed? input 'a)))
;; Run physics pipeline
(player (apply-acceleration player))
(player (apply-gravity player))
(player (apply-velocity-x player))
(player (resolve-tile-collisions-x player tm))
(player (apply-velocity-y player))
(player (resolve-tile-collisions-y player tm))
(player (detect-ground player tm)))
;; Update camera to follow player
(let ((cam-x (max 0 (- (entity-ref player #:x 0) 300))))
(camera-x-set! (scene-camera scene) cam-x))
;; Replace entities in scene
(scene-entities-set! scene (list player))))))
(game-run! *game*)
#+end_src
Key points:
- **=game-load-scene!=** loads a TMX tilemap file (created with the Tiled editor), creates the tileset texture, and builds the scene. It returns the scene so you can add more entities.
- **=init-audio!=** and **=load-sounds!=** initialize the audio subsystem and load sound files. Call these in the =preload:= hook.
- **=play-sound=** plays a loaded sound effect.
- **Physics pipeline**: Functions like =apply-gravity=, =apply-velocity-x=, =resolve-tile-collisions-x= form the physics pipeline. Apply them in order each frame to get correct platformer behavior.
- **Tile collisions**: =resolve-tile-collisions-x= and =resolve-tile-collisions-y= snap entities to tile edges, preventing clipping.
- **Ground detection**: =detect-ground= sets the =#:on-ground?= flag so you know if a jump is valid.
See =demo/platformer.scm= in the engine source for a complete working example.
* Development Tips
** Debug Mode
During development, enable ~debug?: #t~ on ~make-game~ to visualize collision boxes and the tile grid. This helps you understand what the physics engine "sees" and debug collision problems:
#+begin_src scheme
(define my-game
(make-game
title: "My Game" width: 600 height: 400
debug?: #t)) ;; Enable collision visualization
(game-run! my-game)
#+end_src
With debug mode enabled, you'll see:
- **Purple outlines** around all non-zero tiles
- **Blue boxes** around player entities
- **Red boxes** around enemy entities
- **Green boxes** around active attack hitboxes
This is invaluable for tuning collision geometry and understanding why entities are clipping or not colliding as expected.
* Demo Overview
Downstroke includes several demo games that showcase different features:
| Demo | File | What it shows |
|------|------|--------------|
| Platformer | =demo/platformer.scm= | Gravity, jump, tile collision, camera follow, sound effects |
| Top-down | =demo/topdown.scm= | 8-directional movement, no gravity, tilemap, camera follow |
| Physics Sandbox | =demo/sandbox.scm= | Entity-entity collision, multi-entity physics, auto-respawn |
| Shoot-em-up | =demo/shmup.scm= | No tilemap, entity spawning/removal, manual AABB collision |
| Audio | =demo/audio.scm= | Sound effects, music toggle, text rendering |
| Sprite Font | =demo/spritefont.scm= | Bitmap text rendering using non-contiguous tileset ranges |
| Menu | =demo/menu.scm= | State machine menus, keyboard navigation, TTF text rendering |
| Tweens | =demo/tweens.scm= | Easing curves, =tween-step=, =#:skip-pipelines= with tile collision |
Each demo is self-contained and serves as a working reference for a particular game mechanic.
* Build & Run
If you cloned the downstroke source, you can build everything:
#+begin_src bash
cd /path/to/downstroke
make # Compile all engine modules
make demos # Build all demo executables
./bin/demo-platformer
./bin/demo-topdown
# etc.
#+end_src
* Next Steps
You now know how to:
- Create a game with =make-game= and =game-run!=
- Add entities to scenes
- Read input with =input-held?= and =input-pressed?=
- Apply physics to entities
- Load tilemaps with =game-load-scene!=
- Play sounds with =load-sounds!= and =play-sound=
For more details:
- **Full API reference**: See =docs/api.org= for all functions and keyword arguments.
- **Entity model**: See =docs/entities.org= to learn about plist keys, tags, prefabs, and mixins.
- **Physics pipeline**: See =docs/physics.org= for the full physics specification and collision model.
- **Tweens**: See =docs/tweens.org= for time-based property interpolation and combining tweens with physics.
Happy coding!
|