1

I have followed this tutorial: https://gamedevelopment.tutsplus.com/tutorials/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331 to create a 2d physics engine in c# (he works in almost all the time wrong and inconsistent pseudo c++) I've got Circle vs Circle collision and AABB vs AABB collision working fine. But when trying the AABB vs Circle collison (below) the two rigidbodies just stick together and slowly move glitchy in one direction.

I would be super thankful if someone could help me with this as I have spent days and still don't know what's causing the error.

If someone needs more information from my code, I'd be happy to provide it.

public static bool AABBvsCircle(ref Collision result) {
        RigidBody AABB = result.a.Shape is AABB ? result.a : result.b;
        RigidBody CIRCLE = result.b.Shape is Circle ? result.b : result.a;

        Vector2 n = CIRCLE.Position - AABB.Position;

        Vector2 closest = n;

        float x_extent = ((AABB)AABB.Shape).HalfWidth;
        float y_extent = ((AABB)AABB.Shape).HalfHeight;

        closest.X = Clamp(-x_extent, x_extent, closest.X);
        closest.Y = Clamp(-y_extent, y_extent, closest.Y);


        bool inside = false;

        if (n == closest) {
            inside = true;

            if (Abs(n.X) > Abs(n.Y)) {
                // Clamp to closest extent
                if (closest.X > 0)
                    closest.X = x_extent;
                else
                    closest.X = -x_extent;
            }

            // y axis is shorter
            else {
                // Clamp to closest extent
                if (closest.Y > 0)
                    closest.Y = y_extent;
                else
                    closest.Y = -y_extent;
            }
        }

        Vector2 normal = n - closest;
        float d = normal.LengthSquared();
        float r = ((Circle)CIRCLE.Shape).Radius;

        // Early out of the radius is shorter than distance to closest point and
        // Circle not inside the AABB
        if (d > (r * r) && !inside)
            return false;

        // Avoided sqrt until we needed
        d = (float)Sqrt(d);

        if (inside) {
            result.normal = -normal / d;
            result.penetration = r - d;
        }
        else {
            result.normal = normal / d;
            result.penetration = r - d;
        }

        return true;
    }

edit 1 collison resolution method in "Collision" struct

public void Resolve() {
        Vector2 rv = b.Velocity - a.Velocity;
        float velAlongNormal = Vector2.Dot(rv, normal);

        if (velAlongNormal > 0)
            return;

        float e = Min(a.Restitution, b.Restitution);

        float j = -(1 + e) * velAlongNormal;
        j /= a.InvertedMass + b.InvertedMass;

        Vector2 impulse = j * normal;
        a.Velocity -= a.InvertedMass * impulse;
        b.Velocity += b.InvertedMass * impulse;

        const float percent = 0.2f; // usually 20% to 80%
        const float slop = 0.01f; // usually 0.01 to 0.1
        Vector2 correction = Max(penetration - slop, 0.0f) / (a.InvertedMass + b.InvertedMass) * percent * normal;
        if (float.IsNaN(correction.X) || float.IsNaN(correction.Y))
            correction = Vector2.Zero;
        a.Position -= a.InvertedMass * correction;
        b.Position += b.InvertedMass * correction;
    }
schnavid
  • 167
  • 1
  • 3
  • 9

2 Answers2

2

Before doing any detailed examining of the code logic, I spotted this potential mistake:

result.normal = -normal / d;

Since d was set to normal.LengthSquared and not normal.Length as it should be, the applied position correction could either be (much) smaller or (much) bigger than intended. Given that your objects are "sticking together", it is likely to be the former, i.e. d > 1.

(The fix is of course simply result.normal = -normal / Math.Sqrt(d);)

Note that the above may not be the only source of error; let me know if there is still undesirable behavior.

meowgoesthedog
  • 14,670
  • 4
  • 27
  • 40
1

Although your tag specifies C#; here are basic AABB to AABB & AABB to Circle collisions that are done in C++ as these are take from: LernOpenGL:InPractice:2DGame : Collision Detection


AABB - AABB Collsion

// AABB to AABB Collision
GLboolean CheckCollision(GameObject &one, GameObject &two) {
    // Collision x-axis?
    bool collisionX = one.Position.x + one.Size.x >= two.Position.x &&
        two.Position.x + two.Size.x >= one.Position.x;
    // Collision y-axis?
    bool collisionY = one.Position.y + one.Size.y >= two.Position.y &&
        two.Position.y + two.Size.y >= one.Position.y;
    // Collision only if on both axes
    return collisionX && collisionY;
}

