(翻译)介绍Box2D的Cocos2D 2.X教程:弹球
原文地址:http://www.raywenderlich.com/28602/intro-to-box2d-with-cocos2d-2-x-tutorial-bouncing-balls
译文更新:2013-04-27
更新内容:
- 将body统一译为刚体
- 将fixture统一译为夹具
更新日期:2013-01-09
更新内容:完全更新至Cocos2D 2.1-beta4
教程作者:Ray Wenderlich
教程更新:Brian Broom
本教程通过演示一个简单应用程序的创建过程,帮助您在Cocos2D中使用Box2D。该应用程序显示一个小球,旋转iPhone利用加速器能够让小球在屏幕上弹来弹去。
游戏截图如下:
本教程使用的示例程序以iPhoneDev.net上由Kyle编写的一个非常棒的示例为基础,更新到最新版本的Cocos2D并对其工作原理做了详尽的解释。教程中还会对示例项目中一些由Cocos2D的Box2D应用程序模板提供元素的工作原理逐步进行解释。
本教程假设您已经学习过《使用Cocos2D 2.X制作一个简单iPhone游戏教程》,或者具备同等知识。
OK,让我们开始学习Cocos2D 2.X的Box2D吧!
创建空项目
首先在Xcode中新建一个项目,选择cocos2d v2.x\cocos2d iOS with Box2d应用程序模板,并将项目命名为Box2D。如果编译并运行此模板,您将看到一个非常酷的示例展示了Box2D的许多内容。然而,出于本教程的目的,我们准备从零开始创建所有内容,以便我们能够很好地理解其工作原理。
因此,让我们先对模板做一下清理,从而使我们有一个良好的起点。使用如下代码替换HelloWorldLayer.h中的内容:
#import "cocos2d.h" #define PTM_RATIO 32.0 @interface HelloWorldLayer : CCLayer { } + (id) scene; @end
然后使用如下代码替换HelloWorldLayer.mm的内容:
#import "HelloWorldLayer.h" @implementation HelloWorldLayer + (id)scene { CCScene *scene = [CCScene node]; HelloWorldLayer *layer = [HelloWorldLayer node]; [scene addChild:layer]; return scene; } - (id)init { if ((self=[super init])) { } return self; } @end
再次编译并运行,您将看到一个空白的屏幕。OK非常好,现在让我们开始创建自己的Box2D场景吧!
Box2D世界理论
在继续之前,让我们先简单介绍一下在Box2D中是如何工作的。
使用Cocos2D需要做的第一件事情是为Box2D创建一个世界(world)对象。该世界对象是Cocos2D中的主对象,负责管理所有对象和物理仿真。
创建world对象之后,我们需要向世界中添加一些刚体(body)。刚体可以是在游戏中来回移动的对象,如忍者或妖怪,也可以是静止不动的刚体,如平台或墙壁。
要创建一个刚体,您需要做很多事情——创建一个刚体定义、一个刚体对象、一个形状、一个夹具定义和一个夹具对象。下面逐一解释这些让人抓狂的名词都分别意味着什么!
- 首先创建一个刚体定义(body definition)用于指定刚体的初始属性,例如位置或者速度。
- 建立了刚体定义之后,通过指定刚体定义,可以使用世界对象创建一个刚体对象(body object)。
- 然后创建一个希望仿真模拟的几何形状(shape)。
- 然后创建一个夹具定义(fixture definition),将夹具定义的形状设置为您所创建的形状,并设置其他属性,例如密度或者摩擦系数。
- 最后,通过指定夹具定义,使用该刚体对象创建一个夹具对象(fixture object)。
- 请注意,可以将任意多个夹具对象添加至单个刚体对象。这一特性在创建复杂对象时非常有用。
将所有需要的刚体添加至世界之后,您只需要周期性地调用Step函数就可以让Box2D接管工作并开始物理仿真了,因此这会占用一定的处理时间。
但是请注意,Box2D仅仅只更新其内部模型对象的位置,如果想让Cocos2D的精灵同样更新至物理仿真的所在位置,您同样需要周期性地更新精灵的位置。
Ok,现在我们已经对Box2D的工作机制有了基本的认识,接下来让我们看看在代码中是如何实现的!
Box2D世界演练
Ok,首先下载我制作的一张小球图片及其Retina版本,我们准备把这个小球加进场景。下载之后,将它们拖拽至项目中的Resources文件夹,并确保勾选了Copy items into destination group’s folder (if needed)。
接下来,看一下我们此前在HelloWorldLayer.h中添加的这一行代码:
#define PTM_RATIO 32.0
这行代码定义了一个像素与“米”之间的比例。当您在Cocos2D中指定刚体放置位置时,需要给定一个单位。虽然您可能会考虑使用像素,但这样位置是不正确的。根据Box2D参考手册,Box2D在处理小至0.1单位大至10单位的长度做了优化。按照尽可能长的长度推算,大家通常倾向将其视为“米”,因此0.1差不多是一个杯子大小,而10差不多是一个箱子的大小。
因此,我们不能直接传递像素,因为即便是一个很小的对象也差不多会有60×60像素,这已经超出了Box2D优化时限定的最大值。因此,我们需要有一个方法把像素转换成“米”,于是便就有了上面的比例定义。如果我们有一个64像素的对象,除以PTM_RATIO,可以得到2“米”,这是一个Box2D能够处理进行物理仿真的长度。
好了,现在可以来点有意思的东西了。在HelloWorldLayer.h的顶部添加如下代码:
#import "Box2D.h"
在HelloWorldLayer类的接口定义中添加如下成员变量:
b2World *_world; b2Body *_body; CCSprite *_ball;
然后,将如下代码添加至HelloWorldLayer.mm的init方法:
CGSize winSize = [CCDirector sharedDirector].winSize; // Create sprite and add it to the layer _ball = [CCSprite spriteWithFile:@"ball.png" rect:CGRectMake(0, 0, 52, 52)]; _ball.position = ccp(100, 300); [self addChild:_ball]; // Create a world b2Vec2 gravity = b2Vec2(0.0f, -8.0f); _world = new b2World(gravity); // Create ball body and shape b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(100/PTM_RATIO, 300/PTM_RATIO); ballBodyDef.userData = _ball; _body = _world->CreateBody(&ballBodyDef); b2CircleShape circle; circle.m_radius = 26.0/PTM_RATIO; b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.2f; ballShapeDef.restitution = 0.8f; _body->CreateFixture(&ballShapeDef); [self schedule:@selector(tick:)];
除了少数几行通过Cocos2D教程已经熟悉的代码之外,这里的大部分代码都很陌生。让我们一点一点地来解释。我会一段一段地复述以上代码,这样应该会解释的更清楚一些。
CGSize winSize = [CCDirector sharedDirector].winSize; // Create sprite and add it to the layer _ball = [CCSprite spriteWithFile:@"ball.png" rect:CGRectMake(0, 0, 52, 52)]; _ball.position = ccp(100, 300); [self addChild:_ball];
首先,使用Cocos2D的常规方式将精灵添加至场景。如果您已经学习过之前的Cocos2D教程,这里应该没有什么问题。
// Create a world b2Vec2 gravity = b2Vec2(0.0f, -8.0f); _world = new b2World(gravity);
接下来,创建世界对象。在创建此对象时,需要指定一个初始重力向量。我们将其设置为延Y轴方向-8的向量,这样刚体将出现向屏幕底部下落的现象。
// Create ball body and shape b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(100/PTM_RATIO, 300/PTM_RATIO); ballBodyDef.userData = _ball; _body = _world->CreateBody(&ballBodyDef); b2CircleShape circle; circle.m_radius = 26.0/PTM_RATIO; b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.2f; ballShapeDef.restitution = 0.8f; _body->CreateFixture(&ballShapeDef);
接下来,我们创建小球刚体。
- 将其类型指定为动态刚体(dynamic body)。刚体的默认类型是一个静态刚体(static body),表示刚体不能移动也不参与仿真。很显然,我们希望小球参与仿真!
- 把用户数据(user data)参数设置为小球的CCSprite。可以将刚体上的用户数据参数设置为任何您所想要的对象,但通常将其设置为精灵会很方便,如此一来您便可以在其他地方访问到它,例如两个刚体碰撞时的处理。
- 我们必须定义一个圆形的形状(shape)。请记住,Box2D不会去查看精灵的图像,我们必须要告诉它精灵的形状,这样它才能够正确地模拟精灵的运动。
- 最后,设置了一些夹具定义的参数,这些参数的具体含义稍后会介绍。
[self schedule:@selector(tick:)];
方法中最后做的事情是调度一个名为tick的方法被尽可能频繁地调用。请注意,这并不是最理想的处理方式,比较好的方式是让tick方法按照一个固定的频率被调用,例如60次每秒。然而为保证教程内容的简单,我们先这么处理。
下面,让我们来编写tick方法的代码!在init方法之后添加如下代码:
- (void)tick:(ccTime) dt { _world->Step(dt, 10, 10); for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) { if (b->GetUserData() != NULL) { CCSprite *ballData = (CCSprite *)b->GetUserData(); ballData.position = ccp(b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO); ballData.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()); } } }
方法中我们做的第一件事情是,在世界对象上调用Step函数,使其能够执行物理仿真。其中两个参数分别是速度迭代和位置迭代,您通常应该将它们设置为8~10之间的一个值。
接下来的事情是让精灵与物理仿真匹配。因此我们遍历世界中的所有刚体,查找设置有用户数据的刚体。找到之后,将用户数据转换成一个精灵(此前是将精灵设置成用户数据的!),然后更新精灵的位置和角度与物理仿真匹配。
最后一件事情——清理内存!在文件末尾添加如下代码:
- (void)dealloc { delete _world; _body = NULL; _world = NULL; [_ball release]; _ball = nil; [super dealloc]; }
编译并运行应用程序,应该能够看到小球直接从屏幕下方掉出去了。哎呀,我们忘记定义一个地面再把小球弹起来了。
落地反弹
要表示地面,我们在iPhone的屏幕底部创建一个不可见的边界。按照以下步骤操作即可。
- 创建一个刚体定义(body definition)并指定该刚体应该位于屏幕的左下角。由于刚体类型默认是我们需要的静态刚体,因此不需要设置。
- 然后使用世界对象创建刚体对象(body object)。
- 然后为屏幕的底边创建一个边界形状(edge shape)。此“形状”实际上就是一条线。请注意,此处必须使用前面讨论过的转换比例将像素转换为“米”。
- 创建一个夹具定义(fixture definition)指定边界形状。
- 然后使用刚体对象为形状创建一个夹具对象(fixture object)。
- 另外请注意,一个刚体对象可以包含多个夹具对象!
将如下代码添加到init方法中创建世界对象和定义小球的代码之间。
// Create edges around the entire screen b2BodyDef groundBodyDef; groundBodyDef.position.Set(0,0); b2Body *groundBody = _world->CreateBody(&groundBodyDef); b2EdgeShape groundEdge; b2FixtureDef boxShapeDef; boxShapeDef.shape = &groundEdge; //wall definitions groundEdge.Set(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO, 0)); groundBody->CreateFixture(&boxShapeDef);
再次编译并运行,小球落在地面之后会反弹回空中,往复几次之后最终静止停在地面之上。
如何水平运行?
现在我们已经有了基础的知识,接下来让我们做一些更有意思的事情——让一只无形的脚每隔几秒踢一下球。在HelloWorldLayer.h中定义一个新方法:
- (void)kick;
然后,将该方法的实现添加到HelloWorldLayer.mm:
- (void)kick { b2Vec2 force = b2Vec2(30, 30); _body->ApplyLinearImpulse(force,_body->GetPosition()); }
ApplyLinearImpulse方法可以在小球上作用一个力,使小球移动。移动距离的远近取决于小球的质量,之前我们在定义小球时曾设置过它的密度(density)属性。可以尝试不同的密度以及力的值找出您认为不错的值。坐标系与Cocos2D相同,X方向向右正向延展,Y方向向上正向延展。
在init方法中增加如下代码行,每隔5秒运行一次kick方法。
[self schedule:@selector(kick) interval:5.0];
如果现在生成并运行项目,小球被“踢”之后会飞出屏幕。让我们继续并定义其他的墙壁。在init方法中找到wall definitions注释,并在其后添加如下代码行。注意:每面墙壁需要两行代码,一行设置坐标,另一行将边界添加为ground对象的夹具刚体。
groundEdge.Set(b2Vec2(0,0), b2Vec2(0,winSize.height/PTM_RATIO)); groundBody->CreateFixture(&boxShapeDef); groundEdge.Set(b2Vec2(0, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO)); groundBody->CreateFixture(&boxShapeDef); groundEdge.Set(b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO, 0)); groundBody->CreateFixture(&boxShapeDef);
现在生成并运行项目,观赏小球在屏幕上弹来弹去吧。
集成触摸
由于HelloWorldLayer仍然是一个Cocos2D图层,我们可以使用所有的工具,包括曾经学习过的触摸事件。为了演示如何与Box2D之间交互,让我们对程序进行一些修改,触摸屏幕时向左侧方向踢球。
要启用触摸事件,首先在HelloWorldLayer.mm的init方法中添加如下一行代码:
[self setTouchEnabled:YES];
然后添加如下方法以处理触摸事件:
- (void)ccTouchesBegan:(UITouch *)touch withEvent:(UIEvent *)event { b2Vec2 force = b2Vec2(-30, 30); _body->ApplyLinearImpulse(force, _body->GetPosition()); }
与之前类似,我们使用ApplyLinearImpulse方法在小球上作用一个力。给定force的x一个负值将会向左侧踢球。
关于仿真的注释
作为承诺,接下来让我们介绍一下前文为小球设置的:density、friction和restitution分别有什么用处。
- 密度(Density)是单位体积的质量。因此密度越大质量就越大,移动就越困难。
- 摩擦系数(Friction)是一个系数,用于描述对象表面之间相对滑动的困难程度。其范围介于0和1之间,0表示没有摩擦,而1则表示摩擦非常大。
- 恢复系数(Restitution)也是一个系数,用于描述一个对象到底有多“弹”。其范围通常介于0和1之间,0表示对象不会反弹,而1则表示是完全弹性,也就是说对象会以相同的速度反弹。
可以随意修改这些数值,看看修改之后会有什么不同的影响。试试看,能不能让您的小球弹力十足!
收尾
如果我们倾斜屏幕就可以让小球在屏幕上弹来弹去,会非常酷!有一件事情可以有助于我们接下来的试验,那就是现在所有的边界都能正常工作! 剩下的事情就简单了。在init方法中添加如下代码:
[self setAccelerometerEnabled:YES];
将如下方法添加在文件中的某一位置:
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { // Landscape left values b2Vec2 gravity(acceleration.y * 30, -acceleration.x * 30); _world->SetGravity(gravity); }
最后,单击项目导航侧边栏顶部的项目。然后选中TARGETS下的Box2D,并选择Summary选项卡。在Supported Interface Orientations部分,单击Landscape Right按钮取消选中该按钮。此时Landscape Left按钮应该是被唯一选中的按钮。这样做是因为我们不希望在旋转手机时iOS改变应用程序的方向。如下图所示:
我们在这里做的是将用于仿真的重力向量设置为加速器向量的倍数。在设备上编译并运行应用程序,现在倾斜手机应该能够让小球在屏幕上弹来弹去了!
注释:只有在物理设备上运行程序时,才能够获得加速器数据,这需要一个付费的开发者账号并安装了开发者证书才可以。详细信息请参见developer.apple.com的iOS Provisioning Portal。
下一步做些什么?
单击下载本教程的示例代码。
如果您希望学习有关Box2D更多的内容,请看下一篇教程How To Create A Breakout Game with Box2D and Cocos2D!
著作权声明:本文由http://www.cnblogs.com/liufan9翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!