球对球碰撞-检测和处理


266

在Stack Overflow社区的帮助下,我编写了一个非常基本但有趣的物理模拟器。

替代文字

单击并拖动鼠标以启动球。它会反弹并最终停在“地板”上。

我要添加的下一个重要功能是球与球之间的碰撞。球的运动分为轴速度和y速度向量。我有重力(每个步骤y向量的小减小),我有摩擦力(每个与壁碰撞的两个向量的小减小)。球以令人惊讶的现实方式诚实地移动。

我想我的问题分为两个部分:

  1. 检测球与球碰撞的最佳方法是什么?
    我是否只有O(n ^ 2)循环遍历每个球并检查其他每个球以查看其半径是否重叠?
  2. 我使用什么方程式来处理球与球之间的碰撞?物理101
    如何影响两个球的x / y向量速度?两个球朝哪个方向前进?如何将此应用于每个球?

替代文字

处理“墙”的碰撞检测和所产生的矢量变化很容易,但我发现球与球之间的碰撞会更加复杂。对于墙,我只需要取适当的x或y向量的负值,然后沿正确的方向走即可。对于球,我认为不是那样。

一些快速的澄清:为简单起见,我现在可以进行完全弹性的碰撞,而且我的所有球现在都具有相同的质量,但将来可能会改变。


编辑:我发现有用的资源

带矢量的2d球物理:没有Trigonometry.pdf的
二维碰撞。pdf2d球碰撞检测示例:添加碰撞检测


成功!

我的球碰撞检测和响应效果很好!

相关代码:

碰撞检测:

for (int i = 0; i < ballCount; i++)  
{  
    for (int j = i + 1; j < ballCount; j++)  
    {  
        if (balls[i].colliding(balls[j]))  
        {
            balls[i].resolveCollision(balls[j]);
        }
    }
}

这将检查每个球之间的碰撞,但跳过多余的检查(如果您必须检查第1球是否与第2球碰撞,则无需检查第2球是否与第1球碰撞。而且,它也跳过对自身的碰撞的检查。 )。

然后,在我的ball类中,我有colliding()和resolveCollision()方法:

public boolean colliding(Ball ball)
{
    float xd = position.getX() - ball.position.getX();
    float yd = position.getY() - ball.position.getY();

    float sumRadius = getRadius() + ball.getRadius();
    float sqrRadius = sumRadius * sumRadius;

    float distSqr = (xd * xd) + (yd * yd);

    if (distSqr <= sqrRadius)
    {
        return true;
    }

    return false;
}

public void resolveCollision(Ball ball)
{
    // get the mtd
    Vector2d delta = (position.subtract(ball.position));
    float d = delta.getLength();
    // minimum translation distance to push balls apart after intersecting
    Vector2d mtd = delta.multiply(((getRadius() + ball.getRadius())-d)/d); 


    // resolve intersection --
    // inverse mass quantities
    float im1 = 1 / getMass(); 
    float im2 = 1 / ball.getMass();

    // push-pull them apart based off their mass
    position = position.add(mtd.multiply(im1 / (im1 + im2)));
    ball.position = ball.position.subtract(mtd.multiply(im2 / (im1 + im2)));

    // impact speed
    Vector2d v = (this.velocity.subtract(ball.velocity));
    float vn = v.dot(mtd.normalize());

    // sphere intersecting but moving away from each other already
    if (vn > 0.0f) return;

    // collision impulse
    float i = (-(1.0f + Constants.restitution) * vn) / (im1 + im2);
    Vector2d impulse = mtd.normalize().multiply(i);

    // change in momentum
    this.velocity = this.velocity.add(impulse.multiply(im1));
    ball.velocity = ball.velocity.subtract(impulse.multiply(im2));

}

源代码:球对撞机的完整源代码

如果有人对如何改进此基本物理模拟器有任何建议,请告诉我!我要补充的一件事是角动量,这样球将更真实地滚动。还有其他建议吗?发表评论!


