第七章物理引擎
第七章物理引擎
⼤家对下⾯⼏款⾮常流⾏的游戏⼀定是⽿熟能详,如”愤怒的⼩鸟”,”超级⽕柴⼈⾼尔夫”,”神仙道”。它们背后都是靠物理引擎驱动的。Cocos2d⽤来描述⼆维世界,cocos2d⽀持Box2d和chipmunk,Box2d是⽤C++写的,chipmunk是⽤c语⾔写的,相对⽽⾔,Box2d更流⾏⼀些,因此本书主要接受Box2d,对chipmunk只稍作介绍。有兴趣的同学可以⾃⼰下去学习。
7.1 物理引擎
7.1.1 物理引擎的基本概念
Box2D是⽤C++写的。开发者是Erin Catto,他从2005年开始就在每⼀届的Game DevelopersConference(GDC)上进⾏关于物理模拟的演讲。2007年的9⽉,他公开发布了Box2D引擎。从那以后,Box2D的开发⼯作⼀直很活跃。因为很受欢迎, 所以cocos2d整合了Box2D与之⼀起发布。 Box2D是⼀个⽤于游戏的刚体仿真库。程序员可以在他们的游戏⾥使⽤它。它可以是物体的运动更加可信,让游戏看起来更有交互性。从游戏的视觉来看,物理引擎就是⼀个程序性动画的系统,⽽不是有动画师去移动你的物体。⽜顿就是你的导演。 Box2D 是⽤可移植的 C++ 来写成的。引擎中定义的⼤部分类型都有 b2 前缀,可以把它和我们游戏中的其他元素区分开来。
Box2D中的基本对象:
刚体(rigid body):⼀块⼗分坚硬的物质,它上⾯的任何亮点之间的距离都是完全不变的。它就像钻⽯⼀样坚硬。我们也可以理解为物理学中的质点,只有位置,没有⼤⼩,它⼜可以区分为以下⼏类:
Ø 静态刚体:静态刚体没有质量,没有速度,只可以⼿动来改变他的位置。
Ø 棱柱刚体:棱柱刚体没有质量,但是可以有速度,可以⾃⼰更新位置。
Ø 动态刚体:动态刚体有质量也有速度。
在以后的讨论中我们将⽤物体(body)来代替刚体。
形状(shape): ⼀块严格依附于物体(body)的2D碰撞集合结构(collision geomerty)。形状具有磨擦(friction)和恢复(restitution)的材料性质。形状可以通过关节添加到物体上。
夹具(fixture): ⼀个固定装置将⼀个形状捆绑到⼀个body上,并添加材料属性,例如密度,摩擦⼒,恢复等。
约束(constraint): ⼀个约束就是消除物体⾃由度的物理连接。在 2D 中,⼀个物体有 3 个⾃由度。如果我们把⼀个物体钉在墙上(像摆锤那样),那我们就把它约束到了墙上。这样,此物体就只能绕着这个钉⼦旋转,所以这个约束消除了它 2 个⾃由度。还有⼀种不须你创建的接触约束,⼀个防⽌刚体穿透,以及⽤于模拟摩擦和恢复的特殊约束。你永远不必创建⼀个接触约束,它们会被Box2D创建。
关节(Joint): 它是⼀种⽤于把两个物体或多个物体固定到⼀起的约束。Box2D⽀持的关节类型有:旋转,棱柱,距离等等。关节可以⽀持限制和马达。
Ø 关节限制:⼀个关节限制(joint limit)限定了⼀个关节的运动范围。例如中标的钟摆只能在某个范围⾓度内运动。
Ø 关节马达(joint motor)⼀个关节马达能依照关节的⾃由度来驱动所连接的物体。例如你可以⽤⼀个齿轮来驱动钟摆的旋转。
世界(world): 世界是遵循物理的空间,以上的所有都存在于世界中,可以创建多个世界,但很少这样⽤。创建世界需要两个步骤,⼀是⽣成重⼒向量,⼆是根据重⼒⽣成世界对象。
7.1.2物理引擎的局限性
物理引擎有它⾃⾝的局限性:它们必须在模拟效果时使⽤⼀些捷径,也就是说简化物体的复杂性,因为
真实世界过于复杂,完全放到物理引擎中进⾏模拟是不可能的。这就是为什么要使⽤刚体的原因。在某些极端情况下,物理引擎有可能会捕捉不到某些已经发⽣的碰撞 – 例如,当刚体以很快的速度移动时,⼀个刚体可能直接穿透另⼀个刚体。虽然在量⼦物理学中这样的穿透情况会发⽣, 但是我们看到的真实世界中的物体是不会相互穿透的。
刚体有时候会相互穿透卡在⼀起,特别是在使⽤了关节将它们连接在⼀起以后。卡在⼀起的刚体会努⼒要分开,但是为了满⾜关节的连接要求,它们⼜不得不卡在⼀起,结果是卡在⼀起的刚体会产⽣颤动。
我们也可能碰到游戏运⾏的问题。如果我们在游戏⾥使⽤了很多刚体,你永远
不会知道这些刚体相互作⽤后的最终结果。最终,有些玩家会把⾃⼰卡死在刚体中,或者他们也可能会发现如何利⽤物理模拟的漏洞,跑到游戏中他们本来不应该去的区域。
7.2 Box2d设计思路
7.2.1 内存管理
Box2D的许多设计都是决策都是为了能够快速的使⽤内存。
Box2D不倾向分配⼤量的⼩对象(50-300字节)。这样通过malloc或new在系统堆(heap)上分配内存效能太低效,并且容易产⽣内存碎⽚。
Box2D的解决⽅案是使⽤⼩型对象分配器(SOA),SOA维护了许多不定尺⼨的可⽣长的池,当有内存分配请求的时候,SOA会返回⼀块最匹配的内存,当内存释放掉以后,它会回到池中。这些操作都⼗分快速,因⽽只会产⽣很⼩的堆流量。
因为Box2D使⽤了SOA,所以你永远也不必去new⽕malloc物体,形状活关节,都只需要分配⼀个b2World,它为你体总了创建物体,形状和关节的⼯⼚。
7.2.2 ⼯⼚和定义
如上所述,内存管理在Box2D API的设计中担当了⼀个中⼼⾓⾊,所以当你创建⼀个b2Body或⼀个b2Joint的时候,你需要⽤b2World的⼯⼚函数。
下⾯是创建函数
1 b2BodyDefcontainerBodyDef;
2 b2Body*containerBody = world->CreateBody(&containerBodyDef);
下⾯是对应的摧毁函数:
world->DestroyBody(&containerBody);
7.2.3 单位
Box2D使⽤浮点数,所以必须使⽤⼀些公差来保证它们正常⼯作。这些公差已经被调谐得适合⽶,千克,秒单位。尤其是,Box2D被调谐的能良好的处理0.1到10⽶之间的移动物体,这意味着从茶杯,粉笔盒到卡车⼤⼩的对象都能良好的⼯作。但是在你创建的Box2D世界中的刚体的⼤⼩限定在越接近1⽶越好。不过这并不意味着你不能有长度⼩于0.1⽶的刚体,或者长度⼤于10⽶的刚体。但是,太⼩或者太⼤的刚体很可能会在游戏运⾏过程中产⽣错误和奇怪的⾏为。
作为⼀个2D物理引擎,如果能够使⽤像素作为单位是很诱⼈的,很不幸,那将导致不良模拟,会造成古怪的⾏为。⼀个200像素长的物体在Box2D看来就有45层建筑物那么⼤。想象⼀下使⽤⼀个被调谐好的玩偶和⽊桶去模拟⾼楼⼤厦的运动,那⼀定很怪异。
我们⼀定习惯了,⽤像素来计算屏幕中的位置,因为这样对我们来说更为直观,我们可以通过下⾯的⽅式进⾏b2Vec2和CGPoint的转变,这意味着你不能够在需要CGPoint的地⽅使⽤b2Vec2,反过来也不⾏。⽽且,Box2D⾥的点需要转换成⽶为单位,或者从⽶为单位转换回以像素为单位。为了避免出错,⽐如忘记转换单位,或者打错了字,或者把x轴坐标使⽤了两次,我强烈建议你把这些重复的转换代码封装到⽅法中去,⾸先定义⼀个转变基数:
#definePTM_RATIO 32
PTM_RATIO⽤于定义32个像素在Box2D世界中等同于1⽶。⼀个有32像素宽和⾼的盒⼦形状的刚体等同于1⽶宽和⾼的物体。在Box2D 中,4x4像素的⼤⼩是 0.125x0.125。你可以通过PTM_RATIO把刚体的尺⼨设置成最适合Box2D的尺⼨, ⽽PTM_RATIO设置为32,对于拥有1024x768像素的iPad来说也是很合适的。
-(b2Vec2)toMeters:(CGPoint)point
{undefined
return b2Vec2(point.x / PTM_RATIO,point.y / PTM_RATIO);
}
-(CGPoint)toPixels:(b2Vec2)vec
{undefined
return ccpMult(CGPointMake(vec.x, vec.y),PTM_RATIO);
}
这样我们就可以很容易的进⾏b2Vec2和CGPoint的转变
1 CGPoint point = CGPointMake(100, 100);b2Vec
2 vec = b2Vec2(200, 200);
2 CGPoint pointFromVec; pointFromVec = [lftoPixels:vec];
3 b2Vec2 vecFromPoint; vecFromPoint = [lftoMeters:point];
7.2.4 ⽤户数据
b2shape,b2Body和b2Joint类都允许你通过⼀个void指针来附加⽤户数据。这在你测试Box2D数据结构,以及你想把它们联系到⾃⼰的引擎中是较为⽅便的。
举个典型的例⼦,在⾓⾊上的刚体中附加到⾓⾊的指针,这就构成了⼀个循环引⽤。如果你有⾓⾊,你就能得到刚体,如果你有刚体,你就能得到⾓⾊。
这是⼀些需要⽤户数据的案例:
Ø 使⽤碰撞结果给⾓⾊施加伤害。
Ø 当玩家进⼊⼀个包围盒时播放⼀段脚本事件。
Ø 当Box2D通知你⼀个关于即将摧毁时访问⼀个游戏结构。
7.3世界(world)
7.3.1 什么是世界
b2World类包含着物体和关节,它管理者物理模拟的⽅⽅⾯⾯,并允许异步查询(就想AABB查询)你与Box2Dderek⼤部分交互都将通过b2World对象来完成。⼀个世界是⼀个物理引擎的开始,我们从创建⼀个世界开始,讲逐步告诉你怎么创造⼀个物理引擎,⼀个⾃⼰定义的世界。
7.3.2 创建和摧毁⼀个世界
创建⼀个世界和摧毁⼀个世界很简单,你只需要提供⼀个重⼒向量和是否允许物体休眠。
要创建或摧毁⼀个世界你需要使⽤new:
1 -(id) init
2 {undefined
3 if((lf = [super init]))
4 {undefined
5 b2World*world;
6 b2Vec2gravity = b2Vec2(0.0f, -10.0f);
7 bool allowBodiesToSleep =true;
8 world = new b2World(gravity,allowBodiesToSleep);
9 }
7.3.3 使⽤⼀个世界
7.3.3.1 模拟
世界类⽤于驱动模拟。也就是说我们可以决定可以多长时间刷新物理世界,它包括物体的速度和位置等信息的刷新。我们需要制定⼀个刷新的时间间隔和迭代次数。
例如下⾯的代码是按制定的最快的速度刷新,每次刷新是速度会迭代8次,⽽位置的计算迭代⼀次。
-(void) update:(ccTime)delta
{undefined
float timeStep = 0.03f;
int32 velocityIterations = 8;
int32 positionIterations = 1;
world->Step(timeStep,velocityIterations, positionIterations);
}
Box2D建议的刷新的频率是固定的。
那为什么我们还要允许我们⾃⼰定义这些参数呢?当我们的游戏运⾏负担⽐较轻的时候,我们可以给⽤户⼀个较⾼的刷新频率,这样⽤户就能获得更好的体验;当我们的游戏负担⽐较重的时候,我们可以使⽤⼀个较低的刷新频率,已获得⼀个⽤户还算满意的游戏体验。
7.3.3.2 扫描世界
如上所述,世界就是⼀个物体和关节的容器,当我们刷新世界的时候,肯定是想让世界的物体或者关节发⽣某种变化。你可以获取世界中所有物体和关节遍历它们。例如,你可能需要需要改变某个精灵的位置或者唤醒世界中的所有物体,
我们在-(void) update:(ccTime)delta⽅法中添加如下代码
for (b2Body*body = world->GetBodyList(); body != nil; body = body->GetNext())
{undefined
CCSprite* sprite =(CCSprite*)body->GetUrData();
if (sprite != NULL)
{undefined
// update the sprite's position to wheretheir physics bodies are
sprite.position = [lftoPixels:body->GetPosition()];
float angle = body->GetAngle();
}
}
7.4物体(Body)
静态物体(b2_statiBody)
⼀个静态物体不会在模拟中⼀种,并且它⾏动起来就像其有⽆限的质量。内部原因讲,Box2D将质量存储为零,静态物体能被⽤户⼿动操作移动。静态物体含有零向量,不会与其它静态物体或者运动的物体碰撞。
运动但不受⼒物体(b2_kinematicBody)
运动但不受⼒物体凭借向量可以在模拟中运动,它们不受⼒的作⽤。可以通过⽤户⼿动操作⽽做运动,但通常情况下,运动但不受⼒物体通过设置其向量来操作移动。其⾏为看起来也好像有⽆限的重量,但是,Box2D将质量存储为零,运动但不受⼒物体也不会与静态物体或者运动物体碰撞。
动态物体(b2_dynamicBody)
动态的body被完全模拟,他们可以通过⽤户⼿动操作⽽移动,但通常情况下,他们在⼒的作⽤下移动,动态body可以与任何类型的body碰撞,⼀个动态的body旺旺是有限制的,必须为⾮零质量。如果你想把动态body的质量设为零,它将⾃动获得⼀千克的质量。
7.4.1 物体定义
前⾯我们已经创建了⼀个世界,现在我们创建⼀个物体绑到世界上。
在物体创建之前,你必须创建⼀个物体定义(b2BodyDef)来初始化物体所需的数据。
我们先来熟悉⼀个b2BodyDef的各种属性
b2BodyDefcontainerBodyDef;
containerBodyDef.position.Set(0.0f,2.0f);
containerBodyDef.angle= 0.25f*b2_pi;
containerBodyDef.linearDamping = 0.0f;
containerBodyDef.angularDamping=0.01f;
containerBodyDef.allowSleep= true;
containerBodyDef.awake= true
containerBodyDef.urData= &myaction;
containerBodyDef.fixedRotation= true;//固定选装
containerBodyDef.bullet= true;
物体类型(type)属性:在初始化⼀个物体的时候,你就应该确定好改物体的类型,静态物体,动态但不受⼒物理还是动态物体。并轻易不要修改它。
位置(position)和⾓度(angle)属性:定义物理之后,我们可以初始化⼀个位置和物体的⾓度,⽽不是所有的物体从原点建⽴,⽽后移到你所需要的位置上⾯。
阻尼(linearDamping和angularDamping):阻尼是⽤来减⼩物体的速度的,阻尼与摩擦不同,因为只有接触才回产⽣摩擦,阻尼也不是摩擦的取代,这两个效果要⼀起使⽤。阻尼参数范围是0到⽆穷⼤,0是没有阻尼,⽆穷就是满阻尼。
休眠参数(allowSleep和awake):模拟是⾮常昂贵的,我们应当尽量减少模拟物体,当⼀个物体休息时,我们应当停⽌他们的模拟。⼦弹(bullet):有的时候,在同⼀个时间有⼤量的刚体在运动,我们肯定不希望这些物体能够相互穿来穿去的,这被称作隧道效应。
默认情况下,Box2D会通过连续碰撞检测来防⽌动态物体穿越静态物体。但动态物体之间是不使⽤连续碰撞检测的,这是为了保持游戏的性能。告诉移动的物体在Box2D中被称为⼦弹(bullet),⼦弹能够检测到碰撞,⽽不会引起穿壁⽽过的情况。
⽤户数据(urData):⽤户数据是个空指针,它给你提供了⼀个挂钩来将你的应⽤程序对象连接到物体上,对所有物体的⽤户数据,你需要⼀个⼀致的对象类型。
7.4.2 创建物体
上⾯我们已经知道了如何定义⼀个物体的属性,前⾯我们已经说过,所有的物体创建和销毁都是有世界(World)来完成的,这使得世界可以通过⼀个⾼效的分配器来创建物体,并且把物体添加到世界上。我们在上⾯的-(id) init添加如下代买,把创建⼀个物体并绑定到世界(World)上
b2BodyDef containerBodyDef;
b2Body* containerBody =world->CreateBody(&containerBodyDef);
7.4.3 使⽤物体
创建⼀个物体之后,⼀般我们不应该改变它的属性,⽽应该遵循物理规则使其产⽣变化,但是⼀些特殊情况,我们也可以读取和修改其属性。这些可修改的属性有:质量数据,状态信息,位置和速度等。
使⽤最多的是通过⼒和冲量等改变物体的运动。
你可以对⼀个物体应⽤⼒,扭矩,以及冲量。当应⽤⼀个⼒或者冲量时,你需要提供⼀个世界位置。这常常会导致对质⼼的⼀个扭矩。void ApplyForce(const b2Vec2& force,const b2Vec2& point);
void ApplyTorque(float32 torque);
void ApplyAngularImpul(float32 impul);
应⽤⼒,扭矩⼒或冲量会唤醒物体,有时这是不合需求的。例如,你可能想应⽤⼀个稳定的⼒,并允许物体休眠来提升性能,这时,你需要这要来改变物体的属性。
if(containerBody->IsSleepingAllowed() ==fal)
{undefined
containerBody->ApplyForce(myForce, myPoint);
}
7.5形状
形状就是物体上的碰撞⼏何结构,另外形状也⽤于定义物体的质量。也就是说,你来指定密度,Box2D可以帮你计算出质量。