aboutsummaryrefslogtreecommitdiff
path: root/docs/rendering.org
diff options
context:
space:
mode:
authorGene Pasquet <dev@etenil.net>2026-04-18 02:47:10 +0100
committerGene Pasquet <dev@etenil.net>2026-04-18 02:47:10 +0100
commit38eee24832fe6da4f135cae455881ab97953b23a (patch)
treecffc2bb3b45ac11d90f4a2de3e207f65862fb6fd /docs/rendering.org
parenta02b892e2ad1e1605ff942c63afdd618daa48be4 (diff)
Refresh docs and re-indent
Diffstat (limited to 'docs/rendering.org')
-rw-r--r--docs/rendering.org440
1 files changed, 440 insertions, 0 deletions
diff --git a/docs/rendering.org b/docs/rendering.org
new file mode 100644
index 0000000..35d69bb
--- /dev/null
+++ b/docs/rendering.org
@@ -0,0 +1,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.