16
我认为此算法不够好,因为如果您的球移动得太快(例如:每帧速度快于2 *半径,则一个球可以穿过另一个球而不会发生任何碰撞。)
Benji Mizrahi,2009年

@Simulcal您能否再次上传源代码(所有的filedropper.com链接似乎都损坏了)。您还可以放置从[geocities.com/vobarian/2dcollisions/2dcollisions.pdf]中获取的pdf文件,因为geocities最近已离线
-bguiz

1
这是我工作过的BallBounce的最新版本的链接:dl.dropbox.com/u/638285/ballbounce.rar
mmcdole,2009年

@致所有贡献者:能否请您说明一下将此引擎转换为3D的方法。这个出色的引擎如何在Java3D中工作。
静态无效主体

2
线Vector2d impulse = mtd.multiply(i);应为i *归一化的mtd向量。像这样的东西:Vector2d impulse = mtd.normalize().multiply(i);
klenwell

Answers:


117

要检测两个球是否发生碰撞,只需检查两个球的中心之间的距离是否小于半径的两倍即可。为了在球之间进行完美的弹性碰撞,您只需要担心碰撞方向上的速度分量。两个球的其他分量(与碰撞相切)将保持不变。通过创建指向从一个球到另一个球的方向的单位向量,然后将点乘积与球的速度向量相乘,可以得到碰撞分量。然后,您可以将这些组件插入一维完全弹性碰撞方程中。

维基百科对整个过程进行了很好的总结。对于任何质量的球,可以使用以下公式计算新的速度(其中v1和v2是碰撞后的速度,而u1,u2是之前的速度):

v_ {1} = \ frac {u_ {1}(m_ {1} -m_ {2})+ 2m_ {2} u_ {2}} {m_ {1} + m_ {2}}

v_ {2} = \ frac {u_ {2}(m_ {2} -m_ {1})+ 2m_ {1} u_ {1}} {m_ {1} + m_ {2}}

如果球的质量相同,则只需切换速度即可。这是我写的一些类似代码:

void Simulation::collide(Storage::Iterator a, Storage::Iterator b)
{
    // Check whether there actually was a collision
    if (a == b)
        return;

    Vector collision = a.position() - b.position();
    double distance = collision.length();
    if (distance == 0.0) {              // hack to avoid div by zero
        collision = Vector(1.0, 0.0);
        distance = 1.0;
    }
    if (distance > 1.0)
        return;

    // Get the components of the velocity vectors which are parallel to the collision.
    // The perpendicular component remains the same for both fish
    collision = collision / distance;
    double aci = a.velocity().dot(collision);
    double bci = b.velocity().dot(collision);

    // Solve for the new velocities using the 1-dimensional elastic collision equations.
    // Turns out it's really simple when the masses are the same.
    double acf = bci;
    double bcf = aci;

    // Replace the collision velocity components with the new ones
    a.velocity() += (acf - aci) * collision;
    b.velocity() += (bcf - bci) * collision;
}

至于效率,Ryan Fox是对的,您应该考虑将区域划​​分为多个部分,然后在每个部分中进行碰撞检测。请记住,球可能会与节的边界上的其他球碰撞,因此这可能会使您的代码更加复杂。效率可能并不重要,直到您拥有数百个球为止。为了获得奖励积分,您可以在不同的内核上运行每个部分,或在每个部分内拆分冲突处理。


2
可以说两个球的质量不相等。球之间的矢量变化如何影响?
mmcdole

3
从12年级开始已经有一段时间了,但是我认为他们得到的动量比例与质量比例相对应。
瑞安·福克斯

6
@杰伊,只是要指出..您添加的一个方程式图像是针对一维碰撞,而不是二维碰撞。
mmcdole

@simucal。不正确... u和v是该方程式中的向量。也就是说,它们具有x,y(和z)分量。
安德鲁·罗林斯

2
@Simucal,您是对的,它们适用于一维情况。要获得更大的尺寸,只需使用与碰撞一致的速度分量(代码中的aci,bci)。其他组件与碰撞正交,不会改变,因此您不必担心它们。
杰·康罗德

