aboutsummaryrefslogtreecommitdiff
path: root/docs/rendering.org
blob: 35d69bbb853785c213f603341fa6ab01b764aaac (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
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
433
434
435
436
437
438
439
440
#+TITLE: Rendering

Downstroke draws every frame in two steps: the engine calls
=render-scene!= on the current scene, then (if you supplied one) it
calls your =render:= hook for HUDs, text, and overlays. =render-scene!=
is deliberately conservative — it draws the tilemap if there is one,
then each entity in turn. An entity gets a tileset sprite when it
carries =#:tile-id= /and/ a tileset texture is available, a filled
rect when it carries =#:color=, and is silently skipped otherwise.
The renderer never crashes on a missing tilemap or tileset texture;
it simply does less drawing.

This document covers the default rendering pipeline, the scaling /
logical-resolution system, sprite fonts (bitmap text), and the hooks
you can use to layer custom rendering on top — including the built-in
debug overlay.

See also: [[file:guide.org][guide]], [[file:entities.org][entities]],
[[file:scenes.org][scenes]], [[file:animation.org][animation]],
[[file:input.org][input]].

* The minimum you need

The simplest possible renderable entity is a colored rectangle with a
position and size:

#+begin_src scheme
(plist->alist
  (list #:type  'player
        #:x 150 #:y 100
        #:width 32 #:height 32
        #:color '(100 160 255)))    ;; RGB; alpha defaults to 255
#+end_src

Put it in a scene (=make-sprite-scene= needs no TMX map and no
tileset), and you're done:

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

The engine clears the frame with =scene-background= (black if =#f=),
draws every entity in =scene-entities=, and presents. No =render:=
hook is required for this to show something on screen.

When you want sprites instead of colored rects, an entity looks like:

#+begin_src scheme
(list #:type    'player
      #:x 100 #:y 50
      #:width 16 #:height 16
      #:tile-id 29        ;; tileset frame index
      #:facing  1         ;; -1 flips the sprite horizontally
      #:tags    '(player))
#+end_src

…and the scene must carry a tileset /and/ a tileset texture. The
simplest way is to load a TMX map with
=game-load-scene!=, which wires both up for you.

/Full example: the *getting-started demo* — run with
=bin/demo-getting-started=, source in =demo/getting-started.scm=./

* Core concepts

** What =render-scene!= does

=render-scene!= is the one function =game-run!= calls each frame to
draw the current scene. It does three things, in order:

1. Clear the framebuffer using =scene-background= (via
   =renderer-set-clear-color!=). A missing or non-list background
   falls back to opaque black.
2. Draw the tilemap, if /both/ a =tilemap= and a =tileset-texture=
   are present on the scene. A missing tilemap or missing texture
   simply skips this step — no error.
3. Draw each entity in =scene-entities= via =draw-entity=.

=draw-entity= applies a strict priority when deciding /how/ to draw
one entity:

1. If =#:skip-render= is truthy, the entity is skipped entirely.
2. Otherwise, if the entity has a =#:tile-id= /and/ the scene
   provides both a tileset and a tileset texture, it is drawn as a
   tileset sprite.
3. Otherwise, if the entity has a =#:color= list of three or four
   integers =(r g b)= / =(r g b a)=, it is drawn as a filled rect.
4. Otherwise, nothing is drawn for that entity.

The =#:facing= key controls horizontal flip on sprite-drawn entities:
=#:facing -1= renders the tile mirrored, any other value renders it
unflipped. =#:color= entities ignore =#:facing=.

*** Key invariant: both =tileset= /and/ =tileset-texture=

=draw-entity= needs /both/ a =tileset= struct (so it can look up the
source rectangle for a given =tile-id=) /and/ an SDL texture (so it
can blit pixels) before it will draw a sprite. If either is missing,
the entity falls back to =#:color= — or is skipped if that's also
absent. It never crashes.

Because of this, =render-scene!= resolves the tileset from /either/
source:

#+begin_src scheme
(or (scene-tileset scene)
    (and (scene-tilemap scene)
         (tilemap-tileset (scene-tilemap scene))))
#+end_src

That means a sprite-only scene (one with =tilemap: #f=) must set
=tileset:= explicitly on the scene — not on the tilemap, since there
isn't one. =make-sprite-scene= exposes that as a keyword. See the
sprite-only pattern below.

Entity order in =scene-entities= is the draw order — later entries
render on top of earlier ones. There is no z-buffer and no layer
system for entities; if you need one, sort the entity list yourself
before drawing (typically in your =update:= hook).

** Backgrounds, logical resolution, scaling

*** Background color

=scene-background= is either =#f= or a list of three or four
integers. The engine reads it every frame and uses it as the
framebuffer clear color. Missing or malformed backgrounds fall back
to opaque black. This is the simplest way to paint a flat backdrop
without adding an entity.

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

*** Scale factor

=make-game= accepts a =scale:= keyword argument: a /positive integer/
that multiplies the output window size without changing the game's
logical coordinates. =scale: 2= means a =320×240= logical game runs
in a =640×480= window with every pixel drawn twice as large.

Internally the engine does two things when =scale: > 1=:

1. =create-window!= is called with =width × scale= by =height × scale=.
2. =render-logical-size-set!= is called with the /logical/ size so
   that all subsequent drawing is measured in logical pixels.

This means your entity coordinates, camera offsets, and
=render:=-hook drawing all use logical pixels regardless of the
scale factor. You don't have to multiply anything yourself.

=scale: 1= (the default) skips the logical-size call entirely, and
behaves as if scaling were off.

Non-integer and non-positive values raise an error from =make-game=.

/Full example: the *scaling demo* — run with =bin/demo-scaling=,
source in =demo/scaling.scm=./

*** =renderer-set-clear-color!=

This is the function the engine calls before =render-clear!= each
frame. You can call it yourself if you ever need to re-clear to a
specific color mid-frame (for letterboxing effects, say) — it takes
the SDL2 renderer and a scene, and reads the scene's background.

** Sprite fonts

A *sprite font* is a bitmap font encoded as glyphs inside a tileset.
Downstroke's =sprite-font= record captures the tile size, spacing,
and a hash table mapping characters to =tile-id= values. Construct
one with =make-sprite-font*=:

#+begin_src scheme
(make-sprite-font*
  #:tile-size 16
  #:spacing   1
  #:ranges    '((#\A #\M 918)        ;; A-M start at tile 918
                (#\N #\Z 967)
                (#\0 #\9 869)))
#+end_src

Arguments:

- =tile-size= (keyword, required): pixel width and height of each
  glyph tile.
- =spacing= (keyword, default =1=): horizontal pixels between
  glyphs.
- =ranges= (keyword, required): list of triples =(start-char
  end-char first-tile-id)=. The first character in the range maps to
  =first-tile-id=, and each subsequent character maps to the next
  tile id.

Ranges are always stored uppercase; lookups also uppercase, so a
font defined with =#\A…#\Z= will also render lowercase input
correctly. Overlapping ranges raise an error at construction time.

To draw text, call =draw-sprite-text=:

#+begin_src scheme
(draw-sprite-text renderer tileset-texture tileset font text x y)
#+end_src

- =renderer=, =tileset-texture=, =tileset= come from the scene
  (typically =(game-renderer game)=, =(scene-tileset-texture
  scene)=, and =(tilemap-tileset (scene-tilemap scene))=).
- =font= is the =sprite-font= returned by =make-sprite-font*=.
- =text= is a string — characters not in the font's char map are
  silently skipped (the cursor still advances).
- =x=, =y= are pixel coordinates on the screen (/not/ camera-local).

Two helpers are useful for layout:

- =sprite-font-char->tile-id font ch= returns the tile id for one
  character, or =#f= if it isn't mapped. Upcases =ch= first.
- =sprite-text-width font text= returns the total pixel width of
  =text= drawn with this font, accounting for spacing.

Sprite fonts and tile frames share the same tileset in the example
demo, so the same texture that draws the level tiles also draws the
HUD text. See the *spritefont demo* — run with =bin/demo-spritefont=,
source in =demo/spritefont.scm=.

*** Tileset spacing

=tileset-tile= (in =tilemap.scm=) computes the source rectangle for a
given =tile-id=. It uses =(tileset-spacing tileset)= when walking
across columns and down rows, so tilesets exported from Tiled with a
non-zero =spacing= attribute (margins between tile cells) render
correctly. If =tileset-spacing= is =#f= or missing, zero spacing is
assumed.

The same spacing logic governs sprite-font rendering: each glyph is
sourced through =tileset-tile=, so a tileset with =spacing="1"= in
its TSX produces correctly-aligned glyphs without any extra tuning.

** Custom rendering and overlays

The =render:= keyword on =make-game= is an optional hook called
every frame /after/ =render-scene!= (and after the debug overlay, if
enabled) but /before/ =render-present!=. Its signature is
=(lambda (game) …)= — no delta-time, since rendering should be a
pure read of game state.

This is the right place for:

- HUDs (score, health bars, timers).
- Menu text and button overlays.
- Debug visualizations beyond the built-in overlay.
- Any SDL2 drawing that needs to go /on top of/ the scene.

#+begin_src scheme
(make-game
  title: "..." width: 600 height: 400

  render: (lambda (game)
            (let ((renderer (game-renderer game)))
              ;; draw a HUD bar across the top
              (set! (sdl2:render-draw-color renderer)
                    (sdl2:make-color 0 0 0 200))
              (sdl2:render-fill-rect! renderer
                (sdl2:make-rect 0 0 600 24))
              (draw-ui-text renderer *hud-font* "SCORE 00" color 8 4))))
#+end_src

=draw-ui-text= is a thin convenience that wraps =ttf:render-text-solid=
and =create-texture-from-surface=. Pass it a renderer, a TTF font
(from =ttf:open-font=), a string, an SDL color, and the top-left
pixel coordinates. For menus, =draw-menu-items= takes a list of items
and a cursor index and stacks them vertically with a selected prefix
=" > "= in front of the current one.

/Full example: the *menu demo* — run with =bin/demo-menu=, source in
=demo/menu.scm=./ The menu demo uses =draw-ui-text= and
=draw-menu-items= from a =render:= hook that is resolved via
=make-game-state= (per-state render hooks override the top-level
=render:= hook)./

*** Debug overlay

Passing =debug?: #t= to =make-game= flips on an overlay that
=game-render!= draws after =render-scene!= and before your own
=render:= hook. =render-debug-scene!= draws:

- Colored filled rectangles over the AABB of every entity whose
  =#:type= is =player= (blue) or =enemy= (red). Other entity types
  are ignored by the overlay.
- Attack hitboxes — a tile-wide green rect offset from the entity in
  its =#:facing= direction — whenever =#:attack-timer= is greater
  than zero.
- Purple filled rectangles over every non-zero tile in the tilemap
  (when the scene has one), marking the tilemap's footprint for
  collision debugging.

The overlay is tilemap-optional: on a sprite-only scene the tilemap
tiles are skipped and attack-hitbox thickness falls back to the
entity's own =#:width=. There is no crash path through the overlay
code.

The overlay covers up sprites because it draws filled rects, so
enable it only for debugging.

* Common patterns

** Render a solid color entity (no tileset needed)

Any entity with =#:color '(r g b)= draws as a filled rect, even when
the scene has no tileset or tilemap at all. This is the fastest way
to get something on screen.

#+begin_src scheme
(plist->alist
  (list #:type  'player
        #:x 150 #:y 100
        #:width 32 #:height 32
        #:color '(100 160 255)))
#+end_src

See the *getting-started demo* — run with =bin/demo-getting-started=,
source in =demo/getting-started.scm=.

** Scale the whole game 2×

Define your game in its /logical/ resolution and pass =scale:=:

#+begin_src scheme
(make-game
  title: "Demo: Scaling (2×)"
  width: 320 height: 240     ;; logical pixels
  scale: 2                   ;; window is 640×480
  ...)
#+end_src

All subsequent drawing — entities, camera, =render:= hook — uses the
logical =320×240= coordinate space. See the *scaling demo* — run with
=bin/demo-scaling=, source in =demo/scaling.scm=.

** Draw text with a sprite font

Build the font once in =preload:= or =create:=, then call
=draw-sprite-text= from the =render:= hook:

#+begin_src scheme
(render: (lambda (game)
           (let* ((scene    (game-scene game))
                  (renderer (game-renderer game))
                  (tileset  (tilemap-tileset (scene-tilemap scene)))
                  (texture  (scene-tileset-texture scene)))
             (draw-sprite-text renderer texture tileset *sprite-font*
                               "HELLO WORLD" 50 50))))
#+end_src

See the *spritefont demo* — run with =bin/demo-spritefont=, source in
=demo/spritefont.scm=.

** Draw a HUD in the =render:= hook

Use =draw-ui-text= (TTF) or =draw-sprite-text= (bitmap) from a
=render:= hook. The hook runs every frame, /after/ scene rendering,
so anything drawn here sits on top of the game world:

#+begin_src scheme
(render: (lambda (game)
           (let ((renderer (game-renderer game)))
             (draw-ui-text renderer *hud-font*
               (format #f "SCORE ~a" *score*)
               (sdl2:make-color 255 255 255) 8 4))))
#+end_src

See the *menu demo* — run with =bin/demo-menu=, source in
=demo/menu.scm= — which uses =draw-ui-text= and =draw-menu-items=
from per-state render hooks attached via =make-game-state=.

** Enable the debug overlay

Flip on colored AABBs for players, enemies, attacks, and tile cells:

#+begin_src scheme
(make-game
  title: "..." width: 600 height: 400
  debug?: #t                      ;; or (command-line-arguments)-driven
  ...)
#+end_src

The *platformer demo* wires this to a =--debug= command-line flag —
run with =bin/demo-platformer --debug=, source in
=demo/platformer.scm=.

** Sprite-only scene (no TMX tilemap)

When you want sprite drawing but have no Tiled map, build the scene
with =make-sprite-scene= and pass the =tileset=, =tileset-texture=,
and any =background=:

#+begin_src scheme
(create: (lambda (game)
           (let* ((ts  (game-load-tileset! game 'tiles "assets/tiles.tsx"))
                  (tex (create-texture-from-tileset
                         (game-renderer game) ts)))
             (game-scene-set! game
               (make-sprite-scene
                 entities:        (list (make-player))
                 tileset:         ts
                 tileset-texture: tex
                 background:      '(20 22 30))))))
#+end_src

Because there is no tilemap, =render-scene!= skips the tilemap draw
and goes straight to =draw-entities=. The sprite tileset resolves
from =scene-tileset= as documented above.

* See also

- [[file:guide.org][guide]] — minimal game walkthrough that ends at the
  getting-started demo.
- [[file:entities.org][entities]] — the =#:tile-id=, =#:color=,
  =#:facing=, =#:skip-render= keys in context, plus the entity plist
  model used by every drawable.
- [[file:scenes.org][scenes]] — =make-scene=, =make-sprite-scene=,
  =scene-background=, =scene-tileset=, and the scene-loader helpers
  that wire up TMX maps.
- [[file:animation.org][animation]] — how =#:tile-id= changes over
  time for animated sprites. The renderer reads whatever =#:tile-id=
  is current; animation updates it.
- [[file:input.org][input]] — reading key state inside =update:= and
  =render:= hooks for interactive overlays like menus.
- [[file:physics.org][physics]] — the pipeline that precedes
  rendering, and the =#:x=, =#:y=, =#:width=, =#:height= fields that
  =draw-entity= uses to size sprites.