Practical 2D collision detection – Part 2

On our last article, we made a very simple program that helped us detect when two circles were colliding. However, 2D games are usually much more complex than just circles. I shall now introduce the next shape: the rectangle.

rectangle

By now you probably noticed that, for the screenshots I’m using a program called “Collision Test”. This is a small tool I made to help me visualize all this stuff I’m talking about. I used this program to build the collision detection/resolution framework for an indie top-down adventure game I was involved in. I will be talking more about this tool in future articles.

Now, there are many ways to represent a rectangle. I will be representing them as five numbers: The center coordinates, width, height and the rotation angle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CollisionRectangle
{
    public float X { get; set; }
    public float Y { get; set; }
    public float Width { get; set; }
    public float Height { get; set; }
    public float Rotation { get; set; }
 
    public CollisionRectangle(float x, float y, float width, float height, float rotation)
    {
        X = x;
        Y = y;
        Width = width;
        Height = height;
        Rotation = rotation
    }
}

Now, for our first collision, we will collide a circle and a rectangle. There are two types of collisions to consider: When the circle is entirely inside the rectangle…

rectangle_inside

…And when the circle is partly inside the rectangle, that is, it is touching the border

rectangle_intersection

These are two different types of collisions, and use different algorithms to determine whether or not there is a collision.

But first, let’s forget about the rectangle’s position and rotation. Our first approach will deal with a rectangle centered in the world, and not rotated:

rectangle_centered

Under these constraints, the circle is inside the rectangle when both the X coordinate of the circle is between the left and right borders, and the Y coordinate is between the top and bottom borders, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static bool IsCollision(CollisionCircle a, CollisionRectangle b)
{
    // For now, we will suppose b.X==0, b.Y==0 and b.Rotation==0
 
    float halfWidth = b.Width / 2.0f;
    float halfHeight = b.Height / 2.0f;
 
    if (a.X >= -halfWidth && a.X <= halfWidth &&
        a.Y >= -halfHeight && a.Y <= halfHeight)
    {
        // Circle is inside the rectangle
        return true;
    }
 
    return false; // We're not finished yet...
}

But this is not enough. This only works when the center of the circle is inside the rectangle. There are plenty of situations where the center of the circle is outside the rectangle, but the circle is still touching the rectangle.

In this case, we first find the point in the rectangle which is closest to the circle, and if the distance between this point and the center of the circle is smaller than the radius, then the circle is touching the border of the rectangle.

We find the closest point for the X and Y coordinates separately:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
float closestX, closestY;
 
// Find the closest point in the X axis
if (a.X < -halfWidth)
    closestX = -halfWidth;
else if (a.X > halfWidth)
    closestX = halfWidth
else
    closestX = a.X;
 
// Find the closest point in the Y axis
if (a.Y < -halfHeight)
    closestY = -halfHeight;
else if (a.Y > halfHeight)
    closestY = halfHeight;
else
    closestY = a.Y;

And now we bring it all together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static bool IsCollision(CollisionCircle a, CollisionRectangle b)
{
    // For now, we will suppose b.X==0, b.Y==0 and b.Rotation==0
 
    float halfWidth = b.Width / 2.0f;
    float halfHeight = b.Height / 2.0f;
 
    if (a.X >= -halfWidth && a.X <= halfWidth &&
        a.Y >= -halfHeight && a.Y <= halfHeight)
    {
        // Circle is inside the rectangle
        return true;
    }
 
    float closestX, closestY;
 
    // Find the closest point in the X axis
    if (a.X < -halfWidth)
        closestX = -halfWidth;
    else if (a.X > halfWidth)
        closestX = halfWidth
    else
        closestX = a.X;
 
    // Find the closest point in the Y axis
    if (a.Y < -halfHeight)
        closestY = -halfHeight;
    else if (a.Y > halfHeight)
        closestY = halfHeight;
    else
        closestY = a.Y;
 
    float deltaX = a.X - closestX;
    float deltaY = a.Y - closestY;
    float distanceSquared = deltaX * deltaX - deltaY * deltaY;
 
    if (distanceSquared <= a.R * a.R)
        return true;
 
    return false;
}

