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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
|
#+TITLE: Scenes, Cameras, and Game States
#+AUTHOR: Downstroke Project
* Overview
A /scene/ in Downstroke is the immutable record that answers the question "what should the engine simulate and draw right now?". It bundles the entity list, an optional tilemap (for TMX-backed levels), an optional tileset (for sprite-only games that still want tile-sheet rendering), a camera, a background clear color, and a choice of which physics pipeline to run. Each frame the engine reads the current scene from the ~game~ struct, runs an engine-update pass on its entities, lets your ~update:~ hook produce a /new/ scene, snaps the camera to a tagged target if asked, and then hands the new scene to the renderer.
Everything about scenes is built around swapping one immutable value for another: constructors like ~make-scene~ and ~make-sprite-scene~ build them; accessors like ~scene-entities~ and ~scene-camera~ read them; functional transformers like ~scene-map-entities~ and ~scene-add-entity~ return new scenes; and ~game-scene-set!~ is the single point where the engine sees the new state. If you also want more than one "mode" — a title screen and a play screen, say, or a paused state and a gameplay state — Downstroke layers a lightweight /game state/ system on top, so each state gets its own update and render hooks while sharing the same scene or building its own on demand.
* The minimum you need
The smallest scene is a sprite-only one with a single entity, built inside the ~create:~ hook:
#+begin_src scheme
(import scheme
(chicken base)
(only (list-utils alist) plist->alist)
downstroke-engine
downstroke-world
downstroke-entity
downstroke-scene-loader)
(define (make-player)
(plist->alist
(list #:type 'player #:x 100 #:y 100
#:width 32 #:height 32
#:color '(100 160 255))))
(game-run!
(make-game
title: "Minimal Scene"
width: 320 height: 240
create: (lambda (game)
(game-scene-set! game
(make-sprite-scene
entities: (list (make-player))
background: '(20 22 30))))))
#+end_src
That is all that is required to put a blue square on a dark background. ~make-sprite-scene~ fills in sensible defaults for everything you have not specified — no tilemap, a camera at ~(0, 0)~, no camera target, the default (physics) engine update — and ~game-scene-set!~ installs the result on the game. From here on, every section in this document describes a field you can fill in or a function you can compose to reshape what is on screen.
* Core concepts
** The scene record
~scene~ is a record type defined with ~defstruct~ in ~downstroke-world~. Its fields map one-to-one onto the things the engine needs to know about the current frame:
| Field | Type | Purpose |
|-------------------+--------------------------------+--------------------------------------------------------------------------------------------------|
| ~entities~ | list of entity alists | Every entity the engine simulates and draws this frame. |
| ~tilemap~ | ~tilemap~ struct or ~#f~ | Optional TMX-loaded tilemap (layers + embedded tileset). ~#f~ for sprite-only scenes. |
| ~tileset~ | ~tileset~ struct or ~#f~ | Optional tileset used for sprite rect math when ~tilemap~ is ~#f~. |
| ~camera~ | ~camera~ struct | The viewport offset (see /Cameras/ below). |
| ~tileset-texture~ | SDL2 texture or ~#f~ | GPU texture used to draw tiles and any entity with a ~#:tile-id~. ~#f~ is allowed (see note). |
| ~camera-target~ | symbol or ~#f~ | A tag the engine will auto-follow with the camera. ~#f~ disables auto-follow. |
| ~background~ | ~#f~ or ~(r g b)~ / ~(r g b a)~ | Framebuffer clear color. ~#f~ means plain black. |
| ~engine-update~ | ~#f~, procedure, or ~'none~ | Per-scene physics pipeline choice (see below). |
The raw ~make-scene~ constructor is auto-generated by ~defstruct~ and accepts every field as a keyword argument. It does /not/ provide defaults — if you call ~make-scene~ directly you must pass every field, or wrap it with ~make-sprite-scene~ (below) which does the defaulting for you. The paired functional copier ~update-scene~ takes an existing scene and returns a new one with only the named fields replaced:
#+begin_src scheme
(update-scene scene
entities: (list new-player)
camera-target: 'player)
#+end_src
*** What each "optional" field actually means
- ~tilemap: #f~ — sprite-only scenes. No tilemap rendering, no tile collisions (the physics pipeline tile-collision steps no-op when there is no tilemap). Used by all the shmup, topdown, tween, scaling, and sandbox demos when they want full control over layout.
- ~tileset: #f~ — fine when the scene has a ~tilemap~ (the renderer falls back to ~(tilemap-tileset tilemap)~ for rect math) /or/ when no entity has a ~#:tile-id~. Set it explicitly only when you have sprites but no tilemap and still want tile-sheet rendering (the ~animation~ and ~sandbox~ demos do this).
- ~tileset-texture: #f~ — allowed. The renderer's ~draw-entity~ guards on the texture being present and falls back to ~#:color~ rendering when it is missing, so sprite entities are /not/ silently dropped on ~#f~ — they draw as solid colored rectangles. Tilemap layers are simply skipped if either ~tilemap~ or ~tileset-texture~ is ~#f~.
- ~background: #f~ — the renderer clears the framebuffer with opaque black every frame. Any non-~#f~ value must be a list of at least three 0–255 integers; a four-element list includes alpha.
- ~engine-update:~ — three forms are accepted:
- ~#f~ (the default) means "inherit": the engine runs ~default-engine-update~, which is the built-in physics pipeline (tweens → acceleration → gravity → velocity → tile collisions → ground detection → entity collisions → group sync → animation).
- A procedure ~(lambda (game dt) ...)~ replaces the pipeline entirely for this scene. You are fully responsible for whatever transformations of ~(game-scene game)~ should happen each frame.
- The symbol ~'none~ disables engine-update altogether — nothing runs before your ~update:~ hook. Used by the shmup and scaling demos which hand-roll all motion.
- ~camera-target:~ — a symbol that will be looked up via ~scene-find-tagged~ in each entity's ~#:tags~ list. When set, the engine centers the camera on that entity on every frame and clamps the result to ~x ≥ 0~ and ~y ≥ 0~. ~#f~ disables auto-follow (you can still move ~scene-camera~ yourself).
All eight fields are plain slots: read them with ~scene-entities~, ~scene-tilemap~, ~scene-camera~, ~scene-camera-target~, ~scene-background~, ~scene-engine-update~, and so on.
** Constructing a scene
Downstroke offers three escalating ways to build a scene, from full manual control to "load a whole level off disk in one call".
*** ~make-scene~ — full control
The auto-generated constructor accepts every slot as a keyword and does no defaulting. You will use it when you want to build a scene by hand with some exotic combination of fields, most often because you are bypassing the physics pipeline or embedding a procedurally built tilemap:
#+begin_src scheme
(make-scene
entities: (list (make-player))
tilemap: #f
tileset: #f
camera: (make-camera x: 0 y: 0)
tileset-texture: #f
camera-target: #f
engine-update: 'none)
#+end_src
This is the form the ~shmup~ and ~scaling~ demos use to turn off the physics pipeline and drive everything from their own ~update:~ hook. If you omit a field it will be unbound on the struct; prefer ~make-sprite-scene~ below unless you actually need that degree of control.
*** ~make-sprite-scene~ — convenient sprite-only scenes
~make-sprite-scene~ lives in ~downstroke-scene-loader~ and wraps ~make-scene~ with sensible defaults for sprite-only games:
#+begin_src scheme
(make-sprite-scene
entities: (list (make-player))
background: '(20 22 30))
#+end_src
It always passes ~tilemap: #f~, and supplies these defaults for anything you leave out:
| Keyword | Default |
|--------------------+-----------------------------|
| ~entities:~ | ~'()~ |
| ~tileset:~ | ~#f~ |
| ~tileset-texture:~ | ~#f~ |
| ~camera:~ | ~(make-camera x: 0 y: 0)~ |
| ~camera-target:~ | ~#f~ |
| ~background:~ | ~#f~ |
| ~engine-update:~ | ~#f~ (inherit = run physics) |
Use this whenever your game has no TMX map — see ~demo/getting-started.scm~ for the canonical example.
*** ~game-load-scene!~ — load a TMX level
When you have a Tiled (TMX) map on disk, ~game-load-scene!~ wires up the entire pipeline in one call:
#+begin_src scheme
(game-load-scene! game "demo/assets/level-0.tmx")
#+end_src
Internally it performs four steps:
1. Calls ~game-load-tilemap!~ which invokes ~load-tilemap~ to parse the TMX (via expat) and the linked TSX tileset, also loading the tileset's PNG image. The resulting ~tilemap~ struct is stored in the game's asset registry under the key ~'tilemap~.
2. Calls ~create-tileset-texture~ (a thin wrapper around ~create-texture-from-tileset~) to upload the tileset image as an SDL2 texture via the game's renderer.
3. Builds a ~scene~ with ~entities: '()~, the parsed ~tilemap~, the fresh tileset texture, a camera at the origin, and no camera target.
4. Installs the scene on the game with ~game-scene-set!~ and returns it.
Because it returns the new scene, you can chain additional transformations before the frame begins (see the ~topdown~ and ~platformer~ demos):
#+begin_src scheme
(let* ((s0 (game-load-scene! game "demo/assets/level-0.tmx"))
(s1 (scene-add-entity s0 (make-player)))
(s2 (update-scene s1 camera-target: 'player)))
(game-scene-set! game s2))
#+end_src
Note that ~game-load-scene!~ does /not/ automatically populate ~entities~ from the TMX object layer. That conversion is available as a separate function:
#+begin_src scheme
(tilemap-objects->entities tilemap registry)
#+end_src
~tilemap-objects->entities~ walks the parsed ~tilemap-objects~ and, for each TMX ~<object>~ whose ~type~ string matches a prefab in your registry, calls ~instantiate-prefab~ with the object's ~(x, y, width, height)~. Objects with no matching prefab are filtered out. You can call it yourself to build the initial entity list from the TMX objects and then feed the result to ~scene-add-entity~ or a fresh ~update-scene~.
*** Related asset loaders
Three smaller helpers register assets into ~(game-assets game)~ keyed by a symbol, so other code can fetch them with ~game-asset~:
- ~(game-load-tilemap! game key filename)~ — parse a ~.tmx~ and store the ~tilemap~ struct.
- ~(game-load-tileset! game key filename)~ — parse a ~.tsx~ and store the ~tileset~ struct.
- ~(game-load-font! game key filename size)~ — open a TTF font at a given point size and store it.
Each returns the loaded value so you can bind it directly:
#+begin_src scheme
(let* ((ts (game-load-tileset! game 'tileset "demo/assets/monochrome_transparent.tsx"))
(tex (create-texture-from-tileset (game-renderer game) ts)))
...)
#+end_src
This pattern — load once in ~preload:~ (or early in ~create:~), retrieve many times via ~game-asset~ — is how the ~animation~ and ~sandbox~ demos share one tileset across multiple prefabs and scenes.
** Manipulating scene entities
All scene transformers in ~downstroke-world~ are /functional/: they take a scene and return a new one. Nothing is mutated; the idiomatic pattern inside an ~update:~ hook is always
#+begin_src scheme
(game-scene-set! game (<transformer> (game-scene game) ...))
#+end_src
or, when composing multiple transformations, a ~chain~ form from ~srfi-197~.
*** ~scene-add-entity~
~(scene-add-entity scene entity)~ appends one entity to the entity list. Use it after ~game-load-scene!~ to drop the player into a freshly loaded TMX level:
#+begin_src scheme
(chain (game-load-scene! game "demo/assets/level-0.tmx")
(scene-add-entity _ (make-player))
(update-scene _ camera-target: 'player))
#+end_src
*** ~scene-map-entities~
~(scene-map-entities scene proc ...)~ applies each ~proc~ in sequence to every entity in the scene. Each ~proc~ has the signature ~(lambda (scene entity) ...) → entity~ — it receives the /current/ scene (read-only, snapshot at the start of the call) and one entity, and must return a replacement entity. Multiple procs are applied like successive ~map~ passes, in argument order.
This is the workhorse of the physics pipeline; see ~default-engine-update~ in ~downstroke-engine~ for how it is chained to apply acceleration, gravity, velocity, and so on. In game code you use it for per-entity updates that do not need to see each other:
#+begin_src scheme
(scene-map-entities scene
(lambda (scene_ e)
(if (eq? (entity-type e) 'demo-bot)
(update-demo-bot e dt)
e)))
#+end_src
*** ~scene-filter-entities~
~(scene-filter-entities scene pred)~ keeps only entities for which ~pred~ returns truthy. Use it to remove dead/expired/off-screen entities each frame:
#+begin_src scheme
(scene-filter-entities scene
(lambda (e) (or (eq? (entity-type e) 'player) (in-bounds? e))))
#+end_src
*** ~scene-transform-entities~
~(scene-transform-entities scene proc)~ hands /the whole entity list/ to ~proc~ and expects a replacement list back. Use this when an operation needs to see all entities at once — resolving pairwise entity collisions, for instance, or synchronising grouped entities to their origin:
#+begin_src scheme
(scene-transform-entities scene resolve-entity-collisions)
(scene-transform-entities scene sync-groups)
#+end_src
The engine's default pipeline uses ~scene-transform-entities~ for exactly these two steps, because they are inherently N-to-N.
*** Tagged lookups
Two helpers let you find entities by tag without iterating yourself:
- ~(scene-find-tagged scene tag)~ returns the /first/ entity whose ~#:tags~ list contains ~tag~, or ~#f~.
- ~(scene-find-all-tagged scene tag)~ returns every such entity as a list.
Both are O(n) scans and intended for coarse queries — finding the player, all enemies of a type, the camera target — not for every-frame loops over hundreds of entities. The engine itself uses ~scene-find-tagged~ internally to resolve ~camera-target~.
*** The update idiom
Putting it together, the typical shape of an ~update:~ hook is either a single transformer:
#+begin_src scheme
update: (lambda (game dt)
(let* ((input (game-input game))
(scene (game-scene game))
(player (update-player (car (scene-entities scene)) input)))
(game-scene-set! game
(update-scene scene entities: (list player)))))
#+end_src
or a chain of them, when multiple pipelines compose:
#+begin_src scheme
(game-scene-set! game
(chain (update-scene scene entities: all)
(scene-map-entities _
(lambda (scene_ e) (if (eq? (entity-type e) 'player) e (move-projectile e))))
(scene-remove-dead _)
(scene-filter-entities _ in-bounds?)))
#+end_src
In both shapes, ~game-scene-set!~ is called exactly once per frame with the final scene. That single write is what the next frame's engine-update and renderer read from.
** Cameras
A /camera/ is a tiny record with two integer slots, ~x~ and ~y~, holding the top-left pixel coordinate of the viewport in world space. ~make-camera~ is the constructor:
#+begin_src scheme
(make-camera x: 0 y: 0)
#+end_src
The renderer subtracts ~camera-x~ and ~camera-y~ from every sprite and tile's world position before drawing, so moving the camera east visually scrolls everything west.
*** ~camera-follow~
~(camera-follow camera entity viewport-w viewport-h)~ returns a /new/ camera struct centered on the entity, clamped so neither ~x~ nor ~y~ goes below zero. It floors both results to integers (SDL2 wants integer rects). You can call it directly from an ~update:~ hook — for example to manually follow an entity without tagging it — but the engine provides an auto-follow mechanism built on top of it.
*** Auto-follow via ~camera-target:~
If a scene has ~camera-target:~ set to a symbol, the engine runs ~update-camera-follow~ /after/ your ~update:~ hook and /before/ rendering. That function looks up the first entity whose ~#:tags~ list contains the target symbol via ~scene-find-tagged~. If found, it replaces the scene's camera with one centered on that entity (using the game's ~width~ and ~height~ as the viewport size); if no tagged entity is found, the camera is left alone. The clamp to ~x ≥ 0~ and ~y ≥ 0~ is inherited from ~camera-follow~, so the camera never reveals negative world coordinates.
To opt in, just tag the entity you want followed and set ~camera-target:~ to that tag:
#+begin_src scheme
(define (make-player)
(plist->alist
(list #:type 'player
#:x 100 #:y 50
#:tags '(player)
...)))
(update-scene scene camera-target: 'player)
#+end_src
Subsequent frames will keep the camera locked to the player. To stop following, set ~camera-target:~ back to ~#f~ (you keep whatever camera position was most recent).
If you need to animate the camera yourself (shake, parallax, cutscenes), leave ~camera-target:~ at ~#f~ and edit ~scene-camera~ via ~update-scene~ from inside your own update. The engine will not override what you write so long as no target is set.
** Named game states
Many games have more than one "mode": a title menu, a playing state, a game-over screen, maybe a pause overlay. Downstroke's game-state system is a lightweight way to switch update and render hooks between modes without having to build a single mega-update.
A game state is just an alist of lifecycle hooks, built with ~make-game-state~:
#+begin_src scheme
(make-game-state #:create main-menu-create
#:update main-menu-update
#:render main-menu-render)
#+end_src
Each of ~#:create~, ~#:update~, and ~#:render~ is optional (~#f~ by default). The engine uses them as follows:
- ~#:create~ is called by ~game-start-state!~ when the state becomes active. Use it to lazily build the scene for that state (e.g. build the level the first time the player picks "Play").
- ~#:update~, if present, /replaces/ the game's top-level ~update-hook~ while this state is active. The engine's ~resolve-hooks~ prefers the state hook and falls back to the game-level hook when the state has no ~#:update~.
- ~#:render~ is handled the same way: state-level wins when present, otherwise the game-level ~render-hook~ runs.
Registration is done by name with ~game-add-state!~:
#+begin_src scheme
(game-add-state! game 'main-menu
(make-game-state #:update main-menu-update
#:render main-menu-render))
(game-add-state! game 'playing
(make-game-state #:update playing-update
#:render playing-render))
#+end_src
And transitions happen through ~game-start-state!~, which sets the active state and runs the new state's ~#:create~ hook if present:
#+begin_src scheme
(game-start-state! game 'main-menu)
...
(when (input-pressed? input 'a)
(game-start-state! game 'playing))
#+end_src
Only ~update~ and ~render~ are state-scoped; ~preload~ and ~create~ at the ~make-game~ level still fire once each at boot. The engine's built-in engine-update (the physics pipeline) still runs whenever a scene exists, regardless of which state is active — states only swap which user hook drives the frame.
* Common patterns
** Sprite-only scene with no tilemap
The default choice for small demos and menus: one ~make-sprite-scene~ call, no tilemap, a plain background color.
*Getting started demo* — run with ~bin/demo-getting-started~, source in ~demo/getting-started.scm~. A blue square you push around the screen with the arrow keys; canonical minimal sprite scene.
*Tweens demo* — run with ~bin/demo-tweens~, source in ~demo/tweens.scm~. A grid of easing curves applied to moving boxes; also sprite-only.
** TMX-loaded scene with a player prefab
Load the level off disk, append the player, set up camera follow. One chain per create hook.
*Platformer demo* — run with ~bin/demo-platformer~, source in ~demo/platformer.scm~. Uses ~game-load-scene!~ for the TMX map and then ~scene-add-entity~ and ~update-scene~ to attach the player and enable camera follow.
*Topdown demo* — run with ~bin/demo-topdown~, source in ~demo/topdown.scm~. Same pattern, with ~#:gravity? #f~ on the player so the default engine-update gives four-direction motion.
** Camera following a tagged entity
Tag the entity you want the viewport to track, set ~camera-target:~ to the tag, let the engine take care of the rest:
#+begin_src scheme
(define (make-player)
(plist->alist
(list #:type 'player
#:x 100 #:y 50
#:width 16 #:height 16
#:tags '(player)
...)))
;; In create:
(chain (game-load-scene! game "demo/assets/level-0.tmx")
(scene-add-entity _ (make-player))
(update-scene _ camera-target: 'player)
(game-scene-set! game _))
#+end_src
Any entity carrying the right tag will be followed, and the camera clamps to non-negative world coordinates every frame. The ~platformer~ and ~topdown~ demos both use ~camera-target: 'player~ exactly this way.
** Switching between a title screen and a play state
Register two named states in ~create:~, each with its own update and render hooks, and kick off the first one. Transitions happen anywhere you have a ~game~ handle, including from inside another state's update.
*Menu demo* — run with ~bin/demo-menu~, source in ~demo/menu.scm~. Registers ~main-menu~ and ~playing~ states, switches to ~playing~ when the player selects the menu entry, and switches back on ~Escape~.
#+begin_src scheme
create: (lambda (game)
(game-add-state! game 'main-menu
(make-game-state #:update main-menu-update
#:render main-menu-render))
(game-add-state! game 'playing
(make-game-state #:update playing-update
#:render playing-render))
(game-start-state! game 'main-menu))
#+end_src
Each state owns its render surface — in the menu demo both states clear the screen themselves, since there is no scene installed on the game — but nothing stops a state from sharing a scene: the menu state's ~#:create~ could build it, and the play state's ~#:update~ could simply read and transform ~(game-scene game)~.
** Loading multiple assets up front
The ~preload:~ hook is the one place guaranteed to run before the first scene is built, after SDL2 has opened a renderer. Use it to warm up the asset registry so ~create:~ and ~update:~ never block on I/O:
#+begin_src scheme
preload: (lambda (game)
(game-load-tileset! game 'tiles "demo/assets/monochrome_transparent.tsx")
(game-load-font! game 'title "demo/assets/DejaVuSans.ttf" 24)
(game-load-font! game 'body "demo/assets/DejaVuSans.ttf" 14)
(init-audio!)
(load-sounds! '((jump . "demo/assets/jump.wav")
(coin . "demo/assets/coin.wav"))))
#+end_src
Inside ~create:~ or ~update:~ you read them back with ~game-asset~:
#+begin_src scheme
(let ((ts (game-asset game 'tiles))
(fnt (game-asset game 'title)))
...)
#+end_src
Because the registry is a plain hash table, you can store anything — parsed JSON, precomputed tables, your own structs — under any symbol key you like. The three ~game-load-*!~ helpers are there for the file formats the engine itself understands; everything else is ~game-asset-set!~ + ~game-asset~.
* See also
- [[file:guide.org][guide.org]] — step-by-step construction of the getting-started demo, including the first ~make-sprite-scene~ call.
- [[file:entities.org][entities.org]] — the entity alist, conventional keys like ~#:tags~ and ~#:type~, and how prefabs build entities that the scene-loader instantiates.
- [[file:physics.org][physics.org]] — what the default engine-update actually does, and how to skip or replace individual pipeline steps per entity.
- [[file:rendering.org][rendering.org]] — how ~render-scene!~ walks the scene to draw tiles, sprites, and fallback colored rects.
- [[file:animation.org][animation.org]] — the animation pipeline step, run by ~default-engine-update~ on every entity.
- [[file:tweens.org][tweens.org]] — per-entity declarative tweens, stepped as the first stage of the default pipeline.
- [[file:input.org][input.org]] — action mapping and polling, used inside ~update:~ hooks to drive entity changes.
- [[file:audio.org][audio.org]] — loading and playing sound effects alongside your scene-loading hooks.
|