Search code examples
clojurecollision-detection

idiomatic clojure range/collision checking?


getting back into clojure, and i wrote this little thing to check if two boxes are colliding by checking if vertices of one are inside the other:

(defn around
  [val radius]
  (let [half (/ radius 2)
        low (- val half)
        high (+ val half)]
    [low high]))

(defn colliding?
  [this that]
  (let [[this-x1 this-x2] (around (:x this) (:w this))
        [this-y1 this-y2] (around (:y this) (:h this))
        [this-z1 this-z2] (around (:z this) (:l this))
        [that-x1 that-x2] (around (:x that) (:w that))
        [that-y1 that-y2] (around (:y that) (:h that))
        [that-z1 that-z2] (around (:z that) (:l that))]
    (or (and (or (<= that-x1 this-x1 that-x2)
                 (<= that-x1 this-x2 that-x2))
             (or (<= that-y1 this-y1 that-y2)
                 (<= that-y1 this-y2 that-y2))
             (or (<= that-z1 this-z1 that-z2)
                 (<= that-z1 this-z2 that-z2)))
        (and (or (<= this-x1 that-x1 this-x2)
                 (<= this-x1 that-x2 this-x2))
             (or (<= this-y1 that-y1 this-y2)
                 (<= this-y1 that-y2 this-y2))
             (or (<= this-z1 that-z1 this-z2)
                 (<= this-z1 that-z2 this-z2))))))

this smells pretty bad due to repetition but i'm not sure what the best approach is for cleaning this up. is there a better way to do this?


Solution

  • You can simplify the code by using the fact that there is a collision in one direction unless the starting coordinate of one object is higher than the ending coordinate of the other. I.e. a sufficient test is

    (not (or (> this-x1 that-x2)
             (> that-x1 this-x2)))
    

    which is equivalent to

    (and (<= this-x1 that-x2)
         (<= that-x1 this-x2))
    

    Using this, your colliding? may be simplified to

    (defn colliding?
      [this that]
      (let [[this-x1 this-x2] (around (:x this) (:w this))
            [this-y1 this-y2] (around (:y this) (:h this))
            [this-z1 this-z2] (around (:z this) (:l this))
            [that-x1 that-x2] (around (:x that) (:w that))
            [that-y1 that-y2] (around (:y that) (:h that))
            [that-z1 that-z2] (around (:z that) (:l that))]
         (and (<= this-x1 that-x2)
              (<= that-x1 this-x2)
              (<= this-y1 that-y2)
              (<= that-y1 this-y2)
              (<= this-z1 that-z2)
              (<= that-z1 this-z2))))
    

    If you then factor out a function conflicting? that checks for overlapping in one dimension,

    (defn conflicting?
      [this that coordinate size]
      (let [[this-c1 this-c2] (around (coordinate this) (size this))
            [that-c1 that-c2] (around (coordinate that) (size that))]
        (and (<= this-c1 that-c2)
             (<= that-c1 this-c2)))
    

    colliding? can be further simplified by utilizing mapping over dimensions and sizes:

    (defn colliding?
      [this that]
      (every? true? (map #(conflicting? this that %1 %2)
                         [:x :y :z]
                         [:w :h :l])))
    

    Edit:

    Furthermore, conflicting? may be simplified to

    (defn conflicting?
      [this that coordinate size]
      (<= (math/abs (- (coordinate this) (coordinate that)))
          (/ (+ (size this) (size that)) 2)))
    

    making the function around obsolete for the purpose of detecting collisions.