如何优化顺序重要且碰撞是基于对象组的条件的碰撞引擎?


14

如果这是您第一次遇到此问题,建议您先阅读下面的更新前部分,然后阅读本部分。 不过,这是问题的综合内容:

基本上,我有一个带有网格空间分区系统的碰撞检测和解决引擎,碰撞顺序和碰撞组很重要。一次必须移动一个身体,然后检测碰撞,然后解决碰撞。如果我一次移动所有物体,然后生成可能的碰撞对,则显然速度更快,但是由于不遵守碰撞顺序,因此分辨率会下降。如果我一次移动一个身体,我将不得不让身体检查碰撞,这将成为一个^ 2问题。将组混合在一起,您可以想象为什么在很多身体上它变得非常慢。


更新:我已经为此付出了很多努力,但是无法优化任何东西。

我成功实现了Will所描述的“绘画”,并将组更改为位集,但这是非常非常小的加速。

我还发现了一个问题:我的引擎取决于冲突顺序。

我尝试了一种独特的碰撞对生成的实现,该实现肯定可以大大加快一切,但是却破坏了碰撞顺序

让我解释:

  • 在我的原始设计中(不生成对),发生这种情况:

    1. 一个身体移动
    2. 移动后,它会刷新其单元格并使其碰撞到的身体
    3. 如果它与需要解决的物体重叠,则解决碰撞

    这意味着,如果一个物体移动并撞到墙壁(或任何其他物体),则只有已移动的物体才能解决其碰撞,而另一个物体将不受影响。

    这是我想要的行为

    我了解到物理引擎并不常见,但对于复古风格的游戏却有很多优势。

  • 在通常的网格设计(生成唯一对)中,会发生以下情况:

    1. 所有身体移动
    2. 所有身体移动之后,刷新所有单元格
    3. 生成唯一的碰撞对
    4. 对于每对,处理碰撞检测和解决

    在这种情况下,同时移动可能会使两个物体重叠,并且它们将同时分解-这有效地使物体“相互推挤”,并破坏了与多个物体的碰撞稳定性

    这种行为对于物理引擎是很常见的,但在我的情况下是不可接受的

我还发现了另一个主要问题(即使在现实情况中不太可能发生):

  • 考虑A,B和W组的身体
  • A与W和A相撞并解决
  • B与W和B相撞并下定决心
  • A对B无能为力
  • B对A无所作为

可能存在许多A主体和B主体占据同一个单元的情况-在这种情况下,主体之间存在很多不必要的迭代,这些迭代不能相互反应(或仅检测碰撞但不能解决它们) 。

对于占据同一单元的100个物体,这是100 ^ 100次迭代!发生这种情况是因为没有生成唯一对 -但是我无法生成唯一对,否则我将得到我不希望的行为。

有没有一种方法可以优化这种碰撞引擎?

这些是必须遵守的准则:

  • 碰撞顺序非常重要!

    • 身体必须一次移动一个,然后一次检查一个碰撞,然后一次移动一个就解决。
  • 机构必须具有3个群组位组

    • :身体所属的组
    • GroupsToCheck:人体必须检测到碰撞的组
    • GroupsNoResolve:团体不能解决的碰撞
    • 在某些情况下,我只希望检测到碰撞但不能解决



更新前:

前言:我知道优化此瓶颈不是必需的-引擎已经非常快。但是,出于娱乐和教育目的,我很想找到一种使引擎更快的方法。


我正在创建一个通用的C ++ 2D碰撞检测/响应引擎,重点是灵活性和速度。

这是其架构的非常基本的图:

基本引擎架构

基本上,主类是World,它拥有(管理内存)a ResolverBase*,a SpatialBase*和a vector<Body*>

SpatialBase 是处理宽相碰撞检测的纯虚拟类。

ResolverBase 是处理冲突解决的纯虚拟类。

实体World::SpatialBase*SpatialInfo实体本身拥有的对象通信。


当前,有一个空间类别:Grid : SpatialBase,这是一个基本的固定2D网格。它具有自己的信息类GridInfo : SpatialInfo

以下是其架构的外观:

具有网格空间的引擎架构

