改善O(N ^ 2)函数(所有实体迭代所有其他实体)


21

有一点背景知识,我正在和一个C ++的朋友一起编写一个演化游戏,使用ENTT作为实体系统。生物在2D地图中四处走走,吃些绿色或其他生物,繁殖并改变其特性。

此外,当实时运行游戏时,性能还不错(60fps没问题),但我希望能够显着加快速度,而不必等待4小时即可看到任何重大变化。所以我想尽快得到它。

我正在努力寻找一种有效的方法来让生物找到食物。每个生物都应该寻找最接近它们的最佳食物。

游戏示例屏幕截图

如果要吃东西,应该以中心位置为中心的生物以149.64半径(视野距离)环顾四周,并根据营养,距离和种类(肉或植物)来判断应选择哪种食物。 。

负责查找每个生物食物的功能正在消耗大约70%的运行时间。简化当前的编写方式,它是这样的:

for (creature : all_creatures)
{
  for (food : all_entities_with_food_value)
  {
    // if the food is within the creatures view and it's
    // the best food found yet, it becomes the best food
  }
  // set the best food as the target for creature
  // make the creature chase it (change its state)
}

此功能会在每个要寻找食物的生物的每一刻运行,直到该生物找到食物并更改状态。每当为追求某种食物的生物产生新的食物时,它也会运行,以确保每个人都在追求最好的食物。

我对如何提高此过程的效率持开放态度。我很想减少的复杂性,但我不知道那是否有可能。O(N2)

我已经改善它的一种方法是对all_entities_with_food_value组进行排序,以便当一个生物在无法食用的食物上迭代时,它就停在那里。任何其他改进都值得欢迎!

编辑:谢谢大家的答复!我从各种答案中实现了各种东西:

首先,我只是简单地做到了这一点,因此罪恶感功能每五个滴答声仅运行一次,这使游戏速度提高了约4倍,而看不到任何有关游戏的变化。

之后,我在食物搜索系统中存储了一个数组,该数组中产生的食物与运行时相同。这样,我只需要将生物追逐的食物与出现的新食物进行比较即可。

最后,在研究了空间划分并考虑了BVH和四叉树之后,我选择了后者,因为我觉得它更简单并且更适合我的情况。我很快实施了它,并极大地提高了性能,寻找食物几乎不需要任何时间!

现在,渲染让我放慢了脚步,但这又是一个问题。谢谢你们!


2
您是否在同时运行的多个CPU内核上尝试了多个线程?
Ed Marty

6
您平均拥有几个生物?从快照来看,它似乎没有那么高。如果总是这样,那么空间分区将无济于事。您是否考虑过在每个刻运行此功能?您可以每10个滴答声运行一次。模拟结果不应发生质变。
突尼斯

4
您是否进行了详细的分析以找出食品评估中最昂贵的部分?除了查看整体复杂性之外,您可能还需要查看是否存在某些使您感到困惑的特定计算或内存结构访问。
Harabeck

天真的建议:您可以使用四叉树或相关的数据结构,而不是现在使用的O(N ^ 2)方式。
Seiyria

3
正如@Harabeck所建议的那样,我将更深入地了解循环中的所有时间都花在了哪里。例如,如果是距离的平方根计算,则可以先比较XY坐标以预先消除很多候选对象,然后再对其余的对象进行昂贵的sqrt。if (food.x>creature.x+149.64 or food.x<creature.x-149.64) continue;如果性能足够,添加应该比实现“复杂的”存储结构更容易。(相关:如果您在内部循环中发布更多代码,可能会对我们有帮助)
AC

Answers:


34

我知道您不会将其概念化为碰撞,但是您正在做的是使以生物为中心的圆与所有食物碰撞。

您真的不想检查您知道远处的食物,而只想检查附近的食物。这是碰撞优化的一般建议。我想鼓励人们寻找优化碰撞的技术,并且在搜索时不要局限于C ++。


寻找食物的生物

