与AABB的移动和碰撞

我有一点点困难搞清楚下面的情况。

在场景A中,移动的实体已经下降到(并且稍微进入地板)。 当前位置表示如果我像往常一样使用加速度和速度而不担心碰撞的情况下将发生的投影位置。 下一个位置表示碰撞检查后的校正投影位置。 由此产生的最终位置是下落的实体现在停在地板上 – 也就是说,通过将底部的X轴与地板的顶部的X轴共享,实现一致的碰撞状态。

我目前的更新循环如下所示:

// figure out forces & accelerations and project an objects next position // check collision occurrence from current position -> projected position // if a collision occurs, adjust projection position 

这似乎正在为我的对象落在地上的情况。 然而,当试图找出情景的B&C时,情况变得粘稠了。

在场景B中,我试图在X轴上沿着地面移动(玩家正在按右方向button),重力将物体拉到地板上。 问题是,当对象试图移动时,碰撞检测代码将会认识到对象已经与地面碰撞,并自动将任何移动修正回到之前的位置。

在场景C中,我试图跳下场。 同样,因为物体已经与地面不断碰撞,所以当碰撞程序检查以确保从当前位置移动到投影位置不会导致碰撞时,碰撞将会失败,因为在运动开始时,对象已经发生碰撞。

你如何允许沿着物体的边缘移动? 你如何允许远离你已经碰撞的物体移动。

额外的信息我的碰撞程序是基于旧的gamasutra文章的AABB扫描testing, http: //www.gamasutra.com/view/feature/3383/simple_intersection_tests_for_games.php?page =3