所述Grid类拥有的2D阵列Cell*。本Cell类包含的集合(不是所有)Body*:一个vector<Body*>其中包含了所有单元格中的尸体。

GridInfo 对象还包含指向主体所在单元格的非所有指针。


如前所述,引擎基于组。

  • Body::getGroups()返回std::bitset身体所属的所有组中的一个。
  • Body::getGroupsToCheck()返回std::bitset身体必须检查碰撞的所有组中的一个。

身体可以占据一个以上的细胞。GridInfo始终将非所有者指针存储到占用的单元格。


单个身体移动后,就会发生碰撞检测。我假设所有物体都是与轴对齐的边界框。

广相碰撞检测如何工作:

第1部分:空间信息更新

对于每个Body body

    • 计算最左上角的占用单元格和最右下角的占用单元格。
    • 如果它们与先前的单元格不同,则将body.gridInfo.cells其清除,并填充身体所占据的所有单元格(从最左上角的单元格到最右下角的单元格为2D循环)。
  1. body 现在保证知道它占用了什么细胞。

第2部分:实际碰撞检查

对于每个Body body

  • body.gridInfo.handleCollisions 叫做:

void GridInfo::handleCollisions(float mFrameTime)
{
    static int paint{-1};
    ++paint;

    for(const auto& c : cells)
        for(const auto& b : c->getBodies())
        {
            if(b->paint == paint) continue;
            base.handleCollision(mFrameTime, b);
            b->paint = paint;
        }
}

void Body::handleCollision(float mFrameTime, Body* mBody)
    {
        if(mBody == this || !mustCheck(*mBody) || !shape.isOverlapping(mBody->getShape())) return;

        auto intersection(getMinIntersection(shape, mBody->getShape()));

        onDetection({*mBody, mFrameTime, mBody->getUserData(), intersection});
        mBody->onDetection({*this, mFrameTime, userData, -intersection});

        if(!resolve || mustIgnoreResolution(*mBody)) return;
        bodiesToResolve.push_back(mBody);
    }

  • 然后,解决每个人体的碰撞bodiesToResolve

  • 而已。


因此,我已经尝试优化这种广相碰撞检测了一段时间了。每次我尝试使用当前体系结构/设置以外的其他方法时,都没有按计划进行,或者我对模拟进行了假设,后来被证明是错误的。

我的问题是:如何优化碰撞引擎的广泛阶段

是否可以在此处应用某种神奇的C ++优化?

是否可以重新设计架构以实现更高的性能?


最新版本的Callgrind输出:http : //txtup.co/rLJgz


剖析并确定瓶颈。让我们知道它们在哪里,然后我们需要处理一些事情。
Maik Semder

@MaikSemder:我做到了,并在帖子中写了。这是唯一的瓶颈代码段。抱歉,如果它又长又详细,但这是问题的一部分,因为我敢肯定,只有通过更改引擎设计才能解决此瓶颈。
维托里奥·罗密欧

抱歉,很难找到。你能给我们一些数字吗?函数时间和在该函数中处理的对象数?
Maik Semder

@MaikSemder:在用Clang 3.4 SVN -O3编译的二进制文件上,用Callgrind测试:10000个动态实体-函数 getBodiesToCheck()被调用5462334次,并占用了整个配置时间的35.1%(指令读取访问时间)
Vittorio Romeo

2
@Quonux:没有冒犯。我只是喜欢 “重新发明轮子”。我可以选择Bullet或Box2D并使用这些库制作游戏,但这并不是我的目标。通过从头开始创建事物并尝试克服出现的障碍,我感到更加满足,并且学到了更多东西-即使那意味着沮丧和寻求帮助。除了我认为从头开始编写代码对于学习而言是无价之宝之外,我还发现它很有趣,而且很高兴将自己的空闲时间花在上面。
罗密欧 Vittorio Romeo)

Answers:


14

getBodiesToCheck()

该功能可能存在两个问题getBodiesToCheck();第一:

if(!contains(bodiesToCheck, b)) bodiesToCheck.push_back(b);

这部分是O(n 2),不是吗?

与其检查列表中是否已存在主体,不如使用绘画

