zuihaobushi 2012-09-05
第12章物理引擎
cocos2d有两种物理引擎——Box2D和Chipmunk,这两种物理引擎都只支持二维结构,因此它们十分适合cocos2d。
12.1物理引擎的基本概念
可以将物理引擎视为为游戏中各种对象提供的一个动画系统。诚然,这要依靠游戏开发者来连接和同步物理对象,又称“刚体”游戏对象(比如“精灵”)。之所以称这些物理对象为“刚体”,是因为物理引擎实现这种动画的方式是——假设它们是僵硬不动的、不可变形的。这种简化可以让物理引擎对大量的物体进行计算。
总体上有两种物体:动态的(可以移动的)物体和静态(不会移动的)物体。两者的区别非常重要,因为静态物体从来不懂、并且不会移动,因此,物理引擎可以依据静态物体从不可能相互撞击这个特性来进行优化。
另一方面,动态物体可以相互撞击,也可以和静态物体进行撞击。除了position和rotation之外,动态物体之上还有三个参数需要定义:第一个参数是density(密度)或mass(质量),也就是说,用来层楼一个物体有多重;第二个参数是friction(摩擦度),用来表示一个物体在一个平面上移动的摩擦或平滑程度;最后一个参数是restitution(坚固度),它决定了一个物体的弹性。尽管在现实中不太可能,但物理引擎却可以创造出没有摩擦、没有动能减损的物体,设置可以创造出在和其他物体进行碰撞时不减速反而加速的物体。
动态物体和静态物体都有一个或多个形状来决定这个物体周围的情况。最常见的形状是圆形或矩形,当然也可以是由一个多边形、多个顶点形成的任意复杂形状,或者仅仅是一条直线。物体的形状可以决定物体之间相互撞击时的接触面,同样,每一次撞击都会有由两个物体形状共同决定的接触点(contactpoints)。这些接触点可以用来完成碎片效果或者产生物体相互撞击时的撞击痕迹。
动态物体通过力、力矩等被物理引擎实现,而并非直接设置物体的设置和旋转情况。因为物理引擎可以有一些人工更改过物体位置之后,便无法实现的原本对物体的预测,所以不推荐直接更改物体的位置和旋转情况。
最终,物体和物体之间可以通过节点连接点来相互连接,这限制了相互连接的物体的原本运动情况。
12.2物理引擎的局限性
物理引擎有其自身的局限性,因而不得不做一些简化,因为现实生活中的例子有时太复杂以至于难以模仿。“刚体”就是这样一个例子,在一些极端示例中,物理引擎并不能捕捉到所有的撞击。
12.3Box2D与Chipmunk
Box2D完全是用C++编写的,而Chipmunk则是用C语言编写的。
Chipmunk内部用的是C语言中的structure(结构体),如果在试验中,你不知道某一部分的意义,并且这个部分没有被具体说明,这意味着你不能擅自修改它们,因为它们仅在内部使用。
有一个比较流行的Chipmunk软件——SpaceManager,它可以将Chipmunk的接口自动修改成Objective-C的接口类型。SpaceManager使得将cocos2d精灵附加到刚体上变得更为容易,并且简化了调试过程。
12.4Box2D
注意:
Box2D物理引擎是用C++编写的,因此使用时必须使用.mm作为所有项目实现文件的后缀名,而不是通常使用的.m。这告诉Xcode将所有实现文件的源代码以Objective-C或C++的方式编译处理。如果使用.m后缀名,Xcode会将代码以Objective-C或C的方式编译处理,因此无法处理Box2D中的C++代码,这将导致在使用Box2D的代码行是出现编译错误。因此,当发现有大量错误时,请先检查是否所有实现文件都以.mm作为后缀名。
12.4.1Box2D眼中的世界
记住Box2D是用C++编写的。所以在创建一个新的Box2D类时,必须在类名前使用new关键字。C++中的new关键字等价于Objective-C中的alloc关键字。也就是说,必须释放Box2D世界。在C++中,可以通过使用delete关键字进行释放。
deleteworld;
睡眠中的刚体是什么意思呢?它是一种允许物理模拟不需要处理而快速跳过的对象的技巧。当施加在一个动态刚体上的力量小于临界值一段时间后,该刚体进入睡眠状态。换句话说,如果动态刚体处于很缓慢或几乎不移动和旋转状态时,物理引擎将其标记为睡眠状态,并且不再对其施加力量,直到有一个能够使刚体移动或旋转的推动力出现为止。这个技巧允许物理引擎不再处理那些休息中的刚体,以此来节省运行事件。如果游戏中所有的刚体并非一直在运动,那么应该设置allowBoidiesToSleep变量为true来激活该特性。
传给Box2D的重力是一个b2Vec2结构体。它与CGPoint在本质上是相同的,因为它们都存储x轴和y轴的浮点值。
12.4.2把移动范围限制在屏幕内
最简单的创建静态刚体的方法是,通过使用world的CreateBody方法和一个空刚体定义:
b2BodyDefcontainerBodyDef;
b2Body*containerBody=world->CreateBody(&containerBodyDef);
刚体总是通过world的CreateBody方法来创建,这保证了刚体所用的内存是正确申请和释放的。b2BodyDef是一个拥有创建刚体所需所有数据的结构体,如刚体的位置和类型。默认情况下,一个空刚体定义能够在(0,0)位置创建一个静态刚体。
注意:
b2PloygonShape类有一个SetAsBox方法,所以看起来好像我们可以直接把屏幕的宽和高传给这个方法,从而得到包含屏幕区域的刚体。然而,那样做只会生成一个实心的刚体,并且任何添加到屏幕中的动态刚体实际上都会被包含在实心刚体内部。这将使动态刚体与实心刚体发生碰撞,为避免碰撞发生,动态刚体可能以很快的速度离开屏幕。因此,为了使只有屏幕的边界是实心的,屏幕的四条边需要单独创建。
Box2D在0.1到10的范围内优化效果最好。它是因为公制系统而调整的,所有的距离都被认为是米,所有的重量都使用公斤,所有时间都使用秒来度量。
尽可能地将Box2D世界中的对象尺寸控制在1米左右,这并不是说你不能创建小于0.1或大于10米的对象,但是太小或太大的刚体在运行中可能出现错误设置是奇怪的行为。
PTM_RATIO的定义如下:
#definePTM_RATIO32
在Box2D中,屏幕上的32个像素相当于1米。一个宽度和高度均为32像素的盒形刚体相当于宽度和高度均为1米的物体。在Box2D中,4×4像素大小相当于0.125×0.125米,然而对于一个相对巨大的256×256像素大小的对象,则相当于8×8米。
12.4.3转换点
注意,b2Vec结构体不同于CGPoint,这意味着不能使用CGPoint代替b2Vec2,反之也不可行。此外,Box2D中的点需要转换成以米为单位,或者从米转换为像素。
12.4.4在Box2D世界中创建盒子
动态刚体也需要一个容器包含刚体需要的所有参数,包括刚体的形状、密度、摩擦力和复原(影响刚体在Box2D世界中的移动和弹跳的方式)。将容器当做刚体使用的一个数据集。
12.4.5连接精灵和刚体
盒子精灵不会自动跟着刚体做物理运动,并且刚体不会做任何事,除非调用Box2D世界的Step方法。然后,必须通过得到的刚体的位置和角度来更新精灵的位置。这个操作实现在update方法中。
Box2D世界是通过定期调用Step方法来实现动画的。它需要三个参数。第一个是timeStep,用于告诉Box2D自上一次操作过去的时间,它直接影响到刚体在这一步将要移动的距离。对于游戏,不建议传递deltatime作为timeStep的值,因为deltatime会上下浮动,这样的话,刚体的速度就不稳定了。如果没有一个固定的timeStep值,物理引擎会通过让刚体基于不同的时间进行移动,以符合短暂中断的要求。如果时间间隔的差距很大,刚体会在某些帧做大距离的移动。
Step方法的第二和第三个参数是迭代次数。它们决定物理模拟的精确程度,另外还决定计算刚体移动所需的时间。这是关于速度和精确度的取舍。对于位置的迭代次数,一次就足够了。速度的迭代次数更加重要,一个比较好的速度迭代次数是8.在游戏中,超过10词的迭代作用并不明显,但1到4的迭代次数是无法得到稳定的模拟结果的。速度迭代次数越少,刚体的行为就越起伏不定。最好的方法就是对这些值进行一些试验,从而得到想要的结果。
12.4.6碰撞检测
Box2D有一个名为b2ContactListener的类。如果想接收碰撞的回调(callback),就应该创建一个继承自b2ContactListener的新类。
b2Contact包含了与碰撞相关的所有信息。这些信息中包含了代码中以A和B为后缀的两个相碰撞刚体的所有信息。这里没有区别是谁碰了谁,仅仅是两个刚体发生了相互碰撞。碰撞测试方法可能会在一帧里面被调用多次,么一次碰撞都会调用一次这个方法。
虽然通过contact、进入fixture、然后去的刚体、最后用刚体的GetUserData方法得到精灵的整个过程很复杂,但是Box2D的API参考手册可以帮助你理解我们在这里所经过的层级。
想要使用ContactListener,就必须把它添加到world中。
12.7连接刚体
我们可以用关节把刚体连接起来,使用关节类型决定这刚体连接的方式。
b2RevoluteJointDef的Initialize方法需要三个参数:头两个是需要相互连接起来的刚体,第三个是关节本身的坐标。通过使用其中一个刚体的GetWorldCenter方法获得坐标值,那可以将那个刚体放在关节的中心点,并且这个刚体只能围绕自己旋转。
不管通过哪个刚体,只要使用刚体的GetWorld方法就可以得到b2World。
12.5Chipmunk
12.5.1面向对象的Chipmunk
12.5.2构建Chipmunk物理空间
在使用Chipmunk的任何方法之前,一定要调用cpInitChipmunk。在这之后,再使用cpSpaceNew来创建物理空间并设置它的迭代次数。Chipmunk只使用一种迭代——弹性迭代已经废弃不用了。如果你的游戏不允许物体叠在一起,就不要使用小于8的迭代次数;否则,你会发现堆叠在一起的物体会在很长事件内跳跃和滑动。
Chipmunk内部使用cpVect这一结构体,但在cocos2d中你两个都可以用。
12.5.3将盒子添加到物理空间中
与Box2D不同,Chipmunk不需要将像素转换成米,可以直接用屏幕的像素大小来定义4个角,同样也可以用像素来使用刚体。
接着使用带有两个INFINITY参数的cpBodyNew方法来创建静态刚体。这两个参数分别是mass(质量)和inertia(惰性),当设置它们为INFINITY时,刚体丝毫不能移动。
注意:Chipmunk中的质量和惰性对应于Box2D中的密度和摩擦系数。惰性和摩擦系数的区别在于:前者定义刚体开始运动钱的抵抗力,后者定义刚体在与其他刚体接触时会丢失多少动能。
12.5.4添加小盒子
动态刚体由cpBodyNew方法和参数mass以及一个“惰性时刻”(amomentofinertia)来创建。惰性时刻决定了刚体进入运动状态的阻力,由cpMonmentForBox方法协助计算得出,该方法的参数是刚体的质量和盒子的尺寸。
12.5.5更新盒子的精灵
同Box2D一样,必须在每一帧更新精灵的position(位置)和rotation(旋转角度),从而使它与动态刚体的position和rotation同步。
12.5.6Chipmunk碰撞实践
Chipmunk中的碰撞也是由C语言的回调函数来处理。
默认的shape碰撞测试类型是0.每个刚体的shape可以添加一个整型值作为它的collision_type属性,然后把碰撞回调方法添加到space中,这样以来,就只有当碰撞双方的碰撞属性相同时,碰撞才会发生。这称为“碰撞过滤(filteringcollisions)”。
12.5.7Chipmunk中的关节
12.6本章小结