diff options
| author | Gene Pasquet <dev@etenil.net> | 2026-04-08 00:38:55 +0100 |
|---|---|---|
| committer | Gene Pasquet <dev@etenil.net> | 2026-04-08 00:38:55 +0100 |
| commit | 0c3a700aa94a0256c5e5b1a14819f10b3d3e869b (patch) | |
| tree | c1b0dc233769bea9f6a545333687539ace5b3804 | |
| parent | f8cc4a748bb8b6431a1023a876745b1bb473eb19 (diff) | |
Support scling
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | demo/scaling.scm | 77 | ||||
| -rw-r--r-- | docs/api.org | 45 | ||||
| -rw-r--r-- | docs/guide.org | 27 | ||||
| -rw-r--r-- | engine.scm | 23 | ||||
| -rw-r--r-- | tests/engine-test.scm | 30 |
6 files changed, 193 insertions, 11 deletions
@@ -4,7 +4,7 @@ MODULE_NAMES := entity tween 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 spritefont menu tweens +DEMO_NAMES := platformer shmup topdown audio sandbox spritefont menu tweens scaling DEMO_BINS := $(patsubst %,bin/demo-%,$(DEMO_NAMES)) UNIT_NAMES := $(patsubst %,downstroke-%,$(MODULE_NAMES)) diff --git a/demo/scaling.scm b/demo/scaling.scm new file mode 100644 index 0000000..983e583 --- /dev/null +++ b/demo/scaling.scm @@ -0,0 +1,77 @@ +(import scheme + (chicken base) + (prefix sdl2 "sdl2:") + (prefix sdl2-ttf "ttf:") + downstroke-engine + downstroke-world + downstroke-entity + downstroke-input + downstroke-renderer + downstroke-assets) + +;; Logical resolution: 320×240, displayed at 640×480 via scale: 2 + +(define +width+ 320) +(define +height+ 240) +(define +box-size+ 16) +(define +speed+ 2) + +(define *game* + (make-game + title: "Demo: Scaling (2×)" + width: +width+ height: +height+ + scale: 2 + + create: (lambda (game) + (game-scene-set! game + (make-scene + entities: (list (list #:type 'box + #:x (/ +width+ 2) + #:y (/ +height+ 2) + #:width +box-size+ + #:height +box-size+ + #:vx 0 #:vy 0 + #:color '(255 200 0))) + tilemap: #f + camera: (make-camera x: 0 y: 0) + tileset-texture: #f + camera-target: #f + background: '(30 30 50)))) + + update: (lambda (game dt) + (let* ((input (game-input game)) + (scene (game-scene game)) + (box (car (scene-entities scene))) + (vx (cond ((input-held? input 'left) (- +speed+)) + ((input-held? input 'right) +speed+) + (else 0))) + (vy (cond ((input-held? input 'up) (- +speed+)) + ((input-held? input 'down) +speed+) + (else 0))) + (nx (max 0 (min (- +width+ +box-size+) + (+ (entity-ref box #:x 0) vx)))) + (ny (max 0 (min (- +height+ +box-size+) + (+ (entity-ref box #:y 0) vy)))) + (box (entity-set (entity-set box #:x nx) #:y ny))) + (scene-entities-set! scene (list box)))) + + render: (lambda (game) + (let* ((renderer (game-renderer game)) + (scene (game-scene game)) + (box (car (scene-entities scene))) + (bx (inexact->exact (floor (entity-ref box #:x 0)))) + (by (inexact->exact (floor (entity-ref box #:y 0))))) + ;; Draw the colored box + (set! (sdl2:render-draw-color renderer) (sdl2:make-color 255 200 0)) + (sdl2:render-fill-rect! renderer + (sdl2:make-rect bx by +box-size+ +box-size+)) + ;; Draw a border around the logical viewport + (set! (sdl2:render-draw-color renderer) (sdl2:make-color 100 100 100)) + (sdl2:render-draw-rect! renderer + (sdl2:make-rect 0 0 +width+ +height+)) + ;; Draw crosshair at center + (set! (sdl2:render-draw-color renderer) (sdl2:make-color 60 60 80)) + (sdl2:render-draw-line! renderer (/ +width+ 2) 0 (/ +width+ 2) +height+) + (sdl2:render-draw-line! renderer 0 (/ +height+ 2) +width+ (/ +height+ 2)))))) + +(game-run! *game*) diff --git a/docs/api.org b/docs/api.org index 45593e4..c418ec8 100644 --- a/docs/api.org +++ b/docs/api.org @@ -17,6 +17,7 @@ The engine module provides the top-level game lifecycle and state management. (title "Downstroke Game") (width 640) (height 480) + (scale 1) (frame-delay 16) (input-config *default-input-config*) (preload #f) @@ -31,8 +32,9 @@ Creates and initializes a game object. All parameters are optional keywords. | Parameter | Type | Default | Description | |-----------+------+---------+-------------| | ~title~ | string | "Downstroke Game" | Window title | -| ~width~ | integer | 640 | Game window width in pixels | -| ~height~ | integer | 480 | Game window height in pixels | +| ~width~ | integer | 640 | Logical game width in pixels | +| ~height~ | integer | 480 | Logical game height in pixels | +| ~scale~ | positive integer | 1 | Whole-game pixel scaling factor (window = width×scale by height×scale) | | ~frame-delay~ | integer | 16 | Delay between frames in milliseconds (30 FPS ≈ 33) | | ~input-config~ | input-config | *default-input-config* | Keyboard/controller mappings | | ~preload~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called once before create | @@ -43,13 +45,50 @@ Creates and initializes a game object. All parameters are optional keywords. The game object is the central hub. Use it to store/retrieve assets, manage scenes, and access the current input state. +*** Scaling + +The ~scale:~ parameter controls integer pixel scaling for the entire game. When ~scale:~ is greater than 1, the OS window is created at ~width×scale~ by ~height×scale~ pixels, but SDL2's logical renderer size is set to ~width~ by ~height~. This means all game code (rendering, physics, coordinates) works in the logical resolution — SDL2 handles the upscaling automatically. + +This affects everything uniformly: tiles, sprites, text, colored rectangles, and debug overlays. Mouse/touch input coordinates are also automatically mapped back to the logical resolution. + +#+begin_src scheme +;; 320×240 game rendered in a 640×480 window (2× pixel scaling) +(make-game title: "Pixel Art Game" width: 320 height: 240 scale: 2) + +;; 256×224 NES-style with 3× scaling → 768×672 window +(make-game title: "Retro Game" width: 256 height: 224 scale: 3) +#+end_src + +Only positive integers are accepted; fractional or zero values signal an error. + +*** Fullscreen + +Downstroke does not provide a built-in ~fullscreen:~ keyword, but you can make any game fullscreen by setting the SDL2 window flag after the game starts. Use the ~preload:~ hook (the window exists by then): + +#+begin_src scheme +(make-game + title: "My Game" width: 320 height: 240 scale: 2 + preload: (lambda (game) + ;; 'fullscreen-desktop scales to fill the screen without changing resolution + (sdl2:window-fullscreen-set! (game-window game) 'fullscreen-desktop))) +#+end_src + +The two fullscreen modes are: + +| Mode | SDL2 symbol | Behavior | +|------+-------------+-----------| +| Desktop fullscreen | ~'fullscreen-desktop~ | Fills the screen at the desktop resolution; SDL2 handles scaling and letterboxing. Best for most games. | +| Exclusive fullscreen | ~'fullscreen~ | Changes the display resolution to match ~width×scale~ by ~height×scale~. Use only when you need exclusive display control. | + +Combine with ~scale:~ to get pixel-perfect fullscreen: set your logical resolution small (e.g. 320×240), use ~scale:~ for the default windowed size, and let ~'fullscreen-desktop~ handle the rest. + ** ~game-run!~ #+begin_src scheme (game-run! game) #+end_src -Starts the main event loop. Initializes SDL2, opens the window, and runs the frame loop indefinitely until the user quits or the ~quit~ action is pressed. Never returns. +Starts the main event loop. Initializes SDL2, opens the window (at ~width×scale~ by ~height×scale~ pixels), sets the logical render size when ~scale~ > 1, and runs the frame loop indefinitely until the user quits or the ~quit~ action is pressed. Never returns. Lifecycle order within each frame: 1. Collect SDL2 events diff --git a/docs/guide.org b/docs/guide.org index 380f04b..1c5a8f3 100644 --- a/docs/guide.org +++ b/docs/guide.org @@ -216,6 +216,32 @@ See =demo/platformer.scm= in the engine source for a complete working example. * Development Tips +** Pixel Scaling + +Retro-style games often use a small logical resolution (e.g. 320×240) but need a larger window so players can actually see things. Use ~scale:~ to scale everything uniformly: + +#+begin_src scheme +(make-game title: "Pixel Art" width: 320 height: 240 scale: 2) +;; Creates a 640×480 window; all coordinates remain in 320×240 space +#+end_src + +This is integer-only scaling. All rendering — tiles, sprites, text, debug overlays — is scaled automatically by SDL2. Game logic and coordinates are unaffected. + +See =demo/scaling.scm= for a complete example. + +** Fullscreen + +Use SDL2 window flags in the ~preload:~ hook to go fullscreen: + +#+begin_src scheme +(make-game + title: "Fullscreen Game" width: 320 height: 240 scale: 2 + preload: (lambda (game) + (sdl2:window-fullscreen-set! (game-window game) 'fullscreen-desktop))) +#+end_src + +~'fullscreen-desktop~ fills the screen without changing display resolution; SDL2 handles letterboxing and scaling. ~'fullscreen~ uses exclusive fullscreen at the window size. + ** 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: @@ -252,6 +278,7 @@ Downstroke includes several demo games that showcase different features: | 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 | | Tweens | =demo/tweens.scm= | Easing curves, =tween-step=, =#:skip-pipelines= with tile collision | +| Scaling | =demo/scaling.scm= | Integer pixel scaling with =scale: 2=, 2× upscaled rendering | Each demo is self-contained and serves as a working reference for a particular game mechanic. @@ -18,6 +18,7 @@ (defstruct game title width height + scale ;; positive integer: whole-game pixel scaling factor window renderer input ;; input-state record input-config ;; input-config record @@ -41,14 +42,18 @@ (define (make-game #!key (title "Downstroke Game") (width 640) (height 480) + (scale 1) (frame-delay 16) (input-config *default-input-config*) (preload #f) (create #f) (update #f) (render #f) (debug? #f)) + (unless (and (integer? scale) (positive? scale)) + (error "make-game: scale must be a positive integer" scale)) (make-game* title: title width: width height: height + scale: scale window: #f renderer: #f scene: #f @@ -128,12 +133,18 @@ (sdl2:game-controller-open! i)) (init-controllers (+ i 1)))) - ;; 2. Create window + renderer - (game-window-set! game - (sdl2:create-window! (game-title game) 'centered 'centered - (game-width game) (game-height game) '())) - (game-renderer-set! game - (sdl2:create-renderer! (game-window game) -1 '(accelerated))) + ;; 2. Create window + renderer (window size = logical size × scale) + (let ((scale (game-scale game))) + (game-window-set! game + (sdl2:create-window! (game-title game) 'centered 'centered + (* (game-width game) scale) + (* (game-height game) scale) '())) + (game-renderer-set! game + (sdl2:create-renderer! (game-window game) -1 '(accelerated))) + (when (> scale 1) + (sdl2:render-logical-size-set! + (game-renderer game) + (list (game-width game) (game-height game))))) ;; 3. preload: hook — user loads assets here (when (game-preload-hook game) diff --git a/tests/engine-test.scm b/tests/engine-test.scm index f886165..9290ad7 100644 --- a/tests/engine-test.scm +++ b/tests/engine-test.scm @@ -18,6 +18,7 @@ (define (game-controller-open! i) #f) (define (create-window! . args) 'mock-window) (define (create-renderer! . args) 'mock-renderer) + (define (render-logical-size-set! . args) #f) (define (destroy-window! . args) #f) (define (make-color r g b #!optional (a 255)) (list r g b a)) (define render-draw-color (getter-with-setter (lambda (r) #f) (lambda (r c) #f))) @@ -147,7 +148,10 @@ (game-input g)) (test-equal "debug? defaults to #f" #f - (game-debug? g)))) + (game-debug? g)) + (test-equal "scale defaults to 1" + 1 + (game-scale g)))) (test-group "make-game with keyword args" (let ((g (make-game title: "My Game" width: 320 height: 240 frame-delay: 33))) @@ -164,6 +168,30 @@ #t (game-debug? (make-game debug?: #t)))) +(test-group "make-game scale keyword" + (test-equal "scale defaults to 1" + 1 + (game-scale (make-game))) + (test-equal "scale can be set to 2" + 2 + (game-scale (make-game scale: 2))) + (test-equal "scale can be set to 3" + 3 + (game-scale (make-game scale: 3))) + (import (chicken condition)) + (let ((caught #f)) + (condition-case (make-game scale: 0) + (e (exn) (set! caught #t))) + (test-assert "scale: 0 signals error" caught)) + (let ((caught #f)) + (condition-case (make-game scale: -1) + (e (exn) (set! caught #t))) + (test-assert "scale: -1 signals error" caught)) + (let ((caught #f)) + (condition-case (make-game scale: 1.5) + (e (exn) (set! caught #t))) + (test-assert "scale: 1.5 signals error" caught))) + (test-group "game-asset and game-asset-set!" (let ((g (make-game))) (test-equal "missing key returns #f" |