48

好吧,几年前,我像这里介绍的那样制作了该程序。
有一个隐藏的问题(或很多,取决于观点):

  • 如果球的速度太高,您可能会错过碰撞。

而且,几乎在100%的情况下,您的新速度都是错误的。好吧,不是速度,而是位置。您必须在正确的位置精确计算新速度。否则,您只需将球移到一些“错误”量上,这可以从上一个离散步骤中获得。

解决方案很明显:您必须分开时间步长,这样,您首先转移到正确的位置,然后发生碰撞,然后在剩余的时间中转移。


如果在上移动位置timeframelength*speed/2,则位置将在统计上固定。
Nakilon 2011年

@Nakilon:不,它仅在某些情况下有帮助,但通常可能会错过碰撞。错过碰撞的可能性随着时间长度的增加而增加。顺便说一下,看来Aleph展示了正确的解决方案(尽管我只是略过了)。
2011年

1
@avp,我不是在意如果球的速度太高,您可能会错过碰撞。但是关于你的新职位将是错误的。由于检测到碰撞的时间要晚于实际碰撞的时间,因此如果timeframelength*speed/2从该位置减去,则精度将提高两倍。
Nakilon 2011年


13

为了澄清瑞安·福克斯(Ryan Fox)的建议,将屏幕分成多个区域,并且仅检查区域内的碰撞...

例如,将游戏区域划分为正方形网格(每边将任意指定为1个单位长度),并检查每个网格正方形内是否存在碰撞。

那绝对是正确的解决方案。唯一的问题(正如另一位海报指出的那样)是跨越边界的碰撞是一个问题。

解决方案是将第二个栅格以与第一个栅格垂直和水平偏移0.5个单位的方式覆盖。

然后,将跨越第一网格中的边界(因此未被检测到)的任何碰撞将在第二网格中的网格正方形内。只要您跟踪已经处理的冲突(可能存在一些重叠),就不必担心处理极端情况。所有碰撞都将在其中一个网格的网格正方形内。


+1可以提供更准确的解决方案,并应对怯down的下乘推销者
史蒂文·A·洛

1
多数民众赞成在一个好主意。我这样做一次,然后检查了当前单元格和所有相邻单元格,但是您的方法效率更高。我刚刚想到的另一种方法是检查当前单元格,然后检查它是否与当前单元格边界相交,如果是,则检查该相邻单元格中的对象。
LoveMeSomeCode

10

减少冲突检查次数的一个好方法是将屏幕分成不同的部分。然后,您仅将每个球与同一部分中的球进行比较。


5
更正:您需要检查与相同且相邻部分的碰撞
Rint

7

我在这里看到要优化的一件事。

虽然我确实同意,当距离为半径的总和时击中的球永远不应该实际计算该距离!相反,计算它的平方并以这种方式使用它。没有必要进行昂贵的平方根运算。

同样,一旦发现碰撞,则必须继续评估碰撞,直到不再有碰撞为止。问题在于,第一个可能会导致其他人必须先解决,然后才能获得准确的图片。考虑如果球在边缘击球会发生什么?第二个球撞到边缘,立即反弹到第一个球。如果您撞到拐角处的一堆球,那么可能会有很多冲突需要解决,然后才能迭代下一个循环。

至于O(n ^ 2),您所能做的就是最大程度地减少拒绝错过​​的商品的成本:

1)静止不动的球不能击中任何东西。如果地板上有足够数量的球,这可以节省很多测试。(请注意,您仍然必须检查是否有东西击中固定球。)

2)可能值得做的事情:将屏幕划分为多个区域,但线条应模糊不清-区域边缘的球被列为在所有相关的区域(可能是4个)中。我将使用4x4网格,将区域存储为位。如果两个球区域的AND值返回零,则测试结束。

3)正如我提到的,不要做平方根。


感谢您提供有关平方根技巧的信息。与广场相比,不知道其昂贵的性质。
mmcdole

另一个优化方法是找到距离其他任何球都不远的球。仅当球的速度受到约束时,这才可靠地起作用。
布拉德·吉尔伯特

