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:

  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.

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:

  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.

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

(draw-sprite-text renderer tileset-texture tileset font text x y)
  • 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.

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 #: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.

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-render keys in context, plus the entity plist model used by every drawable.
  • scenesmake-scene, make-sprite-scene, scene-background, scene-tileset, and the scene-loader helpers that wire up TMX maps.
  • animation — how #:tile-id changes over time for animated sprites. The renderer reads whatever #:tile-id is current; animation updates it.
  • input — reading key state inside update: and render: hooks for interactive overlays like menus.
  • physics — the pipeline that precedes rendering, and the #:x, #:y, #:width, #:height fields that draw-entity uses to size sprites.

Author: Gene Pasquet

Created: 2026-04-20 Mon 15:22

Validate