有没有办法提高n个对象系统的碰撞检查效率?


9

我正在制作一个包含许多屏幕对象的游戏,其中一个是玩家。我需要知道哪些对象在每次迭代中都发生冲突。

我做了这样的事情:

for (o in objects)
{
   o.stuff();
   for (other in objects)
      if (collision(o, other))
          doStuff();

   bla.draw();
}

这个有O(n ^ 2),我被告知是不好的。我如何更有效地做到这一点,甚至有可能吗?我在用Javascript编写,n通常小于30,如果保持不变,会不会有问题?


3
您是否尝试过运行代码以查看其性能?
thedaian 2012年

不,我没有,我只是假设这很糟糕,因为O(n ^ 2)。
jcora 2012年

1
只有30个对象?我本来建议空间分区,但是只有30个对象将是徒劳的。其他人指出了一些次要的优化,但是它们都是您所讨论的规模上的次要的优化。
约翰·麦克唐纳

Answers:


16

最多只能容纳30个对象,除了每帧不对相同的两对对象进行相互检查之外,您根本不需要进行太多优化。下面的代码示例将涵盖其中。但是,如果您对物理引擎将使用的不同优化感兴趣,请继续阅读本文的其余部分。

您将需要一个空间分区实现,例如Octree(用于3D游戏)或Quadtree(用于2D游戏)。这些将世界划分为多个子区域,然后将每个子区域在同一庄园中进一步划分,直到将其细分为最小大小为止。这样,您可以非常快速地检查哪些其他对象与另一个对象在同一区域,从而限制了必须检查的碰撞量。

除了空间分区之外,我还建议为您的每个物理对象创建一个AABB(与轴对齐的边界框)。这使您可以对照一个对象检查一个对象的AABB,这比对象之间进行详细的按多边形检查要快得多。

对于复杂的或大型的物理对象,可以采取进一步的措施,您可以细分物理网格本身,为每个子形状赋予自己的ABB,只有两个对象的ABB重叠时,您才能进行检查。

一旦大多数物理引擎停止运行,它们就会停用其对物理主体的主动物理模拟。停用物理实体时,只需在每个帧上检查与它的AABB的碰撞,如果任何物体与AABB碰撞,则它将重新激活并进行更细化的碰撞检查。这样可以缩短仿真时间。

同样,许多物理引擎都使用“模拟岛”,这是一组紧密靠在一起的物理物体组合在一起的地方。如果模拟岛中的所有内容均处于静止状态,则模拟岛本身将停用。仿真岛的好处是,一旦岛处于非活动状态,其中的所有物体都可以停止检查碰撞,并且每帧的唯一检查就是查看是否有物体进入了岛的AABB。只有一旦有东西进入岛上的AABB,岛上的每个物体都需要检查碰撞。如果模拟岛中的任何物体开始再次自行移动,它也会重新激活。如果物体远离组中心的距离足够远,则会将其从岛上移开。

最后,剩下的东西是这样的(用伪代码):