Looks good, but we’re still operating under the assumption that the rectangle is centered and not rotated.

To overcome this limitation, we can move the entire world -that is, both the rectangle and the circle-, so the rectangle ends centered and non-rotated:

rotated

In other words, we have to find the position of the circle, relative to the rectangle. This is pretty straightforward trigonometry:

1
2
3
4
5
6
float relativeX = a.X - b.X;
float relativeY = a.Y - b.Y;
float relativeDistance = (float)Math.Sqrt(relativeX * relativeX + relativeY * relativeY);
float relativeAngle = (float)Math.Atan2(relativeY, relativeX);
float newX = relativeDistance * (float)Math.Cos(relativeAngle - b.Rotation);
float newY = relativeDistance * (float)Math.Sin(relativeAngle - b.Rotation);

And then put it all together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class CollisionRectangle
{
    public float X { get; set; }
    public float Y { get; set; }
    public float Width { get; set; }
    public float Height { get; set; }
    public float Rotation { get; set; }
 
    public CollisionRectangle(float x, float y, float width, float height, float rotation)
    {
        X = x;
        Y = y;
        Width = width;
        Height = height;
        Rotation = rotation
    }
 
    public static bool IsCollision(CollisionCircle a, CollisionRectangle b)
    {
        float relativeX = a.X - b.X;
        float relativeY = a.Y - b.Y;
        float relativeDistance = (float)Math.Sqrt(relativeX * relativeX + relativeY * relativeY);
        float relativeAngle = (float)Math.Atan2(relativeY, relativeX);
        float newX = relativeDistance * (float)Math.Cos(relativeAngle - b.Rotation);
        float newY = relativeDistance * (float)Math.Sin(relativeAngle - b.Rotation);
 
        float halfWidth = b.Width / 2.0f;
        float halfHeight = b.Height / 2.0f;
 
        if (newX >= -halfWidth && newX <= halfWidth &&
            newY >= -halfHeight && newY <= halfHeight)
        {
            // Circle is inside the rectangle
            return true;
        }
 
        float closestX, closestY;
 
        // Find the closest point in the X axis
        if (newX < -halfWidth)
            closestX = -halfWidth;
        else if (newX > halfWidth)
            closestX = halfWidth
        else
            closestX = newX;
 
        // Find the closest point in the Y axis
        if (newY < -halfHeight)
            closestY = -halfHeight;
        else if (newY > halfHeight)
            closestY = halfHeight;
        else
            closestY = newY;
 
        float deltaX = newX - closestX;
        float deltaY = newY - closestY;
        float distanceSquared = deltaX * deltaX - deltaY * deltaY;
 
        if (distanceSquared <= a.R * a.R)
            return true;
 
        return false;
    }
}

In the next article, we’ll put some structure to all of this.

