aboutsummaryrefslogtreecommitdiff
path: root/docs/guide.org
blob: 1c5a8f3888948a783817d317522087c2b14e1edc (plain)
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#+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. Optional =background:= ~(r g b)~ or ~(r g b a)~ sets the color used to clear the window each frame (default is black).
- **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-on-solid   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.
- **On-solid check**: =detect-on-solid= sets the =#:on-ground?= flag from tiles below the feet and, if you pass other scene entities, from standing on solids (moving platforms, crates). Call it after collisions; use the flag next frame to gate jumps. (Despite the =?=-suffix, it returns an updated entity, not a boolean.)

See =demo/platformer.scm= in the engine source for a complete working example.

* Development Tips

** Pixel Scaling

Retro-style games often use a small logical resolution (e.g. 320×240) but need a larger window so players can actually see things. Use ~scale:~ to scale everything uniformly:

#+begin_src scheme
(make-game title: "Pixel Art" width: 320 height: 240 scale: 2)
;; Creates a 640×480 window; all coordinates remain in 320×240 space
#+end_src

This is integer-only scaling. All rendering — tiles, sprites, text, debug overlays — is scaled automatically by SDL2. Game logic and coordinates are unaffected.

See =demo/scaling.scm= for a complete example.

** Fullscreen

Use SDL2 window flags in the ~preload:~ hook to go fullscreen:

#+begin_src scheme
(make-game
  title: "Fullscreen Game" width: 320 height: 240 scale: 2
  preload: (lambda (game)
    (sdl2:window-fullscreen-set! (game-window game) 'fullscreen-desktop)))
#+end_src

~'fullscreen-desktop~ fills the screen without changing display resolution; SDL2 handles letterboxing and scaling. ~'fullscreen~ uses exclusive fullscreen at the window size.

** 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 |
| Scaling | =demo/scaling.scm= | Integer pixel scaling with =scale: 2=, 2× upscaled rendering |

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!