Circle-Rectangle collision side detection in libgdx

前端 未结 2 707
野的像风
野的像风 2020-12-30 10:34

I have spent hours looking for the solution to this: I am developing a little top-down game with libgdx (maybe it matters what engine i am using). Now i have to implement th

相关标签:
2条回答
  • 2020-12-30 11:20

    You can find an explanation for circle/rectangle collision below, but please note that this type of collision might not be necessary for your needs. If, for example, you had a rectangle bounding box for your character the algorithm would be simpler and faster. Even if you are using a circle, it is probable that there is a simpler approach that is good enough for your purposes.

    I though about writing the code for this, but it would take too long so here is only an explanation:

    Here is a example movement of your character circle, with its last (previous) and current positions. Wall rectangle is displayed above it.


    enter image description here


    Here is that same movement, dotted lines represent the area the circle sweeps in this move. The sweep area is capsule shaped.


    enter image description here

    It would be difficult to calculate the collision of these two object, so we need to do this differently. If you look at the capsule on the previous image, you will see that it is simply the movement line extended in every direction by the radius of the circle. We can move that "extension" from the movement line to the wall rectangle. This way we get a rounded rectangle like on the image below.


    enter image description here

    The movement line will collide with this extended (rounded) rectangle if and only if the capsule collides with the wall rectangle, so they are somehow equivalent and interchangeable.

    Since this collision calculation is still non-trivial and relatively expensive, you can first do a fast collision check between the extended wall rectangle (non-rounded this time) and the bounding rectangle of the movement line. You can see these rectangles on the image below - they are both dotted. This is a fast and easy calculation, and while you play the game there will probably NOT be an overlap with a specific wall rectangle >99% of the time and collision calculation will stop here.


    enter image description here

    If however there is an overlap, there is probably a collision of the character circle with wall rectangle, but it is not certain as will be demonstrated later.

    Now you need to calculate the intersection between the movement line itself (not its bounding box) and the extended wall rectangle. You can probably find an algorithm how to do this online, search for line/rectangle intersection, or line/aabb intersection (aabb = Axis Aligned Bounding Box). The rectangle is axis-aligned and this makes the calculation simpler. The algorithm can give you intersection point or points since it is possible that there are two - in this case you choose the closest one to the starting point of the line. Below is an example of this intersection/collision.


    enter image description here

    When you get an intersection point, it should be easy to calculate on which part of the extended rectangle this intersection is located. You can see these parts on the image above, separated by red lines and marked with one or two letters (l - left, r - right, b - bottom, t - top, tl - top and left etc).
    If the intersection is on parts l, r, b or t (the single letter ones, in the middle) then you are done. There is definitely a collision between character circle and wall rectangle, and you know on which side. In the example above, it is on the bottom side. You should probably use 4 variables called something like isLeftCollision, isRightCollision, isBottomCollsion and isTopCollision. In this case you would set isBottomCollision to true, while the other 3 would remain at false.

    However, if the intersection is on the corner, on the two-letter sections, additional calculations are needed to determine if there is an actual collision between character circle and wall rectangle. Image below shows 3 such intersections on the corners, but there is an actual circle-rectangle collision on only 2 of them.


    enter image description here

    To determine if there is a collision, you need to find an intersection between the movement line and the circle centered in the closest corner of the original non-extended wall rectangle. The radius of this circle is equal to the radius of character circle. Again, you can google for line/circle intersection algorithm (maybe even libgdx has one), it isn't complex and shouldn't be hard to find.
    There is no line/circle intersection (and no circle/rectangle collision) on bl part, and there are intersections/collisions on br and tr parts.
    In the br case you set both isRightCollision, isBottomCollsion to true and in the tr case you set both isRightCollision and isTopCollision to true.

    There is also one edge case you need to look out for, and you can see it on the image below.


    enter image description here

    This can happen if the movement of previous step ends in the corner of the the extended rectangle, but outside the radius of the inner rectangle corner (there was no collision).
    To determine if this is the case, simply check if movement staring point is inside the extended rectangle.
    If it is, after the initial rectangle overlap test (between extended wall rectangle and bounding rectangle of movement line), you should skip line/rectangle intersection test (because in this case there might not be any intersection AND still be a circle/rectangle collision), and also simply based on movement stating point determine which corner you are in, and then only check for line/circle intersection with that corner's circle. If there is intersection, there is a character circle/wall rectangle collision, otherwise not.

    After all of this, the collision code should be simple:

    // x, y - character coordinates
    // r - character circle radius
    // speedX, speedY - character speed
    // intersectionX, intersectionY - intersection coordinates
    // left, right, bottom, top - wall rect positions
    
    // I strongly recomment using a const "EPSILON" value
    // set it to something like 1e-5 or 1e-4
    // floats can be tricky and you could find yourself on the inside of the wall
    // or something similar if you don't use it :)
    
    if (isLeftCollision) {
        x = intersectionX - EPSILON;
        if (speedX > 0) {
            speedX = 0;
        }
    } else if (isRightCollision) {
        x = intersectionX + EPSILON;
        if (speedX < 0) {
            speedX = 0;
        }
    }
    
    if (isBottomCollision) {
        y = intersectionY - EPSILON;
        if (speedY > 0) {
            speedY = 0;
        }
    } else if (isTopCollision) {
        y = intersectionY + EPSILON;
        if (speedY < 0) {
            speedY = 0;
        }
    }
    

    [Update]

    Here is a simple and I believe efficient implementation of segment-aabb intersection that should be good enough for your purposes. It is a slightly modified Cohen-Sutherland algorithm. Also you can check out the second part of this answer.

    public final class SegmentAabbIntersector {
    
        private static final int INSIDE = 0x0000;
        private static final int LEFT = 0x0001;
        private static final int RIGHT = 0x0010;
        private static final int BOTTOM = 0x0100;
        private static final int TOP = 0x1000;
    
        // Cohen–Sutherland clipping algorithm (adjusted for our needs)
        public static boolean cohenSutherlandIntersection(float x1, float y1, float x2, float y2, Rectangle r, Vector2 intersection) {
    
            int regionCode1 = calculateRegionCode(x1, y1, r);
            int regionCode2 = calculateRegionCode(x2, y2, r);
    
            float xMin = r.x;
            float xMax = r.x + r.width;
            float yMin = r.y;
            float yMax = r.y + r.height;
    
            while (true) {
                if (regionCode1 == INSIDE) {
                    intersection.x = x1;
                    intersection.y = y1;
                    return true;
                } else if ((regionCode1 & regionCode2) != 0) {
                    return false;
                } else {
                    float x = 0.0f;
                    float y = 0.0f;
    
                    if ((regionCode1 & TOP) != 0) {
                        x = x1 + (x2 - x1) / (y2 - y1) * (yMax - y1);
                        y = yMax;
                    } else if ((regionCode1 & BOTTOM) != 0) {
                        x = x1 + (x2 - x1) / (y2 - y1) * (yMin - y1);
                        y = yMin;
                    } else if ((regionCode1 & RIGHT) != 0) {
                        y = y1 + (y2 - y1) / (x2 - x1) * (xMax - x1);
                        x = xMax;
                    } else if ((regionCode1 & LEFT) != 0) {
                        y = y1 + (y2 - y1) / (x2 - x1) * (xMin - x1);
                        x = xMin;
                    }
    
                    x1 = x;
                    y1 = y;
                    regionCode1 = calculateRegionCode(x1, y1, r);
                }
            }
        }
    
        private static int calculateRegionCode(double x, double y, Rectangle r) {
            int code = INSIDE;
    
            if (x < r.x) {
                code |= LEFT;
            } else if (x > r.x + r.width) {
                code |= RIGHT;
            }
    
            if (y < r.y) {
                code |= BOTTOM;
            } else if (y > r.y + r.height) {
                code |= TOP;
            }
    
            return code;
        }
    }
    

    Here is some code example usage:

    public final class Program {
    
        public static void main(String[] args) {
    
            float radius = 5.0f;
    
            float x1 = -10.0f;
            float y1 = -10.0f;
            float x2 = 31.0f;
            float y2 = 13.0f;
    
            Rectangle r = new Rectangle(3.0f, 3.0f, 20.0f, 10.0f);
            Rectangle expandedR = new Rectangle(r.x - radius, r.y - radius, r.width + 2.0f * radius, r.height + 2.0f * radius);
    
            Vector2 intersection = new Vector2();
    
            boolean isIntersection = SegmentAabbIntersector.cohenSutherlandIntersection(x1, y1, x2, y2, expandedR, intersection);
            if (isIntersection) {
                boolean isLeft = intersection.x < r.x;
                boolean isRight = intersection.x > r.x + r.width;
                boolean isBottom = intersection.y < r.y;
                boolean isTop = intersection.y > r.y + r.height;
    
                String message = String.format("Intersection point: %s; isLeft: %b; isRight: %b; isBottom: %b, isTop: %b",
                        intersection, isLeft, isRight, isBottom, isTop);
                System.out.println(message);
            }
    
            long startTime = System.nanoTime();
            int numCalls = 10000000;
            for (int i = 0; i < numCalls; i++) {
                SegmentAabbIntersector.cohenSutherlandIntersection(x1, y1, x2, y2, expandedR, intersection);
            }
            long endTime = System.nanoTime();
            double durationMs = (endTime - startTime) / 1e6;
    
            System.out.println(String.format("Duration of %d calls: %f ms", numCalls, durationMs));
        }
    }
    

    This is the result I get from executing this:

    Intersection point: [4.26087:-2.0]; isLeft: false; isRight: false; isBottom: true, isTop: false
    Duration of 10000000 calls: 279,932343 ms
    

    Please note that this is desktop performance, on an i5-2400 CPU. It will probably be much slower on Android devices, but I believe still more than sufficient.
    I only tested this superficially, so if you find any errors, let me know.

    If you use this algorithm, I believe you don't need special handling for that case where starting point is in the corner of the extended wall rectangle, since in this case you will get the intersection point at line start, and the collision detection procedure will continue to the next step (line-circle collision).

    0 讨论(0)
  • 2020-12-30 11:23

    I suppose you determine the collision by calculating the distance of the circles center with the lines. We can simplify the case and tell that the circle colliding with the corner if both distances are equal and smaller than the radius. The equality should have a tolerance of course.

    More - may be not necessary- realistic approach would be to consider x,y speed and factor it in the equality check.

    0 讨论(0)
提交回复
热议问题