AABB To Circle Collision Without Resolution

// AABB to Circle Collision without Resolution
GLboolean CheckCollision(BallObject &one, GameObject &two) {
    // Get center point circle first 
    glm::vec2 center(one.Position + one.Radius);
    // Calculate AABB info (center, half-extents)
    glm::vec2 aabb_half_extents(two.Size.x / 2, two.Size.y / 2);
    glm::vec2 aabb_center(
        two.Position.x + aabb_half_extents.x, 
        two.Position.y + aabb_half_extents.y
    );
    // Get difference vector between both centers
    glm::vec2 difference = center - aabb_center;
    glm::vec2 clamped = glm::clamp(difference, -aabb_half_extents, aabb_half_extents);
    // Add clamped value to AABB_center and we get the value of box closest to circle
    glm::vec2 closest = aabb_center + clamped;
    // Retrieve vector between center circle and closest point AABB and check if length <= radius
    difference = closest - center;
    return glm::length(difference) < one.Radius;
}

Then in the next section of his online tutorial he shows how to do Collision Resolution using the above method found here: LearnOpenGL : Collision Resolution

In this section he adds an enumeration, another function and an std::tuple<> to refine the above detection system while trying to keep the code easier & cleaner to manage and read.

enum Direction {
    UP,
    RIGHT,
    DOWN,
    LEFT
};     

Direction VectorDirection(glm::vec2 target)
{
    glm::vec2 compass[] = {
        glm::vec2(0.0f, 1.0f),  // up
        glm::vec2(1.0f, 0.0f),  // right
        glm::vec2(0.0f, -1.0f), // down
        glm::vec2(-1.0f, 0.0f)  // left
    };
    GLfloat max = 0.0f;
    GLuint best_match = -1;
    for (GLuint i = 0; i < 4; i++)
    {
        GLfloat dot_product = glm::dot(glm::normalize(target), compass[i]);
        if (dot_product > max)
        {
            max = dot_product;
            best_match = i;
        }
    }
    return (Direction)best_match;
} 

typedef std::tuple<GLboolean, Direction, glm::vec2> Collision; 

However there is a slight change to the original CheckCollsion() function for AABB to Circle by changing its declaration/definition to return a Collision instead of a GLboolean.

AABB - Circle Collision With Collision Resolution

// AABB - Circle Collision with Collision Resolution
Collision CheckCollision(BallObject &one, GameObject &two) {
    // Get center point circle first 
    glm::vec2 center(one.Position + one.Radius);
    // Calculate AABB info (center, half-extents)
    glm::vec2 aabb_half_extents(two.Size.x / 2, two.Size.y / 2);
    glm::vec2 aabb_center(two.Position.x + aabb_half_extents.x, two.Position.y + aabb_half_extents.y);
    // Get difference vector between both centers
    glm::vec2 difference = center - aabb_center;
    glm::vec2 clamped = glm::clamp(difference, -aabb_half_extents, aabb_half_extents);
    // Now that we know the the clamped values, add this to AABB_center and we get the value of box closest to circle
    glm::vec2 closest = aabb_center + clamped;
    // Now retrieve vector between center circle and closest point AABB and check if length < radius
    difference = closest - center;

    if (glm::length(difference) < one.Radius) // not <= since in that case a collision also occurs when object one exactly touches object two, which they are at the end of each collision resolution stage.
        return std::make_tuple(GL_TRUE, VectorDirection(difference), difference);
    else
        return std::make_tuple(GL_FALSE, UP, glm::vec2(0, 0));
}

Where the above functions or methods are called within this function that does the actually logic if a collision is detected.

