愤怒的小鸟像一阵旋风样刮便了大江南北,在惊异于游戏可爱的画面的有趣的音效的同时,本人比较感兴趣的还有那些木头、石头倒塌的效果是怎么实现的。后来发现这一切都源自于一个叫Box2d的物理引擎,有意思。
最近一段时间准备着从原来的公司离职,于是有了比较多的空闲时间。自己又是个闲不住的人,于是准备做一个小项目。碰巧看到有人在玩一种小玩具,在一个塑料盒里有一些小钢珠,盒子的底部有一些凹进去的小孔,玩家要通过控制盒子来使得所有钢珠都进入小孔中(不知道这玩具叫啥名字,应该有盆友玩过)。
准备将这个小游戏在Android平台上实现出来,开始觉得很简单:不就是通过重力感应控制小球的移动,直到所有小球都进入洞里游戏就胜利。但经过分析发现,实际情况远比想象中的复杂,游戏的难点在于小球的碰撞和堆叠。小球的碰撞是指球与球之间,或球与墙壁之间发生碰撞后的解析。小球的堆叠是指如上图所示的许多小球堆在一起的情况。
开始的想法是利用开源的物理引擎,JBox2d是Box2d的java实现,适合在Android平台上使用,基本上是在Android平台上的一个最佳选择。于是在这个小项目中引入了JBox2d,但下面的一些原因让我决定放弃JBox2d
1、 这个小游戏里面涉及的到只有圆和圆的碰撞和堆叠,远比JBox2d物理引擎考虑的东西小很多,使用JBox2d是不是有种杀鸡用牛刀的感觉。
2、 也许是对于JBox2d了解的不够深入,将其引入到项目之后,游戏运行的速度不是很理想,球在滚动的过程中有种明显的延迟感觉,尤其是很多球在一起滚动的时候。
3、 既然要自己做一个项目,用这种开源的代码拼凑起来完全达不到锻炼的目的,即使成功了也没啥挑战性。
于是决定自己写一个只涉及圆形和圆形碰撞的小物理引擎,用于这个项目。这样的一个物理引擎涉及到两个核心问题:圆和圆的碰撞处理、圆和圆的堆叠处理。第一个问题碰撞检测与处理是一个相对简单的问题,而难点就在于很多圆之间的堆叠处理。
碰撞检测与解析
圆和圆的碰撞检测和解析不是困难的问题,只要检测到两个圆的圆心距小于半径之和,就说明两个圆之间存在冲突。而要解析这个碰撞也比较简单,如下图所示:
首先计算出圆A和圆B的相对速度Vab= Vb – Va(这里都是二维向量的运算)。法向向量为n = Pb – Pa。Pa和Pb分别为A、B圆心的坐标。
如果Vab*n < 0,说明A和B在靠近,否则说明在分离。对于分离的情况,我们不必处理。
通过计算,可以算出碰撞的冲量为:
其中e为恢复系数,Vab为A和B的相对速度,n为法向向量。
通过将冲量j应用到A和B上,可以计算出A和B碰撞后的速度。
va’ = va + j*n/ma;
vb’ = vb – j*n/mb;
堆叠处理
碰撞检测和解析是一个相对简单的问题,只要有些物理和数学常识的人都不难实现。而堆叠处理是一个比较麻烦的问题。所谓堆叠,就是如本文开始游戏截图中所示的那样,很多球堆在一起。在这种情况下,需要考虑的不再是弹性碰撞,而是非弹性碰撞,如何解析球之间的力是问题的难点和关键。凭笔者这种脑袋很难自创一个优秀的解决方案,于是上网搜了一下这方面的论文。
Nonconvex算法
首先搜到的是2003年斯坦福大学的一篇论文,提到了关于这个问题的解决方案,论文的题目是《NonconvexRagid Bodies with Stacking》。这篇论文提出了一个算法,利用冲量来解决非凸刚体的碰撞和堆叠。
比较仔细耐心地研究了一个这篇论文,并参照了这篇论文的一个实现jiglib(http://www.rowlhouse.co.uk/ jiglib/,是基于该论文算法实现的一个3d物理引擎),试图去在java平台上实现一个2d的圆形碰撞和堆叠的引擎。这篇论文提出的算法流程是这样的:
1. 计算施加在刚体上的力(一般来说就是重力)
冲突解析部分(CollisionResolution)
2. 保存刚体信息(线速度,角速度,位置,方向)
3. 计算新的速度
4. 计算新的位置
5. 进行冲突检测(对于圆就是检查圆和圆之间有没有重叠)
6. 将速度恢复为之前的值
7. 将冲突列表按照重叠的程度由大到小排序
8.如果这个项目的relVel*colNorm< 0,将碰撞和摩擦冲量应用到冲突列表一个项目中(只更新速度,不更新位置)。说明:其中relVel为冲突的两个刚体之间的相对速度,colNorm为碰撞的法向向量。relVel*colNorm<0表示这两个刚体在靠拢,而不是分离。
9. 更新所有受到该冲量影响的对象的相对速度
10. 遍历冲突列表,去掉所有relVel*colNorm>=0的项。
11. 重复8-10,知道冲突列表为空
12. 将位置和方向恢复为之前的值
13. 重复2-12五次
14. 计算施加在刚体上的力
15. 计算新速度
接触解析部分(Contact Resolution)
16. 保存刚体的位置和方向
17. 使用新的速度计算位置
18. 进行冲突检测
19. 根据重力的从小到大的方向将冲突列表排序
20. 如果这个项目的relVel*colNorm< 0,将碰撞和摩擦冲量应用到冲突列表一个项目中(只更新速度,不更新位置)。
21. 更新所有受到该冲量影响的对象的相对速度
22. 重复20-21,知道完成整个列表
23. 将位置和方向恢复到之前的值
24. 重复16-23十次。当应用碰撞冲量的时候,开始使用e=-0.9,每次迭代增加0.1。
25. 计算新的位置和方向。
根据GameDev.net上一篇关于该算法的帖子,以及借鉴jiglib这个基于该算法的3d实现,笔者也尝试根据上面的流程去实现一个简单的只涉及2D圆碰撞的Java版本。冲突解析部分可能很顺利的实现,但对于堆叠的处理,也就是接触解析部分。原论文中提到了使用一种排序算法来对冲突列表进行某种方式的排序,由于可参考的资料不多,jiglib的源码是C语言写得,夹杂着很多Opengl的东西,这两方面都不是很熟,所以一直没弄清楚这个排序是怎样进行的,于是该算法也就没能够完整的实现出来。
Box2d算法
正在迷惑的时候,看到了另外一篇论文。这篇论文的名字是《IterativeDynamics with Temporal Coherence》。去下了这篇论文,突然发现论文的作者ErinCatto其实正是Box2d的作者。绕了一圈又回来了,把这篇论文简单的看了一下。说实话,这篇论文写的比较难懂。又把ErinCatto在GDC(GameDevelopment Conference)上的PPT下来看了一下,发现所提出的算法过程比较简单,下面是该算法的实现过程:
1. 计算接触点
2. 应用外力(例如重力)
3. 应用冲量
4. 更新位置
5. 循环
这个算法过程看起来很简单,而事实也正是如此。相比上个算法复杂的流程,Box2d所采用的算法看起来清晰和简单很多。
第一步计算接触点就是找到冲突。对于我要实现的圆和圆的系统,其实就是找到存在重叠的地方。对于Box2d这种涉及多种形状的就要复杂一些,例如矩形与矩形的冲突检测就要用到SAT测试的算法。
第二步应用外力就是设置重力。
第三步应用冲量是算法的关键和核心。需要在每一个接触点应用冲量,法向冲量用于阻止刚体穿透彼此,切向冲量用于施加摩擦(本游戏只考虑法向冲量,未考虑切向冲量)。根据前面碰撞检测与解析中所述的那样,应用冲量到参与碰撞的两个刚体上,得到新的速度。
除了上面计算的法向冲量,算法中还引入了一个叫Bias的冲量,这个Bias冲量是为了给法向冲量一定的修正,它和两个圆重叠的大小成正比,引入这个冲量是为了使重叠的刚体分开,同时也允许一些穿透。
算法中还引入了对冲量的积蓄处理(Accumulated Impulse)。
第四步就是使用新的速度来计算新的位置。
Box2d提出的这个算法开起来相当简单,实现也相当容易。从Box2d的官方网站上我们可以下到关于上述算法的一个实现。http://code.google.com/p/box2d/downloads/list
列表中的Box2d_lite.zip是上述算法的最初c语言实现,后续的Box2d版本都是基于这个代码的。这段代码很清晰地反映了论文以及PPT中所述的过程。而Box2d_lite的代码量比较小,思路清晰,看起来比较容易。
两种算法的比较
Nonconvex算法和Box2d算法都出自高人之手,我这里只是粗略地对两种算法进行了学习。从两个算法的流程就可以看出,Box2d算法更为简洁明了,实现起来也比较容易。Box2d算法没有区分弹性碰撞和非弹性碰撞,不像nonconvex算法需要区分这两个阶段。但从模拟的效果来看,nonconvex算法显得更加优雅。例如以一个球放在地面上的例子而言,nonconvex算法会推到出由于支撑力,作用在球上的冲量为0,所有球会静止不动。而对于Box2d算法,会有一个非弹性碰撞速度阈值,小于这个速度的冲突都会认为是非弹性碰撞。由于重力作用于球产生的速度小于这个阈值,才使球静止不动。这样的处理看上去不如nonconvex算法优雅。
BallWorld项目
本人参照Box2d_lite以及JBox2d的源码,为游戏写了一个简单的引擎。由于本人的目的不是写一个完美的物理引擎,所以这个小引擎中还存在着诸多的问题。例如并没有考虑TOI(TimeOf Impact),在这个小游戏中,球的速度不会很大,在游戏中不会出现穿越的情况,因此没有考虑TOI,如果考虑TOI,将会对性能造成比较大的影响。
虽然这个小引擎中还存在诸多问题,但已经基本上能够满足游戏的要求了。于是参照《BeginningAndroid Games》,制作了本人的首款android平台上的游戏。
关于这个小游戏的内容和源代码,可以在下面的网址上找到:
http://code.google.com/p/android-ball-world/
以上内容,作为前段时间为这个小项目付出工作的总结。也希望能够对需要了解这方面内容的朋友有那么一点帮助。
参考资料
1、 http://compsci.ca/v3/viewtopic.php?t=14897Perfect Circle-Circle Collision Detection
这篇文章讲述了圆圆碰撞冲突检测的处理方式。如果只考虑碰撞,不考虑堆叠的话,这篇文章的内容足以。
2、 http://phoenix.goucher.edu/~kelliher/s2005/cs320/feb25.htmlCollision Detection and
Resolution这篇文章讲述了圆圆碰撞解析中所需要用到的数学知识,以及碰撞冲量的计算过程。
3、 《Nonconvex Ragid Bodies with Stacking》(2003年) 这篇论文提出了一种解决非凸刚体碰撞与堆叠的算法。
4、 http://www.rowlhouse.co.uk/jiglib/ Jiblib是一个基于论文《Nonconvex Ragid Bodies withStacking》实现的3d物理引擎。
5、 GameDev.net这个帖子是关于《Nonconvex》论文所提出算法的讨论。
6、 《IterativeDynamics with Temporal Coherence》(2005) 这篇论文是Box2d作者Erin Catto提出的一种物理引擎算法。
7、 http://code.google.com/p/box2d/downloads/list Box2d的官方资料。其中Box2d_lite.zip是基于《Iterative》论文的基本实现,代码量很小。GDC200*.zip是Erin Catto在GDC上演讲的PPT,内容大体上是一样的,主要是对论文中所述算法的介绍。
8、 http://www.jbox2d.org/JBox2d的官方网站。JBox2d是Box2d的Java实现。
9、Beginning Android Games 一本非常棒的Android游戏入门的书籍,虽然目前还没有中文版出现,但这本英文版讲的浅显易懂,的确是一本很棒的Android游戏入门书籍。
10、 http://code.google.com/p/android-ball-world/ 本人写的关于本文中所述的小游戏。