0

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
RMP777
  • 13
  • 3
  • 2
    Please show the definition of `#square_hit_circle?` so we can see what might be wrong with it. Or have you not defined it yet and want help to do so? If so, can you figure out the mathematics of checking whether a square and circle intersect, and we can help you translate that into Ruby? – Alex D Aug 25 '19 at 15:24
  • You need to clarify your question (by editing it). Something like, "given a circle and a square in a two-dimensional plane I wish to determine if there is a point that is within both the circle and square, including the possibility that a point is on both the circumference of the circle and on a side of the square. The circle is described by the coordinates of its center and its radius; the square by the coordinates of its four corners." You should also remove the links to the videos as they are irrelevant. I'll have an answer for you later. – Cary Swoveland Aug 25 '19 at 20:23
  • Thanks so much i clarified my question – RMP777 Aug 26 '19 at 00:14
  • in `
    ': uninitialized constant Game (NameError) is my error
    – RMP777 Aug 26 '19 at 02:34
  • Use the `contains?(x, y)` method. It's available for all the ruby2d shapes and image Class. – Nifriz Aug 26 '19 at 11:40
  • I'm sorry I said how I don't really know ruby that well I just started. How would you do the contains method? – RMP777 Aug 26 '19 at 13:33
  • https://www.rubydoc.info/gems/ruby2d/0.9.2/Ruby2D/Circle#contains%3F-instance_method – Nifriz Aug 27 '19 at 08:22

1 Answers1

1

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
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • I'm sorry I'm still confused. How do I put this in my code? Do I do it with a new file? – RMP777 Aug 28 '19 at 00:10
  • You could do that. Create a file (say) `intersect_circle_and_square.rb` containing the methods I've created above: `intersect?`, `sides`, `compute_line`, `circle_and_line_intersections` and `on_side?`. `intersect_circle` needs to be a public method; the others are best private methods. In you code execute the method [Kernel#require](https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-require) or [require_relative](https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-require_relative) Alternatively, you could simply add those methods to your Ruby file. – Cary Swoveland Aug 28 '19 at 04:34