libgdx中的Circle-Rectangle碰撞侧检测

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

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

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

编辑:我不得不说,rects不是平行于x轴旋转。

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

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

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

在此处输入图像描述

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

在此处输入图像描述

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

在此处输入图像描述

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

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

在此处输入图像描述

然而,如果存在重叠,则可能存在角色圆与墙矩形的碰撞,但是不确定,如稍后将说明的。

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

在此处输入图像描述

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

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

在此处输入图像描述

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

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

在此处输入图像描述

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

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

 // 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算法 。 您也可以查看此答案的第二部分。

 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 = rx; float xMax = rx + r.width; float yMin = ry; float yMax = ry + 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 < rx) { code |= LEFT; } else if (x > rx + r.width) { code |= RIGHT; } if (y < ry) { code |= BOTTOM; } else if (y > ry + 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(rx - radius, ry - 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 < rx; boolean isRight = intersection.x > rx + r.width; boolean isBottom = intersection.y < ry; boolean isTop = intersection.y > ry + 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设备上它可能会慢得多,但我相信还是绰绰有余。
我只是在表面上测试过,所以如果你发现任何错误,请告诉我。

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

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

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