Files
Pacman-Project/pacman-project/adt/draw.rkt
joren 5b43b3c8d5 Optimize(Draw): Skip unchanged elements in draw callback
Previously every frame: cleared+redrawed 868 cells for coins, cleared+
redrawed all UI text, emptied+recreated pause layer, and swapped key
sprites repeatedly.

Now uses dirty flags and cached values:
- Coins: only redrawn when coins-dirty? is set (on coin eat/key pickup)
- Maze: only redrawn when a door is removed
- UI: only redrawn when score or time string actually changes
- Key: sprite swap happens exactly once, not every frame
- Pause: layer only modified when paused? state transitions

Exposes mark-coins-dirty! and mark-maze-dirty! messages so the game
ADT can signal changes from the level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:21:20 +01:00

289 lines
10 KiB
Racket

#lang r7rs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Draw ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; All graphics logic is isolated in this ADT. Game logic knows nothing about
;; pixels, windows, or sprites. Grid-to-pixel conversion happens exclusively
;; here.
;;
;; Performance: coins, UI, maze, key, and pause are only redrawn when their
;; underlying state changes. The draw callback tracks previous values and
;; skips unchanged elements.
(define-library (pacman-project adt draw)
(import (scheme base)
(pp1 graphics)
(pacman-project constants))
(export make-draw)
(begin
;; make-draw :: number, number -> draw
;; Creates the draw object that handles all rendering.
(define (make-draw width height)
(let ((window (make-window width height "Pacman")))
((window 'set-background!) "black")
;;
;; Layers (order determines draw order)
;;
(define maze-layer ((window 'new-layer!)))
(define coins-layer ((window 'new-layer!)))
(define key-layer ((window 'new-layer!)))
(define pacman-layer ((window 'new-layer!)))
(define ui-layer ((window 'new-layer!)))
(define pause-layer ((window 'new-layer!)))
;;
;; Maze tiles
;;
(define maze-tile (make-tile width height))
((maze-layer 'add-drawable!) maze-tile)
;;
;; Coins tile
;;
(define coins-tile (make-tile width height))
((coins-layer 'add-drawable!) coins-tile)
;;
;; Key sprite (in the maze)
;;
(define key-sprite (make-bitmap-tile "pacman-sprites/key.png"))
((key-sprite 'set-scale!) sprite-scale-key)
((key-layer 'add-drawable!) key-sprite)
;; Key UI indicator (next to the score)
(define key-ui-sprite (make-bitmap-tile "pacman-sprites/key.png"))
((key-ui-sprite 'set-scale!) sprite-scale-key-ui)
((key-ui-sprite 'set-x!) key-ui-x)
((key-ui-sprite 'set-y!) key-ui-y)
;;
;; Pac-Man sprite
;;
(define pacman-bitmap-tiles
(list (make-bitmap-tile "pacman-sprites/pacman-death-1.png")
(make-bitmap-tile "pacman-sprites/pacman-closed.png")
(make-bitmap-tile "pacman-sprites/pacman-open.png")))
(define pacman-sprite (make-tile-sequence pacman-bitmap-tiles))
((pacman-sprite 'set-scale!) sprite-scale-pacman)
((pacman-layer 'add-drawable!) pacman-sprite)
;; Animation state
(define time-since-last-animation 0)
;;
;; UI tiles
;;
(define ui-tile (make-tile width height))
((ui-layer 'add-drawable!) ui-tile)
;;
;; Change tracking — skip redraws when state hasn't changed
;;
(define cached-score -1)
(define cached-time "")
(define cached-key-taken? #f)
(define key-sprite-swapped? #f)
(define cached-paused? #f)
(define coins-dirty? #t)
;;
;; Coordinate conversion
;;
;; grid->pixel-x :: number -> number
(define (grid->pixel-x col)
(* cell-size-px col))
;; grid->pixel-y :: number -> number
(define (grid->pixel-y row)
(+ (* row cell-size-px) maze-offset-y))
;;
;; Draw functions
;;
;; draw-maze! :: maze -> /
;; Draws all walls and doors. Called once at startup and after door
;; removal — never per-frame.
(define (draw-maze! maze)
((maze-tile 'clear!))
((maze 'for-each-cell)
(lambda (row col cell-type)
(cond
((= cell-type cell-type-wall)
((maze-tile 'draw-rectangle!)
(grid->pixel-x col)
(grid->pixel-y row)
(- cell-size-px maze-wall-shrink)
(- cell-size-px maze-wall-shrink)
"blue"))
((= cell-type cell-type-door)
((maze-tile 'draw-rectangle!)
(grid->pixel-x col)
(grid->pixel-y row)
(- cell-size-px maze-wall-shrink)
(- cell-size-px maze-wall-shrink)
"pink"))))))
;; draw-coins! :: maze -> /
;; Redraws all coins. Only called when coins-dirty? is true.
(define (draw-coins! maze)
((coins-tile 'clear!))
((maze 'for-each-cell)
(lambda (row col cell-type)
(when (= cell-type cell-type-coin)
((coins-tile 'draw-rectangle!)
(+ (grid->pixel-x col) coin-inset)
(+ (grid->pixel-y row) coin-inset)
(- cell-size-px (* 2 coin-inset) 6)
(- cell-size-px (* 2 coin-inset) 6)
"yellow")))))
;; draw-key! :: key -> /
;; Swaps the key sprite once when taken. No-op on subsequent frames.
(define (draw-key! key-obj)
(when (and (key-obj 'taken?) (not key-sprite-swapped?))
((key-layer 'remove-drawable!) key-sprite)
((key-layer 'add-drawable!) key-ui-sprite)
(set! key-sprite-swapped? #t)))
;; animate-pacman! :: number -> /
;; Advances the Pac-Man sprite animation based on elapsed time.
(define (animate-pacman! delta-time)
(set! time-since-last-animation (+ time-since-last-animation delta-time))
(when (>= time-since-last-animation animation-interval-ms)
((pacman-sprite 'set-next!))
(set! time-since-last-animation 0)))
;; draw-pacman! :: pacman -> /
;; Updates Pac-Man position and rotation. Lightweight — just sets
;; properties on an existing sprite.
(define (draw-pacman! pacman)
(let* ((pos (pacman 'position))
(direction (pacman 'direction)))
((pacman-sprite 'set-x!) (grid->pixel-x (pos 'col)))
((pacman-sprite 'set-y!) (grid->pixel-y (pos 'row)))
(cond ((eq? direction 'right) ((pacman-sprite 'rotate!) rotation-right))
((eq? direction 'left) ((pacman-sprite 'rotate!) rotation-left))
((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up))
((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down)))))
;; draw-ui! :: score, timer -> /
;; Redraws score and time only when their values have changed.
(define (draw-ui! score timer)
(let ((current-score (score 'points))
(current-time ((timer 'format-time))))
(when (or (not (= current-score cached-score))
(not (string=? current-time cached-time)))
((ui-tile 'clear!))
;; Score
((ui-tile 'draw-text!)
(number->string current-score)
score-text-size score-text-x score-text-y "white")
;; Separator line
((ui-tile 'draw-rectangle!)
separator-x 0 separator-width height "white")
;; Time limit
((ui-tile 'draw-text!)
"Time remaining:" time-text-size time-label-x time-label-y "white")
((ui-tile 'draw-text!)
current-time
score-text-size time-value-x time-value-y "white")
;; Update cache
(set! cached-score current-score)
(set! cached-time current-time))))
;; draw-pause! :: boolean -> /
;; Only redraws when pause state actually changes.
(define (draw-pause! paused?)
(when (not (eq? paused? cached-paused?))
((pause-layer 'empty!))
(when paused?
(let ((pause-tile (make-tile width height)))
((pause-layer 'add-drawable!) pause-tile)
((pause-tile 'draw-rectangle!) 0 90 670 height "black")
((pause-tile 'draw-text!) "Game Paused" 40 200 400 "red")))
(set! cached-paused? paused?)))
;; mark-coins-dirty! :: -> /
;; Called by the game when a coin is eaten, so coins are redrawn
;; next frame.
(define (mark-coins-dirty!)
(set! coins-dirty? #t))
;; mark-maze-dirty! :: -> /
;; Called by the game when a door is removed, so the maze is
;; redrawn next frame.
(define (mark-maze-dirty!)
(set! coins-dirty? #t))
;;
;; Main draw function
;;
;; draw-game! :: game -> /
;; Draws the full game. Only redraws changed elements.
(define (draw-game! game)
(let ((level (game 'level)))
;; Always update (lightweight sprite property sets)
(draw-pacman! (level 'pacman))
;; Only redraw when dirty / changed
(draw-key! (level 'key))
(when coins-dirty?
(draw-coins! (level 'maze))
(draw-maze! (level 'maze))
(set! coins-dirty? #f))
(draw-ui! (level 'score) (level 'timer))
(draw-pause! (level 'paused?))))
;;
;; Callback registration
;;
;; set-game-loop! :: (number -> /) -> /
(define (set-game-loop! fun)
((window 'set-update-callback!) fun))
;; set-key-callback! :: (symbol, any -> /) -> /
(define (set-key-callback! fun)
((window 'set-key-callback!) fun))
;; start-drawing! :: game -> /
;; Starts drawing by setting the draw callback.
(define (start-drawing! game)
;; Initial draw (one-time)
(draw-maze! ((game 'level) 'maze))
(draw-coins! ((game 'level) 'maze))
(set! coins-dirty? #f)
((window 'set-draw-callback!)
(lambda () (draw-game! game))))
;;
;; Dispatch
;;
(define (dispatch-draw msg)
(cond ((eq? msg 'set-game-loop!) set-game-loop!)
((eq? msg 'set-key-callback!) set-key-callback!)
((eq? msg 'start-drawing!) start-drawing!)
((eq? msg 'animate-pacman!) animate-pacman!)
((eq? msg 'mark-coins-dirty!) mark-coins-dirty!)
((eq? msg 'mark-maze-dirty!) mark-maze-dirty!)
(else (error "Draw ADT -- Unknown message:" msg))))
dispatch-draw))))