How do you detect when simple objects intersect in ruby 2D?

459 Views Asked by At

So I'm a beginner to ruby and I was playing around with collision detection in ruby 2d. I have a controllable square and a square that stays still. My question is how do to detect when the square collides with the circle.

This is my main code the rest is just controls to move the square

    @square = Square.new(x: 10, y: 20, size: 25, color: 'blue')

    @circle = Circle.new(x: 100, y: 100, radius: 10, color: 'red')

    @x_speed = 0
    @y_speed = 0

    game = Game.new


    class Game
        @score = 0
    end

and this is what is updating,

    update do
      if game.square_hit_circle?(square.x, square.y)
        puts "hit"
      end
    end

Here is what square_hit_circle? means

    def square_hit_circle?(x, y)
      @circle_x == x && @circle_y == y
    end
1

There are 1 best solutions below

2
Cary Swoveland On

I thought it might be of interest to provide a solution that does not make use of the ruby2d gem, so show how the required calculations are performed.

Example data

Suppose

center = [2, 7]
radius = 4

corners = [[1,3], [5,3], [3,1], [3,5]]

This looks like the following, with Y the center of the circle and the X's the corners of the square.

7    Y
6   
5        X
4         
3  X           X    
2                
1        X              
0  1  2  3  4  5

Determine the sides of the rectangle

Select any of these corners (the first, say):

first, *rest = corners
  #=> [[1, 3], [5, 3], [3, 1], [3, 5]] 
first
  #=> [1, 3] 
rest
  #=> [[5, 3], [3, 1], [3, 5]] 

Determine which corner is farthest from c1:

x1, y1 = first
farthest = rest.max_by { |x,y| (x-x1)**2 + (y-y1)**2 }
  #=> [5, 3] 

Compute the sides of the square as arrays of endpoints:

rest.delete(farthest)
  #=> [5, 3] 
rest
  #=> [[3, 1], [3, 5]] 
sides = [first,farthest].product(rest)  
  #=> [[[1, 3], [3, 1]],
  #    [[1, 3], [3, 5]],
  #    [[5, 3], [3, 1]],
  #    [[5, 3], [3, 5]]]

Let's make a method to do this.

def sides(corners)
  first, *rest = corners
  x1, y1 = first
  farthest = rest.max_by { |x,y| (x-x1)**2 + (y-y1)**2 }
  rest.delete(farthest)
  [first,farthest].product(rest)
end

sides(corners)  
  #=> <as above>

Compute intercept and slope for the lines coincident with the sides of the square

For each of these sides there is a line in space that is coincide with the side. Each of these lines is described by an intercept i and a slope b, such that for any value of x the point [x, y] is on the line if y = i + b*x. We can compute the intercept and slope for each of these lines.

def compute_line(side)
  (ux,uy), (vx,vy) = side
  b = ux==uy ? 0.0 : (uy - vy).fdiv(ux - vx)
  [uy - b*ux, b]
end

sides.map { |side| compute_line(side) }
  #=> [[4.0, -1.0], [2.0, 1.0], [-2.0, 1.0], [8.0, -1.0]] 

Note:

i, b = lines.first
  #=> [4.0, -1.0] 
i + b*0
  #=> 4.0 (the point [0, 4.0]) 
i + b*1
  #=> 3.0 (the point [1, 3.0]) 
i + b*2
  #=> 2.0 (the point [2, 2.0])
i + b*3  
  #=> 1.0 (the point [3, 1.0])
i + b*4
  #=> 0.0 (the point [4, 0.0])

Compute the points where the circle intersects lines coincident with the sides

Let

cx, cy = center
  #=> [2, 7]

Suppose we consider a side for which the coincident line has intercept i and slope s. We then have the quadratic expression:

(cx-x)2 + (cy-i-s*x)2 = radius2

By defining:

e = cy - i

the equation reduces to:

cx2 - 2*cx*x + x2 + e2 - 2*e*s*x + s2*x2 = radius2

or

(1 + s2)*x2 + 2*(-cx -e*s)*x + cx2 + e2 - radius2 = 0

or

ax2 + bx + c = 0

where:

a = (1 + s2)

b = -2*(cx + e*s)

c = cx2 + e2 - radius2

The real root or roots, if it/they exist, are given by the quadratic equation. First compute the discriminate:

d = b2 - 4*a*c

If the discriminate is negative the quadratic has no real roots (only complex roots). Here that means the circle is not large enough to intersect the line that is coincident with this side.

If the discriminate d is positive, there are two real roots (one real root only if d is zero). Let:

w = d1/2

The roots (values of x) are:

(-b + w)/(2*a)

and

(-b - w)/(2*a)

Let's wrap this in a method:

def circle_and_line_intersections(center, radius, side)
  i, s = compute_line(side)
  cx, cy = center
  e = cy - i
  a = 1 + s**2
  b = -2*(cx + e*s)
  c = cx**2 + e**2 - radius**2 
  d = b**2 - 4*a*c
  return [] if d < 0
  return [-b/(2*a)] if d.zero?
  w = Math.sqrt(d)
  r1 = (-b + w)/(2*a)
  r2 = (-b - w)/(2*a)
  [[r1, i + s*r1], [r2, i + s*r2]] 
end

sides.map { |side| circle_and_line_intersections(center, radius, side) }
  #=> [[[ 0.82287, 3.17712], [-1.82287, 5.82287]],
  #    [[ 5.89791, 7.89791], [ 1.10208, 3.10208]],
  #    [],
  #    [[4.28388, 3.71611], [-1.28388, 9.28388]]]    

It remains to do the following.

Determine if one of the points of intersection is on a side

That is simple and straightforward:

def on_side?(pt, side)
  low, high = side.map(&:first).sort
  (low..high).cover?(pt.first)
end 

For example:

on_side?([0.82287, 3.17712], sides[0])
  #=> false
on_side?([1.10208, 3.10208], sides[1])    
  #=> true

Putting it all together

def intersect?(center, radius, corners)
  sides(corners).any? do |side|
    circle_and_line_intersections(center, radius, side).any? { |pt|
    on_side?(pt, side) }
  end
end

intersect?(center, radius, corners)
  #=> true