问题
大部分的碰撞检测方法只在两个物体发生物理碰撞时才检测。但是,如果有一个小物体快速地穿过另一个物体,你的程序的更新速度就有可能跟不上而无法检测到碰撞。
举一个具体的例子,比如一枚子弹打穿一个瓶子。子弹以5000km/h 的速度射向一个瓶子,而瓶子的宽度只有15cm。XNA程序每秒更新60次,所以每次更新子弹会前进23米的距离。这样的话,在调用Update方法时几乎没有可能检测到子弹和瓶子的碰撞,即使上一帧子弹的确穿过了瓶子。
解决方案
你可以在子弹的上一个位置和当前位置之间创建一个Ray。然后通过调用Ray或包围球的Intersect方法检测Ray是否和包围球发生碰撞。如果发生碰撞,这个方法返回碰撞点与Ray的终点间的距离(你可以使用子弹的前一个位置)。
你可以使用这个距离检测在子弹的前一个位置和当前位置之间是否真的发生了碰撞。
工作原理
这个方法需要储存在模型Tag属性中的全局包围球,如教程4-5所示:
myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank", Content);
你将创建一个方法以模型,模型的世界矩阵,快速物体的前一个位置和当前位置为参数。
private bool RayCollision(Model model, Matrix world, Vector3 lastPosition, Vector3 currentPosition) { BoundingSphere modelSpere = (BoundingSphere)model.Tag; BoundingSphere transSphere = TransformBoundingSphere(modelSpere, world); }
首先需要将模型的包围球移动到模型在3D空间中的当前位置,这一步可以通过使用模型的世界矩阵转换包围球做到(在前一个教程中已经解释过了)。 注意:如果你还想事先缩放模型,那么还要把缩放矩阵也组合到世界矩阵中去,这样可以让前面的代码正确地缩放包围球。
接下来找到快速物体的运动方向和自上一帧以来运动的距离:
Vector3 direction = currentPosition - lastPosition; float distanceCovered = direction.Length(); direction.Normalize();
你可以将B减去A获得A至B的方向,调用这个方向的Length 方法获得两者的距离。对于方向来说,通常需要将它归一化使它的长度变为1。因此,请在存储了长度之后再将它归一化,否则距离将一直为1。
现在获得了Ray的一个点和它的方向就可以创建Ray了。你将使用这个Ray的Intersect方法检测与慢模型的包围球之间的碰撞。
Ray ray = new Ray(lastPosition, direction);
现在有了子弹的Ray,你就做好了完成该方法的准备:
bool collision = false; float? intersection = ray.Intersects(transSphere); if (intersection != null) if (intersection <= distanceCovered) collision = true; return collision;
首先定义一个collision变量,除非发生碰撞这个变量将保持为false。在方法的最后将返回这个变量。
调用Ray的Intersect方法并传递到模型的变换过的包围球。
Intersection方法有点特别。如果发生碰撞,这个方法返回碰撞点和用来定义Ray的点之间的距离。但是,如果Ray和包围球没有发生碰撞,这个方法返回null。所以你需要一个 float?变量而不是float,因为它需要能够存储null值。
注意:如果碰撞点位于定义ray的点之后,这个方法也会返回null。因为你在创建ray时指定了方向,XNA知道ray的哪一边是否在这个点的前面或后面。
如果ray和包围球之间发生碰撞并且碰撞点在最终位置点之前,intersection就不是null。要验证两帧之间是否发生碰撞,你需要检查碰撞点和快速物体前一个点之间的距离是否小于这个物体在上一帧帧运动的距离。如果是,则发生了碰撞。
更精确的方法
前面的教程中已经解释过了,因为使用的是包围球,这个方法可能在实际上并没有发生碰撞时也会检测到碰撞。所以再次,你可以通过在模型的不同ModelMesh上的小包围球上进行ray和包围球的碰撞检测提高精度。
大而快的物体
前面介绍的碰撞检测的方法要求快速物体非常小,类似于3D空间中的一个点。但在快物体很大不能看成一个点的情况下,你需要将快速物体当作一个包围球。一个解决方法是基于快速物体上的不同点进行多次ray检测,但这样做比较复杂。
一个快速且更简单的方法是将快速物体的半径添加到慢物体的半径上,如图4-15所示。这个图显示了两个物体的碰撞边缘。如果你将小半径添加到大半径上,结果是相同的。使用这个方法,你可以用一个简单的点表示快物体,并使用前面相同的碰撞检测方法。
图4-15 增大包围球可以获得相同的结果。
代码
加载了慢模型后,你需要在Tag属性中存储它的包围球:
protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank", Content); }
有了包围球就可以调用下面的方法,显示在上一帧的过程中是否发生碰撞:
private bool RayCollision(Model model, Matrix world, Vector3 lastPosition, Vector3 currentPosition) { BoundingSphere modelSpere = (BoundingSphere)model.Tag; BoundingSphere transSphere = XNAUtils.TransformBoundingSphere(modelSpere, world); Vector3 direction = currentPosition - lastPosition; float distanceCovered = direction.Length(); direction.Normalize(); Ray ray = new Ray(lastPosition, direction); bool collision = false; float? intersection = ray.Intersects(transSphere); if (intersection != null) if (intersection <= distanceCovered) collision = true; return collision; }