1
我不同意寻找孤立的球。这与检测碰撞一样昂贵。要改善事情,您需要的球小于O(n)。
洛伦·佩希特尔


3

您有两种简单的方法可以做到这一点。周杰伦介绍了从球心检查的准确方法。

更简单的方法是使用矩形边界框,将框的大小设置为球的80%,这样您就可以很好地模拟碰撞。

在您的Ball类中添加一个方法:

public Rectangle getBoundingRect()
{
   int ballHeight = (int)Ball.Height * 0.80f;
   int ballWidth = (int)Ball.Width * 0.80f;
   int x = Ball.X - ballWidth / 2;
   int y = Ball.Y - ballHeight / 2;

   return new Rectangle(x,y,ballHeight,ballWidth);
}

然后,在您的循环中:

// Checks every ball against every other ball. 
// For best results, split it into quadrants like Ryan suggested. 
// I didn't do that for simplicity here.
for (int i = 0; i < balls.count; i++)
{
    Rectangle r1 = balls[i].getBoundingRect();

    for (int k = 0; k < balls.count; k++)
    {

        if (balls[i] != balls[k])
        {
            Rectangle r2 = balls[k].getBoundingRect();

            if (r1.Intersects(r2))
            {
                 // balls[i] collided with balls[k]
            }
        }
    }
}

1
这将使球在水平和垂直碰撞中相互进入20%。由于效率差异可以忽略,因此最好使用圆形边界框。另外,(x-width)/2应该是x-width/2
Markus Jarderot,

优先打错打字。您会发现大多数2d游戏都使用非矩形形状的矩形边界框,因为它很快,而且用户几乎从不注意。
FlySwat

您可以执行矩形边界框,然后单击选中圆形边界框。
布拉德·吉尔伯特

1
@Jonathan Holland,您的内部循环应该为for(int k = i + 1; ...)这将消除所有多余的检查。(即检查自身的碰撞并检查Ball1与Ball2的碰撞,然后Ball2与Ball1的碰撞)。
mmcdole

4
其实,一个正方形边框很可能是糟糕的性能方面比圆形边框(假设你已经优化的平方根的距离)
Ponkadoodle

3

我在这里和那里都看到了提示,但是您也可以首先进行更快的计算,例如比较重叠的边界框,然后在第一次测试通过的情况下进行基于半径的重叠。

边界框的加/差数学比半径的所有触发器快得多,并且大多数情况下,边界框测试将消除发生碰撞的可能性。但是,如果您随后使用Trig进行重新测试,则会获得所需的准确结果。

是的,这是两个测试,但总体上会更快。


6
您不需要触发。bool is_overlapping(int x1, int y1, int r1, int x2, int y2, int r2) { return (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)<(r1+r2)*(r1+r2); }
Ponkadoodle


2

我使用HTML Canvas元素在JavaScript中实现了此代码,并且以每秒60帧的速度产生了出色的模拟效果。我从随机位置和速度的十几个球的集合开始模拟。我发现,在较高的速度下,一个小球与一个更大的球之间的掠影碰撞会导致该小球看上去在大球的边缘,并在分离之前围绕大球向上移动大约90度。(我想知道是否有人观察到这种行为。)

对计算的一些记录表明,在这些情况下的最小平移距离不足以防止相同的球在下一时间步碰撞。我做了一些实验,发现我可以通过根据相对速度放大MTD来解决此问题:

dot_velocity = ball_1.velocity.dot(ball_2.velocity);
mtd_factor = 1. + 0.5 * Math.abs(dot_velocity * Math.sin(collision_angle));
mtd.multplyScalar(mtd_factor);

我验证了此修复前后,每次碰撞都保留了总动能。mtd_factor中的0.5值约为在碰撞后始终导致球分离的最小值。

尽管此修复程序在系统的确切物理结构中引入了少量错误,但权衡的是,现在可以在浏览器中模拟非常快的球而不会减小时间步长。


1
sin(..)不是便宜函数
PaulHK
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.