Add smooth sub-tile interpolation for Pac-Man and ghosts

Entities now move smoothly between tiles instead of snapping. Previous
positions are tracked in pacman and ghost ADTs; the draw layer linearly
interpolates between prev and current based on movement timer progress.

Residual time is carried across movement ticks for consistent speed at
varying frame rates. Teleportation and ghost house exits call sync-prev!
to prevent cross-map interpolation artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-23 11:52:00 +01:00
parent 9028dd031c
commit f251478dd6
4 changed files with 88 additions and 22 deletions

View File

@@ -256,27 +256,45 @@
(for-each (lambda (gds) ((gds 'animate!))) ghost-draw-states) (for-each (lambda (gds) ((gds 'animate!))) ghost-draw-states)
(set! time-since-last-animation 0))) (set! time-since-last-animation 0)))
;; draw-pacman! :: pacman -> / ;; lerp :: number, number, number -> number
(define (draw-pacman! pacman) ;; Linear interpolation between a and b by factor t (0..1).
(define (lerp a b t)
(+ a (* t (- b a))))
;; draw-pacman! :: pacman, number -> /
;; Draws Pac-Man with smooth interpolation between tiles.
(define (draw-pacman! pacman progress)
(let* ((pos (pacman 'position)) (let* ((pos (pacman 'position))
(row (pos 'row))
(col (pos 'col))
(prev-row (pacman 'prev-row))
(prev-col (pacman 'prev-col))
(t (min progress 1))
(render-col (lerp prev-col col t))
(render-row (lerp prev-row row t))
(direction (pacman 'direction))) (direction (pacman 'direction)))
((pacman-sprite 'set-x!) (grid->pixel-x (pos 'col))) ((pacman-sprite 'set-x!) (grid->pixel-x render-col))
((pacman-sprite 'set-y!) (grid->pixel-y (pos 'row))) ((pacman-sprite 'set-y!) (grid->pixel-y render-row))
(cond ((eq? direction 'right) ((pacman-sprite 'rotate!) rotation-right)) (cond ((eq? direction 'right) ((pacman-sprite 'rotate!) rotation-right))
((eq? direction 'left) ((pacman-sprite 'rotate!) rotation-left)) ((eq? direction 'left) ((pacman-sprite 'rotate!) rotation-left))
((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up)) ((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up))
((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down))))) ((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down)))))
;; draw-ghosts! :: list -> / ;; draw-ghosts! :: list -> /
;; Updates all ghost sprite positions and directions. ;; Updates all ghost sprite positions with smooth interpolation.
(define (draw-ghosts! ghosts) (define (draw-ghosts! ghosts)
(for-each (for-each
(lambda (ghost ghost-draw) (lambda (ghost ghost-draw)
(let* ((pos (ghost 'position)) (let* ((pos (ghost 'position))
(row (pos 'row)) (row (pos 'row))
(col (pos 'col)) (col (pos 'col))
(prev-row (ghost 'prev-row))
(prev-col (ghost 'prev-col))
(t (min (/ (ghost 'movement-timer) ghost-speed-ms) 1))
(render-row (lerp prev-row row t))
(render-col (lerp prev-col col t))
(dir (ghost 'direction))) (dir (ghost 'direction)))
((ghost-draw 'update!) row col dir))) ((ghost-draw 'update!) render-row render-col dir)))
ghosts ghost-draw-states)) ghosts ghost-draw-states))
;; draw-ui! :: score, timer -> / ;; draw-ui! :: score, timer -> /
@@ -343,9 +361,10 @@
;; draw-game! :: game -> / ;; draw-game! :: game -> /
(define (draw-game! game) (define (draw-game! game)
(let* ((level (game 'level)) (let* ((level (game 'level))
(timer (level 'timer))) (timer (level 'timer))
(pac-progress (/ (level 'pacman-movement-timer) pacman-speed-ms)))
;; Always update (lightweight sprite property sets) ;; Always update (lightweight sprite property sets)
(draw-pacman! (level 'pacman)) (draw-pacman! (level 'pacman) pac-progress)
(draw-ghosts! (level 'ghosts)) (draw-ghosts! (level 'ghosts))
;; Only redraw when dirty / changed ;; Only redraw when dirty / changed
(draw-key! (level 'key)) (draw-key! (level 'key))

View File

@@ -27,7 +27,9 @@
(scatter-target (make-position scatter-row scatter-col)) (scatter-target (make-position scatter-row scatter-col))
(house-timer exit-delay) (house-timer exit-delay)
(movement-timer 0) (movement-timer 0)
(reverse-queued? #f)) (reverse-queued? #f)
(prev-row start-row)
(prev-col start-col))
;; direction! :: symbol -> / ;; direction! :: symbol -> /
(define (direction! new-dir) (define (direction! new-dir)
@@ -44,10 +46,20 @@
(set! mode new-mode)) (set! mode new-mode))
;; move! :: number, number -> / ;; move! :: number, number -> /
;; Saves previous position, then moves by delta.
(define (move! delta-row delta-col) (define (move! delta-row delta-col)
(set! prev-row (position 'row))
(set! prev-col (position 'col))
((position 'row!) (+ (position 'row) delta-row)) ((position 'row!) (+ (position 'row) delta-row))
((position 'col!) (+ (position 'col) delta-col))) ((position 'col!) (+ (position 'col) delta-col)))
;; sync-prev! :: -> /
;; Sets previous position to current. Call after teleportation
;; or ghost house exit to prevent long-range interpolation.
(define (sync-prev!)
(set! prev-row (position 'row))
(set! prev-col (position 'col)))
;; consume-reverse! :: -> boolean ;; consume-reverse! :: -> boolean
;; Returns #t and clears the flag if a reverse was queued. ;; Returns #t and clears the flag if a reverse was queued.
(define (consume-reverse!) (define (consume-reverse!)
@@ -67,6 +79,10 @@
(define (reset-movement-timer!) (define (reset-movement-timer!)
(set! movement-timer 0)) (set! movement-timer 0))
;; set-movement-timer! :: number -> /
(define (set-movement-timer! val)
(set! movement-timer val))
;; advance-movement-timer! :: number -> number ;; advance-movement-timer! :: number -> number
;; Returns updated movement timer value. ;; Returns updated movement timer value.
(define (advance-movement-timer! delta-time) (define (advance-movement-timer! delta-time)
@@ -82,6 +98,9 @@
((eq? msg 'mode) mode) ((eq? msg 'mode) mode)
((eq? msg 'mode!) mode!) ((eq? msg 'mode!) mode!)
((eq? msg 'move!) move!) ((eq? msg 'move!) move!)
((eq? msg 'prev-row) prev-row)
((eq? msg 'prev-col) prev-col)
((eq? msg 'sync-prev!) sync-prev!)
((eq? msg 'scatter-target) scatter-target) ((eq? msg 'scatter-target) scatter-target)
((eq? msg 'in-house?) (eq? mode 'in-house)) ((eq? msg 'in-house?) (eq? mode 'in-house))
((eq? msg 'consume-reverse!) consume-reverse!) ((eq? msg 'consume-reverse!) consume-reverse!)
@@ -89,6 +108,7 @@
((eq? msg 'movement-timer) movement-timer) ((eq? msg 'movement-timer) movement-timer)
((eq? msg 'advance-movement-timer!) advance-movement-timer!) ((eq? msg 'advance-movement-timer!) advance-movement-timer!)
((eq? msg 'reset-movement-timer!) reset-movement-timer!) ((eq? msg 'reset-movement-timer!) reset-movement-timer!)
((eq? msg 'set-movement-timer!) set-movement-timer!)
(else (error "Ghost ADT -- Unknown message:" msg)))) (else (error "Ghost ADT -- Unknown message:" msg))))
dispatch-ghost)))) dispatch-ghost))))

View File

@@ -263,9 +263,11 @@
;; Handle tunnel teleportation ;; Handle tunnel teleportation
(cond (cond
((< next-col 0) ((< next-col 0)
((pos 'col!) (- (maze 'cols) 1))) ((pos 'col!) (- (maze 'cols) 1))
((ghost 'sync-prev!)))
((>= next-col (maze 'cols)) ((>= next-col (maze 'cols))
((pos 'col!) 0)) ((pos 'col!) 0)
((ghost 'sync-prev!)))
(else (else
((ghost 'move!) (car delta) (cdr delta)))))) ((ghost 'move!) (car delta) (cdr delta))))))
@@ -289,6 +291,7 @@
((pos 'row!) ghost-house-exit-row) ((pos 'row!) ghost-house-exit-row)
((pos 'col!) ghost-house-exit-col) ((pos 'col!) ghost-house-exit-col)
((ghost 'direction!) 'left) ((ghost 'direction!) 'left)
((ghost 'sync-prev!))
(on-ghosts-changed!))) (on-ghosts-changed!)))
;; ;;
@@ -348,12 +351,12 @@
(when (and was-in-house? (not (ghost 'in-house?))) (when (and was-in-house? (not (ghost 'in-house?)))
(exit-ghost-house! ghost) (exit-ghost-house! ghost)
((ghost 'mode!) global-mode))) ((ghost 'mode!) global-mode)))
;; Movement tick for active ghosts ;; Movement tick for active ghosts (carry residual time)
(when (not (ghost 'in-house?)) (when (not (ghost 'in-house?))
(let ((mt ((ghost 'advance-movement-timer!) delta-time))) (let ((mt ((ghost 'advance-movement-timer!) delta-time)))
(when (>= mt ghost-speed-ms) (when (>= mt ghost-speed-ms)
(advance-ghost! ghost) (advance-ghost! ghost)
((ghost 'reset-movement-timer!)))))) ((ghost 'set-movement-timer!) (- mt ghost-speed-ms))))))
ghosts)) ghosts))
;; ;;
@@ -389,7 +392,9 @@
((pac-pos 'row!) row)) ((pac-pos 'row!) row))
((>= col (maze 'cols)) ((>= col (maze 'cols))
((pac-pos 'col!) 0) ((pac-pos 'col!) 0)
((pac-pos 'row!) row))))) ((pac-pos 'row!) row)))
;; Prevent interpolation across the entire map
((pacman 'sync-prev!))))
;; ;;
;; Movement logic ;; Movement logic
@@ -427,13 +432,19 @@
;; advance-pacman! :: -> / ;; advance-pacman! :: -> /
(define (advance-pacman!) (define (advance-pacman!)
(when (not (or ((timer 'time-up?)) game-over?)) (when (not (or ((timer 'time-up?)) game-over?))
(let ((current-dir (pacman 'direction))) (let* ((current-dir (pacman 'direction))
(pos-before-row ((pacman 'position) 'row))
(pos-before-col ((pacman 'position) 'col)))
(cond (cond
((and queued-direction (can-move? queued-direction)) ((and queued-direction (can-move? queued-direction))
(move-pacman! queued-direction) (move-pacman! queued-direction)
(set! queued-direction #f)) (set! queued-direction #f))
((can-move? current-dir) ((can-move? current-dir)
(move-pacman! current-dir)))))) (move-pacman! current-dir)))
;; If pacman didn't actually move, sync prev to avoid drift
(when (and (= ((pacman 'position) 'row) pos-before-row)
(= ((pacman 'position) 'col) pos-before-col))
((pacman 'sync-prev!))))))
;; ;;
;; Pause logic ;; Pause logic
@@ -465,11 +476,11 @@
(define (update! delta-time) (define (update! delta-time)
(when (not (or paused? game-over?)) (when (not (or paused? game-over?))
((timer 'decrease!) delta-time) ((timer 'decrease!) delta-time)
;; Pac-Man movement ;; Pac-Man movement (carry residual for smoother interpolation)
(set! movement-timer (+ movement-timer delta-time)) (set! movement-timer (+ movement-timer delta-time))
(when (>= movement-timer pacman-speed-ms) (when (>= movement-timer pacman-speed-ms)
(advance-pacman!) (advance-pacman!)
(set! movement-timer 0) (set! movement-timer (- movement-timer pacman-speed-ms))
(check-ghost-collision!)) (check-ghost-collision!))
;; Ghost mode cycling and movement ;; Ghost mode cycling and movement
(update-ghost-mode! delta-time) (update-ghost-mode! delta-time)
@@ -502,6 +513,7 @@
((eq? msg 'timer) timer) ((eq? msg 'timer) timer)
((eq? msg 'paused?) paused?) ((eq? msg 'paused?) paused?)
((eq? msg 'game-over?) game-over?) ((eq? msg 'game-over?) game-over?)
((eq? msg 'pacman-movement-timer) movement-timer)
((eq? msg 'key-press!) key-press!) ((eq? msg 'key-press!) key-press!)
((eq? msg 'update!) update!) ((eq? msg 'update!) update!)
((eq? msg 'set-on-coins-changed!) set-on-coins-changed!) ((eq? msg 'set-on-coins-changed!) set-on-coins-changed!)

View File

@@ -4,8 +4,9 @@
;; Pac-Man ADT ;; ;; Pac-Man ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Manages the logical state of the player: grid position and current ;; Manages the logical state of the player: grid position, direction, and
;; direction. Contains NO graphics code. ;; previous position for smooth rendering interpolation. Contains NO
;; graphics code.
(define-library (pacman-project adt pacman) (define-library (pacman-project adt pacman)
(import (scheme base) (import (scheme base)
@@ -18,7 +19,9 @@
;; Creates a Pac-Man object at the given start position (row, col). ;; Creates a Pac-Man object at the given start position (row, col).
(define (make-pacman start-row start-col) (define (make-pacman start-row start-col)
(let ((position (make-position start-row start-col)) (let ((position (make-position start-row start-col))
(direction 'right)) (direction 'right)
(prev-row start-row)
(prev-col start-col))
;; position! :: position -> / ;; position! :: position -> /
(define (position! new-position) (define (position! new-position)
@@ -29,11 +32,20 @@
(set! direction new-direction)) (set! direction new-direction))
;; move! :: number, number -> / ;; move! :: number, number -> /
;; Moves Pac-Man by a delta on the grid. ;; Saves previous position, then moves by delta on the grid.
(define (move! delta-row delta-col) (define (move! delta-row delta-col)
(set! prev-row (position 'row))
(set! prev-col (position 'col))
((position 'row!) (+ (position 'row) delta-row)) ((position 'row!) (+ (position 'row) delta-row))
((position 'col!) (+ (position 'col) delta-col))) ((position 'col!) (+ (position 'col) delta-col)))
;; sync-prev! :: -> /
;; Sets previous position to current. Call after teleportation
;; to prevent interpolation across the map.
(define (sync-prev!)
(set! prev-row (position 'row))
(set! prev-col (position 'col)))
;; dispatch-pacman :: symbol -> any ;; dispatch-pacman :: symbol -> any
(define (dispatch-pacman msg) (define (dispatch-pacman msg)
(cond ((eq? msg 'position) position) (cond ((eq? msg 'position) position)
@@ -41,6 +53,9 @@
((eq? msg 'direction) direction) ((eq? msg 'direction) direction)
((eq? msg 'direction!) direction!) ((eq? msg 'direction!) direction!)
((eq? msg 'move!) move!) ((eq? msg 'move!) move!)
((eq? msg 'prev-row) prev-row)
((eq? msg 'prev-col) prev-col)
((eq? msg 'sync-prev!) sync-prev!)
(else (error "Pac-Man ADT -- Unknown message:" msg)))) (else (error "Pac-Man ADT -- Unknown message:" msg))))
dispatch-pacman)))) dispatch-pacman))))