aboutsummaryrefslogtreecommitdiff
path: root/docs/guide.org
blob: 21a70bcce6ca72a6472c62cc861ed908ed4fdeca (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
#+TITLE: Getting Started with Downstroke
#+AUTHOR: Downstroke Project

* 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.

* 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:

#+begin_src scheme
(import scheme
        (chicken base)
        (downstroke engine))

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

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).

* Core concepts

** 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!~:

#+begin_src scheme
(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))))))
#+end_src

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.

** 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:

#+begin_src scheme
(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))))
#+end_src

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:

#+begin_src scheme
(make-sprite-scene
  entities:   (list (make-player))
  background: '(20 22 30))
#+end_src

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.

** 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:

#+begin_src scheme
(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)))
#+end_src

~+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.

** 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:

#+begin_src scheme
(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))))
#+end_src

~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.

#+begin_src scheme
(update-scene scene
  entities: (list (move-player player input)))
#+end_src

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

#+begin_src scheme
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))))))
#+end_src

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 [[file:physics.org][physics.org]] for the engine's built-in pipeline which does this for you.

* Putting it together

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

#+begin_src scheme
(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))))))))
#+end_src

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 [[file:physics.org][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.

* 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:

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

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