Search code examples
javascriptmathgame-physics

Circle/rectangle collision response


So I built some time ago a little Breakout clone, and I wanted to upgrade it a little bit, mostly for the collisions. When I first made it I had a basic "collision" detection between my ball and my brick, which in fact considered the ball as another rectangle. But this created an issue with the edge collisions, so I thought I would change it. The thing is, I found some answers to my problem:

for example this image

enter image description here

and the last comment of this thread : circle/rect collision reaction but i could not find how to compute the final velocity vector.

So far I have :

- Found the closest point on the rectangle,
- created the normal and tangent vectors,

And now what I need is to somehow "divide the velocity vector into a normal component and a tangent component; negate the normal component and add the normal and tangent components to get the new Velocity vector" I'm sorry if this seems terribly easy but I could not get my mind around that ... code :

function collision(rect, circle){
  var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
  var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.w));

  var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
  var dnormal = createVector(- dist.y, dist.x);
//change current circle vel according to the collision response
}

Thanks !

EDIT: Also found this but I didn't know if it is applicable at all points of the rectangle or only the corners.


Solution

  • Best explained with a couple of diagrams:

    angles

    Have angle of incidence = angle of reflection. Call this value θ.

    angles2

    Have θ = normal angle - incoming angle.

    atan2 is the function for computing the angle of a vector from the positive x-axis.

    Then the code below immediately follows:

    function collision(rect, circle){
      var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
      var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
    
      var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
      var dnormal = createVector(- dist.y, dist.x);
    
      var normal_angle = atan2(dnormal.y, dnormal.x);
      var incoming_angle = atan2(circle.vel.y, circle.vel.x);
      var theta = normal_angle - incoming_angle;
      circle.vel = circle.vel.rotate(2*theta);
    }
    

    Another way of doing it is to get the velocity along the tangent and then subtracting twice this value from the circle velocity.

    angles3

    Then the code becomes

    function collision(rect, circle){
      var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
      var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
    
      var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
      var tangent_vel = dist.normalize().dot(circle.vel);
      circle.vel = circle.vel.sub(tangent_vel.mult(2));
    }
    

    Both of the code snippets above do basically the same thing in about the same time (probably). Just pick whichever one you best understand.

    Also, as @arbuthnott pointed out, there's a copy-paste error in that NearestY should use rect.h instead of rect.w.

    Edit: I forgot the positional resolution. This is the process of moving two physics objects apart so that they're no longer intersecting. In this case, since the block is static, we only need to move the ball.

    penetration diagram

    function collision(rect, circle){
      var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
      var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));    
      var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
    
      if (circle.vel.dot(dist) < 0) { //if circle is moving toward the rect
        //update circle.vel using one of the above methods
      }
    
      var penetrationDepth = circle.r - dist.mag();
      var penetrationVector = dist.normalise().mult(penetrationDepth);
      circle.pos = circle.pos.sub(penetrationVector);
    }