diff options
| -rw-r--r-- | LICENSE | 24 | ||||
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | demo/menu.scm | 70 | ||||
| -rw-r--r-- | demo/platformer.scm | 4 | ||||
| -rw-r--r-- | demo/spritefont.scm | 51 | ||||
| -rw-r--r-- | docs/api.org | 413 | ||||
| -rw-r--r-- | docs/guide.org | 30 | ||||
| -rw-r--r-- | downstroke.egg | 4 | ||||
| -rw-r--r-- | engine.scm | 11 | ||||
| -rw-r--r-- | physics.scm | 6 | ||||
| -rw-r--r-- | renderer.scm | 140 | ||||
| -rw-r--r-- | tests/engine-test.scm | 16 | ||||
| -rw-r--r-- | tests/physics-test.scm | 11 | ||||
| -rw-r--r-- | tests/renderer-test.scm | 158 | ||||
| -rw-r--r-- | world.scm | 6 |
15 files changed, 908 insertions, 38 deletions
@@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2026, Gene Pasquet + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @@ -4,7 +4,7 @@ MODULE_NAMES := entity tilemap world input physics renderer assets engine mixer sound animation ai prefabs scene-loader OBJECT_FILES := $(patsubst %,bin/%.o,$(MODULE_NAMES)) -DEMO_NAMES := platformer shmup topdown audio sandbox +DEMO_NAMES := platformer shmup topdown audio sandbox spritefont menu DEMO_BINS := $(patsubst %,bin/demo-%,$(DEMO_NAMES)) UNIT_NAMES := $(patsubst %,downstroke-%,$(MODULE_NAMES)) diff --git a/demo/menu.scm b/demo/menu.scm new file mode 100644 index 0000000..2564bd0 --- /dev/null +++ b/demo/menu.scm @@ -0,0 +1,70 @@ +(import scheme + (chicken base) + (prefix sdl2 "sdl2:") + (prefix sdl2-ttf "ttf:") + downstroke-engine + downstroke-renderer + downstroke-input) + +;; Global state for menu cursor +(define *menu-cursor* 0) +(define *menu-items* '("Play" "Quit")) +(define *title-font* #f) +(define *menu-font* #f) + +(define *game* + (make-game + title: "Demo: Menu System" width: 600 height: 400 + + preload: (lambda (game) + (set! *title-font* (ttf:open-font "demo/assets/DejaVuSans.ttf" 24)) + (set! *menu-font* (ttf:open-font "demo/assets/DejaVuSans.ttf" 16))) + + create: (lambda (game) + ;; Register the main-menu state + (game-add-state! game 'main-menu + (make-game-state + #:update (lambda (game dt) + (let ((input (game-input game))) + ;; Navigate menu with up/down + (when (input-pressed? input 'up) + (set! *menu-cursor* (max 0 (- *menu-cursor* 1)))) + (when (input-pressed? input 'down) + (set! *menu-cursor* (min (- (length *menu-items*) 1) (+ *menu-cursor* 1)))) + ;; Confirm selection + (when (input-pressed? input 'a) + (case *menu-cursor* + ((0) ; Play + (game-start-state! game 'playing)) + ((1) ; Quit + (sdl2:quit!) + (exit)))))) + #:render (lambda (game) + (let ((renderer (game-renderer game))) + (set! (sdl2:render-draw-color renderer) (sdl2:make-color 0 0 0 255)) + (sdl2:render-fill-rect! renderer (sdl2:make-rect 0 0 600 400)) + (let ((white (sdl2:make-color 255 255 255 255))) + (draw-ui-text renderer *title-font* "MENU DEMO" white 200 80) + (draw-menu-items renderer *menu-font* *menu-items* *menu-cursor* + 150 150 50)))))) + + ;; Register the playing state + (game-add-state! game 'playing + (make-game-state + #:update (lambda (game dt) + (let ((input (game-input game))) + ;; Return to menu on quit (Escape) + (when (input-pressed? input 'quit) + (set! *menu-cursor* 0) + (game-start-state! game 'main-menu)))) + #:render (lambda (game) + (let ((renderer (game-renderer game))) + (set! (sdl2:render-draw-color renderer) (sdl2:make-color 0 0 0 255)) + (sdl2:render-fill-rect! renderer (sdl2:make-rect 0 0 600 400)) + (let ((white (sdl2:make-color 255 255 255 255))) + (draw-ui-text renderer *menu-font* "PLAYING! PRESS ESC TO RETURN" white 120 150)))))) + + ;; Start with the main menu + (game-start-state! game 'main-menu)))) + +(game-run! *game*) diff --git a/demo/platformer.scm b/demo/platformer.scm index 09f9b56..77fef72 100644 --- a/demo/platformer.scm +++ b/demo/platformer.scm @@ -1,5 +1,6 @@ (import scheme (chicken base) + (chicken process-context) (prefix sdl2 "sdl2:") (prefix sdl2-ttf "ttf:") (prefix sdl2-image "img:") @@ -14,9 +15,12 @@ downstroke-sound downstroke-scene-loader) +(define +debug?+ (member "--debug" (command-line-arguments))) + (define *game* (make-game title: "Demo: Platformer" width: 600 height: 400 + debug?: (and +debug?+ #t) preload: (lambda (game) (init-audio!) diff --git a/demo/spritefont.scm b/demo/spritefont.scm new file mode 100644 index 0000000..8895d3c --- /dev/null +++ b/demo/spritefont.scm @@ -0,0 +1,51 @@ +(import scheme + (chicken base) + (prefix sdl2 "sdl2:") + (prefix sdl2-ttf "ttf:") + (prefix sdl2-image "img:") + downstroke-engine + downstroke-world + downstroke-tilemap + downstroke-renderer + downstroke-assets + downstroke-scene-loader) + +(define *sprite-font* #f) + +(define *game* + (make-game + title: "Demo: Sprite Font" width: 600 height: 400 + + create: (lambda (game) + (let* ((scene (game-load-scene! game "demo/assets/level-0.tmx")) + (tileset (tilemap-tileset (scene-tilemap scene)))) + ;; Create sprite font with character ranges + ;; A-M: tiles 917-929, N-Z: tiles 966-978, 0-9: tiles 868-877 + (set! *sprite-font* + (make-sprite-font* + #:tile-size 16 + #:spacing 1 + #:ranges '((#\A #\M 918) + (#\N #\Z 967) + (#\0 #\9 869)))))) + + render: (lambda (game) + (let* ((renderer (game-renderer game)) + (scene (game-scene game)) + (tileset (tilemap-tileset (scene-tilemap scene))) + (tileset-texture (scene-tileset-texture scene))) + ;; Clear background + (set! (sdl2:render-draw-color renderer) (sdl2:make-color 30 30 60 255)) + (sdl2:render-fill-rect! renderer (sdl2:make-rect 0 0 600 400)) + + ;; Draw sprite text at various positions + (draw-sprite-text renderer tileset-texture tileset *sprite-font* + "HELLO WORLD" 50 50) + (draw-sprite-text renderer tileset-texture tileset *sprite-font* + "DOWNSTROKE" 100 120) + (draw-sprite-text renderer tileset-texture tileset *sprite-font* + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 20 200) + (draw-sprite-text renderer tileset-texture tileset *sprite-font* + "0123456789" 150 280))))) + +(game-run! *game*) diff --git a/docs/api.org b/docs/api.org index 3925bc2..f18b80a 100644 --- a/docs/api.org +++ b/docs/api.org @@ -22,7 +22,8 @@ The engine module provides the top-level game lifecycle and state management. (preload #f) (create #f) (update #f) - (render #f)) + (render #f) + (debug? #f)) #+end_src Creates and initializes a game object. All parameters are optional keywords. @@ -38,6 +39,7 @@ Creates and initializes a game object. All parameters are optional keywords. | ~create~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called once at startup | | ~update~ | procedure/false | #f | Hook: ~(lambda (game dt) ...)~ called each frame | | ~render~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called after render-scene! | +| ~debug?~ | boolean | #f | Enable debug overlay drawing (collision boxes) | The game object is the central hub. Use it to store/retrieve assets, manage scenes, and access the current input state. @@ -152,7 +154,8 @@ Auto-generated by defstruct. Use keyword arguments: (entities '()) (tilemap #f) (camera #f) - (tileset-texture #f)) + (tileset-texture #f) + (camera-target #f)) #+end_src Creates a scene record representing the current level state. @@ -163,6 +166,7 @@ Creates a scene record representing the current level state. | ~tilemap~ | tilemap/false | #f | Tile grid and collisions | | ~camera~ | camera/false | #f | Viewport position | | ~tileset-texture~ | SDL2 texture/false | #f | Rendered tileset image | +| ~camera-target~ | symbol/false | #f | Tag symbol of the entity to follow (see ~scene-camera-target-set!~) | ** ~make-camera~ @@ -267,6 +271,15 @@ Returns a list of all entities whose ~#:tags~ list contains the given tag. Retur - ~scene-tilemap~, ~scene-tilemap-set!~ - ~scene-camera~, ~scene-camera-set!~ - ~scene-tileset-texture~, ~scene-tileset-texture-set!~ +- ~scene-camera-target~, ~scene-camera-target-set!~ + +** ~tilemap-tile-at~ + +#+begin_src scheme +(tilemap-tile-at tilemap col row) +#+end_src + +Returns the tile ID at grid position ~(col, row)~ across all layers. Returns ~0~ if the position is out of bounds or all layers have a 0 tile at that cell. Used internally by the physics module; also useful for manual tile queries. * Entity (~downstroke-entity~) @@ -360,7 +373,7 @@ All entities can have these keys. Not all are required: (import downstroke-physics) #+end_src -The physics module implements the main collision and movement pipeline. The physics pipeline runs automatically before the user's ~update:~ hook. +The physics module provides functions for movement, collision detection, and ground sensing. Call them manually in your ~update:~ hook in the order that suits your game type (see ~docs/physics.org~ for examples). ** Physics Pipeline Order @@ -525,6 +538,51 @@ Returns true if the action was released in this frame (held previously but not n Initializes an input state record from a configuration. All actions start as unpressed. +** ~input-any-pressed?~ + +#+begin_src scheme +(input-any-pressed? state config) +#+end_src + +Returns true if any action in the configuration was pressed this frame. Useful for "press any key to continue" prompts. + +** ~input-state->string~ + +#+begin_src scheme +(input-state->string state config) +#+end_src + +Returns a human-readable string of all currently held actions, e.g., ~"[Input: left a]"~. Useful for debug displays. + +** ~set-facing-from-vx~ + +#+begin_src scheme +(set-facing-from-vx entity vx) +#+end_src + +Sets ~#:facing~ to ~1~ if ~vx > 0~, ~-1~ if ~vx < 0~, and returns the entity unchanged if ~vx = 0~. Convenience helper for keeping the facing direction in sync with horizontal velocity. + +** ~apply-input-to-entity~ + +#+begin_src scheme +(apply-input-to-entity entity held?) +#+end_src + +Applies an input map stored on the entity to compute a new ~#:vx~. The entity must have an ~#:input-map~ key: an alist of ~(action . (dvx . dvy))~ pairs. ~held?~ is a predicate ~(lambda (action) ...)~ (typically ~(lambda (a) (input-held? input a))~). Also updates ~#:facing~ from the resulting velocity. + +Example: + +#+begin_src scheme +;; Entity with embedded input map: +(list #:type 'player + #:input-map '((left . (-3 . 0)) (right . (3 . 0))) + #:move-speed 1 + ...) + +;; In update: +(apply-input-to-entity player (lambda (a) (input-held? input a))) +#+end_src + ** Example: Player Movement #+begin_src scheme @@ -584,6 +642,203 @@ Returns an SDL2 flip list based on the entity's ~#:facing~ field. Returns ~'(hor Renders a single line of text to the screen at the given pixel coordinates. ~color~ is an SDL2 color struct. Positions are in screen (viewport) space, not world space. Does not cache; call once per frame for each text element. +** Debug Drawing + +Debug drawing displays collision boxes and tile grid overlays to visualize physics during development. Enable with the ~debug?:~ keyword on ~make-game~. + +*** ~render-debug-scene!~ + +#+begin_src scheme +(render-debug-scene! renderer scene) +#+end_src + +Renders debug overlays for the entire scene: tile grid boundaries and entity collision boxes. Call this in your render hook to see what the physics engine sees. + +*** ~draw-debug-tiles~ + +#+begin_src scheme +(draw-debug-tiles renderer camera tilemap) +#+end_src + +Draws a grid outline around all non-zero tiles in the tilemap. Useful for understanding tilemap layout and collision geometry. Color: purple (~+debug-tile-color+~). + +*** ~draw-debug-entities~ + +#+begin_src scheme +(draw-debug-entities renderer camera scene) +#+end_src + +Draws colored bounding boxes around all entities in the scene. Also draws attack hitboxes when the entity's ~#:attack-timer~ is active. Entity colors are: + +| Type | Color | Value | +|------|-------|-------| +| Player | Blue (~+debug-player-color+~) | rgb(64, 128, 255) | +| Enemy | Red (~+debug-enemy-color+~) | rgb(220, 40, 40) | +| Attack Hitbox | Green (~+debug-attack-color+~) | rgb(0, 200, 80) | +| Tile | Purple (~+debug-tile-color+~) | rgb(140, 0, 220) | + +*** Example: Enable Debug Mode + +#+begin_src scheme +(define my-game + (make-game + title: "My Game" width: 600 height: 400 + debug?: #t)) ;; Enable debug overlay + +(game-run! my-game) +#+end_src + +With debug mode enabled, the game renders the normal scene first, then overlays collision boxes and tile boundaries on top. This is useful for finding collision bugs and understanding the physics layout. + +** Menus + +The ~draw-menu-items~ helper renders a list of menu options with a cursor indicator. + +*** ~draw-menu-items~ + +#+begin_src scheme +(draw-menu-items renderer font items cursor x y-start y-step + #!key (label-fn identity) (color #f) (prefix "> ") (no-prefix " ")) +#+end_src + +Renders a vertical menu with a cursor indicator on the currently selected item. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| ~renderer~ | renderer | (required) | SDL2 renderer | +| ~font~ | TTF font | (required) | Font for rendering text | +| ~items~ | list | (required) | List of menu items (any values) | +| ~cursor~ | integer | (required) | Index of highlighted item (0-based) | +| ~x~ | integer | (required) | X position in screen space | +| ~y-start~ | integer | (required) | Y position of first item | +| ~y-step~ | integer | (required) | Pixel spacing between items | +| ~label-fn~ | procedure | ~identity~ | Function to convert item to string (called for each item) | +| ~color~ | SDL2 color | white (255, 255, 255) | Text color | +| ~prefix~ | string | "~> ~" | Prefix for highlighted item | +| ~no-prefix~ | string | "~ ~" | Prefix for non-highlighted items | + +*** Example: Menu State + +#+begin_src scheme +(import downstroke-engine + downstroke-world + (prefix sdl2 "sdl2:") + (prefix sdl2-ttf "ttf:")) + +(define menu-items '(("Start Game") ("Options") ("Quit"))) +(define cursor (make-parameter 0)) + +(define my-game + (make-game + title: "Menu Game" width: 600 height: 400 + + preload: (lambda (game) + (let ((font (ttf:open-font "assets/font.ttf" 24))) + (game-asset-set! game 'menu-font font))) + + create: (lambda (game) + (game-add-state! game 'menu + (make-game-state + render: (lambda (game) + (let ((font (game-asset game 'menu-font))) + (draw-menu-items (game-renderer game) font menu-items + (cursor) + 100 100 40)))))))) + +(game-run! my-game) +#+end_src + +** Sprite Fonts + +Sprite fonts render text using a bitmap tileset instead of system fonts. Characters are stored as tiles in your tileset image, making them pixel-perfect and zero-dependency on TTF files. + +*** ~make-sprite-font*~ + +#+begin_src scheme +(make-sprite-font* #!key tile-size spacing ranges) +#+end_src + +Creates a sprite font from character ranges. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| ~tile-size~ | integer | (required) | Pixel width/height of each character tile | +| ~spacing~ | integer | 1 | Pixels between characters when rendered | +| ~ranges~ | list | (required) | List of ~(start-char end-char first-tile-id)~ triples | + +The ~ranges~ parameter defines which tile IDs correspond to which characters. For example, ~(list (#\A #\M 917) (#\N #\Z 966) (#\0 #\9 868))~ maps characters A–M to tile IDs 917–929, N–Z to 966–978, and 0–9 to 868–877. + +Characters are automatically uppercased when rendered (all lookups use uppercase). + +*** ~sprite-font-char->tile-id~ + +#+begin_src scheme +(sprite-font-char->tile-id font ch) +#+end_src + +Returns the tile ID for a character, or ~#f~ if not found. The character is automatically uppercased before lookup. + +*** ~sprite-text-width~ + +#+begin_src scheme +(sprite-text-width font text) +#+end_src + +Computes the pixel width of a string when rendered with the given font. Useful for centering text. Formula: ~(* n tile-size) + (* (- n 1) spacing)~ where ~n~ is the string length. + +*** ~draw-sprite-text~ + +#+begin_src scheme +(draw-sprite-text renderer tileset-texture tileset font text x y) +#+end_src + +Renders sprite-font text to the screen. ~tileset-texture~ is the SDL2 texture of your tileset image, and ~tileset~ is the tilemap structure containing tile layout information. Characters not in the font are silently skipped. + +| Parameter | Type | Description | +|-----------|------|-------------| +| ~renderer~ | renderer | SDL2 renderer | +| ~tileset-texture~ | SDL2 texture | Rendered tileset image | +| ~tileset~ | tileset struct | Tileset from loaded tilemap | +| ~font~ | sprite-font struct | Font created with ~make-sprite-font*~ | +| ~text~ | string | Text to render | +| ~x, y~ | integers | Screen position in pixels | + +*** Example: Sprite Font for Score Display + +#+begin_src scheme +(import downstroke-engine + downstroke-scene-loader + downstroke-renderer) + +;; Create a sprite font with A-Z at tiles 917–942, 0-9 at tiles 868–877 +(define score-font + (make-sprite-font* + tile-size: 16 + spacing: 1 + ranges: (list + (list #\A #\Z 917) + (list #\0 #\9 868)))) + +(define my-game + (make-game + title: "Score Game" width: 600 height: 400 + + create: (lambda (game) + (game-load-scene! game "assets/level.tmx")) + + render: (lambda (game) + (let ((scene (game-scene game))) + ;; Display "SCORE: 1500" using sprite font + (draw-sprite-text (game-renderer game) + (scene-tileset-texture scene) + (tilemap-tileset (scene-tilemap scene)) + score-font + "SCORE: 1500" + 10 10))))) + +(game-run! my-game) +#+end_src + * Assets (~downstroke-assets~) #+begin_src scheme @@ -716,6 +971,22 @@ Releases all audio resources. Call at shutdown or in a cleanup hook. The animation module provides simple frame-based sprite animation. +** ~animation-frames~ + +#+begin_src scheme +(animation-frames anim) +#+end_src + +Returns the ~#:frames~ list from an animation plist. Each element is a 0-indexed tile ID. + +** ~animation-duration~ + +#+begin_src scheme +(animation-duration anim) +#+end_src + +Returns the ~#:duration~ value from an animation plist (ticks per frame). + ** ~set-animation~ #+begin_src scheme @@ -753,6 +1024,76 @@ Example: Converts a frame index to a tile ID (1-indexed). Used internally by ~animate-entity~. +* Tilemap (~downstroke-tilemap~) + +#+begin_src scheme +(import downstroke-tilemap) +#+end_src + +The tilemap module parses Tiled TMX and TSX files using the expat XML library, and provides struct accessors for tile and map data. + +** ~load-tilemap~ + +#+begin_src scheme +(load-tilemap filename) +#+end_src + +Loads and parses a TMX file. Automatically resolves and loads the referenced TSX tileset and its image. Returns a ~tilemap~ struct. + +** ~load-tileset~ + +#+begin_src scheme +(load-tileset filename) +#+end_src + +Loads and parses a TSX tileset file. Loads the image referenced in the tileset. Returns a ~tileset~ struct. + +** ~tileset-tile~ + +#+begin_src scheme +(tileset-tile tileset tile-id) +#+end_src + +Returns a ~tile~ struct for the given 1-indexed tile ID. The tile struct has ~tile-id~ and ~tile-rect~ (an SDL2 rect) fields with the source rectangle in the tileset image. + +** ~tileset-rows~ + +#+begin_src scheme +(tileset-rows tileset) +#+end_src + +Returns the number of rows in the tileset image (computed from ~tilecount / columns~). + +** Tileset Accessors (auto-generated by defstruct) + +- ~tileset-tilewidth~, ~tileset-tileheight~ — tile dimensions in pixels +- ~tileset-spacing~ — pixel gap between tiles in the source image +- ~tileset-tilecount~ — total number of tiles +- ~tileset-columns~ — number of tile columns +- ~tileset-image-source~ — path to the tileset image file +- ~tileset-image~ — loaded SDL2 surface + +** Tilemap Accessors (auto-generated by defstruct) + +- ~tilemap-width~, ~tilemap-height~ — map dimensions in tiles +- ~tilemap-tilewidth~, ~tilemap-tileheight~ — tile dimensions in pixels +- ~tilemap-tileset~ — the embedded ~tileset~ struct +- ~tilemap-layers~ — list of ~layer~ structs +- ~tilemap-objects~ — list of ~object~ structs (from the Tiled object layer) + +** Layer Accessors (auto-generated by defstruct) + +- ~layer-name~ — layer name string +- ~layer-width~, ~layer-height~ — layer dimensions in tiles +- ~layer-map~ — 2D list of tile GIDs (rows × columns) + +** Object Accessors (auto-generated by defstruct) + +- ~object-name~, ~object-type~ — object name and type (strings from XML) +- ~object-x~, ~object-y~ — position in pixels +- ~object-width~, ~object-height~ — size in pixels +- ~object-properties~ — alist of custom properties from Tiled + * Scene Loader (~downstroke-scene-loader~) #+begin_src scheme @@ -785,6 +1126,30 @@ Example: ) #+end_src +** ~game-load-tilemap!~ + +#+begin_src scheme +(game-load-tilemap! game key filename) +#+end_src + +Loads a TMX tilemap file, stores it in the game asset registry under ~key~, and returns the tilemap struct. + +** ~game-load-tileset!~ + +#+begin_src scheme +(game-load-tileset! game key filename) +#+end_src + +Loads a TSX tileset file, stores it in the game asset registry under ~key~, and returns the tileset struct. + +** ~game-load-font!~ + +#+begin_src scheme +(game-load-font! game key filename size) +#+end_src + +Opens a TTF font at ~size~ points, stores it in the game asset registry under ~key~, and returns the font. Wraps ~ttf:open-font~. + ** ~create-tileset-texture~ #+begin_src scheme @@ -793,54 +1158,54 @@ Example: Creates an SDL2 texture from the tileset image embedded in a tilemap struct. Useful when you need the texture independently of ~game-load-scene!~. -** ~make-prefab-registry~ +** ~load-prefabs~ #+begin_src scheme -(make-prefab-registry name1 constructor1 name2 constructor2 ...) +(load-prefabs filename engine-mixin-table user-hooks) #+end_src -Creates a hash-table mapping symbol names to entity constructor functions. Constructors have the signature ~(lambda (x y w h) entity)~. +Loads a prefab definition file and returns a ~prefab-registry~ struct. The file must contain a Scheme expression with ~mixins~ and ~prefabs~ sections (see ~docs/entities.org~). + +| Parameter | Type | Description | +|-----------|------|-------------| +| ~filename~ | string | Path to the prefab data file (e.g., ~"assets/prefabs.scm"~) | +| ~engine-mixin-table~ | alist | Extra engine-level mixins to merge; pass ~'()~ if none | +| ~user-hooks~ | alist | Extra instantiation hooks; pass ~'()~ if none | Example: #+begin_src scheme -(define (make-player x y w h) - (list #:type 'player #:x x #:y y #:width w #:height h - #:vx 0 #:vy 0 #:gravity? #t #:tile-id 29)) +(define *prefabs* (load-prefabs "assets/prefabs.scm" '() '())) +#+end_src -(define (make-enemy x y w h) - (list #:type 'enemy #:x x #:y y #:width w #:height h - #:vx -2 #:vy 0 #:gravity? #t #:tile-id 5)) +** ~reload-prefabs!~ -(define prefabs - (make-prefab-registry - 'player make-player - 'enemy make-enemy)) +#+begin_src scheme +(reload-prefabs! registry) #+end_src +Reloads the prefab file that the registry was originally loaded from. Returns a new registry. Useful for hot-reloading prefab data during development without restarting the game. + ** ~instantiate-prefab~ #+begin_src scheme (instantiate-prefab registry type x y w h) #+end_src -Looks up a constructor by type in the registry and calls it with the given position and size. Returns the entity plist, or ~#f~ if the type is not registered. +Looks up a prefab by type symbol in the registry and returns a fresh entity plist at the given position and size. Returns ~#f~ if the type is not registered. If the resulting entity has an ~#:on-instantiate~ hook, it is called with the entity before returning. ** ~tilemap-objects->entities~ #+begin_src scheme -(tilemap-objects->entities tilemap instantiate-fn) +(tilemap-objects->entities tilemap registry) #+end_src -Converts the TMX object list (from Tiled) into entity plists. Each object's type (string from XML) is converted to a symbol and passed to ~instantiate-fn~. Filters out ~#f~ results (unregistered types). - -~instantiate-fn~ should have the signature ~(lambda (type x y w h) entity-or-#f)~, typically the curried version of ~instantiate-prefab~: +Converts the TMX object list (from Tiled) into entity plists using the prefab registry. Each object's type string is converted to a symbol and passed to ~instantiate-prefab~. Objects whose type has no registered prefab are silently filtered out. Example: #+begin_src scheme -(let* ((prefabs (make-prefab-registry 'player make-player 'enemy make-enemy)) - (inst (lambda (t x y w h) (instantiate-prefab prefabs t x y w h))) - (entities (tilemap-objects->entities tilemap inst))) +(let* ((registry (load-prefabs "assets/prefabs.scm" '() '())) + (entities (tilemap-objects->entities tilemap registry))) (scene-entities-set! scene entities)) #+end_src diff --git a/docs/guide.org b/docs/guide.org index af6319e..ca9cfbf 100644 --- a/docs/guide.org +++ b/docs/guide.org @@ -214,9 +214,33 @@ Key points: See =demo/platformer.scm= in the engine source for a complete working example. +* Development Tips + +** Debug Mode + +During development, enable ~debug?: #t~ on ~make-game~ to visualize collision boxes and the tile grid. This helps you understand what the physics engine "sees" and debug collision problems: + +#+begin_src scheme +(define my-game + (make-game + title: "My Game" width: 600 height: 400 + debug?: #t)) ;; Enable collision visualization + +(game-run! my-game) +#+end_src + +With debug mode enabled, you'll see: + +- **Purple outlines** around all non-zero tiles +- **Blue boxes** around player entities +- **Red boxes** around enemy entities +- **Green boxes** around active attack hitboxes + +This is invaluable for tuning collision geometry and understanding why entities are clipping or not colliding as expected. + * Demo Overview -Downstroke includes five complete demo games that showcase different features: +Downstroke includes seven complete demo games that showcase different features: | Demo | File | What it shows | |------|------|--------------| @@ -225,6 +249,8 @@ Downstroke includes five complete demo games that showcase different features: | Physics Sandbox | =demo/sandbox.scm= | Entity-entity collision, multi-entity physics, auto-respawn | | Shoot-em-up | =demo/shmup.scm= | No tilemap, entity spawning/removal, manual AABB collision | | Audio | =demo/audio.scm= | Sound effects, music toggle, text rendering | +| Sprite Font | =demo/spritefont.scm= | Bitmap text rendering using non-contiguous tileset ranges | +| Menu | =demo/menu.scm= | State machine menus, keyboard navigation, TTF text rendering | Each demo is self-contained and serves as a working reference for a particular game mechanic. @@ -235,7 +261,7 @@ If you cloned the downstroke source, you can build everything: #+begin_src bash cd /path/to/downstroke make # Compile all engine modules -make demos # Build all 5 demo executables +make demos # Build all demo executables ./bin/demo-platformer ./bin/demo-topdown # etc. diff --git a/downstroke.egg b/downstroke.egg index 32f839e..3572d66 100644 --- a/downstroke.egg +++ b/downstroke.egg @@ -1,9 +1,9 @@ ((version "0.1.0") (synopsis "2D tile-driven game engine for Chicken Scheme, built on SDL2") (author "Gene Pasquet") - (license "MIT") + (license "BSD-2-Clause") (category games) - (dependencies sdl2 sdl2-image sdl2-ttf expat defstruct srfi-1 srfi-13 srfi-197 matchable simple-logger) + (dependencies sdl2 sdl2-image sdl2-ttf expat defstruct srfi-1 srfi-13 srfi-69 srfi-197 matchable simple-logger) (components (extension downstroke-entity (source "entity.scm")) @@ -29,7 +29,8 @@ render-hook ;; (lambda (game) ...) — post-render overlay scene ;; current scene struct; #f until create: runs states ;; hash-table of name → state-plist - active-state) ;; symbol or #f — currently active state name + active-state ;; symbol or #f — currently active state name + debug?) ;; boolean: enable debug overlay drawing ;; Store the auto-generated constructor as make-game* (define make-game* make-game) @@ -42,7 +43,8 @@ (width 640) (height 480) (frame-delay 16) (input-config *default-input-config*) - (preload #f) (create #f) (update #f) (render #f)) + (preload #f) (create #f) (update #f) (render #f) + (debug? #f)) (make-game* title: title width: width @@ -59,7 +61,8 @@ update-hook: update render-hook: render states: (make-hash-table) - active-state: #f)) + active-state: #f + debug?: debug?)) ;; ── Convenience accessors ────────────────────────────────────────────────── @@ -167,6 +170,8 @@ (sdl2:render-clear! (game-renderer game)) (when (game-scene game) (render-scene! (game-renderer game) (game-scene game))) + (when (and (game-debug? game) (game-scene game)) + (render-debug-scene! (game-renderer game) (game-scene game))) (when render-fn (render-fn game))) (sdl2:render-present! (game-renderer game)) (sdl2:delay! (game-frame-delay game)) diff --git a/physics.scm b/physics.scm index 06cb12c..627dbea 100644 --- a/physics.scm +++ b/physics.scm @@ -90,6 +90,8 @@ ;; Resolve collisions with tiles along a single axis. ;; push-fn: (v col row) -> new-pos + ;; For v>0 (moving right/down): snap to the FIRST solid cell (shallowest penetration). + ;; For v<0 (moving left/up): snap to the LAST solid cell (deepest penetration from above/left). (define (resolve-tile-collisions-axis entity tilemap vel-key pos-key push-fn) (let ((v (entity-ref entity vel-key 0))) (if (zero? v) @@ -101,7 +103,9 @@ (tile-id (tilemap-tile-at tilemap col row))) (if (zero? tile-id) acc - (entity-set (entity-set acc pos-key (push-fn v col row)) vel-key 0)))) + (if (and (> v 0) (zero? (entity-ref acc vel-key v))) + acc ; v>0: first collision already resolved, don't overwrite + (entity-set (entity-set acc pos-key (push-fn v col row)) vel-key 0))))) entity (entity-tile-cells entity tilemap))))) diff --git a/renderer.scm b/renderer.scm index 28c7079..e7daf11 100644 --- a/renderer.scm +++ b/renderer.scm @@ -3,12 +3,72 @@ (import scheme (chicken base) (only srfi-1 iota for-each) + srfi-69 (prefix sdl2 "sdl2:") (prefix sdl2-ttf "ttf:") downstroke-entity downstroke-tilemap downstroke-world) + (import defstruct) + + ;; --- Debug colors --- + + (define +debug-player-color+ (sdl2:make-color 64 128 255)) + (define +debug-enemy-color+ (sdl2:make-color 220 40 40)) + (define +debug-attack-color+ (sdl2:make-color 0 200 80)) + (define +debug-tile-color+ (sdl2:make-color 140 0 220)) + + ;; --- Sprite font data structure --- + + (defstruct sprite-font + tile-size ;; integer: pixel width/height of each glyph tile + spacing ;; integer: pixels between characters + char-map) ;; hash-table: char -> tile-id + + ;; Public constructor for sprite-font + ;; ranges: list of (start-char end-char first-tile-id) triples + (define (make-sprite-font* #!key tile-size (spacing 1) ranges) + (let ((ht (make-hash-table))) + (for-each + (lambda (range) + (let ((start-char (car range)) + (end-char (cadr range)) + (first-tile-id (caddr range))) + (let loop ((ch start-char) (tile-id first-tile-id)) + (when (char<=? ch end-char) + (let ((upcase-ch (char-upcase ch))) + (when (hash-table-exists? ht upcase-ch) + (error "sprite-font: overlapping range at char" upcase-ch)) + (hash-table-set! ht upcase-ch tile-id)) + (loop (integer->char (+ (char->integer ch) 1)) (+ tile-id 1)))))) + ranges) + (make-sprite-font tile-size: tile-size spacing: spacing char-map: ht))) + + ;; Look up char tile-id (always upcase) + (define (sprite-font-char->tile-id font ch) + (hash-table-ref/default (sprite-font-char-map font) (char-upcase ch) #f)) + + ;; Compute pixel width of text + (define (sprite-text-width font text) + (let ((n (string-length text))) + (if (zero? n) 0 + (+ (* n (sprite-font-tile-size font)) + (* (- n 1) (sprite-font-spacing font)))))) + + ;; Draw sprite text using a bitmap font + (define (draw-sprite-text renderer tileset-texture tileset font text x y) + (let ((ts (sprite-font-tile-size font)) + (sp (sprite-font-spacing font))) + (let loop ((i 0) (cx x)) + (when (< i (string-length text)) + (let ((tile-id (sprite-font-char->tile-id font (string-ref text i)))) + (when tile-id + (sdl2:render-copy! renderer tileset-texture + (tile-rect (tileset-tile tileset tile-id)) + (sdl2:make-rect cx y ts ts))) + (loop (+ i 1) (+ cx ts sp))))))) + ;; --- Pure functions (no SDL2, testable) --- ;; Returns (x y w h) as a plain list — testable without SDL2 @@ -85,6 +145,19 @@ (sdl2:render-copy! renderer texture #f (sdl2:make-rect x y w h)))) + ;; --- Menu drawing --- + + (define (draw-menu-items renderer font items cursor x y-start y-step + #!key (label-fn identity) (color #f) (prefix "> ") (no-prefix " ")) + (let loop ((i 0) (rest items)) + (unless (null? rest) + (draw-ui-text renderer font + (string-append (if (= i cursor) prefix no-prefix) + (label-fn (car rest))) + (or color (sdl2:make-color 255 255 255)) + x (+ y-start (* i y-step))) + (loop (+ i 1) (cdr rest))))) + ;; --- Scene drawing --- (define (render-scene! renderer scene) @@ -98,4 +171,71 @@ (let ((tileset (tilemap-tileset tilemap))) (draw-entities renderer camera tileset tileset-texture entities)))))) + ;; --- Debug drawing --- + + (define (draw-debug-tiles renderer camera tilemap) + (let ((tw (tilemap-tilewidth tilemap)) + (th (tilemap-tileheight tilemap)) + (cx (camera-x camera)) + (cy (camera-y camera))) + (set! (sdl2:render-draw-color renderer) +debug-tile-color+) + (for-each + (lambda (layer) + (let row-loop ((rows (layer-map layer)) (row 0)) + (unless (null? rows) + (let col-loop ((tiles (car rows)) (col 0)) + (unless (null? tiles) + (unless (zero? (car tiles)) + (sdl2:render-fill-rect! renderer + (sdl2:make-rect (- (* col tw) cx) + (- (* row th) cy) + tw th))) + (col-loop (cdr tiles) (+ col 1)))) + (row-loop (cdr rows) (+ row 1))))) + (tilemap-layers tilemap)))) + + (define (draw-debug-entities renderer camera scene) + (let* ((tilemap (scene-tilemap scene)) + (tw (tilemap-tilewidth tilemap)) + (cx (camera-x camera)) + (cy (camera-y camera))) + (for-each + (lambda (e) + (let ((type (entity-type e)) + (rect (entity->screen-rect e camera))) + (cond + ((eq? type 'player) + (set! (sdl2:render-draw-color renderer) +debug-player-color+) + (sdl2:render-fill-rect! renderer rect) + (when (> (entity-ref e #:attack-timer 0) 0) + (let* ((px (inexact->exact (floor (entity-ref e #:x 0)))) + (py (inexact->exact (floor (entity-ref e #:y 0)))) + (pw (inexact->exact (floor (entity-ref e #:width 0)))) + (ph (inexact->exact (floor (entity-ref e #:height 0)))) + (facing (entity-ref e #:facing 1)) + (ax (if (> facing 0) (+ px pw) (- px tw)))) + (set! (sdl2:render-draw-color renderer) +debug-attack-color+) + (sdl2:render-fill-rect! renderer + (sdl2:make-rect (- ax cx) (- py cy) tw ph))))) + ((eq? type 'enemy) + (set! (sdl2:render-draw-color renderer) +debug-enemy-color+) + (sdl2:render-fill-rect! renderer rect) + (when (> (entity-ref e #:attack-timer 0) 0) + (let* ((ex (inexact->exact (floor (entity-ref e #:x 0)))) + (ey (inexact->exact (floor (entity-ref e #:y 0)))) + (eh (inexact->exact (floor (entity-ref e #:height 0)))) + (facing (entity-ref e #:facing 1)) + (ax (if (> facing 0) (+ ex tw) (- ex tw)))) + (set! (sdl2:render-draw-color renderer) +debug-attack-color+) + (sdl2:render-fill-rect! renderer + (sdl2:make-rect (- ax cx) (- ey cy) tw eh)))))))) + (scene-entities scene)))) + + (define (render-debug-scene! renderer scene) + (let ((camera (scene-camera scene)) + (tilemap (scene-tilemap scene))) + (when tilemap + (draw-debug-tiles renderer camera tilemap)) + (draw-debug-entities renderer camera scene))) + ) ;; end module renderer diff --git a/tests/engine-test.scm b/tests/engine-test.scm index 79b475e..85481ac 100644 --- a/tests/engine-test.scm +++ b/tests/engine-test.scm @@ -104,7 +104,8 @@ ;; --- Renderer module (mock) --- (module downstroke-renderer * (import scheme (chicken base)) - (define (render-scene! . args) #f)) + (define (render-scene! . args) #f) + (define (render-debug-scene! . args) #f)) (import downstroke-renderer) ;; --- Engine module (real) --- @@ -141,7 +142,10 @@ (test-assert "assets registry is created" (game-assets g)) (test-assert "input state is created" - (game-input g)))) + (game-input g)) + (test-equal "debug? defaults to #f" + #f + (game-debug? g)))) (test-group "make-game with keyword args" (let ((g (make-game title: "My Game" width: 320 height: 240 frame-delay: 33))) @@ -150,6 +154,14 @@ (test-equal "custom height" 240 (game-height g)) (test-equal "custom frame-delay" 33 (game-frame-delay g)))) +(test-group "make-game debug? keyword" + (test-equal "debug? defaults to #f" + #f + (game-debug? (make-game))) + (test-equal "debug? can be set to #t" + #t + (game-debug? (make-game debug?: #t)))) + (test-group "game-asset and game-asset-set!" (let ((g (make-game))) (test-equal "missing key returns #f" diff --git a/tests/physics-test.scm b/tests/physics-test.scm index 858dec8..67c8377 100644 --- a/tests/physics-test.scm +++ b/tests/physics-test.scm @@ -294,6 +294,17 @@ (test-equal "pushed above floor" 28 (entity-ref result #:y)) (test-equal "vy zeroed" 0 (entity-ref result #:vy)))))) + (test-group "high-velocity fall: snaps to first solid row, not last" + ;; Regression: entity falls fast enough that apply-velocity-y moves it into TWO solid rows. + ;; Rows 2 and 3 are both solid (tileheight=16, so row 2 = y=[32,47], row 3 = y=[48,63]). + ;; After apply-velocity-y the entity lands at y=34 (overlapping both rows 2 and 3). + ;; Correct: snap to top of row 2 → y=16. Bug was: fold overwrote row 2 snap with row 3 snap → y=32 (inside row 2). + (let* ((tm (make-test-tilemap '((0 0 0) (0 0 0) (1 0 0) (1 0 0) (0 0 0)))) + (e '(#:type player #:x 0 #:y 34 #:width 16 #:height 16 #:vx 0 #:vy 20))) + (let ((result (resolve-tile-collisions-y e tm))) + (test-equal "snapped to first solid row" 16 (entity-ref result #:y)) + (test-equal "vy zeroed" 0 (entity-ref result #:vy))))) + ;; Integration test: simulate the actual game physics loop (test-group "multi-frame physics simulation" (test-group "player falls and lands on floor (10 frames)" diff --git a/tests/renderer-test.scm b/tests/renderer-test.scm index fc5c8f2..1a8f7df 100644 --- a/tests/renderer-test.scm +++ b/tests/renderer-test.scm @@ -3,6 +3,7 @@ (chicken base) (chicken keyword) (only srfi-1 fold iota for-each) + srfi-69 defstruct srfi-64) @@ -21,9 +22,13 @@ (module sdl2 * (import scheme (chicken base)) (define (make-rect x y w h) (list x y w h)) + (define (make-color r g b) (list r g b)) (define (render-copy! . args) #f) (define (render-copy-ex! . args) #f) - (define (create-texture-from-surface . args) #f)) + (define (create-texture-from-surface . args) #f) + (define (render-fill-rect! . args) #f) + ;; Mock SRFI-17 setter for render-draw-color + (define render-draw-color (getter-with-setter (lambda (r) #f) (lambda (r c) #f)))) (import (prefix sdl2 "sdl2:")) ;; Mock sdl2-ttf @@ -110,4 +115,155 @@ (test-assert "does not crash on valid scene" (begin (render-scene! #f scene) #t)))) +(test-group "sprite-font" + (test-group "make-sprite-font*" + (let ((font (make-sprite-font* tile-size: 8 spacing: 1 + ranges: (list (list #\A #\C 100))))) + (test-equal "A maps to 100" + 100 + (sprite-font-char->tile-id font #\A)) + (test-equal "B maps to 101" + 101 + (sprite-font-char->tile-id font #\B)) + (test-equal "C maps to 102" + 102 + (sprite-font-char->tile-id font #\C)))) + + (test-group "sprite-font-char->tile-id" + (let ((font (make-sprite-font* tile-size: 8 spacing: 1 + ranges: (list (list #\A #\Z 100))))) + (test-equal "returns #f for unmapped char" + #f + (sprite-font-char->tile-id font #\1)) + (test-equal "auto-upcase: lowercase a maps to uppercase" + 100 + (sprite-font-char->tile-id font #\a)))) + + (test-group "overlapping ranges" + (import (chicken condition)) + (let ((caught-error #f)) + (condition-case + (make-sprite-font* tile-size: 8 spacing: 1 + ranges: (list (list #\A #\C 100) + (list #\B #\D 200))) + (e (exn) + (set! caught-error #t))) + (test-assert "signals error on overlapping range" + caught-error))) + + (test-group "sprite-text-width" + (let ((font (make-sprite-font* tile-size: 8 spacing: 1 + ranges: (list (list #\A #\Z 100))))) + (test-equal "empty string width is 0" + 0 + (sprite-text-width font "")) + (test-equal "single char width is tile-size" + 8 + (sprite-text-width font "A")) + (test-equal "two chars: 2*tile-size + 1*spacing" + 17 + (sprite-text-width font "AB")) + (test-equal "three chars: 3*tile-size + 2*spacing" + 26 + (sprite-text-width font "ABC")))) + + (test-group "draw-sprite-text" + (let* ((font (make-sprite-font* tile-size: 8 spacing: 1 + ranges: (list (list #\A #\Z 100)))) + (tileset (make-tileset tilewidth: 8 tileheight: 8 + spacing: 0 tilecount: 100 columns: 10 + image-source: "" image: #f)) + (renderer #f) + (texture #f)) + (test-assert "does not crash with valid text" + (begin (draw-sprite-text renderer texture tileset font "HELLO" 10 20) #t)) + (test-assert "does not crash with unmapped chars" + (begin (draw-sprite-text renderer texture tileset font "A1B" 0 0) #t))))) + +(test-group "draw-menu-items" + (test-assert "does not crash with 3 items, cursor=1" + (let ((font #f)) ; mock font + (begin + (draw-menu-items #f font '("Item 1" "Item 2" "Item 3") 1 10 20 30) + #t))) + + (test-assert "does not crash with keyword args label-fn:" + (let ((font #f)) + (begin + (draw-menu-items #f font '("A" "B" "C") 0 10 20 30 + label-fn: (lambda (x) (string-append "[" x "]"))) + #t))) + + (test-assert "does not crash with keyword args prefix:" + (let ((font #f)) + (begin + (draw-menu-items #f font '("Item 1" "Item 2") 1 10 20 30 + prefix: ">>> " no-prefix: " ") + #t)))) + +(test-group "debug-drawing" + (test-group "draw-debug-tiles" + (let* ((cam (make-camera x: 0 y: 0)) + (tileset (make-tileset tilewidth: 16 tileheight: 16 + spacing: 0 tilecount: 100 columns: 10 + image-source: "" image: #f)) + (layer (make-layer name: "ground" width: 2 height: 2 + map: '((0 1) (2 3)))) + (tilemap (make-tilemap width: 2 height: 2 + tilewidth: 16 tileheight: 16 + tileset-source: "" + tileset: tileset + layers: (list layer) + objects: '())) + (renderer #f)) + (test-assert "does not crash with 2x2 tilemap" + (begin (draw-debug-tiles renderer cam tilemap) #t)))) + + (test-group "draw-debug-entities" + (let* ((cam (make-camera x: 0 y: 0)) + (tileset (make-tileset tilewidth: 16 tileheight: 16 + spacing: 0 tilecount: 100 columns: 10 + image-source: "" image: #f)) + (layer (make-layer name: "ground" width: 1 height: 1 + map: '((0)))) + (tilemap (make-tilemap width: 1 height: 1 + tilewidth: 16 tileheight: 16 + tileset-source: "" + tileset: tileset + layers: (list layer) + objects: '())) + (player (list #:type 'player #:x 10 #:y 20 #:width 16 #:height 16 #:facing 1)) + (enemy (list #:type 'enemy #:x 50 #:y 60 #:width 16 #:height 16 #:facing -1)) + (scene (make-scene entities: (list player enemy) + tilemap: tilemap + camera: cam + tileset-texture: #f + camera-target: #f)) + (renderer #f)) + (test-assert "does not crash with player and enemy entities" + (begin (draw-debug-entities renderer cam scene) #t)))) + + (test-group "render-debug-scene!" + (let* ((cam (make-camera x: 0 y: 0)) + (tileset (make-tileset tilewidth: 16 tileheight: 16 + spacing: 0 tilecount: 100 columns: 10 + image-source: "" image: #f)) + (layer (make-layer name: "ground" width: 1 height: 1 + map: '((0)))) + (tilemap (make-tilemap width: 1 height: 1 + tilewidth: 16 tileheight: 16 + tileset-source: "" + tileset: tileset + layers: (list layer) + objects: '())) + (player (list #:type 'player #:x 10 #:y 20 #:width 16 #:height 16 #:facing 1)) + (scene (make-scene entities: (list player) + tilemap: tilemap + camera: cam + tileset-texture: #f + camera-target: #f)) + (renderer #f)) + (test-assert "does not crash with full scene" + (begin (render-debug-scene! renderer scene) #t))))) + (test-end "renderer") @@ -61,8 +61,10 @@ ;; Center camera on entity. Clamps to >= 0 on both axes. ;; viewport-w and viewport-h are the game window dimensions (pixels). (define (camera-follow! camera entity viewport-w viewport-h) - (camera-x-set! camera (max 0 (- (entity-ref entity #:x 0) (/ viewport-w 2)))) - (camera-y-set! camera (max 0 (- (entity-ref entity #:y 0) (/ viewport-h 2))))) + (let* ((entity-x (entity-ref entity #:x 0)) + (entity-y (entity-ref entity #:y 0))) + (camera-x-set! camera (inexact->exact (floor (max 0 (- entity-x (/ viewport-w 2)))))) + (camera-y-set! camera (inexact->exact (floor (max 0 (- entity-y (/ viewport-h 2)))))))) ;; Returns the first entity in scene whose #:tags list contains tag, or #f. (define (scene-find-tagged scene tag) |