void Game::DoCollisions()
{
    for (GameObject &box : this->Levels[this->Level].Bricks)
    {
        if (!box.Destroyed)
        {
            Collision collision = CheckCollision(*Ball, box);
            if (std::get<0>(collision)) // If collision is true
            {
                // Destroy block if not solid
                if (!box.IsSolid)
                    box.Destroyed = GL_TRUE;
                // Collision resolution
                Direction dir = std::get<1>(collision);
                glm::vec2 diff_vector = std::get<2>(collision);
                if (dir == LEFT || dir == RIGHT) // Horizontal collision
                {
                    Ball->Velocity.x = -Ball->Velocity.x; // Reverse horizontal velocity
                    // Relocate
                    GLfloat penetration = Ball->Radius - std::abs(diff_vector.x);
                    if (dir == LEFT)
                        Ball->Position.x += penetration; // Move ball to right
                    else
                        Ball->Position.x -= penetration; // Move ball to left;
                }
                else // Vertical collision
                {
                    Ball->Velocity.y = -Ball->Velocity.y; // Reverse vertical velocity
                    // Relocate
                    GLfloat penetration = Ball->Radius - std::abs(diff_vector.y);
                    if (dir == UP)
                        Ball->Position.y -= penetration; // Move ball bback up
                    else
                        Ball->Position.y += penetration; // Move ball back down
                }
            }
        }    
    }
    // Also check collisions for player pad (unless stuck)
    Collision result = CheckCollision(*Ball, *Player);
    if (!Ball->Stuck && std::get<0>(result))
    {
        // Check where it hit the board, and change velocity based on where it hit the board
        GLfloat centerBoard = Player->Position.x + Player->Size.x / 2;
        GLfloat distance = (Ball->Position.x + Ball->Radius) - centerBoard;
        GLfloat percentage = distance / (Player->Size.x / 2);
        // Then move accordingly
        GLfloat strength = 2.0f;
        glm::vec2 oldVelocity = Ball->Velocity;
        Ball->Velocity.x = INITIAL_BALL_VELOCITY.x * percentage * strength; 
        //Ball->Velocity.y = -Ball->Velocity.y;
        Ball->Velocity = glm::normalize(Ball->Velocity) * glm::length(oldVelocity); // Keep speed consistent over both axes (multiply by length of old velocity, so total strength is not changed)
        // Fix sticky paddle
        Ball->Velocity.y = -1 * abs(Ball->Velocity.y);
    }
}

Now some of the code above is GameSpecific as in the Game class, Ball class, Player etc. where these are considered and inherited from a GameObject, but the algorithm itself should provide useful as this is exactly what you are looking for but from a different language. Now as to your actually problem it appears you are using more than basic motion as it appears you are using some form of kinetics that can be seen from your Resolve() method.

The overall Pseudo Algorithm for doing AABB to Circle Collision with Resolution would be as follows:

  • Do Collisions:

    • Check For Collision: Ball With Box

      • Get Center Point Of Circle First
      • Calculate AABB Info (Center & Half-Extents)
      • Get Difference Vector Between Both Centers
      • Clamp That Difference Between The [-Half-Extents, Half-Extents]
      • Add The Clamped Value To The AABB-Center To Give The Point Of Box Closest To The Circle
      • Retrieve & Return The Vector Between Center Circle & Closest Point AABB & Check If Length Is < Radius (In this case a Collision).
        • If True Return tuple(GL_TRUE, VectorDirection(difference), difference))
          • See Function Above For VectorDirection Implementation.
        • Else Return tuple(GL_FALSE, UP, glm::vec2(0,0))
    • Perform Collision Resolution (Test If Collision Is True)

      • Extract Direction & Difference Vector
      • Test Direction For Horizontal Collision
        • If True Reverse Horizontal Velocity
        • Get Penetration Amount (Ball Radius - abs(diff_vector.x))
        • Test If Direction Is Left Or Right (W,E)
          • If Left - Move Ball To Right (ball.position.x += penetration)
          • Else Right - Move Ball To Left (ball.position.x -= penetration)
      • Else Test Direction For Vertical Collision
        • If True Reverse Vertical Velocity
        • Get Penetration Amount (Ball Radius - abs(diff_vector.y))
        • Test If Direction Is Up Or Down (N,S)
          • If Up - Move Ball Up (ball.position.y -= penetration)
          • Else Down - Move Ball Down (ball.position.y += penetration)

Now the above algorithm shown assumes that the boxes are not rotated and that their top & bottom edges are parallel with the horizontal and that their sides are parallel with the left and right edges of the window-screen coordinates. Also in the bottom section with the vertical displacement this also assumes that the top left corner of the screen - the first pixel is (0,0), thus the opposite operation for vertical displacement. This also assumes 2D collisions and not 3D Ridged or Ragdoll type collisions. You can use this to compare against your own source - implementation, but as far as just looking at your code without running it through a debugger it is extremely hard for me to see or find out what is actually causing your bug. I hope this provides you with the help that you need.


The above code from the mentioned OpenGL tutorial website does work as I have tested it myself. This algorithm is of the simplest of collision detections and is by far no means a comprehensive system and it still has caveats or pitfalls not mentioned here, but does suffice for the application it was used in. If you need more information about Collision Detections there is a few chapters that can be read in Ian Millington's book Game Physics Engine Development Although his book is based on a generalized 3D Physics Engine and only briefly discuses Collision Detection as their are full Books dedicated to the growing popularity of such complex beasts.

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59