From 38eee24832fe6da4f135cae455881ab97953b23a Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sat, 18 Apr 2026 02:47:10 +0100 Subject: Refresh docs and re-indent --- docs/rendering.org | 440 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 docs/rendering.org (limited to 'docs/rendering.org') 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. -- cgit v1.2.3