我的边界框实现基于左上/右下,而不是中点/范围,所以我的最小/最大function进行了调整。 否则,这是我的包围盒类碰撞例程:

 public class BoundingBox { public XYZ topLeft; public XYZ bottomRight; public BoundingBox(float x, float y, float z, float w, float h, float d) { topLeft = new XYZ(); bottomRight = new XYZ(); topLeft.x = x; topLeft.y = y; topLeft.z = z; bottomRight.x = x+w; bottomRight.y = y+h; bottomRight.z = z+d; } public BoundingBox(XYZ position, XYZ dimensions, boolean centered) { topLeft = new XYZ(); bottomRight = new XYZ(); topLeft.x = position.x; topLeft.y = position.y; topLeft.z = position.z; bottomRight.x = position.x + (centered ? dimensions.x/2 : dimensions.x); bottomRight.y = position.y + (centered ? dimensions.y/2 : dimensions.y); bottomRight.z = position.z + (centered ? dimensions.z/2 : dimensions.z); } /** * Check if a point lies inside a bounding box * @param box * @param point * @return */ public static boolean isPointInside(BoundingBox box, XYZ point) { if(box.topLeft.x <= point.x && point.x <= box.bottomRight.x && box.topLeft.y <= point.y && point.y <= box.bottomRight.y && box.topLeft.z <= point.z && point.z <= box.bottomRight.z) return true; return false; } /** * Check for overlap between two bounding boxes using separating axis theorem * if two boxes are separated on any axis, they cannot be overlapping * @param a * @param b * @return */ public static boolean isOverlapping(BoundingBox a, BoundingBox b) { XYZ dxyz = new XYZ(b.topLeft.x - a.topLeft.x, b.topLeft.y - a.topLeft.y, b.topLeft.z - a.topLeft.z); // if b - a is positive, a is first on the axis and we should use its extent // if b -a is negative, b is first on the axis and we should use its extent // check for x axis separation if ((dxyz.x >= 0 && a.bottomRight.xa.topLeft.x < dxyz.x) // negative scale, reverse extent sum, flip equality ||(dxyz.x < 0 && b.topLeft.xb.bottomRight.x > dxyz.x)) return false; // check for y axis separation if ((dxyz.y >= 0 && a.bottomRight.ya.topLeft.y < dxyz.y) // negative scale, reverse extent sum, flip equality ||(dxyz.y < 0 && b.topLeft.yb.bottomRight.y > dxyz.y)) return false; // check for z axis separation if ((dxyz.z >= 0 && a.bottomRight.za.topLeft.z < dxyz.z) // negative scale, reverse extent sum, flip equality ||(dxyz.z < 0 && b.topLeft.zb.bottomRight.z > dxyz.z)) return false; // not separated on any axis, overlapping return true; } public static boolean isContactEdge(int xyzAxis, BoundingBox a, BoundingBox b) { switch(xyzAxis) { case XYZ.XCOORD: if(a.topLeft.x == b.bottomRight.x || a.bottomRight.x == b.topLeft.x) return true; return false; case XYZ.YCOORD: if(a.topLeft.y == b.bottomRight.y || a.bottomRight.y == b.topLeft.y) return true; return false; case XYZ.ZCOORD: if(a.topLeft.z == b.bottomRight.z || a.bottomRight.z == b.topLeft.z) return true; return false; } return false; } /** * Sweep test min extent value * @param box * @param xyzCoord * @return */ public static float min(BoundingBox box, int xyzCoord) { switch(xyzCoord) { case XYZ.XCOORD: return box.topLeft.x; case XYZ.YCOORD: return box.topLeft.y; case XYZ.ZCOORD: return box.topLeft.z; default: return 0f; } } /** * Sweep test max extent value * @param box * @param xyzCoord * @return */ public static float max(BoundingBox box, int xyzCoord) { switch(xyzCoord) { case XYZ.XCOORD: return box.bottomRight.x; case XYZ.YCOORD: return box.bottomRight.y; case XYZ.ZCOORD: return box.bottomRight.z; default: return 0f; } } /** * Test if bounding box A will overlap bounding box B at any point * when box A moves from position 0 to position 1 and box B moves from position 0 to position 1 * Note, sweep test assumes bounding boxes A and B's dimensions do not change * * @param a0 box a starting position * @param a1 box a ending position * @param b0 box b starting position * @param b1 box b ending position * @param aCollisionOut xyz of box a's position when/if a collision occurs * @param bCollisionOut xyz of box b's position when/if a collision occurs * @return */ public static boolean sweepTest(BoundingBox a0, BoundingBox a1, BoundingBox b0, BoundingBox b1, XYZ aCollisionOut, XYZ bCollisionOut) { // solve in reference to A XYZ va = new XYZ(a1.topLeft.x-a0.topLeft.x, a1.topLeft.y-a0.topLeft.y, a1.topLeft.z-a0.topLeft.z); XYZ vb = new XYZ(b1.topLeft.x-b0.topLeft.x, b1.topLeft.y-b0.topLeft.y, b1.topLeft.z-b0.topLeft.z); XYZ v = new XYZ(vb.x-va.x, vb.y-va.y, vb.z-va.z); // check for initial overlap if(BoundingBox.isOverlapping(a0, b0)) { // java pass by ref/value gotcha, have to modify value can't reassign it aCollisionOut.x = a0.topLeft.x; aCollisionOut.y = a0.topLeft.y; aCollisionOut.z = a0.topLeft.z; bCollisionOut.x = b0.topLeft.x; bCollisionOut.y = b0.topLeft.y; bCollisionOut.z = b0.topLeft.z; return true; } // overlap min/maxs XYZ u0 = new XYZ(); XYZ u1 = new XYZ(1,1,1); float t0, t1; // iterate axis and find overlaps times (x=0, y=1, z=2) for(int i = 0; i < 3; i++) { float aMax = max(a0, i); float aMin = min(a0, i); float bMax = max(b0, i); float bMin = min(b0, i); float vi = XYZ.getCoord(v, i); if(aMax < bMax && vi < 0) XYZ.setCoord(u0, i, (aMax-bMin)/vi); else if(bMax < aMin && vi > 0) XYZ.setCoord(u0, i, (aMin-bMax)/vi); if(bMax > aMin && vi < 0) XYZ.setCoord(u1, i, (aMin-bMax)/vi); else if(aMax > bMin && vi > 0) XYZ.setCoord(u1, i, (aMax-bMin)/vi); } // get times of collision t0 = Math.max(u0.x, Math.max(u0.y, u0.z)); t1 = Math.min(u1.x, Math.min(u1.y, u1.z)); // collision only occurs if t0 < t1 if(t0 <= t1 && t0 != 0) // not t0 because we already tested it! { // t0 is the normalized time of the collision // then the position of the bounding boxes would // be their original position + velocity*time aCollisionOut.x = a0.topLeft.x + va.x*t0; aCollisionOut.y = a0.topLeft.y + va.y*t0; aCollisionOut.z = a0.topLeft.z + va.z*t0; bCollisionOut.x = b0.topLeft.x + vb.x*t0; bCollisionOut.y = b0.topLeft.y + vb.y*t0; bCollisionOut.z = b0.topLeft.z + vb.z*t0; return true; } else return false; } } 

一如往常,取决于您的目标(简单,速度,准确性)有多种可能性。 一个简单的解决scheme是独立处理每个轴。

  • 找出力量和加速度,然后投射物体的下一个位置
  • 从当前位置检查碰撞发生 – >投影位置
    • 如果x轴上没有发生碰撞,则将x设置为投影位置
    • 如果y轴上没有发生碰撞,则将y设置为投影位置

没有必要使用扫描testing,一个简单的相交testing就足够了。 使用这种直接方法的不利之处在于,如果目的地点导致另一次冲突,它仍然会给出不正确的结果。 提高精确度的技巧是限制角色的速度,或者使用一个小的时间跨度,可能每帧执行多次碰撞检测。

稍微难一点的解决scheme会给出更好的结果,使用扫描testing来返回碰撞时间(或深度vector),然后将字符移到这一点。 这是比其他方法更通用的方法,并且会比其他方法更准确,这会增加需要根据碰撞时间缩放的力和加速度分量的复杂度。 积分器的稳定性也变得更加相关,并且多个同时碰撞变得更加成问题。

编辑:

@杰里米:在碰撞后你不能重置位置。 从当前位置和预测位置开始。 例如:如果对象在X轴上的预测位置发生碰撞,则不做任何事情,保持X位置不变,但如果它不在x轴上碰撞,您将新的x位置设置为预测的x位置。 对其他轴重复此操作。 (相当粗糙)的结果是,只要没有碰撞,角色就可以沿轴移动。 这样做可能会导致您的角色不接触墙壁或地板,但似乎碰撞一小段距离。 避免这种情况的解决scheme是

  1. 使用低速度,使当前位置和预测位置之间的变化永远不会超过某个可接受的值(比如一个像素)。 无可否认,这可能是最糟糕的也是最有限的解决scheme。

  2. 使用一个小的时间步长,以便在任何时刻的速度永远不会超过一个阈值(比如一个像素)。 稍好于以前的解决scheme,但不太实际,因为更高的速度将需要更小的时间步长。

  3. recursion地细分时间步长,以find不会导致碰撞的墙壁/地板的最近点。 在性能和复杂性方面这是一个很好的折衷。 不是最好的性能,但不是太复杂的实施。

  4. 更新扫描function以返回影响时间(而不是真/假值),并使用此function根据时间缩放投影位置。

为了获得最佳精度(例如,如果需要反弹或摩擦),可能需要细分模拟步骤。 例如,如果您跳向墙壁,在时间步骤中途出现冲击,则需要在冲击点重新计算力,然后再用剩余时间段运行模拟步骤。 这并不是说这会导致无限recursion(例如,如果两个对象堆叠在一起),所以需要一些方法来防止这种情况(即,限制recursion次数或计算所花费的时间量)。