Rendering
Table of Contents
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: guide, entities, scenes, animation, input.
1. The minimum you need
The simplest possible renderable entity is a colored rectangle with a position and size:
(plist->alist
(list #:type 'player
#:x 150 #:y 100
#:width 32 #:height 32
#:color '(100 160 255))) ;; RGB; alpha defaults to 255
Put it in a scene (make-sprite-scene needs no TMX map and no
tileset), and you're done:
(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: ...))
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:
(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))
…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.
2. Core concepts
2.1. 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:
- Clear the framebuffer using
scene-background(viarenderer-set-clear-color!). A missing or non-list background falls back to opaque black. - Draw the tilemap, if both a
tilemapand atileset-textureare present on the scene. A missing tilemap or missing texture simply skips this step — no error. - Draw each entity in
scene-entitiesviadraw-entity.
draw-entity applies a strict priority when deciding how to draw
one entity:
- If
#:skip-renderis truthy, the entity is skipped entirely. - Otherwise, if the entity has a
#:tile-idand the scene provides both a tileset and a tileset texture, it is drawn as a tileset sprite. - Otherwise, if the entity has a
#:colorlist of three or four integers(r g b)/(r g b a), it is drawn as a filled rect. - 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.
2.1.1. 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:
(or (scene-tileset scene)
(and (scene-tilemap scene)
(tilemap-tileset (scene-tilemap scene))))
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).
2.2. Backgrounds, logical resolution, scaling
2.2.1. 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.
(make-sprite-scene entities: (list (make-player)) background: '(20 22 30)) ;; dark navy
2.2.2. 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:
create-window!is called withwidth × scalebyheight × scale.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.
2.2.3. 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.
2.3. 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*:
(make-sprite-font*
#:tile-size 16
#:spacing 1
#:ranges '((#\A #\M 918) ;; A-M start at tile 918
(#\N #\Z 967)
(#\0 #\9 869)))
Arguments:
tile-size(keyword, required): pixel width and height of each glyph tile.spacing(keyword, default1): horizontal pixels between glyphs.ranges(keyword, required): list of triples(start-char end-char first-tile-id). The first character in the range maps tofirst-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:
(draw-sprite-text renderer tileset-texture tileset font text x y)
renderer,tileset-texture,tilesetcome from the scene (typically(game-renderer game),(scene-tileset-texture scene), and(tilemap-tileset (scene-tilemap scene))).fontis thesprite-fontreturned bymake-sprite-font*.textis a string — characters not in the font's char map are silently skipped (the cursor still advances).x,yare pixel coordinates on the screen (not camera-local).
Two helpers are useful for layout:
sprite-font-char->tile-id font chreturns the tile id for one character, or#fif it isn't mapped. Upcaseschfirst.sprite-text-width font textreturns the total pixel width oftextdrawn 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.
2.3.1. 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.
2.4. 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.
(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))))
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)./
2.4.1. 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
#:typeisplayer(blue) orenemy(red). Other entity types are ignored by the overlay. - Attack hitboxes — a tile-wide green rect offset from the entity in
its
#:facingdirection — whenever#:attack-timeris 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.
3. Common patterns
3.1. 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.
(plist->alist
(list #:type 'player
#:x 150 #:y 100
#:width 32 #:height 32
#:color '(100 160 255)))
See the getting-started demo — run with bin/demo-getting-started,
source in demo/getting-started.scm.
3.2. Scale the whole game 2×
Define your game in its logical resolution and pass scale::
(make-game title: "Demo: Scaling (2×)" width: 320 height: 240 ;; logical pixels scale: 2 ;; window is 640×480 ...)
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.
3.3. Draw text with a sprite font
Build the font once in preload: or create:, then call
draw-sprite-text from the render: hook:
(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))))
See the spritefont demo — run with bin/demo-spritefont, source in
demo/spritefont.scm.
3.4. 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:
(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))))
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.
3.5. Enable the debug overlay
Flip on colored AABBs for players, enemies, attacks, and tile cells:
(make-game title: "..." width: 600 height: 400 debug?: #t ;; or (command-line-arguments)-driven ...)
The platformer demo wires this to a --debug command-line flag —
run with bin/demo-platformer --debug, source in
demo/platformer.scm.
3.6. 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:
(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))))))
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.
4. See also
- guide — minimal game walkthrough that ends at the getting-started demo.
- entities — the
#:tile-id,#:color,#:facing,#:skip-renderkeys in context, plus the entity plist model used by every drawable. - scenes —
make-scene,make-sprite-scene,scene-background,scene-tileset, and the scene-loader helpers that wire up TMX maps. - animation — how
#:tile-idchanges over time for animated sprites. The renderer reads whatever#:tile-idis current; animation updates it. - input — reading key state inside
update:andrender:hooks for interactive overlays like menus. - physics — the pipeline that precedes
rendering, and the
#:x,#:y,#:width,#:heightfields thatdraw-entityuses to size sprites.