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