第七章 物理引擎 大家对下面几款非常流行的游戏一定是耳熟能详,如”愤怒的小鸟”,”超级火柴人高尔夫”,”神仙道”。它们背后都是靠物理引擎驱动的。
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来说也是很合适的。
[Microsoft1]
-(b2Vec2)toMeters:(CGPoint)point?
{
return b2Vec2(point.x / PTM_RATIO,point.y / PTM_RATIO);
}
-(CGPoint)toPixels:(b2Vec2)vec
{
return ccpMult(CGPointMake(vec.x, vec.y),PTM_RATIO);
}
这样我们就可以很容易的进行b2Vec2和CGPoint的转变
1 CGPoint point = CGPointMake(100, 100);
b2Vec2 vec = b2Vec2(200, 200);
2 CGPoint pointFromVec;
pointFromVec = [selftoPixels:vec];
3 b2Vec2 vecFromPoint;
vecFromPoint = [selftoMeters: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 {
3if((self = [super init]))
4{
5b2World*world;
6b2Vec2gravity = b2Vec2(0.0f, -10.0f);
7bool allowBodiesToSleep =true;
8world = new b2World(gravity,allowBodiesToSleep);
9 }
7.3.3 使用一个世界
7.3.3.1 模拟
世界类用于驱动模拟。也就是说我们可以决定可以多长时间刷新物理世界,它包括物体的速度和位置等信息的刷新。我们需要制定一个刷新的时间间隔和迭代次数。
例如下面的代码是按制定的最快的速度刷新,每次刷新是速度会迭代8次,而位置的计算迭代一次。
-(void) update:(ccTime)delta
{
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())
{
CCSprite* sprite =(CCSprite*)body->GetUserData();
if (sprite != NULL)
{
// update the sprite's position to wheretheir physics bodies are
sprite.position = [selftoPixels:body->GetPosition()];
float angle = body->GetAngle();
sprite.rotation =CC_RADIANS_TO_DEGREES(angle) * -1;
}
}
7.4物体(Body)静态物体(b2_statiBody)
一个静态物体不会在模拟中一种,并且它行动起来就像其有无限的质量。内部原因讲,Box2D将质量存储为零,静态物体能被用户手动操作移动。静态物体含有零向量,不会与其它静态物体或者运动的物体碰撞。
运动但不受力物体(b2_kinematicBody)
运动但不受力物体凭借向量可以在模拟中运动,它们不受力的作用。可以通过用户手动操作而做运动,但通常情况下,运动但不受力物体通过设置其向量来操作移动。其行为看起来也好像有无限的重量,但是,Box2D将质量存储为零,运动但不受力物体也不会与静态物体或者运动物体碰撞。
动态物体(b2_dynamicBody)
动态的body被完全模拟,他们可以通过用户手动操作而移动,但通常情况下,他们在力的作用下移动,动态body可以与任何类型的body碰撞,一个动态的body旺旺是有限制的,必须为非零质量。如果你想把动态body的质量设为零,它将自动获得一千克的质量。
7.4.1 物体定义
前面我们已经创建了一个世界,现在我们创建一个物体绑到世界上。
在物体创建之前,你必须创建一个物体定义(b2BodyDef)来初始化物体所需的数据。
我们先来熟悉一个b2BodyDef的各种属性
b2BodyDefcontainerBodyDef;
containerBodyDef.type= b2_dynamicBody;
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.userData= https://www.it610.com/article/&myaction;
containerBodyDef.fixedRotation= true;
//固定选装
containerBodyDef.bullet= true;
物体类型(type)属性:在初始化一个物体的时候,你就应该确定好改物体的类型,静态物体,动态但不受力物理还是动态物体。并轻易不要修改它。
位置(position)和角度(angle)属性:定义物理之后,我们可以初始化一个位置和物体的角度,而不是所有的物体从原点建立,而后移到你所需要的位置上面。
阻尼(linearDamping和angularDamping):阻尼是用来减小物体的速度的,阻尼与摩擦不同,因为只有接触才回产生摩擦,阻尼也不是摩擦的取代,这两个效果要一起使用。阻尼参数范围是0到无穷大,0是没有阻尼,无穷就是满阻尼。
休眠参数(allowSleep和awake):模拟是非常昂贵的,我们应当尽量减少模拟物体,当一个物体休息时,我们应当停止他们的模拟。
子弹(bullet):有的时候,在同一个时间有大量的刚体在运动,我们肯定不希望这些物体能够相互穿来穿去的,这被称作隧道效应。
默认情况下,Box2D会通过连续碰撞检测来防止动态物体穿越静态物体。但动态物体之间是不使用连续碰撞检测的,这是为了保持游戏的性能。告诉移动的物体在Box2D中被称为子弹(bullet),子弹能够检测到碰撞,而不会引起穿壁而过的情况。
用户数据(userData):用户数据是个空指针,它给你提供了一个挂钩来将你的应用程序对象连接到物体上,对所有物体的用户数据,你需要一个一致的对象类型。
7.4.2 创建物体
上面我们已经知道了如何定义一个物体的属性,前面我们已经说过,所有的物体创建和销毁都是有世界(World)来完成的,这使得世界可以通过一个高效的分配器来创建物体,并且把物体添加到世界上。我们在上面的-(id) init添加如下代买,把创建一个物体并绑定到世界(World)上
[Microsoft2]
b2BodyDef containerBodyDef;
containerBodyDef.type = b2_dynamicBody;
b2Body* containerBody =world->CreateBody(&containerBodyDef);
7.4.3 使用物体
创建一个物体之后,一般我们不应该改变它的属性,而应该遵循物理规则使其产生变化,但是一些特殊情况,我们也可以读取和修改其属性。这些可修改的属性有:质量数据,状态信息,位置和速度等。
使用最多的是通过力和冲量等改变物体的运动。
你可以对一个物体应用力,扭矩,以及冲量。当应用一个力或者冲量时,你需要提供一个世界位置。这常常会导致对质心的一个扭矩。
void ApplyForce(const b2Vec2& force,const b2Vec2& point);
void ApplyTorque(float32 torque);
void ApplyAngularImpulse(float32 impulse);
应用力,扭矩力或冲量会唤醒物体,有时这是不合需求的。例如,你可能想应用一个稳定的力,并允许物体休眠来提升性能,这时,你需要这要来改变物体的属性。
if(containerBody->IsSleepingAllowed() ==false)
{
containerBody->ApplyForce(myForce, myPoint);
}
7.5形状 形状就是物体上的碰撞几何结构,另外形状也用于定义物体的质量。也就是说,你来指定密度,Box2D可以帮你计算出质量。
形状具有磨擦和恢复的性质。形状还可以携带筛选信息,是你可以防止某些游戏对象之间的碰撞。
形状永远属于某物体,单个物体可以拥有多个形状。
形状不能直接创建在物体上,形状应创建在夹具上,夹具再创建在物体。
通用的形状数据会保存在b2Shape中,一些特殊的形状数据会保存在其派生类中。
下面我们讲一些基本的形状定义
7.5.1 圆形定义
b2CircleShape扩充了b2Shape并增加一个半径和一个局部位置。
b2CircleShapecircle;
circle.m_p.Set(2.0f,3.0f);
circle.m_radius = 0.5f;
7.5.2 多边形定义
b2PolygonShape用于定义凸多边形。
当创建多边形定义时,你需要给出所用的顶点数目。这些顶点必须按照相对于右手坐标系之Z轴逆时针(CCW)的顺序定义。
下面给出一个三角形的多边形定义的代码
b2Vec2 vertices[3];
vertices[0].Set(0.0f, 0.0f);
vertices[1].Set(1.0f, 0.0f);
vertices[2].Set(0.0f, 1.0f);
int32count = 3;
b2PolygonShape polygon;
polygon.Set(vertices, count);
7.5.3 边缘形状
边缘形状就是线段。它可以帮助你设计游戏的曲面静态环境。边缘形状的一个最主要的限制就是它们只会与圆形或者多边形碰撞,但本身不会碰撞,Box2D碰撞算法要求—两个碰撞形状其中的至少一个含有体积。边缘外形没有体积,所以边缘性状和边缘形状是不可碰撞的。
定义边缘形状,需要制定线段的起点和终点。
7.6夹具 我们已经知道形状需要通过夹具才能附属到物体上,Box2D提供了b2Fixture类将shape附属到body上。
7.6.1 创建Fixture及夹具属性
Fixture是通过初始化fixture定义并把定义传递给父类body创建的。
我们在上面的类中添加一个新方法-(void) bodyCreateFixture:(b2Body*)body 来把形状附属到物体上
-(void)bodyCreateFixture:(b2Body*)body
{
// Define another box shape for our dynamicbodies.
b2PolygonShape dynamicBox;
float tileInMeters = TILESIZE / PTM_RATIO;
dynamicBox.SetAsBox(tileInMeters * 0.5f,tileInMeters * 0.5f);
// Define the dynamic body fixture.
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 0.3f;
fixtureDef.friction= 0.5f;
fixtureDef.restitution = 0.6f;
fixtureDef.isSensor= true
body->CreateFixture(&fixtureDef);
}
这创建了fixture,并把它附属到body上,你没必要去存储fixture指针,因为父辈body被破坏时,fixture会自动被破坏。
你可以在父辈body上破坏fixture,也可以用这种方法创建一个易碎的对象。也可以自己破坏(DestroyFixture)
在上面我们不但创建了一个fixture,并且设定了一些基本属性。
Density是设置密度的,fixture的密度可以用来计算父辈body的质量性能。密度可以为零或者为正数。
对所有的fixture,你应该使用相似的密度,这样可以提高堆的稳定性。
Friction是设置摩擦系数的,摩擦的参数经常会设置在0到1之间,0意味着没有参数,1会产生强摩擦。计算两个物体形状之间的摩擦公式是
float friction=sqr(fixture1->GetFriction()*fixture2->GetFriction())
restitution是设置恢复的,想象一下,在桌面上方丢一个小球。恢复的值通常设置到0至1之间,0意味着小球不会弹起,这称为非弹性碰撞;1的意思是小球的速度会得到精确的反射,这称为弹性碰撞。恢复是通过下面的公式计算的:
Float32 restitution =b2Max(fixture1->getRestitution(), fixture2->getRestitution());
isSensor是传感器开关,有时候游戏逻辑需要判断两个形状是否交叉,但却不应该相互有碰撞反应。这时候可以通过传感器来完成,传感器会侦测碰撞而不产生碰撞反应。
你可以将任一形状标记为传感器,传感器可以是静止或动态的。记得,每个物体上可以有多个形状,并且传感器和实体形状是可以混合的。
7.7关节 关节的作用是把物体约束到世界里,或约束到其他物体上。在游戏中典型的例子是木偶,跷跷板和滑轮。关节可以用许多不同的方法结合起来,创造出又去的活动。
有些关节提供了限制(limit),以便你控制运动范围。有些关节还提供了马达(motor),它可以以指定的速度驱动关节,直到你指定了更大的力或扭矩。
关节马达有许多不同的用途,你可以使用关节来控制位置,只要提供一个与目标之距离成正比例的关节速度即可。你还可以模拟关节摩擦;将关节速度置零,并且提供一个小的,但有效的做大力或扭矩;那么马达就会努力保持关节不动,直到负载变得过大。
7.7.1 距离关节
距离关节是最简单的关节之一,它描述了两个物体上的两个点之间的距离应该是常量。当你指定一个距离关节时,两个物体必须已在应有的位置上。随后,你指定两个世界坐标中的锚点。第一个锚点连接到物体worldAnchorOnbody1 ,第二个锚点连接到物体worldAnchorOnbody2。这些点隐含了距离约束的长度。
b2DistanceJointDefjointDisDef;
jointDisDef.Initialize(bodyA,bodyB, worldAnchorOnbody1, worldAnchorOnbody2);
jointDisDef.collideConnected= true;
7.7.2 旋转关节
一个旋转关节会强制两个物体共享一个锚点,即所谓的铰接点。选装关节只有一个自由度,两个物体相对旋转。这称之为关节角。
要指定一个旋转关节,你需要提供两个物体以及一个世界坐标的锚点。初始化函数会假定物体已经在应有位置了。
下面我们在上面的函数添加一个-(void) addSomeJoinedBodies:(CGPoint)pos
-(void)addSomeJoinedBodies:(CGPoint)pos
{
// Create a body definition and set it to bea dynamic body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
// position must be converted to meters
bodyDef.position = [self toMeters:pos];
bodyDef.position = bodyDef.position +b2Vec2(-1, -1);
bodyDef.userData = https://www.it610.com/article/[selfaddRandomSpriteAt:pos];
b2Body* bodyA =world->CreateBody(&bodyDef);
[self bodyCreateFixture:bodyA];
bodyDef.position = [self toMeters:pos];
bodyDef.userData = https://www.it610.com/article/[selfaddRandomSpriteAt:pos];
b2Body* bodyB =world->CreateBody(&bodyDef);
[self bodyCreateFixture:bodyB];
bodyDef.position = [self toMeters:pos];
bodyDef.position = bodyDef.position +b2Vec2(1, 1);
bodyDef.userData = https://www.it610.com/article/[selfaddRandomSpriteAt:pos];
b2Body* bodyC = world->CreateBody(&bodyDef);
[self bodyCreateFixture:bodyC];
b2RevoluteJointDef jointDef;
jointDef.Initialize(bodyA, bodyB,bodyB->GetWorldCenter());
bodyA->GetWorld()->CreateJoint(&jointDef);
jointDef.Initialize(bodyB, bodyC,bodyC->GetWorldCenter());
bodyA->GetWorld()->CreateJoint(&jointDef);
// create an invisible static body to attachto
bodyDef.type = b2_staticBody;
bodyDef.position = [self toMeters:pos];
b2Body* staticBody =world->CreateBody(&bodyDef);
jointDef.Initialize(staticBody, bodyA,bodyA->GetWorldCenter());
bodyA->GetWorld()->CreateJoint(&jointDef);
}
7.7.3 移动关节
移动关节允许两个物体沿指定轴相对移动,它会阻止相对旋转。因此,移动关节只有一个自由度。
移动关节的定义有些类似于旋转关节,只是旋转角度换成了平移,扭矩换成了力。以这样的类似,我们来看一个带有关节限制以及马达摩擦的移动关节定义:
b2PrismaticJointDef jointRef2;
b2Vec2 worldAxis(1.0f,0.0f);
jointRef2.Initialize(bodyA,bodyB, bodyA->GetWorldCenter(),worldAxis);
其它的关节类型包括滑轮关节,齿轮关节,鼠标关节,摩擦关节,绳子关节和焊接关节等。
7.8 接触 接触(contact)是由Box2D创建的用于管理fixture间碰撞的对象。接触不同的种类,它们都会派生字b2Contact,用于管理不同类型形状之间的接触。例如,有管理多边形之间碰撞的类,有管理圆形之间碰撞的类,有管理边缘形状之间碰撞的类。
7.8.1接触监听器
通过实现b2ContactListener你就可以接受接触数据。当一个触点被创建时,当它持有超过一个时间步时,以及当它被摧毁时,这个监听器就会发出报告,请留意两个形状之间可能会有多个触电。接触监听器支持几种事件:开始(begin),结束(end),求解前(pre-solve),求解后(post-solve)。
我们首先创建一个ContactListener类,头文件如下:
#import"Box2D.h"
classContactListener : public b2ContactListener
{
private:
void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
};
我们在ContactListener.mm实现两个方法
#import "ContactListener.h"[Microsoft3]
#import "cocos2d.h"
void ContactListener::BeginContact(b2Contact*contact)
{
b2Body*bodyA = contact->GetFixtureA()->GetBody();
b2Body*bodyB = contact->GetFixtureB()->GetBody();
CCSprite*spriteA = (CCSprite*)bodyA->GetUserData();
CCSprite*spriteB = (CCSprite*)bodyB->GetUserData();
if(spriteA != NULL && spriteB != NULL)
{
spriteA.color= ccMAGENTA;
spriteB.color= ccMAGENTA;
}
}
void ContactListener::EndContact(b2Contact*contact)
{
b2Body*bodyA = contact->GetFixtureA()->GetBody();
b2Body*bodyB = contact->GetFixtureB()->GetBody();
CCSprite*spriteA = (CCSprite*)bodyA->GetUserData();
CCSprite*spriteB = (CCSprite*)bodyB->GetUserData();
if(spriteA != NULL && spriteB != NULL)
{
spriteA.color= ccWHITE;
spriteB.color= ccWHITE;
}
}
事件 |
说明 |
Begin 事件 |
当两个fixture开始有重叠时,事件会被触发。传感器和非传感器都会触发这事件。这事件只能在时间步内(也就是b2World::step函数内部)发生。 |
End 事件 |
当两个fixture不再重叠时,事件会被触发。传感器和非传感器都会触发这事件,当一个body被摧毁时,事件也有可能被触发。所有这些时间也有可能发生在时间步之外。 |
Pre-Solve事件 |
当碰撞检测之后,但是碰撞求解之前,事件会被触发。这样可以给你一个机会,根据当前情况来决定是否使这个接触失效。 |
Post-Solve事件 |
当你可以得到碰撞冲量(collision impulse)的结果时,Pos-Solve事件会发生,如果你不关心冲量,你可能只需要实现pre-solve事件。 |
7.9 ChipmunkChipmunk的原理和Box2D差不多,毕竟我们所处的是同一个物理世界。二则的差别只是在实现的方式,Chipmunk 用的是 C。d但Chipmunk有一个很受欢迎的Objective-C接口,叫做SpaceManager。你可以利用SpaceManger很容易地把cocos2d精灵添加到刚体上,添加调试用的绘图等。
7.9.1 Chipmunk的物理世界
在Chipmunk的世界里,世界叫做cpSpace,它基本等同于Box2D里面的World,我们可以通过下面的方式来创建一个cpSpace,同样的我们在init方法里面添加如下代码
cpInitChipmunk();
space =cpSpaceNew();
space->iterations= 8;
space->gravity= CGPointMake(0, -100);
在使用任何Chipmunk方法之前,第一件必须做的事情是调用cpInitChipmunk方 法。然后,你可以使用 cpSpaceNew来生成space,并且设置space的迭代次数 – 在我们的例子中我将其设置为8。这个迭代次数和我在Box2D例子里的update 方法中用到的值是一样的。Chipmunk只有一种迭代– elasticIterations这项已经过时不用了。如果你熟悉Chipmunk,你需要注意到这一点。如果你的游戏不需要刚体可以叠在一起,你可以用小于8的迭代次数; 否则,你会发现叠在一 起的刚体要经过很长时间才会停止颤动和滑动,最终停下来。
你可能注意到Chipmunk可以使用应用于iPhone SDK中的CGPoint结构。Chipmunk 内部使用的结构叫做cpVect,但是在cocos2d中你可以使用任何一个。我在这里使用了一个CGPoint将重力设为-100 – 这个数值所产生的重力将会和Box2D项目中用到的重力大致相同。
在dealloc里面调用pSpaceFree进行内存释放:cpSpaceFree(space);
7.9.2生成包含屏幕的静态刚体
Chipmunk刚体和Box2D的不同之处你不需要把像素转换成以米为单位。你可以直接使用屏幕的像素尺寸来定义四个角,你也可以在Chipmunk的刚体上使用像素为单位。
首先我们在init方法定义屏幕四个角的变量并生成刚体(cpBody):
CGSizescreenSize = [CCDirector sharedDirector].winSize; ?
CGPointlowerLeftCorner = CGPointMake(0, 0); ?
CGPointlowerRightCorner = CGPointMake(screenSize.width, 0);
?CGPointupperLeftCorner = CGPointMake(0, screenSize.height); ?
CGPointupperRightCorner = CGPointMake(screenSize.width, screenSize.height);
float mass =INFINITY; float inertia = INFINITY;
cpBody*staticBody = cpBodyNew(mass, inertia);
注[Microsoft4] :Chipmunk中的质量(Mass)和惯性(Inertia)是和Box2D中的密(Density)和摩擦力(Friction)相对应的。惯性和摩擦力的区别是:前者决定着刚体开始移动时的阻力,后者决定着当刚体与别的刚体发生碰撞时会丢失的动能。
下面我们将定义刚体的形状(shape),这个形状组成了屏幕边界:
cpSegmentShapeNew方法用于生成4个新线段,用于定义屏幕的4个边。为了方便 起见,shape变量在这里被重复利用,但是shape变量要求在每次调用cpSegmentShapeNew方法以后都要设置弹性值(elasticity)(这和Box2D中的回 复力(restitution)是一样的)和摩擦力(friction)。然后,我们通过cpSpaceAddStaticShape方法将每个shape作为静态刚体添加到space中去。
[Microsoft5]
[Microsoft6]
cpShape* shape; float elasticity = 1.0f;
float friction = 1.0f; float radius = 0.0f;
shape = cpSegmentShapeNew(staticBody, lowerLeftCorner,lowerRightCorner, radius);
shape->e = elasticity; ?shape->u = friction;
cpSpaceAddStaticShape(space, shape);
shape = cpSegmentShapeNew(staticBody, upperLeftCorner,upperRightCorner, radius);
shape->e = elasticity; ?shape->u = friction; ?
cpSpaceAddStaticShape(space, shape);
shape = cpSegmentShapeNew(staticBody, lowerLeftCorner,upperLeftCorner, radius);
shape->e = elasticity; ?
shape->u = friction; ?cpSpaceAddStaticShape(space, shape);
shape = cpSegmentShapeNew(staticBody, lowerRightCorner,upperRightCorner, radius); shape->e = elasticity; ?shape->u = friction;
cpSpaceAddStaticShape(space, shape);
7.9.3添加盒子
我们通过使用cpBodyNew方法来生成代表盒子的动态刚体,这个方法需要两个参 数:质量(mass)和惯性力矩(moment of inertia)。惯性力矩决定着刚体移 动时遇到的阻力,它是通过 cpMomentForBox这个帮助方法(helper method)来计算的。cpMomentForBox以刚体的质量和盒子的尺寸作为参数。
-(void) addNewSpriteAt:(CGPoint)pos
{
float mass = 0.5f;
float moment =cpMomentForBox(mass, TILESIZE, TILESIZE);
cpBody* body =cpBodyNew(mass, moment);
body->p = pos;
cpSpaceAddBody(space,body);
float halfTileSize= TILESIZE * 0.5f;
int numVertices =4;
CGPoint vertices[]=
{
CGPointMake(-halfTileSize,-halfTileSize),
CGPointMake(-halfTileSize,halfTileSize),
CGPointMake(halfTileSize,halfTileSize),
CGPointMake(halfTileSize,-halfTileSize),
};
CGPoint offset =CGPointZero;
float elasticity =0.3f;
float friction =0.7f;
cpShape* shape =cpPolyShapeNew(body, numVertices, vertices, offset);
shape->e =elasticity;
shape->u =friction;
shape->data =https://www.it610.com/article/[self addRandomSpriteAt:pos];
cpSpaceAddShape(space,shape);
}
在我们的例 子里,盒子的尺寸就是瓷砖的尺寸,也就是32x32像素大小。然后我们通过cpSpaceAddBody方法来更新刚体的位置信息(p)和把刚体添加到 space中。请注意:和Box2D不同,你不需要在Chipmunk里把像素转换成米; 你 可以直接使用像素坐标。
接着,我创建了一系列的顶点(Vertex),这些顶点将会作为定义盒子的四个 角。因为盒子的四个角的位置是相对于盒子的中心点来放置的,所以它们离开 中心点的位置都是瓷砖尺寸的一半大小。否则,盒子将会变成两倍于瓷砖的大 小。我们将生成的刚体,顶点数组,顶点数组的顶点数量和一个可选的偏移值 (offset)(在我们的例子里设成了CGPointZero),传给cpPolyShapeNew方法。 得到的结果是盒子形状(shape)的cpShape指针。获取的形状指针拥有和Box2D 例子中的盒子类似的弹性和摩擦力属性。然后,精灵被赋值给这个指针的data 属性,接着我们通过cpSpaceAddShape方法把shape指针添加到space中。
7.9.4更新盒子的精灵
和Box2D一样,你必须每一帧都更新精灵的位置和旋转信息来使它和动态刚体的 位置和旋转同步。这需要在update方法中进行实现:
-(void) update:(ccTime)delta
{
floattimeStep = 0.03f;
cpSpaceStep(space,timeStep);
//call forEachShape C method to update sprite positions
cpSpaceHashEach(space->activeShapes,&forEachShape, nil);
cpSpaceHashEach(space->staticShapes,&forEachShape, nil);
}
和Box2D一样,你必须使用step方法推进物理模拟。在Chipmunk中,我们需要使用cpSpaceStep方法,此方法以space和timeStep作为参数。我们使用的timeStep是固定的数值,因为如果使用delta时间的话会使物理模拟变得不稳定
7.9.5 Chipmunk的碰撞测试
Chipmunk的碰撞测试也是由C写的回调方法来处理的。我们的项目中,添加 cotactBegin和contactEnd这两个静态方法,它们的功能和Box2D中的一样:在碰撞后会把发生碰撞的盒子颜色变成洋红色。
// C callback methods for collisionhandling
static int contactBegin(cpArbiter*arbiter, struct cpSpace* space, void* data)
{
boolprocessCollision = YES;
cpShape*shapeA;
cpShape*shapeB;
cpArbiterGetShapes(arbiter,&shapeA, &shapeB);
CCSprite*spriteA = (CCSprite*)shapeA->data;
CCSprite*spriteB = (CCSprite*)shapeB->data;
if(spriteA != nil && spriteB != nil)
{
spriteA.color= ccMAGENTA;
spriteB.color= ccMAGENTA;
}
returnprocessCollision;
}
static void contactEnd(cpArbiter*arbiter, cpSpace* space, void* data)
{
cpShape*shapeA;
cpShape*shapeB;
cpArbiterGetShapes(arbiter,&shapeA, &shapeB);
CCSprite*spriteA = (CCSprite*)shapeA->data;
CCSprite*spriteB = (CCSprite*)shapeB->data;
if(spriteA != nil && spriteB != nil)
{
spriteA.color= ccWHITE;
spriteB.color= ccWHITE;
}
}
如果测得碰撞并且方法正常运[Microsoft7] 行的话,contactBegin会返回YES。如果返回的是NO 或者0,你可以忽略碰撞。为了得到精灵,首先你必须从cpArbiter得到 shape。CpArbiter就像b2Contact一样,包含着碰撞各方的信息。通过cpArbiterGetShapes方法,把两个shape传给它作为赋值只用,你会得到发生碰撞的两个刚体shapeA和shapeB。然后,你可以通过shapeA和shapeB获取它们各自的CCSprite指针。如果两个精灵指针都有效的话,它们的颜色将会发生变化。和Box2D一样,这些回调方法不会被自动调用。在HelloWorldScene的init方法中,在space生成以后,你必须使用 cpSpaceAddCollisionHandler方法把上述 两个回调方法添加进space里
unsigned int defaultCollisionType =0;
cpSpaceAddCollisionHandler(space,defaultCollisionType, defaultCollisionType,?&contactBegin, NULL, NULL,
&contactEnd, NULL);
7.9.6 Chipmunk中的关节
Chipmunk[Microsoft8] 实现起来比Box2D复杂,如果你会任何大多数设置静态和动态刚体的代码。你可以直接跳到最后关于生成关节的代码。我们在这而只贴出生成关节的代码
cpConstraint* constraint1 =cpPivotJointNew(staticBody, bodyA, staticBody->p);
cpConstraint*constraint2 = cpPivotJointNew(bodyA, bodyB, bodyA->p);
cpConstraint*constraint3 = cpPivotJointNew(bodyB, bodyC, bodyB->p);
cpSpaceAddConstraint(space,constraint1);
cpSpaceAddConstraint(space,constraint2);
cpSpaceAddConstraint(space,constraint3);
[Microsoft1]代码前的行号别丢掉
[Microsoft2]框太宽了
[Microsoft3]插入代码框
[Microsoft4]正常字体大小按示例格式修改
[Microsoft5]空行太大
[Microsoft6]边框大小为1~35 行号
[Microsoft7]缩进呢
[Microsoft8]段起缩进
【第七章 物理引擎】 [Microsoft9]14 号字体