对于您的情况,我建议将世界放在网格上。使单元格至少要碰撞的圆角半径。然后,您可以选择该生物所在的一个单元格及其最多八个邻居,并仅搜索最多九个单元格。

注意:您可以制作更小的单元格,这意味着您要搜索的圆将延伸到紧邻的圆圈之外,需要您在此处进行迭代。但是,如果问题是食物过多,则较小的单元格可能意味着要遍历总共较少的食物实体,这在某些时候对您有利。如果您怀疑是这种情况,请进行测试。

如果食物不移动,则可以在创建时将食物实体添加到网格中,这样就不必搜索单元格中的实体。而是查询该单元格,它具有列表。

如果将单元格的大小设为2的幂,则可以通过截断其坐标来找到该生物所在的单元格。

进行比较以找到最接近的平方距离时,您可以使用平方距离(也就是不执行sqrt)。更少的sqrt操作意味着更快的执行速度。


添加了新食物

添加新食物后,只需唤醒附近的生物即可。这是相同的想法,只不过现在您需要获取单元格中生物的列表。

更有趣的是,如果您在生物中注释了它与所追逐的食物有多远……您可以直接检查该距离。

可以帮助您的另一件事是让食物知道哪些生物正在追逐它。这样一来,您便可以运行代码,为正在追逐刚刚吃掉的食物的所有生物寻找食物。

实际上,在没有食物的情况下开始仿真,并且任何生物都有无穷远的注释距离。然后开始添加食物。随着生物的移动更新距离...吃完食物后,以正在追踪的生物列表为例,然后找到新的目标。除此之外,添加食物时还会处理所有其他更新。


跳过模拟

了解了生物的速度,您便知道它达到目标之前的速度。如果所有生物具有相同的速度,则首先到达的是具有最小注释距离的生物。

如果您还知道添加更多食物之前的时间... 并且希望您对繁殖和死亡具有相似的可预测性,那么您就知道下一次事件的时间(添加食物或生物正在进食)。

跳到那一刻。您无需模拟四处走动的生物。


1
“只在那儿搜索。” 和细胞直接相邻-意味着总共9个细胞。为什么是9?因为如果生物在细胞的一角正确呢?
UKMonkey

1
@UKMonkey“如果要使单元格至少要碰撞的圆的半径,”如果单元格的侧边是半径,并且该生物在拐角处……那么,我想在这种情况下,您只需要搜索四个即可。但是,可以肯定的是,我们可以使细胞变小,如果食物过多且生物过多,这可能会很有用。编辑:我会澄清。
Theraot

2
当然-如果您想找出是否需要在多余的细胞中进行搜索...,但是考虑到大多数细胞都没有食物(根据给定的图像);搜索9个单元格比确定要搜索的4个单元格要快。
UKMonkey

@UKMonkey,这就是为什么我最初没有提及的原因。
Theraot

16

您应采用BVH之类的空间划分算法以降低复杂性。要针对您的情况,您需要制作一棵树,该树由包含食物块的与轴对齐的边界框组成。

要创建层次结构,请将食品块彼此相邻放置在AABB中,然后再根据它们之间的距离将这些AABB放入较大的AABB中。这样做直到拥有根节点。

要使用该树,首先对根节点执行circle-AABB相交测试,然后如果发生冲突,则对每个连续节点的子级进行测试。最后,您应该有一组食物。

您也可以使用AABB.cc库。


1
这确实可以将复杂度降低到N log N,但是进行分区也将是昂贵的。看到我需要对每个刻度进行划分(因为生物移动了每个刻度),还是值得吗?是否有解决方案来减少分区频率?
Alexandre Rodrigues

3
@AlexandreRodrigues,您不必在每个刻度上都重建整个树,只需要更新移动的部分,并且仅当某些东西超出特定AABB容器时才需要重建。为了进一步提高性能,您可能需要加肥节点(在子代之间留一些空间),这样就不必在叶更新时重建整个分支。
豹猫

6
我认为BVH在这里可能太复杂了-用哈希表实现的统一网格就足够了。
史蒂文

