diff options
| author | Gene Pasquet <dev@etenil.net> | 2026-04-05 14:17:51 +0100 |
|---|---|---|
| committer | Gene Pasquet <dev@etenil.net> | 2026-04-05 14:17:51 +0100 |
| commit | 526e6cdcdf1025d5e29680bc99ab910c79789764 (patch) | |
| tree | 2a91b3e96f2b97cfc81169627f222a5393982830 /physics.scm | |
Initial port of macroknight to an engine
Diffstat (limited to 'physics.scm')
| -rw-r--r-- | physics.scm | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/physics.scm b/physics.scm new file mode 100644 index 0000000..83cc85b --- /dev/null +++ b/physics.scm @@ -0,0 +1,238 @@ +(module physics * + (import scheme + (chicken base) + (chicken keyword) + (only srfi-1 fold iota) + defstruct + tilemap + entity + world + simple-logger) + + ;; Gravity constant: pixels per frame per frame + (define *gravity* 1) + + ;; Jump force: vertical acceleration applied on jump (one frame) + (define *jump-force* 15) + + ;; Consume #:ay into #:vy and clear it (one-shot acceleration) + (define (apply-acceleration entity) + (if (not (entity-ref entity #:gravity? #f)) + entity + (let ((ay (entity-ref entity #:ay 0)) + (vy (entity-ref entity #:vy 0))) + (entity-set (entity-set entity #:vy (+ vy ay)) #:ay 0)))) + + ;; Apply gravity to an entity if it has gravity enabled + (define (apply-gravity entity) + (if (entity-ref entity #:gravity? #f) + (entity-set entity #:vy (+ (entity-ref entity #:vy) *gravity*)) + entity)) + + ;; Update entity's x by its vx velocity + (define (apply-velocity-x entity) + "Update entity's x by its vx velocity." + (let ((x (entity-ref entity #:x 0)) + (vx (entity-ref entity #:vx 0))) + (entity-set entity #:x (+ x vx)))) + + ;; Update entity's y by its vy velocity + (define (apply-velocity-y entity) + "Update entity's y by its vy velocity." + (let ((y (entity-ref entity #:y 0)) + (vy (entity-ref entity #:vy 0))) + (entity-set entity #:y (+ y vy)))) + + ;; Legacy function: update both x and y by velocities + (define (apply-velocity entity) + "Legacy function: update both x and y by velocities." + (let ((x (entity-ref entity #:x 0)) + (y (entity-ref entity #:y 0)) + (vx (entity-ref entity #:vx 0)) + (vy (entity-ref entity #:vy 0))) + (entity-set (entity-set entity #:x (+ x vx)) #:y (+ y vy)))) + + ;; Build list of (col . row) pairs to check for collisions + (define (build-cell-list col-start col-end row-start row-end) + (let loop ((col col-start) (row row-start) (acc '())) + (log-debug "Build-cell-list loop with: ~a" (list col row acc)) + (if (> col col-end) + (if (>= row row-end) + (reverse acc) + (loop col-start (+ row 1) acc)) + (loop (+ col 1) row (cons (cons col row) acc))))) + + ;; Convert a pixel coordinate to a tile grid index + (define (pixel->tile pixel tile-size) + (inexact->exact (floor (/ pixel tile-size)))) + + ;; Return all tile cells (col . row) overlapping the entity's bounding box + (define (entity-tile-cells entity tilemap) + (let ((x (entity-ref entity #:x 0)) + (y (entity-ref entity #:y 0)) + (w (entity-ref entity #:width 0)) + (h (entity-ref entity #:height 0)) + (tw (tilemap-tilewidth tilemap)) + (th (tilemap-tileheight tilemap))) + (build-cell-list + (pixel->tile x tw) + (pixel->tile (- (+ x w) 1) tw) + (pixel->tile y th) + (pixel->tile (- (+ y h) 1) th)))) + + ;; Snap position to the near or far edge of a tile after collision. + ;; Moving forward (v>0): snap entity's leading edge to tile's near edge. + ;; Moving backward (v<0): snap entity's trailing edge to tile's far edge. + (define (tile-push-pos v coord tile-size entity-size) + (if (> v 0) + (- (* coord tile-size) entity-size) + (* (+ coord 1) tile-size))) + + ;; Resolve collisions with tiles along a single axis. + ;; push-fn: (v col row) -> new-pos + (define (resolve-tile-collisions-axis entity tilemap vel-key pos-key push-fn) + (let ((v (entity-ref entity vel-key 0))) + (if (zero? v) + entity + (fold (lambda (cell acc) + (log-debug "resolve-~a: cell=~a acc=~a" vel-key cell acc) + (let* ((col (car cell)) + (row (cdr cell)) + (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)))) + entity + (entity-tile-cells entity tilemap))))) + + ;; Resolve horizontal collisions with solid tiles + (define (resolve-tile-collisions-x entity tilemap) + (let ((w (entity-ref entity #:width 0)) + (tw (tilemap-tilewidth tilemap))) + (resolve-tile-collisions-axis entity tilemap #:vx #:x + (lambda (v col row) (tile-push-pos v col tw w))))) + + ;; Resolve vertical collisions with solid tiles + (define (resolve-tile-collisions-y entity tilemap) + (let ((h (entity-ref entity #:height 0)) + (th (tilemap-tileheight tilemap))) + (resolve-tile-collisions-axis entity tilemap #:vy #:y + (lambda (v col row) (tile-push-pos v row th h))))) + + ;; Detect if entity is standing on ground by probing 1px below feet + (define (detect-ground entity tilemap) + (if (not (entity-ref entity #:gravity? #f)) + entity + (let* ((x (entity-ref entity #:x 0)) + (w (entity-ref entity #:width 0)) + (tw (tilemap-tilewidth tilemap)) + (th (tilemap-tileheight tilemap)) + (probe-y (+ (entity-ref entity #:y 0) (entity-ref entity #:height 0) 1)) + (row (pixel->tile probe-y th)) + (col-left (pixel->tile x tw)) + (col-right (pixel->tile (- (+ x w) 1) tw)) + (on-ground? (or (not (zero? (tilemap-tile-at tilemap col-left row))) + (not (zero? (tilemap-tile-at tilemap col-right row)))))) + (entity-set entity #:on-ground? on-ground?)))) + + ;; Set vertical acceleration for jump (consumed next frame by apply-acceleration) + (define (apply-jump entity jump-pressed?) + "Set #:ay to jump force if jump pressed and entity is on ground." + (if (and jump-pressed? (entity-ref entity #:on-ground? #f)) + (entity-set entity #:ay (- (entity-ref entity #:jump-force *jump-force*))) + entity)) + + ;; Replace element at idx in lst with val + (define (list-set lst idx val) + (let loop ((lst lst) (i 0) (acc '())) + (if (null? lst) + (reverse acc) + (loop (cdr lst) (+ i 1) + (cons (if (= i idx) val (car lst)) acc))))) + + ;; Generate all unique (i . j) index pairs where i < j + (define (index-pairs n) + (if (< n 2) '() + (apply append + (map (lambda (i) + (map (lambda (j) (cons i j)) + (iota (- n i 1) (+ i 1)))) + (iota (- n 1)))))) + + (define (axis->dimension axis) + (case axis + ((#:x) #:width) + ((#:y) #:height))) + + (define (axis->velocity axis) + (case axis + ((#:x) #:vx) + ((#:y) #:vy))) + + ;; Push entity along one axis by half-overlap, setting velocity in push direction + (define (push-entity entity pos-key vel-key pos overlap sign) + (entity-set (entity-set entity pos-key (+ pos (* sign (/ overlap 2)))) vel-key sign)) + + + (define (entity-center-on-axis entity axis) + (let ((dimension (axis->dimension axis))) + (+ (entity-ref entity axis 0) (/ (entity-ref entity dimension 0) 2)))) + + (define (aabb-overlap-on-axis axis a b) + (let ((dimension (axis->dimension axis))) + (- (/ (+ (entity-ref a dimension 0) (entity-ref b dimension 0)) 2) + (abs (- (entity-center-on-axis b axis) (entity-center-on-axis a axis)))))) + + (define (push-along-axis axis a b overlap) + (let* ((a-center (entity-center-on-axis a axis)) + (b-center (entity-center-on-axis b axis)) + (delta (if (< a-center b-center) -1 1)) + (axis-velocity-key (axis->velocity axis))) + (cons (push-entity a axis axis-velocity-key (entity-ref a axis 0) overlap delta) + (push-entity b axis axis-velocity-key (entity-ref b axis 0) overlap (- delta))))) + + ;; Push two overlapping entities apart along the minimum penetration axis. + ;; Returns (a2 . b2) with updated positions and velocities. + (define (push-apart a b) + (let* ((ovx (aabb-overlap-on-axis #:x a b)) + (ovy (aabb-overlap-on-axis #:y a b))) + (if (<= ovx ovy) + (push-along-axis #:x a b ovx) + (push-along-axis #:y a b ovy)))) + + ;; Check if two axis-aligned bounding boxes overlap. + ;; Returns #t if they overlap, #f if they don't (including edge-touching). + (define (aabb-overlap? x1 y1 w1 h1 x2 y2 w2 h2) + (not (or (>= x1 (+ x2 w2)) + (>= x2 (+ x1 w1)) + (>= y1 (+ y2 h2)) + (>= y2 (+ y1 h1))))) + + ;; Resolve AABB collision between two solid entities. + ;; Returns (a2 . b2) with positions/velocities adjusted, or #f if no collision. + (define (resolve-pair a b) + (and (entity-ref a #:solid? #f) + (entity-ref b #:solid? #f) + (aabb-overlap? (entity-ref a #:x 0) (entity-ref a #:y 0) + (entity-ref a #:width 0) (entity-ref a #:height 0) + (entity-ref b #:x 0) (entity-ref b #:y 0) + (entity-ref b #:width 0) (entity-ref b #:height 0)) + (push-apart a b))) + + ;; Detect and resolve AABB overlaps between all pairs of solid entities. + ;; Returns a new entity list with collisions resolved. + (define (resolve-entity-collisions entities) + (fold (lambda (pair ents) + (let* ((i (car pair)) (j (cdr pair)) + (result (resolve-pair (list-ref ents i) (list-ref ents j)))) + (if result + (list-set (list-set ents i (car result)) j (cdr result)) + ents))) + entities + (index-pairs (length entities)))) + + ;; Wrapper for scene-resolve-collisions + (define (scene-resolve-collisions scene) + (scene-entities-set! scene + (resolve-entity-collisions (scene-entities scene))) + scene)) |