One Comment

  1. jaykop
    Posted 2016-07-19 at 16:17 | Permalink

    Hello.
    I am a student from South Korea, working on custom game engine, and I’ve followed your tutorial to make simple 2d physics.
    I am not good at physics and most what I have done is almost copied your sample code.
    But There is a critical bug on my work; when the ball is collided to box’s vertex, then two objects stick together and vibrate.
    I removed the part setting new velocity and speed but still problem happens. I think it is caused by part setting new mtd(penetration depth).
    If you’re available, can you help me?
    thank you.
    I post my code here;

    /******************************************************************************/
    /*!
    \brief – Do Collision Intersect between box and ball

    \param box – get 1st body’s verts
    \param ball – get 2nd body’s position and radius

    \return bool
    */
    /******************************************************************************/
    bool World::IntersectBoxToBall(Sprite* box, Sprite* ball)
    {
    // Call this radius
    float radius = ball->GetRigidBody()->GetScale().x / 2.f;

    vec3 relPos = ball->GetPosition() – box->GetPosition();
    float relDIs = sqrt(relPos.x * relPos.x + relPos.y * relPos.y);
    float relDeg = atan2(relPos.y, relPos.x) – Math::DegToRad(box->GetRotation());

    vec3 new_xy = vec3(cosf(relDeg), sinf(relDeg)) * relDIs;
    vec3 halfSize = box->GetRigidBody()->GetScale() / 2.f;

    if (new_xy.x >= -halfSize.x && new_xy.x = -halfSize.y && new_xy.y <= halfSize.y)
    return true;

    // Init closest;
    vec3 closest;

    // Find the closest point in the X axis
    if (new_xy.x halfSize.x)
    closest.x = halfSize.x;
    else
    closest.x = new_xy.x;

    // Find the closest point in the Y axis
    if (new_xy.y halfSize.y)
    closest.y = halfSize.y;
    else
    closest.y = new_xy.y;

    vec3 dt = new_xy – closest;
    float distance_sq = dt.DotProduct(dt);

    //
    if (distance_sq <= radius * radius)
    {
    float dist = sqrt(distance_sq);
    if (dist < 0.0000001f) return false;

    mtd = dt * (radius – dist) / dist;

    if (relPos.DotProduct(mtd) GetRigidBody()->GetMass();
    float iMass_box = box->GetRigidBody()->GetMass();
    float iMass = iMass_ball + iMass_box;
    if (iMass GetRigidBody()->GetVelocity() – n ;
    new_vel[1] = ball->GetRigidBody()->GetVelocity() + n ;

    // Save new speed
    new_speed[0] = box->GetRigidBody()->GetSpeed() *
    (box->GetRigidBody()->GetMass() /
    (ball->GetRigidBody()->GetMass() + box->GetRigidBody()->GetMass()));

    new_speed[1] = ball->GetRigidBody()->GetSpeed() *
    (ball->GetRigidBody()->GetMass() /
    (ball->GetRigidBody()->GetMass() + box->GetRigidBody()->GetMass()));

    // If both 2 sprites are movable…
    if (box->GetRigidBody()->GetMoveToggle() && ball->GetRigidBody()->GetMoveToggle())
    {
    // Set sprites’ position with mtd
    box->SetPosition(box->GetPosition() – mtd * .5f *(iMass_box / iMass) );
    ball->SetPosition(ball->GetPosition() + mtd * .5f *(iMass_ball / iMass));

    // Set new velocity
    box->GetRigidBody()->SetVelocity(new_vel[0]);
    ball->GetRigidBody()->SetVelocity(new_vel[1]);

    // Set new speed
    box->GetRigidBody()->SetSpeed(new_speed[0] + new_speed[1] / ball->GetRigidBody()->GetMass());
    ball->GetRigidBody()->SetSpeed(new_speed[1] + new_speed[0] / box->GetRigidBody()->GetMass());
    }

    // If only box sprite is movable…
    else if (!ball->GetRigidBody()->GetMoveToggle() && box->GetRigidBody()->GetMoveToggle())
    {
    // Set sprites’ position with mtd
    box->SetPosition(box->GetPosition() – mtd * (iMass_box / iMass));

    // Set new velocity
    box->GetRigidBody()->SetVelocity(new_vel[0]);

    // Set new speed
    box->GetRigidBody()->SetSpeed(new_speed[0] + new_speed[1] / ball->GetRigidBody()->GetMass());
    }

    // If only ball sprite is movable…
    else if (!box->GetRigidBody()->GetMoveToggle() && ball->GetRigidBody()->GetMoveToggle())
    {
    // Set sprites’ position with mtd
    ball->SetPosition(ball->GetPosition() + mtd * (iMass_ball / iMass));

    // Set new velocity
    ball->GetRigidBody()->SetVelocity(new_vel[1]);

    // Set new speed
    ball->GetRigidBody()->SetSpeed(new_speed[1] + new_speed[0] / box->GetRigidBody()->GetMass());
    }
    }


Post a Comment

Required fields are marked *
*
*