如果你的代码依赖于“tick”(并测试以确定对象在tick时是否重叠),那么:
- 当对象移动“太快”时,它们会在不碰撞的情况下跳过彼此
- 当多个对象在同一个tick中发生碰撞时,最终结果(例如它们弹回的方式、承受的伤害量等)取决于检查碰撞的顺序而不是碰撞发生的顺序。在极少数情况下,这可能导致游戏锁定(例如,在同一个tick中有3个对象发生碰撞;调整object1和object2的碰撞后,再调整object2和object3的碰撞,导致object2再次与object1碰撞,因此必须重新进行object1和object2之间的碰撞,但这会导致object2再次与object3碰撞,如此往复...)。
注意:理论上,第二个问题可以通过“递归刻度细分”来解决(如果超过2个对象发生碰撞,则将刻度长度减半并重试,直到只有2个对象在该“子刻度”中发生碰撞)。这也可能导致游戏锁定和/或崩溃(当3个或更多的对象在完全相同的瞬间发生碰撞时,您会得到一个“无限递归”场景)。
此外,有时当游戏开发人员使用“刻度”时,他们还会说“1个固定长度的刻度= 1 / 可变帧速率”,这是荒谬的,因为应该是固定长度的东西不能依赖于某些可变的东西(例如,当GPU无法达到每秒60帧时,整个模拟会慢动作进行);如果他们不这样做,而是改用“可变长度的刻度”,那么“刻度”的两个问题都会显著加剧(特别是在低帧速率下),模拟就会变得非确定性(这可能对多人游戏有问题,并且可能导致玩家保存、加载或暂停游戏时出现不同的行为)。
唯一正确的方法是添加一个维度(时间),并为每个对象分配一个线段,描述为“起始坐标和结束坐标”,再加上“结束坐标后的轨迹”。当任何对象改变其轨迹(无论是因为发生了不可预测的事件还是到达了其“结束坐标”)时,您需要通过对于已改变的对象和每个其他对象进行“两条线之间的距离 <(object1.radius + object2.radius)”的计算来找到“最快”的碰撞;然后修改两个对象的“结束坐标”和“结束坐标后的轨迹”。
外部的“游戏循环”应该是这样的:
while(running) {
frame_time = estimate_when_frame_will_be_visible();
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();
}
请注意,有多种优化方法,包括:
将世界分成“区域” - 例如,如果您知道对象1将通过区域1和2,则它不能与不通过区域1或区域2的任何其他对象发生碰撞
将对象保存在“end_time%bucket_size”的桶中,以最小化查找“下一个即将到来的结束时间”的时间
使用多个线程并行处理每个对象的“calculate_object_position_at_time(frame_time);”
将“将模拟状态提前到下一帧时间”的所有工作与“渲染(render())”并行处理(特别是如果大部分渲染由GPU完成,使CPU / s空闲)。
为了提高性能:
它还使得“模拟时间”和“真实时间”之间的任意关系变得微不足道——例如快进和慢动作不会导致任何问题(即使模拟运行得尽可能快或者非常缓慢以至于很难看出是否有任何东西在移动);而且(在没有不可预测性的情况下)您可以提前计算到未来的任意时间,并且(如果在过期时不将旧的“对象线段”信息丢弃而是储存它)您可以跳转到过去的任意时间,而且(如果只在特定时间点存储旧信息以最小化存储成本)您可以跳回到由存储信息描述的时间,然后向前计算到任意时间。这些组合也使得像“即时慢动作重播”之类的事情变得容易。
最后;对于多人游戏场景,它也更为方便,您不需要浪费大量带宽在每个周期向每个客户端发送每个对象的“新位置”。
当然,缺点是复杂性——一旦你想处理加速度/减速度(重力、摩擦、颠簸运动)、平滑曲线(椭圆轨道、样条)或不同形状的物体(例如任意网格/多边形而不是球体/圆形),计算最早碰撞发生的数学问题会变得更加困难和昂贵;这就是为什么游戏开发者会采用比N个球体或圆形具有线性运动更复杂的模拟时,使用劣质的“刻度”方法。