loop_count++;
if(!loop_count) { // if loop_count can wrap,
    // you just need to iterate all bodies to reset it here
}
bodiesToCheck.clear();
for(const auto& q : queries)
    for(const auto& b : *q)
        if(b->paint != loop_count) {
            bodiesToCheck.push_back(b);
            b->paint = loop_count;
        }
return bodiesToCheck;

您正在收集阶段取消引用指针,但是无论如何在测试阶段都将取消引用指针,因此,如果您有足够的L1,则没什么大不了的。您也可以通过向编译器添加预取提示来提高性能,例如__builtin_prefetch,尽管使用经典for(int i=q->length; i-->0; )循环等会更容易。

这是一个简单的调整,但是我的第二个想法是,可以有一种更快的方式来组织此操作:

您可以转到使用 不过,位图,并避免整个过程bodiesToCheck矢量。这是一种方法:

您已经在为实体使用整数键,但是随后在地图和事物中查找它们并保留它们的列表。您可以移动到插槽分配器,它基本上只是一个数组或向量。例如:

class TBodyImpl {
   public:
       virtual ~TBodyImpl() {}
       virtual void onHit(int other) {}
       virtual ....
       const int slot;
   protected:
      TBodyImpl(int slot): slot(slot_) {}
};

struct TBodyBase {
    enum ... type;
    ...
    rect_t rect;
    TQuadTreeNode *quadTreeNode; // see below
    TBodyImpl* imp; // often null
};

std::vector<TBodyBase> bodies; // not pointers to them

这意味着发生实际冲突所需的所有东西都在线性高速缓存友好的内存中,并且只有在需要时才转到实现特定的位并将其附加到这些插槽之一中。

要跟踪此主体矢量中的分配,您可以使用整数数组作为位图,并使用twiddling__builtin_ffs。这对于移至当前已占用的插槽或在数组中找到未占用的插槽非常有效。有时,如果阵列过大地增大阵列的大小,甚至可以压缩阵列,可以通过移动末端的阵列来填补空白,从而将许多阵列标记为已删除。

每次碰撞仅检查一次

如果你如果选中一个与碰撞b,你并不需要检查,如果b与碰撞太。

使用整数ID可以避免使用简单的if语句避免进行这些检查。如果潜在碰撞的ID小于或等于正在检查的当前ID,则可以跳过该ID!这样,您将只检查一次每个可能的配对。这将超过碰撞检查次数的一半。

unsigned * bitmap;
int bitmap_len;
...

