Skip to content

Commit e01d7a5

Browse files
authored
Merge pull request #139 from burinc/fix-collision-bugs
Fix asteroids collision bug
2 parents 964e143 + 589ffa5 commit e01d7a5

File tree

2 files changed

+156
-8
lines changed

2 files changed

+156
-8
lines changed

src/scittle/games/asteroids.cljs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -443,18 +443,21 @@
443443
:when (> (:life new-p) 0)]
444444
new-p))))
445445

446-
;; Check bullet-asteroid collisions (FIXED to prevent duplicate hits)
447-
(let [hit-bullets (atom #{})
446+
;; Check bullet-asteroid collisions (FIXED to use current state)
447+
(let [current-bullets (:bullets @game-state)
448+
current-asteroids (:asteroids @game-state)
449+
current-ufos (:ufos @game-state)
450+
hit-bullets (atom #{})
448451
hit-asteroids (atom #{})
449452
new-asteroids (atom [])
450453
score-added (atom 0)
451454
new-particles (atom [])]
452455

453456
;; Find all collisions (but don't apply yet)
454-
(doseq [bullet bullets
457+
(doseq [bullet current-bullets
455458
:when (and (not (:from-ufo bullet))
456459
(not (contains? @hit-bullets bullet)))
457-
asteroid asteroids
460+
asteroid current-asteroids
458461
:when (not (contains? @hit-asteroids asteroid))]
459462
(when (check-collision :obj1 bullet :obj2 asteroid
460463
:radius1 2 :radius2 (:size asteroid))
@@ -485,16 +488,18 @@
485488
(swap! game-state update :particles
486489
#(vec (take max-particles (concat % @new-particles))))))
487490

488-
;; Check bullet-UFO collisions (FIXED to prevent duplicate hits)
489-
(let [hit-bullets (atom #{})
491+
;; Check bullet-UFO collisions (FIXED to use current state)
492+
(let [current-bullets (:bullets @game-state)
493+
current-ufos (:ufos @game-state)
494+
hit-bullets (atom #{})
490495
hit-ufos (atom #{})
491496
score-added (atom 0)
492497
new-particles (atom [])]
493498

494-
(doseq [bullet bullets
499+
(doseq [bullet current-bullets
495500
:when (and (not (:from-ufo bullet))
496501
(not (contains? @hit-bullets bullet)))
497-
ufo ufos
502+
ufo current-ufos
498503
:when (not (contains? @hit-ufos ufo))]
499504
(when (check-collision :obj1 bullet :obj2 ufo
500505
:radius1 2 :radius2 ufo-size)

src/scittle/games/asteroids_article.clj

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,149 @@
816816
;; 5. **Optimize particle counts**: Fewer particles with better placement looks just as good
817817
;; 6. **Profile on mobile**: Desktop performance doesn't predict mobile behavior
818818

819+
;; ### The Stale Data Bug - Collision Detection with Old Positions
820+
821+
;; After fixing the exponential explosion bug, we discovered another critical issue: bullets weren't destroying asteroids reliably! This was the **exact same bug** we encountered in our Galaga implementation.
822+
823+
;; #### The Problem: Using Captured State
824+
825+
;; The collision detection code was using bullets and asteroids captured at the START of the `update-game!` function, but checking collisions AFTER updating their positions:
826+
827+
;; ```clojure
828+
;; ;; ❌ BROKEN: Stale data causes missed collisions
829+
;; (defn update-game! []
830+
;; (when (= (:game-status @game-state) :playing)
831+
;; (let [{:keys [bullets asteroids ufos]} @game-state] ; ← Captured at START
832+
;;
833+
;; ;; Update bullet positions
834+
;; (swap! game-state update :bullets
835+
;; (fn [bs]
836+
;; (vec (for [b bs
837+
;; :let [new-b (-> b
838+
;; (update :x #(wrap-position ...))
839+
;; (update :y #(wrap-position ...)))]
840+
;; :when (> (:life new-b) 0)]
841+
;; new-b))))
842+
;;
843+
;; ;; Update asteroid positions
844+
;; (swap! game-state update :asteroids
845+
;; (fn [as]
846+
;; (vec (for [a as]
847+
;; (-> a
848+
;; (update :x #(wrap-position ...))
849+
;; (update :y #(wrap-position ...)))))))
850+
;;
851+
;; ;; Check collisions - BUG: Uses OLD positions from line 2!
852+
;; (doseq [bullet bullets ; ← OLD positions before movement
853+
;; asteroid asteroids] ; ← OLD positions before movement
854+
;; (when (check-collision bullet asteroid)
855+
;; ...))
856+
;; ```
857+
858+
;; **Why This Fails:**
859+
;;
860+
;; 1. Frame starts: Bullet at x=100, Asteroid at x=105 (not colliding)
861+
;; 2. Bullet moves to x=108 (NOW colliding with asteroid!)
862+
;; 3. Asteroid moves to x=110
863+
;; 4. Collision check uses OLD positions (100 vs 105) - NO collision detected!
864+
;; 5. Result: Bullet passes right through asteroid
865+
866+
;; #### The Fix: Fresh State Captures
867+
868+
;; Capture the CURRENT state right before collision detection, AFTER all position updates:
869+
870+
;; ```clojure
871+
;; ;; ✅ FIXED: Use current state after position updates
872+
;; (defn update-game! []
873+
;; (when (= (:game-status @game-state) :playing)
874+
;; (let [{:keys [bullets asteroids ufos]} @game-state]
875+
;;
876+
;; ;; Update bullet positions
877+
;; (swap! game-state update :bullets ...)
878+
;;
879+
;; ;; Update asteroid positions
880+
;; (swap! game-state update :asteroids ...)
881+
;;
882+
;; ;; Capture FRESH state after all updates
883+
;; (let [current-bullets (:bullets @game-state) ; ← FRESH after updates
884+
;; current-asteroids (:asteroids @game-state) ; ← FRESH after updates
885+
;; current-ufos (:ufos @game-state) ; ← FRESH after updates
886+
;; hit-bullets (atom #{})
887+
;; hit-asteroids (atom #{})]
888+
;;
889+
;; ;; Now collision detection uses CURRENT positions
890+
;; (doseq [bullet current-bullets ; ← Current frame positions
891+
;; :when (not (contains? @hit-bullets bullet))
892+
;; asteroid current-asteroids] ; ← Current frame positions
893+
;; (when (check-collision bullet asteroid)
894+
;; ...))
895+
;;
896+
;; ;; UFO collisions also use current state
897+
;; (doseq [bullet current-bullets ; ← Reuse fresh capture
898+
;; :when (not (contains? @hit-bullets bullet))
899+
;; ufo current-ufos] ; ← Current frame positions
900+
;; (when (check-collision bullet ufo)
901+
;; ...))
902+
;; ```
903+
904+
;; #### The Key Insight
905+
906+
;; The `let` binding at the function start creates a **snapshot** of the game state. Any subsequent `swap!` calls modify the atom, but the `let` variables still point to the old data. For collision detection to work correctly, we must capture fresh state AFTER all position updates complete.
907+
908+
;; #### Scope Considerations
909+
910+
;; Initially, we made a mistake: we defined `current-bullets` and `current-ufos` in the first collision detection block (asteroids) and tried to use them in the second block (UFOs). This failed because each `let` block has its own scope!
911+
912+
;; **The solution:** Each collision detection block captures its own fresh data:
913+
914+
;; ```clojure
915+
;; ;; Bullet-Asteroid collisions
916+
;; (let [current-bullets (:bullets @game-state)
917+
;; current-asteroids (:asteroids @game-state)
918+
;; current-ufos (:ufos @game-state) ; ← Also captured for UFO collisions
919+
;; ...]
920+
;; ...)
921+
;;
922+
;; ;; Bullet-UFO collisions (separate scope!)
923+
;; (let [current-bullets (:bullets @game-state) ; ← Fresh capture again
924+
;; current-ufos (:ufos @game-state) ; ← Fresh capture again
925+
;; ...]
926+
;; ...)
927+
;; ```
928+
929+
;; #### Learning from Galaga
930+
931+
;; This was the identical bug we fixed in our Galaga game! The pattern is common in game loops:
932+
;;
933+
;; 1. Capture state at function start for reference
934+
;; 2. Update multiple collections via `swap!`
935+
;; 3. Check interactions between updated objects
936+
;; 4. **Mistake**: Using initial captures instead of current state
937+
;;
938+
;; The fix is always the same: capture fresh state right before collision detection.
939+
940+
;; #### Impact After Fix
941+
942+
;; **Before:**
943+
;; - Bullets often passed through asteroids
944+
;; - Fast-moving bullets especially problematic
945+
;; - UFOs seemed invulnerable
946+
;; - Frustrating gameplay experience
947+
948+
;; **After:**
949+
;; - Precise collision detection
950+
;; - Bullets reliably destroy asteroids
951+
;; - UFOs react properly to hits
952+
;; - Satisfying, responsive gameplay
953+
954+
;; #### Additional Lessons
955+
956+
;; 7. **Timing matters**: State captured at start vs. end of update cycle matters!
957+
;; 8. **Test collision detection**: Easy to miss stale data bugs during casual play
958+
;; 9. **Reuse patterns**: Same bug/fix across multiple games suggests a general principle
959+
;; 10. **Scope awareness**: Remember that `let` blocks don't share bindings
960+
;; 11. **Think in terms of frames**: What state exists at each point in the game loop?
961+
819962
;; ## Retro Sound Effects with Web Audio API
820963

821964
;; No arcade game is complete without sound! We added authentic retro sound effects using the Web Audio API, following the same pattern used in our Galaga implementation.

0 commit comments

Comments
 (0)