检查大量圈子碰撞的最佳方法是什么? 检测两个圆之间的碰撞非常容易,但是如果我们检查每个组合,那么它是 O(n 2 ),这绝对不是最佳解决方案。
我们可以假设圆形对象具有以下属性:
速度是恒定的,但方向可以改变。
我提出了两种解决方案,但也许有更好的解决方案。
解决方案1
将整个空间划分为重叠的正方形,并仅检查与同一正方形的圆圈的碰撞。正方形需要重叠,因此当圆从一个正方形移动到另一个正方形时不会出现问题。
解决方案2
在开始时,需要计算每对圆之间的距离
如果距离很小,那么这些对存储在一些列表中,我们需要在每次更新时检查碰撞
如果距离很大,那么我们存储后更新可能会发生碰撞(可以计算,因为我们知道距离和速度)。它需要存储在某种优先级队列中。在先前计算的更新数量之后,需要再次检查距离,然后我们执行相同的过程 - 将其放在列表中或再次放在优先级队列中。
Mark Byers问题的答案
答案 0 :(得分:17)
有“spatial index”数据结构用于存储您的圈子以便以后快速比较; Quadtree,r-tree和kd-tree就是例子。
解决方案1似乎是一个空间索引,每次重新计算对时,解决方案2都会受益于空间索引。
使问题复杂化,你的物体在移动 - 它们有速度。
在游戏和模拟中对象使用空间索引是正常的,但主要用于静止物体,通常是通过移动不会对碰撞做出反应的物体。
它是normal in games并且您可以按设定的时间间隔(离散)计算所有内容,因此可能是两个对象相互通过但您没有注意到因为它们移动得太快。许多游戏实际上甚至不按严格的时间顺序评估碰撞。它们具有静止物体的空间索引,例如墙壁,以及他们详尽检查的所有移动物体的清单(尽管我按照概述的放松离散检查)。
准确的连续碰撞检测以及物体对模拟中碰撞的反应通常要求更高。
这对配对你概述的听起来很有希望。您可以通过下一次碰撞对这些对进行排序,并在它们碰到相应的新位置时重新插入它们。您只需要为两个对象排序新生成的冲突列表(O(n lg n)),然后合并两个列表(每个对象的新冲突和现有冲突列表;插入新冲突,删除这些冲突)列出碰撞的两个对象的陈旧碰撞,即O(n)。
另一个解决方案是调整您的空间索引,将对象不是严格地存储在一个扇区中,而是存储在自上次计算以来它经过的每个扇区中,并且离散地执行操作。这意味着在您的空间结构中存储快速移动的对象,并且您需要针对这种情况对其进行优化。
请记住,链接列表或指针列表在现代处理器上非常bad for caching。我建议您存储圆圈的副本 - 它们在任何速率下的碰撞检测的重要属性 - 在任何空间索引的每个扇区中的数组(顺序存储器)中,或者您在上面列出的对中。
正如Mark在评论中所说,将计算并行化可能非常简单。
答案 1 :(得分:15)
我假设您正在进行简单的硬球分子动态模拟,对吧?我在Monte Carlo和分子动力学模拟中多次遇到同样的问题。在有关模拟的文献中经常提到您的两种解决方案。 Personaly我更喜欢解决方案1 ,但略有修改。
解决方案1
将您的空间划分为不重叠的矩形单元格。因此,当您检查一个圆圈是否发生碰撞时,您会查找第一个圆圈所在的单元格内的所有圆圈,并在每个方向上查看X个单元格。我尝试了很多X值,发现X = 1是最快的解决方案。因此,您必须将空间划分为每个方向的单元格大小等于:
Divisor = SimulationBoxSize / MaximumCircleDiameter;
CellSize = SimulationBoxSize / Divisor;
除数应大于3,否则会导致错误(如果它太小,则应放大模拟框)。
然后你的算法将如下所示:
如果你能正确地写出那么你就会有一些关于 O(N)复杂度的东西,因为9个单元格(2D中)或27个单元格(3D中)内的最大圆圈数是恒定的任何圆圈总数。
解决方案2
Ususaly这样做是这样的:
R < R_max
的圆圈列表,计算我们应该更新列表的时间(约为T_update = R_max / V_max
;其中V_max为最大当前速度)T_update
,请转到1. 此列表的解决方案通常会通过添加另一个包含R_max_2 > R_max
的列表并具有自己的T_2
到期时间来改进。在此解决方案中,第二个列表用于更新第一个列表。当然,在T_2
之后,您必须更新所有 O(N ^ 2)的列表。同时要小心T
和T_2
次,因为如果碰撞可以改变速度,那么这些时间就会改变。此外,如果您在系统中引入一些前言,那么它也会导致速度变化。
解决方案1 + 2 您可以使用列表进行冲突检测,使用单元格更新列表。在一本书中写道,这是最好的解决方案,但我认为如果你创建小单元格(就像我的例子中那样),那么解决方案1 会更好。但这是我的看法。
其他内容
您还可以做其他事情来提高模拟速度:
r = sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) + ...)
时,您不必进行平方根操作。您可以将r^2
与某个值进行比较 - 没关系。此外,您不必执行所有(x1-x2)*(x1-x2)
操作(我的意思是,对于所有维度),因为如果x*x
大于某些r_collision^2
,那么所有其他y*y
等等总而言之,会更大。对于硬球,还有一种有效的算法,它不会及时进行,但它会及时查找最近的碰撞并跳转到此时并更新所有位置。对于不太可能发生碰撞的密集系统,这可能是好事。
答案 2 :(得分:4)
一种可能的技巧是使用圈子中心的Delaunay triangulation。
考虑每个圆的中心并应用delaunay三角剖分。这将使您的表面成为三角形。这允许您构建graph,其中每个节点存储三角形的中心,并且每个边连接到相邻圆的中心。上面运行的细分将把邻居的数量限制在一个合理的值(平均6个邻居)
现在,当一个圆圈移动时,你有一组有限的圆圈需要考虑碰撞。然后,您必须再次将细分应用于受移动影响的圆圈集,但此操作仅涉及一小部分圆圈(移动圆的邻居以及邻居的一些邻居)
关键部分是第一次细分,需要一段时间才能完成,以后的细分不是问题。当然,你需要在时间和空间方面有效地实现图表......
答案 3 :(得分:2)
将您的空间细分为区域,并维护每个区域中居中的圆圈列表。
即使您使用一个非常简单的方案,例如将所有圆圈放在一个列表中,按centre.x排序,那么您可以大大加快速度。要测试给定的圆,您只需要对照列表中任意一侧的圆圈进行测试,直到找到x坐标超过 radius 的圆。
答案 4 :(得分:2)
你可以制作一个“球体树”的2D版本,这是一个特殊的(并且很容易实现)将建议的“空间索引”的情况。我们的想法是将“圈”组合成一个“包含”的圈子,直到你得到一个“包含”“大量圈子”的圆圈。
只是为了表明计算“包含圈子”(我的头脑)的简单性: 1)添加两个圆的中心位置(作为矢量)并缩放1/2,即包含圆的中心 2)减去两个圆的中心位置(作为矢量),将半径和比例加1/2,即包含圆的半径
答案 5 :(得分:2)
最有效的答案在某种程度上取决于圈子的密度。如果密度较低,则在地图上放置一个低分辨率网格并标记包含圆的网格元素可能是最有效的。每次更新大约需要O(N*m*k)
,其中N
是圈子总数,m
是每个网格点的平均圈数,k
是平均数由一个圆圈覆盖的网格点数。如果一个圆圈每回合移动多个网格点,则必须修改m
以包括扫描的网格点数。
另一方面,如果密度非常高,那么最好尝试使用图形行走方法。对于每个圆半径R
,每个圆圈包含距离R
(r_i
&gt; r_i
内的所有邻居。然后,如果你移动,你可以查询他们所拥有的邻居的“前进”方向的所有圈子并抓住任何将在D内的圈子;然后你会忘记现在比D更向后的所有那些。现在完整的更新需要O(N*n^2)
,其中n
是半径R
内的平均圆圈数。对于类似于紧密间隔的六边形网格的东西,这将比上面的网格方法给出更好的结果。
答案 6 :(得分:1)
建议 - 我不是游戏开发者
为什么不在碰撞发生时预先计算
指定
我们可以假设圆形对象具有以下属性:
坐标 -
-radius
-Velocity
方向
速度是恒定的,但方向可以改变。
然后,当一个对象的方向发生变化时,重新计算受影响的对。如果方向变化不太频繁,此方法很有效。
答案 7 :(得分:1)
正如Will在他的回答中提到的,空间分区树是解决这个问题的常用方法。这些算法有时需要进行一些调整才能有效地处理移动对象。您将需要使用宽松的铲斗配合规则,以便大多数移动步骤不需要对象来更改铲斗。
我之前看到过你的“解决方案1”用于此问题,并被称为“碰撞哈希”。如果您正在处理的空间足够小以便可管理,并且您希望您的对象至少模糊地接近均匀分布,那么它可以很好地工作。如果您的对象可能是群集的,那么很明显这会导致问题。在每个哈希框内使用某种类型的分区树的混合方法可以帮助解决这个问题,并且可以将纯树方法转换为更容易同时扩展的方法。
重叠区域是处理跨越树桶或散列框边界的对象的一种方法。一个更常见的解决方案是测试任何跨越边缘的对象与相邻框中的所有对象,或者将对象插入到两个框中(尽管这需要一些额外的处理以避免断开遍历)。
答案 8 :(得分:0)
如果您的代码依赖于“滴答”(并且在滴答声中进行测试以确定对象是否重叠),则:
当对象“过快”移动时,它们会相互跳过而不会发生碰撞
当多个物体在同一刻度上碰撞时,最终结果(例如,它们如何弹跳,受到多少伤害等)取决于检查碰撞的顺序,而不取决于碰撞的顺序/应该发生。 在极少数情况下,这可能会导致游戏锁定(例如,三个对象在同一刻度中发生碰撞;针对对象1和对象2的碰撞进行了调整,然后针对对象2和对象3的碰撞进行了调整,导致对象2发生了碰撞。再次与object1碰撞,因此必须重做object1和object2之间的碰撞,但这导致object2再次与object3碰撞,所以...)。
注意:理论上,第二个问题可以通过“递归刻度细分”来解决(如果两个以上的对象发生碰撞,请将刻度的长度分成两半,然后重试直到只有两个对象发生碰撞“子报价”)。这也可能导致游戏锁定和/或崩溃(当3个或更多对象在您遇到“永远递归”的情况的同一时刻发生碰撞)。
此外;有时,当游戏开发人员使用“滴答声”时,他们还会说“ 1固定长度的滴答= 1 /可变帧速率”,这是荒谬的,因为应该是固定长度的东西不能依赖于某些变量(例如,当GPU未能以每秒60帧的速度完成整个模拟的慢动作);如果他们不这样做,而是使用“可变长度刻度”,那么“刻度”的两个问题就会变得更加严重(尤其是在低帧速率下),并且模拟变得不确定性(对于多玩家,并且在玩家保存,加载或暂停游戏时可能会产生不同的行为。
唯一正确的方法是添加一个维度(时间),并为每个对象指定一个线段,称为“起始坐标和终止坐标”,再加上“终止坐标后的轨迹”。当任何对象改变其轨迹时(由于发生了某些不可预测的事件或由于其到达了其“结束坐标”),您将通过进行“两行之间的距离<(object1.radius + object2.radius)”来找到“最快”的碰撞。计算更改的对象以及其他所有对象;然后修改两个对象的“结束坐标”和“结束坐标后的轨迹”。
外部“游戏循环”类似于:
while(running) {
frame_time = estimate_when_frame_will_be_visible(); // Note: Likely to be many milliseconds after you start drawing the frame
while(soonest_object_end_time < frame_time) {
update_path_of_object_with_soonest_end_time();
}
for each object {
calculate_object_position_at_time(frame_time);
}
render();
}
请注意,有多种方法可以对此进行优化,包括:
将世界划分为“区域”-例如这样一来,如果您知道object1将要通过区域1和2,那么它就不会与其他也不会通过区域1或区域2的对象发生碰撞
将对象保留在“ end_time%bucket_size”存储桶中,以最大程度地减少查找“下一个最快结束时间”所需的时间
使用多个线程来执行“ calculate_object_position_at_time(frame_time);”对于每个平行的对象
将所有“直到下一帧时间的高级模拟状态”与“ render()”并行工作(特别是如果大多数渲染是由GPU完成的,则使CPU / s空闲)。
< / li>性能:
不经常发生碰撞时,它可能比“滴答声”要快得多(您可以在相对较长的时间内几乎不做任何工作);并且当您有空闲时间(无论出于何种原因,例如,由于玩家暂停游戏而导致的空闲时间)时,您可以根据需要进行进一步的计算(有效地,随着时间的流逝“消除”开销以避免性能峰值)。
< / li>当碰撞频繁发生时,它会为您提供正确的结果,但比在相同条件下为您提供不正确结果的开玩笑的速度要慢。
在“仿真时间”和“实时”之间具有任意关系也很容易-快进和慢动作之类的东西不会导致任何中断(即使仿真以硬件可以处理的速度运行)或太慢,以至于很难分辨是否有任何动静);并且(在没有不可预测性的情况下)您可以提前计算到将来的任意时间,并且(如果您存储旧的“对象线段”信息而不是在过期时将其丢弃),则可以跳到过去的任意时间,(如果您仅在特定的时间点存储旧信息以最大程度地降低存储成本),则可以跳回到存储的信息所描述的时间,然后向前计算任意时间。这些东西结合在一起,还可以轻松地执行“即时慢动作重播”之类的事情。
最后;对于多人游戏场景来说,这也更加方便,在这种情况下,您不想浪费大量带宽,就不会在每个时刻将每个对象的“新位置”发送给每个客户端。
当然,缺点是复杂性-您要处理加速/减速(重力,摩擦,倾斜运动),平滑曲线(椭圆形轨道,样条曲线)或不同形状的对象(例如任意网格/多边形)时(而不是球体/圆形)计算最早的碰撞何时发生的数学变得更加困难和昂贵。这就是为什么游戏开发人员使用劣等的“勾号”方法进行模拟的原因,这种方法比N个球体或带有线性运动的圆的情况更为复杂。