libgdx中的Circle-Rectangle碰撞侧检测

时间:2014-01-17 16:11:23

标签: java libgdx collision-detection intersection

我花了几个小时寻找解决方案:我正在用libgdx开发一个小型自上而下的游戏(也许我使用的引擎很重要)。现在我必须在我的角色(圆圈)和墙壁(矩形)之间实现碰撞检测。如果可以滑动,我希望角色在碰撞时沿着墙壁滑动。 让我解释一下:

  
      
  • 如果我向上移动45度,我可以与向下碰撞   左边或墙角。
  •   
  • 如果我与左边相撞,我想停止x移动并向上移动。如果我离开墙壁,那么我想继续前进。相同   向下(停止y运动)
  •   
  • 如果我与角落碰撞,我想停止运动(不可能滑动)。
  •   

我实际上要做的是检查矩形的左边是否与我的圆相交。然后我检查左边的墙和我的圆圈以及墙的底线和我的圆圈之间的交叉点。根据哪个交点发生,我设置了我的圆圈的x / y,并将x / y速度设置为0.问题是,大多数情况下不是碰撞而是重叠发生。所以底部检查返回true,即使实际上圆圈只会与右边碰撞。在这种情况下,两个交叉点测试都将返回true,我将重置两个速度,就像在Corner碰撞中一样。 我怎么解决这个问题?是否有更好的方法来检测碰撞和碰撞的侧面或角落? 我不需要在矩形的一侧确切的碰撞点。

修改 我不得不说,rects不是平行于x轴旋转的。

2 个答案:

答案 0 :(得分:18)

您可以在下方找到有关圆/矩形碰撞的说明,但请注意,此类碰撞可能不是您需要的。例如,如果您的角色有一个矩形边界框,则算法会更简单,更快捷。即使您使用的是圆形,也可能有一种更简单的方法可以满足您的需要。

我虽然为此编写代码,但这需要太长时间,所以这里只是一个解释:

以下是角色圈的示例移动,包括其最后(前一个)和当前位置。墙上方显示墙面矩形。


enter image description here


这是相同的运动,虚线表示圆圈在此移动中扫过的区域。扫掠区域是胶囊形状。


enter image description here

计算这两个对象的碰撞是很困难的,所以我们需要以不同的方式做到这一点。如果你看一下上一张图像上的胶囊,你会看到它只是在每个方向上由圆的半径延伸的运动线。我们可以移动那个"扩展"从运动线到墙矩形。这样我们得到一个圆角矩形,如下图所示。


enter image description here

当且仅当胶囊与墙矩形碰撞时,移动线将与此扩展(圆角)矩形碰撞,因此它们在某种程度上是等效且可互换的。

由于此碰撞计算仍然非常重要且相对昂贵,因此您可以先在扩展墙矩形(此时为非舍入)和运动线的边界矩形之间进行快速碰撞检查。您可以在下面的图像上看到这些矩形 - 它们都是点缀的。这是一个快速简便的计算,当你玩游戏时,可能不会与特定的墙矩形重叠> 99%的时间和碰撞计算将在这里停止。


enter image description here

然而,如果存在重叠,则可能是角色圆与墙矩形发生碰撞,但不确定如下所述。

现在您需要计算移动线本身(不是其边界框)与扩展墙矩形之间的交点。您可以找到一个算法如何在线执行此操作,搜索线/矩形交点或线/ aabb交点(aabb = Axis Aligned Bounding Box)。矩形是轴对齐的,这使计算更简单。该算法可以为您提供一个或多个交叉点,因为有可能有两个 - 在这种情况下,您可以选择最接近该线的起点。以下是此交叉/碰撞的示例。


enter image description here

当你得到一个交叉点时,应该很容易计算这个交叉点所在的扩展矩形的哪个部分。您可以在上面的图像上看到这些部分,用红线分隔并标有一个或两个字母(l - 左,r - 右,b - 底部,t - 顶部,tl - 顶部和左侧等)。
如果交叉点位于部分l,r,b或t(中间的单字母),那么您就完成了。字符圈和墙矩形之间肯定存在碰撞,你知道在哪一边。在上面的例子中,它位于底部。您应该使用4个称为isLeftCollisionisRightCollisionisBottomCollsionisTopCollision的变量。在这种情况下,您可以将isBottomCollision设置为true,而将其他3设置为false。

但是,如果交叉点位于角上,在两个字母的部分上,则需要进行其他计算以确定字符圆和墙矩形之间是否存在实际碰撞。下图显示了角落上的3个这样的交叉点,但只有2个实际的圆形 - 矩形碰撞。


enter image description here

要确定是否存在碰撞,您需要在移动线和以原始非扩展墙矩形的最近角为中心的圆之间找到交点。该圆的半径等于字符圆的半径。再一次,你可以google for line / circle intersection算法(甚至libgdx也有),它不复杂,不应该很难找到。
bl部分没有直线/圆形交叉点(没有圆/矩形碰撞),br和tr部分有交叉/碰撞。
在这种情况下,您将isRightCollisionisBottomCollsion设置为true,在tr情况下,您将isRightCollisionisTopCollision都设置为true。

您还需要注意一个边缘情况,您可以在下面的图片中看到它。


enter image description here

如果上一步的移动在扩展矩形的角落结束,但在内部矩形角的半径之外(没有碰撞),则会发生这种情况。
要确定是否是这种情况,只需检查运动起始点是否在扩展矩形内 如果是,在初始矩形重叠测试之后(在扩展墙矩形和移动线的边界矩形之间),你应该跳过线/矩形交叉测试(因为在这种情况下可能没有任何交叉并且仍然是圆/矩形碰撞),并且还简单地基于运动说明点确定您所在的角落,然后仅检查与该角落圆圈的线/圆交叉点。如果有交叉点,则会出现字符圆/墙矩形碰撞,否则不会。

毕竟,碰撞代码应该很简单:

// 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;
    }
}

<强> [更新]

这是一个简单的,我认为有效的段 - aabb交叉实现应该足够您的目的。它略有修改Cohen-Sutherland algorithm。您还可以查看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;
    }
}

以下是一些代码示例用法:

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));
    }
}

这是我执行此操作后得到的结果:

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

请注意,这是i5-2400 CPU上的桌面性能。在Android设备上它可能会慢得多,但我相信还是绰绰有余 我只是在表面上测试过,所以如果你发现任何错误,请告诉我。

如果您使用此算法,我相信您不需要特殊处理,因为起始点位于扩展墙矩形的角落,因为在这种情况下,您将获得行开始处的交叉点,并且碰撞检测程序将继续下一步(线圆碰撞)。

答案 1 :(得分:0)

我想你通过计算圆心与线的距离来确定碰撞。 如果距离相等且小于半径,我们可以简化情况并告知圆与角相撞。平等当然应该有宽容。

更多 - 可能不是必要的 - 现实的方法是考虑x,y速度并将其考虑在等式检查中。