(翻译)介绍Box2D的Cocos2D 2.X教程:弹球

原文地址:http://www.raywenderlich.com/28602/intro-to-box2d-with-cocos2d-2-x-tutorial-bouncing-balls

译文更新:2013-04-27

更新内容:

  1. body统一译为刚体
  2. 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翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!

posted @ 2013-04-11 14:41  趣味苹果开发  阅读(3918)  评论(0编辑  收藏  举报