for(int i=0; i<bitmap_len; i++) {
  unsigned mask = bitmap[i];
  while(mask) {
      const int j = __builtin_ffs(mask);
      const int slot = i*sizeof(unsigned)*8+j;
      for(int neighbour: get_neighbours(slot))
          if(neighbour > slot)
              check_for_collision(slot,neighbour);
      mask >>= j;
  }

尊重碰撞的顺序

与其立即计算碰撞,不如评估碰撞,而是计算碰撞的距离并将其存储在二进制堆中。这些堆是您通常在路径查找中执行优先级队列的方式,因此非常有用的实用程序代码。

用序列号标记每个节点,因此您可以说:

  • A 10命中B 12在6
  • A 10在3 命中C 12

显然,在收集了所有冲突之后,您便开始从优先级队列中弹出它们,最快的是第一时间。因此,首先得到的是A 10击中C 12的3。您增加了每个对象的序列号(10位),评估了碰撞,计算了它们的新路径,并将它们的新碰撞存储在同一队列中。新的碰撞是A 11命中B 12在7队列现在有:

  • A 10次点击B在6 12
  • A 11命中B 12在7

然后你从优先级队列和弹出10次点击乙12在第6但你看到一个10陈旧的 ; A当前为11。因此您可以放弃此冲突。

重要的是不要费力尝试从树中删除所有过时的碰撞;从堆中删除很昂贵。弹出它们时,只需丢弃它们。

网格

您应该考虑改为使用四叉树。它是一个非常简单的数据结构。通常,您会看到存储点的实现,但是我更喜欢存储rect,并将元素存储在包含它的节点中。这意味着要检查冲突,您仅需要遍历所有主体,并且针对每个主体,对它进行检查,以与同一四叉树节点中的那些主体(使用上面概述的排序技巧)以及父四叉树节点中的所有主体进行比较。四叉树本身就是可能的冲突列表。

这是一个简单的四叉树:

struct Object {
    Rect bounds;
    Point pos;
    Object * prev, * next;
    QuadTreeNode * parent;
};

struct QuadTreeNode {
    Rect bounds;
    Point centre;
    Object * staticObjects;
    Object * movableObjects;
    QuadTreeNode * parent; // null if root
    QuadTreeNode * children[4]; // null for unallocated children
};

我们将可移动对象分开存储,因为我们不必检查静态对象是否会与任何物体碰撞。

我们将所有对象建模为轴对齐的边界框(AABB),并将它们放置在包含它们的最小的QuadTreeNode中。当QuadTreeNode有很多子代时,可以对其进行进一步细分(如果这些对象很好地将自己分配到了子代中)。

每个游戏滴答声,您都需要递归到四叉树并计算每个可移动对象的移动(和碰撞)。必须通过以下方式检查是否存在碰撞:

  • 节点中的每个静态对象
  • 可移动对象列表中其节点之前(或之后;仅选择一个方向)中节点中的每个可移动对象
  • 所有父节点中的每个可移动和静态对象

这将生成所有可能的无序冲突。然后,您进行移动。您必须按距离和“谁先移动”(这是您的特殊要求)确定这些移动的优先级,并按该顺序执行它们。为此使用堆。

您可以优化此四叉树模板。您不需要实际存储边界和中心点;当您走树时,这是完全可导的。您无需检查模型是否在范围内,只需检查模型在中心点的哪一侧(“分离轴”测试)。

要为快速飞行的物体(例如弹丸)建模,而不是一步步移动它们或始终查看单独的“子弹”列表,只需将它们与飞行区域一起放置在四叉树中进行一定数量的游戏步骤。这意味着它们很少会在四叉树中移动,但是您不会在远离墙壁的地方检查子弹,因此这是一个不错的权衡。

大型静态对象应分为组成部分;例如,一个大立方体应分别存储每个面。


“绘画”听起来不错,我会尝试一下并尽快报告结果。不过,我不明白您回答的第二部分-我会尝试阅读一些有关预取的内容。
维托里奥·罗密欧

我不建议使用QuadTree,它比做网格要复杂得多,如果做得不好,它将无法正确工作,并且会经常创建/删除节点。
ClickerMonkey 2013年

关于堆:移动顺序受到尊重吗?考虑身体和身体。A向右移向B,而B向右移向A。现在-当它们同时发生碰撞时,应首先解决最先移动的那个,而另一个不受影响。
罗密欧

@VittorioRomeo如果A向B移动并且B在相同的滴答声中以相同的速度向A移动,它们是否在中间相遇?还是先走的A在B开始的地方遇到B?
威尔


3

我敢打赌,遍历正文时,您只会有大量的缓存丢失。您是否正在使用某种面向数据的设计方案将所有主体集合在一起?使用N ^ 2广相时,我可以模拟成百上千的物体,同时通过frap录制,而没有任何帧率下降到下界区域(小于60)的物体,而这一切都没有自定义分配器。试想一下,正确使用缓存可以做什么。

线索在这里:

const std::vector<Body *>

这立即引起了巨大的危险信号。您是否在分配这些机构原始呼叫?是否使用了自定义分配器?最重要的是,您的所有身体都成线性排列巨大阵列。如果您觉得不能线性遍历内存,则可以考虑使用侵入式链接列表来实现。

另外,您似乎正在使用std :: map。您知道std :: map中的内存分配方式吗?每个地图查询都将具有O(lg(N))复杂度,并且可能通过哈希表将其增加到O(1)。最重要的是,由std :: map分配的内存也将严重破坏您的缓存。

我的解决方案是使用侵入式哈希表代替std :: map。帕特里克·怀亚特(Patrick Wyatt)在他的coho项目中的基础就是侵入式链接列表和侵入式哈希表的一个很好的例子:https//github.com/webcoyote/coho

简而言之,您可能需要为自己创建一些自定义工具,即分配器和一些侵入式容器。这是我最好的方法,而无需自己编写代码。


“您是在为这些机构分配原始呼叫吗?” new将实体推到getBodiesToCheck矢量时,我没有明确调用-您是说它在内部发生?有没有办法在仍然具有动态尺寸的物体集合的同时防止这种情况?
维托里奥·罗密欧

std::map这不是瓶颈-我还记得尝试dense_hash_set并没有获得任何表现。
维托里奥·罗密欧2013年

@Vittorio,然后该部分getBodiesToCheck是瓶颈?我们需要信息以提供帮助。
Maik Semder 2013年

@MaikSemder:探查器不会比函数本身更深入。整个功能是瓶颈,因为每个身体每帧被调用一次。10000个身体= getBodiesToCheck每帧10000个调用。我怀疑不断清洗/推动向量是函数本身的瓶颈。该contains方法也是减慢速度的一部分,但是由于它bodiesToCheck永远不会超过8-10个物体,所以应该这么慢
Vittorio Romeo 2013年

@Vittorio很好,如果您将此信息放入问题中,那就是改变游戏规则的;)特别是我的意思是所有身体都要调用getBodiesToCheck的部分,因此每帧10000次。我想知道,您说它们是分组的,所以如果您已经有了分组信息,为什么还要将它们放入bodyToCheck-array。您可能会在那部分进行详细说明,对我来说似乎是一个非常好的优化候选人。
Maik Semder 2013年