// Go through each leaf node in the octree. This could be more efficient
// by keeping a list of leaf nodes with objects in it.
for ( node in octreeLeafNodes )
{
    // We only need to check for collision if more than one object
    // or island is in the bounds of this octree node.
    if ( node.numAABBsInBounds > 1)
    {
        for ( int i = 0; i < AABBNodes.size(); ++i )
        {
           // Using i+1 here allows us to skip duplicate checks between AABBS
           // e.g (If there are 5 bodies, and i = 0, we only check i against
           //      indexes 1,2,3,4. Once i = 1, we only check i against indexes
           //      2,3,4)
           for ( int j = i + 1; j < AABBNodes.size(); ++j )
           {
               if ( AABBOverlaps( AABBNodes[i], AABBNodes[j] ) )
               {
                   // If the AABB we checked against was a simulation island
                   // then we now check against the nodes in the simulation island

                   // Once you find overlaps between two actual object AABBs
                   // you can now check sub-nodes with each object, if you went
                   // that far in optimizing physics meshes.
               {
           }
        }
    }
}

我还建议不要在这样的循环中包含太多的循环,上面的示例只是为了让您有一个主意,我将其分解为多个函数,这些函数可以为您提供与上述功能相同的功能。

另外,请确保在循环访问AABBNodes容器时不要更改它,因为这可能意味着错过了碰撞检查。这听起来像是常识,但是您会感到惊讶的是,让事物对碰撞做出反应会导致您意料之外的变化是多么容易。例如,如果碰撞导致碰撞对象之一改变位置足以将其从您正在检查的Octree节点的AABB中移除,则它可能会更改该容器。为了解决这个问题,我建议保留检查期间发生的所有碰撞事件的列表,然后在所有检查完成后遍历该列表并发出所有碰撞事件。


4
非常一致的答案,具有很好而有用的技术精度,使读者可以了解现有方法。+1
Valkea

如果我需要删除碰撞对象怎么办?我可以更改容器吗?我的意思是将其从容器中删除,因为我不再需要该对象,因为它已“被破坏”。如果在碰撞检测过程中没有将其删除,则还需要一个循环来处理碰撞事件。
newguy

删除碰撞对象很好,但是我建议等到整个模拟过程完成碰撞遍历后再进行操作。通常,您只是标记需要删除的对象,或生成要删除的对象的列表,然后在完成碰撞模拟后,应用这些更改。
Nic Foster

4

您的示例多次测试每对对象。

让我们举一个非常简单的示例,其中包含一个0、1、2、3的数组

使用您的代码,您会得到:

  • 在循环0处,您针对1、2和3进行测试
  • 在循环1中,您针对0、2和3进行了测试===>(已测试0-1)
  • 在循环2中,您针对0、1和3进行了测试===>(0-2 / 1-2已测试)
  • 在循环3中,您针对0、1和2进行了测试===>(0-3 / 1-3 / 2-3已测试)

现在让我们看下面的代码:

for(i=0;i<=objects.length;i++)
{
    objects[i].stuff();

    for(j=i+1;j<=objects.length;j++)
    {
        if (collision(objects[i], objects[j]))
        doStuff();
    }

    bla.draw();
}

如果再次使用包含0、1、2、3的数组,则将具有以下行为:

  • 在循环0,您针对1、2、3进行测试
  • 在循环1中,您针对2、3进行测试
  • 在循环2中,您针对3
  • 在循环3中,您什么都不做测试

使用第二种算法,我们有6个碰撞测试,而前一个算法要求进行12个碰撞测试。


该算法进行的N(N-1)/2比较仍然是O(N ^ 2)性能。
2012年

1
好吧,按要求有30个对象,这意味着对870个对象进行了465次碰撞测试...从您的角度来看,这可能很相似,但从我的角度来看并不相同。此外,其他答案中提供的解决方案是完全相同的算法:)
Valkea 2012年

1
@Valkea:好吧,部分是。:)
Nic Foster 2012年

@NicFoster:是的,您是对的;)我严格地说的是所选对象之间的碰撞测试,而不是算法的分区部分,这显然是非常有价值的补充,在我的示例中甚至没有想到添加我在写。
Valkea

这叫摊销吗?还是谢谢你!
jcora 2012年

3

根据您的需求设计算法,但封装实现细节。即使使用Javascript,也适用基本的OOP概念。

对于N =~ 30, O(N*N)是不是一个问题,你的线性搜索很可能是一样快,因为任何其他那里。但是,您不想将假设硬编码到代码中。用伪代码,您将有一个接口

interface itemContainer { 
    add(BoundingBox);
    remove(BoundingBox);
    BoundingBox[] getIntersections();
}

那描述了你的物品清单可以做什么。然后,您可以编写一个实现该接口的ArrayContainer类。在Javascript中,代码如下所示:

function ArrayContainer() { ... } // this uses an array to store my objects
ArrayContainer.prototype.add = function(box) { ... };
ArrayContainer.prototype.remove = function(box) { ... };
ArrayContainer.prototype.getIntersections = function() { ... };

function QuadTreeContainer { ... } // this uses a quadtree to store my objects
... and implement in the add/remove/getIntersections for QuadTreeContainer too

这是创建300个边界框并获取所有交点的示例代码。如果正确实现了ArrayContainer和QuadTreeContainer,则只需更改代码var allMyObjects = new ArrayContainer()即可var allMyObjects = QuadTreeContainer()

var r = Math.random;
var allMyObjects = new ArrayContainer();
for(var i=0; i<300; i++)
    allMyObjects.add(new BoundingBox(r(), r()));
var intersections = allMyObjects.getIntersections();

我继续并在这里整理了标准ArrayContainer的实现:

http://jsfiddle.net/SKkN5/1/


注意:这个答案是由于Bane抱怨他的代码库太大,凌乱并且难以管理而引起的。尽管在使用数组与树的讨论中并没有增加太多,但我希望这是一个相关的答案,因为他将如何更好地组织代码。
吉米

2

您还应该考虑对象之间的类型,以免发生明智的碰撞。

例如,可能需要检查玩家是否与除他自己的子弹外的所有物体相撞。但是,敌人可能只需要检查玩家的子弹即可。子弹几乎肯定不需要相互碰撞。

为了有效地实现此目的,您可能需要保留单独的对象列表,每种对象类型一个。

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.