1
@Steven通过实施BVH,可以在将来轻松扩展模拟规模。如果您进行小规模仿真,也不会丢失任何内容。
豹猫

2

尽管所描述的空间分区方法确实可以减少时间,但您的主要问题不只是查找。其庞大的查询量使您的任务变慢。因此,您可以优化内部循环,但也可以优化外部循环。

您的问题是您一直在轮询数据。这有点像让孩子坐在后座上,问“我们到那儿了”一千次,不需要做的就是驾驶员会在您到那里时通知您。

相反,如果可能的话,您应该努力解决每个操作完成的问题,然后将其放入队列中,然后将那些冒泡事件排除在外,这可以对队列进行更改,但是可以。这称为离散事件模拟。如果您可以通过这种方式实现仿真,那么您正在寻找一个可观的加速比,远远超过了从更好的空间分区查找中可以获得的加速。

为了强调以前的职业生涯,我制作了工厂模拟器。我们用这种方法在不到一个小时的时间内模拟了大型工厂/机场几周内每个物料层的整个物料流。虽然基于时间步长的仿真只能比实时快4-5倍。

另外,作为一种真正的低落水果,请考虑将绘图例程与模拟解耦。尽管您的模拟很简单,但是绘图仍然有一些开销。更糟糕的是,显示驱动程序可能会将您限制为每秒x次更新,而实际上,您的处理器可以将处理速度提高100倍。这说明需要进行概要分析。


@Theraot我们不知道图纸的结构。但是,是的,一旦您足够快地进行,通话便会成为瓶颈
joojaa

1

您可以使用扫掠线算法将复杂度降低到Nlog(N)。该理论是Voronoi图的原理,它可以将生物周围的区域划分为由比其他任何点都更靠近该生物的所有点组成的区域。

所谓的Fortune算法在Nlog(N)中为您完成了该工作,其上的Wiki页面包含实现该目标的伪代码。我确信那里也有库实现。 https://zh.wikipedia.org/wiki/财富%27s_algorithm


欢迎使用GDSE,感谢您的回答。您如何将其正确地应用于OP的情况?问题描述指出,实体应在其可视范围内考虑所有食物并选择最佳食物。传统的沃罗诺伊(Voronoi)会将范围较近的食物排除在外。我并不是说Voronoi不能正常工作,但是从您的描述中并不能很明显地看出OP应该如何利用它来解决所描述的问题。
皮卡列克

我喜欢这个想法,我希望看到它得到扩展。您如何表示voronoi图(如在内存数据结构中)?您如何查询?
Theraot

@Theraot您不需要voronoi图就可以了。
joojaa

-2

最简单的解决方案是集成物理引擎并仅使用碰撞检测算法。只需在每个实体周围建立一个圆/球,然后让物理引擎计算碰撞。对于2D,我建议使用Box2DChipmunk,对3D 建议使用Bullet

如果您觉得集成整个物理引擎太多,我建议您研究一下特定的碰撞算法。大多数冲突检测库分两个步骤工作:

  • 广泛阶段检测:此阶段的目标是尽可能快地获取可能碰撞的候选对象对的列表。两种常见的选择是:
    • 扫描和修剪:沿X轴对边界框进行排序,并标记相交的那对对象。对其他所有轴重复上述步骤。如果一个候选对通过所有测试,则进入下一阶段。该算法非常善于利用时间一致性:您可以保留已排序实体的列表并在每一帧对其进行更新,但是由于它们几乎已排序,因此速度非常快。它还利用了空间一致性:由于实体是按升序排列的,因此在检查碰撞时,只要实体不发生碰撞,就可以立即停止,因为所有下一个实体都将相距较远。
    • 空间分区数据结构,例如四叉树,八叉树和网格。网格易于实现,但是如果实体密度低,则网格将非常浪费,并且对于无边界空间而言,网格非常难以实现。静态空间树也很容易实现,但是很难平衡或就地更新,因此您必须在每帧重建它。
  • 狭窄阶段:使用更精确的算法进一步测试在广泛阶段发现的候选对。
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.