1

减少检查每个帧的物体数:

只检查可以实际移动的物体。创建静态对象后,只需将其分配给您的碰撞单元即可。现在仅检查确实包含至少一个动态对象的组的碰撞。这样可以减少每帧检查的次数。

使用四叉树。在这里查看我的详细答案

从物理代码中删除所有分配。您可能想为此使用探查器。但是我只分析了C#中的内存分配,所以我无法帮助C ++。

祝好运!


0

我在您的瓶颈功能中看到了两个问题候选人:

首先是“包含”部分-这可能是瓶颈的主要原因。它遍历每个身体已经找到的身体。也许您应该使用某种hash_table / hash_map而不是vector。然后插入应该更快(搜索重复项)。但是我不知道任何具体数字-我不知道在这里迭代了多少个物体。

第二个问题可能是vector :: clear和push_back。Clear可能会或可能不会引起重新分配。但是您可能想要避免这种情况。解决方案可能是一些标志数组。但是您可能有很多对象,因此为每个对象列出所有对象的列表对内存的影响不大。其他方法可能很好,但我不知道哪种方法:/


关于第一个问题:我试过使用density_hash_set代替vector + contains,但是它比较慢。我尝试填充向量,然后删除所有重复项,但速度较慢。
维托里奥·罗密欧

0

注意:我对C ++一无所知,对Java则一无所知,但是您应该能够弄清楚代码。物理学是通用语言吧?我也意识到这是一岁的帖子,但我只想与大家分享。

我有一个观察者模式,基本上,在实体移动之后,它会返回与之碰撞的对象,包括NULL对象。简单的说:

我在改造我的世界

public Block collided(){
   return World.getBlock(getLocation());
}

所以说你在世界上徘徊。每当您打电话时,您都move(1)可以打电话collided()。如果获得了所需的块,则也许粒子飞了,您可以左右左右移动,但不能向前移动。

不仅以我的世界为例,更通用地使用它:

public Object collided(){
   return threeDarray[3][2][3];
}

简单地说,有一个数组来指出坐标,从字面上看,Java是如何使用指针的。

使用此方法仍然需要先验条件之外的其他条件碰撞检测方法方法。您可以循环执行此操作,但这不能达到目的。您可以将其应用于宽,中,窄碰撞技术,但仅此而已,它尤其适用于3D和2D游戏。

现在再看一看,这意味着,根据我的minecraft collide()方法,我将最终进入块内,因此必须将播放器移出该块。除了检查播放器外,我还需要添加一个边界框,该边界框可以检查哪个方块击中了盒子的每一侧。问题已解决。

如果需要精度,上一段对于多边形而言可能并不那么容易。为了准确起见,我建议定义一个不是正方形但不是镶嵌的多边形边界框。如果没有,那么矩形就可以了。

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.