怎样使用 Box2D 和 Cocos2D 制作类似Fruit Ninja的游戏 Part 1

2012 年 9 月 3 日
Iven

翻译:大侠,李延波,sharyu,杰西,蓝羽   校对:蓝羽

这篇教程是由ios教程组成员Allen Tan提供,他是一位ios开发者和white widget创始者.

在这篇教程里,你将会学习到使用强大的cocos2dbox2d库和一些预制工具,来制作一个切东西的iPhone游戏,类似于Halfbrick Studios公司的水果忍者。

在大多数切东西的游戏里,当你画一条切线穿过一个精灵,不管你切到什么地方,游戏一般都是转换精灵图片为两个预先准备好的被从中间切开后的图片。

但是在这篇教程里我们将会见识到一个更酷的技术,我们的水果能够多次被切,并且精确的基于切割线,动态被分开。

正如你可能想到的,这是非常先进的技术,所以这篇教程是为那些高级的Cocos2D和Box2D开发者准备的。假如你Cocos2D或者Box2D新手,你应该在开始这篇教程开始前看看 intro to Cocos2D 和 intro to Box2D 教程。


这个系列的教程被分成了三部分:

1.在这个系列的第一部分,你将会致力于这个游戏的基础部分,学习怎样创建带纹理的多边形。

2.第二部分将会给你展示怎么样切开这些带纹理的多边形。

3.第三部分将会给你展示怎么样通过增加玩法和一些效果,把前面准备好的做成一个完整的游戏。

特别感谢Rick Smorawski 为这篇教程提供基础,他负责移植flash-based slicing demo 到Cocos2D,并且也移植了CCBlade和PRKit到Cocos2D 2.0。仔细看看你将制作的游戏的视频,然后准备开始学习一些非常酷的新技术吧。

游戏Demo

这里是一个demo的视频,将会展示给你在这个系列的教程里面将会做些什么:

正如我提到的,你将会看到切水果的效果确实是动态的。这个水果被切的动态效果完全依据你所切的位置,既然你还可以多次的切这个物体,可以把它切碎。

你可以看见你将会实现一个酷的切割拖曳效果,一些粒子系统,游戏玩法逻辑和增添乐趣的声音。

这里涉及很多东西,所以让我们开始吧!

开始:工程设置

你将会在这个项目里面使用Cocos2D 2.X,所以你没有这个版本,你可以下载一个。提醒一下,你也可以使用Cocos2D 1.X来代替2.X版本的,如果你使用的1.X,你可以跳过转化PRKit和CCBlade到Cocos2D 2.X这个部分,但是要注意对这些类作一些其他小的改变。

下载完成后,双击tar来进行解压,然后运行终端,在终端里面输入下面的命令来安装templates:

cd ~/Downloads/cocos2d-iphone-2.0-beta ./install-templates.sh -f –u

Project Start


运行xcode,用iOS\cocos2d v2.x\cocos2d iOS with Box2d 模板创建一个新项目,命名为CutCutCut你的新项目看起来就像下面的一样: 为了有个好的开始,第一件你需要做的事就是清理一下这个模板。 打开HelloworldLayer.h,移除下面这行代码:

CCTexture2D *spriteTexture_;// weak ref

打开HelloWorldLayer.mm文件,修改如下:

// Remove this line from the top

#import "PhysicsSprite.h"

// Replace the init method with this

-(id) init

{

if( (self=[super init])) {

// enable events

self.isTouchEnabled = YES;

self.isAccelerometerEnabled = YES;

CGSize s = [CCDirector sharedDirector].winSize;

// init physics

[self initPhysics];

[self scheduleUpdate];

}

return self;

}

// Remove these two methods

-(void) createMenu {

//all content

}

-(void) addNewSpriteAtPosition:(CGPoint)p methods {

//all content

}

// Remove this line from ccTouchesEnded

[self addNewSpriteAtPosition: location];

这时候,你就已经移除了HelloworldLayer所有对PhysicsSprite的引用,但是还不能从这个项目里面移除PhysicsSprite,之后你需要在别的地方复制PhysicsSprite.mm包含的一个方法,所以现在先放一放。点击Command+R来编译运行你的项目,你将会看到一个有绿色边界包含空白的屏幕。

Clean Slate

这个经过修改的模板代码,已经设置了Box2D的debug drawing(box2d调试模式),会在屏幕上画出box2d刚体的边界。在屏幕四周看到了很细的绿色的线条了吗?这些线条就相当于是这个场景的墙,是这个模板里面默认的initPhysics方法创建的。

看看剩下的模板代码,确保你理解了目前为止事情的进展—–初始化box2d世界,设置这个场景范围(绿色的边界),设置debug drawing等等。这是项目带有Box2D一个非常好的“几乎空白”的起点,可以从这里开始我们的工程。

资源套件

接下来把这个工程的资源下载下来并且解压文件。

现在不要把全部的资源都加到这个工程里面,一些文件是可选的。把这个文件夹放在你容易找到的地方,因为在你学习这个教程时我随时都可能让你增加一些文件到这个工程里面.

下面就是这个文件夹里你可以找到的文件:

1. 在Images(图片)文件夹里有一张背景图片和一些Vicki制作的水果的艺术图片,还有一些其它各种各样的图片。

2. 在Sounds(声音)文件夹里面有使用gomix.it 制作的混合背景音乐。

3. 在Sounds(声音)文件夹里面有使用bfxr制作的音效,还有些是从 freesound下载的。

4. 在Particles(粒子效果)文件夹里面有通过Particle Designer创建的粒子效果的文件。

5. 在Misc 文件夹里面有通过PhysicsEditor 制作的一个plist文件,这个文件主要是包含了这个水果和炸弹类所需要的顶点信息。

6. 在Classes文件夹里面有水果和炸弹的类文件

7. 在Classes文件夹里面有这个教程需要的 PRKitCCBlade 类

8. 在Misc文件夹里面有一个这些资源的来源列表.

用PRKit画带纹理的多边形

我们的目标是把这个精灵切成很多片。无论这个图像是什么形状,一个典型的CCSprite包含了一个纹理和一个边界盒。既然创建能够被切开的精灵非常重要的一个步骤,是要知晓图像内实际的形状,所以上面那种方式是不适合这个游戏的。

你需要创建多边形纹理:

· 创建一个多边形和一个图片的对应关系(纹理映射)

· 只显示图像在多边形边框边界内的部分(纹理填充)

Cocos2dBox2D都没有内置类来处理你想要的特性.所以你需要一些三角测量知识加上定制的OpenGL绘制代码.

听起来很困难吧?

幸运的是,所有这些复杂的计算和需要实现的绘制代码已经被Precognitive Research的人们实现了.他们创建一个名叫PRKitcocos2d的扩展库,这个库就是处理纹理映射和填充的.

为了开始做带纹理的多边形,下载PRKit,解压,把PRKit文件夹拖到你的项目中,确定“Copy items into destination group’s folder”和“Create groups for any added folders”是勾选上的.

注意PRKit是一直被Precognitive Research维护的,所以有些时候它可能会被更新。为了避免混淆,资源套件包含了制作教程时使用的PRKit版本.

你的工程现在应该包含下面的这些文件:

PRKit Yey!

编译运行,你将会得到一些错误:

PRKit Needs to be Converted

出现这些错误的原因是因为PRKit是为Cocos2D 1.X制作的,使用的是OpenGL ES 1.1,但是我们现在使用的是Cocos2D 2.X,使用的是OpenGL ES 2.0,这两者间是有很大不同的.

为了修复这些问题,打开PRFilledPolygon.m,并且作以下的改变:

// Add inside the initWithPoints: andTexture: usingTriangulator: method

self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];

// Replace the calculateTextureCoordinates method with this

-(void) calculateTextureCoordinates {

for (int j = 0; j <; areaTrianglePointCount; j++) {

textureCoordinates[j] = ccpMult(areaTrianglePoints[j],1.0f/texture.pixelsWide*CC_CONTENT_SCALE_FACTOR());

textureCoordinates[j].y = 1 - textureCoordinates[j].y;

}

}

// Replace the draw method with this

-(void) draw{

CC_NODE_DRAW_SETUP();

ccGLBindTexture2D( self.texture.name );

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

ccGLBlendFunc( blendFunc.src, blendFunc.dst);

ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords );

glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, areaTrianglePoints);

glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, textureCoordinates);

glDrawArrays(GL_TRIANGLES, 0, areaTrianglePointCount);

}

让我们一点点查看这些变更。

首先,在Cocos2D中,每个CCNode都附带有一个OpenGL ES 2.0 渲染程序。为了绘制PRFilledPolygon,你需要请求内置的“位置/纹理”渲染程序来完成,它在init方法中指定。

接下来,你需要为多边形的每个点设置每个点正确的纹理坐标,所以你不得不对calculateTextureCoordinates作2点改变。

· Scale: 既然这个类有自己纹理坐标的计算方法,只是不会自动处理高清显示。为了解决这点,你必须用texture.pixelsWide(纹理的像素宽度)乘以CC_CONTENT_SCALE_FACTOR-一个简便的乘数因子由Cocos2D提供,用于普通和高清的转换。

· Flip Y: 因为一些原因,PRFIlledPolygon颠倒地绘制纹理,所以你在这里简单地翻转y

最后,绘制代码被更新到OpenGL ES 2.0,以此类推,精灵绘制是怎样从Cocos2D 1.X变到Cocos2D 2.X的:

  • 通过调用CC_NODE_DRAW_SETUP()开始准备绘制节点.
  • glDisableClientState() 和 glEnableClientState()的调用是过时的,可以被抛弃的
  • glVertexPointer()和glTexCoordPointer()命令都被glVertexAttribPointer()替代,glVertexAttribPointer()现在接受顶点位置或者纹理坐标作为它的第一个参数
  • glTexEnvf()的配置,glTexEnvf()负责在多边形尺寸大于纹理时重复精灵,被glTexParameteri()调用所代替。

如果对这个感到困惑,你可能需要看看OpenGL ES 2.0 for iPhone 和 Custom Cocos2D 2.X Shaders 来获得更多背景知识。但是你不需要担心太多,因为我们现在正在做的事只是移植这个类,使它可以在Cocos2D 2.X下工作。

编译运行,所有的PRKit错误都应该消失了。

是时候使PRKit工作了, 你需要继承PRKitPRFilledPolygon类,获得一个PolygonSprite基类,用于绘制我们的水果。

PolygonSprite是在PRFilledPolygon基础上,附加一个Box2D物体到精灵上。在游戏的实现中,这个类也包含了一些用于水果的特定的变量和方法。

让我们开始做吧,按下Commad+N,用iOS\cocos2d v2.x\CCNode类模版创建一个新的文件。使这个新文件成为PRFilledPolygon子类,并命名为PolygonSprite.m

// Add to top of file

//加到文件头

#import "Box2D.h"

#import "PRFilledPolygon.h"

#define PTM_RATIO 32

// Add inside @interface

//加到@interface里面

b2Body *_body;

BOOL _original;

b2Vec2 _centroid;

// Add after the @interface

//加到@interface后

@property(nonatomic,assign)b2Body *body;

@property(nonatomic,readwrite)BOOL original;

@property(nonatomic,readwrite)b2Vec2 centroid;

// Add before the @end

//加到@end前

-(id)initWithTexture:(CCTexture2D*)texture body:(b2Body*)body original:(BOOL)original;

-(id)initWithFile:(NSString*)filename body:(b2Body*)body original:(BOOL)original;

+(id)spriteWithFile:(NSString*)filename body:(b2Body*)body original:(BOOL)original;

+(id)spriteWithTexture:(CCTexture2D*)texture body:(b2Body*)body original:(BOOL)original;

-(id)initWithWorld:(b2World*)world;

+(id)spriteWithWorld:(b2World*)world;

-(b2Body*)createBodyForWorld:(b2World*)world position:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution;

-(void)activateCollisions;

-(void)deactivateCollisions;

上面的代码声明了创建一个PolygonSprite所需要的变量和方法,它们是:

· body: 这是附着在我们精灵上的Box2D物体,用于物理模拟。

· original: 完整的和切掉的精灵将会使用相同的PolygonSprite类,照这样,如何区分将会非常重要。 如果这个数值是YES,意味着这个精灵没有切,或者说它是你最初创建的那个物体。否则,它仅是整体的一部分。

· centroid: 图片里多边形的中心并不总和图片中心相同,所以存储这个值是有用的。

· properties:所有变量用properties成为全局变量,使其它类可以自由访问。

· init/spriteWith*: 我们主要的初始化方法,遵循Cocos2D的命名约定。

· other method: 这些方法创建和处理连接的Box2D物体和它的属性。

· PTM_RATIO: 像素到距离的比率。Box2D需要这个转换值,因为它处理的是距离而不是像素。

快速切换到PolygonSprite.m并重命名为PolygonSprite.mm. 所有混合Objective-C (Cocos2D)和C++ (Box2D)的代码需要有一个“.mm”扩展名,来告知编译器使用混合语法。

接下来,对PolygonSprite.mm作如下改变

// Add inside the @implementation
@synthesize body = _body;
@synthesize original = _original;
@synthesize centroid = _centroid;

+(id)spriteWithFile:(NSString *)filename body:(b2Body *)body  original:(BOOL)original
{
    return [[[self alloc]initWithFile:filename body:body original:original] autorelease];
}

+(id)spriteWithTexture:(CCTexture2D *)texture body:(b2Body *)body  original:(BOOL)original
{
    return [[[self alloc]initWithTexture:texture body:body original:original] autorelease];
}

+(id)spriteWithWorld:(b2World *)world
{
    return [[[self alloc]initWithWorld:world] autorelease];
}

-(id)initWithFile:(NSString*)filename body:(b2Body*)body  original:(BOOL)original
{
    NSAssert(filename != nil, @"Invalid filename for sprite");
    CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage: filename];
    return [self initWithTexture:texture body:body original:original];
}

-(id)initWithTexture:(CCTexture2D*)texture body:(b2Body*)body original:(BOOL)original
{
    // gather all the vertices from our Box2D shape
    b2Fixture *originalFixture = body->GetFixtureList();
    b2PolygonShape *shape = (b2PolygonShape*)originalFixture->GetShape();
    int vertexCount = shape->GetVertexCount();
    NSMutableArray *points = [NSMutableArray arrayWithCapacity:vertexCount];
    for(int i = 0; i <; vertexCount; i++) {         CGPoint p = ccp(shape->GetVertex(i).x * PTM_RATIO, shape->GetVertex(i).y * PTM_RATIO);
        [points addObject:[NSValue valueWithCGPoint:p]];
    }

    if ((self = [super initWithPoints:points andTexture:texture]))
    {
        _body = body;
        _body->SetUserData(self);
        _original = original;
        // gets the center of the polygon
        _centroid = self.body->GetLocalCenter();
        // assign an anchor point based on the center
        self.anchorPoint = ccp(_centroid.x * PTM_RATIO / texture.contentSize.width,
                               _centroid.y * PTM_RATIO / texture.contentSize.height);
        // more init stuff here later when you expand PolygonSprite
    }
    return self;
}

-(id)initWithWorld:(b2World *)world
{
    //nothing to do here
    return nil;
}

Cocos2d类似,所有以spriteWith开头的方法不过是以initWith开头的方法的autoreleae版本,尽管initWithWorld方法在PolygonSprite类中还没有实际用途,但将来会被它的子类所使用。大部分的变更可以在initWithFileinitWithTexture方法中找到。为了展示调用流程,创建一个水果对象的过程将会按照如下方式调用:

Init Sequence

· initWithWorld: 这个方法适用于PolygonSprite的子类,在目前不需要做什么,只要返回nil就可以了,将来再处理它。

· initWithFile: 这个方法从我们的文件中加载纹理,然后把相关的所有参数传给initWithTexture方法。

· initWithTexture: 我们的初始化主函数。PRFilledPolygon类需要一个纹理内容和它需要填充的多边形的所有顶点。既然之前的步骤已经处理完成了纹理部分,这一步将通过对应精灵的Box2d物体(body)获得这些顶点信息。在将它们传给PRFilledPolygon之后,还需要初始化之前声明的哪些变量。

· initWithPoints: 这个方法的所有内容都包含在PRKit里面,好消息是在你更新完这些代码之后,就不再需要和PRKit打交道了。仍旧在PolygonSprite.mm中,添加如下方法:

-(void)setPosition:(CGPoint)position

{

[super setPosition:position];

_body->SetTransform(b2Vec2(position.x/PTM_RATIO,position.y/PTM_RATIO), _body->GetAngle());

}

-(b2Body*)createBodyForWorld:(b2World *)world position:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution

{

b2BodyDef bodyDef;

bodyDef.type = b2_dynamicBody;

bodyDef.position = position;

bodyDef.angle = rotation;

b2Body *body = world->CreateBody(&bodyDef);

b2FixtureDef fixtureDef;

fixtureDef.density = density;

fixtureDef.friction = friction;

fixtureDef.restitution = restitution;

fixtureDef.filter.categoryBits = 0;

fixtureDef.filter.maskBits = 0;

b2PolygonShape shape;

shape.Set(vertices, count);

fixtureDef.shape = &shape;

body->CreateFixture(&fixtureDef);

return body;

}

-(void)activateCollisions

{

b2Fixture *fixture = _body->GetFixtureList();

b2Filter filter = fixture->GetFilterData();

filter.categoryBits = 0x0001;

filter.maskBits = 0x0001;

fixture->SetFilterData(filter);

}

-(void)deactivateCollisions

{

b2Fixture *fixture = _body->GetFixtureList();

b2Filter filter = fixture->GetFilterData();

filter.categoryBits = 0;

filter.maskBits = 0;

fixture->SetFilterData(filter);

}

在上面的代码中,首先重载了CCNodesetPosition方法,这样一来,当你更新精灵的位置的时候,与它关联的Box2d物体(body)的位置也同步更新了。

我们创建了一个便利的方法用于创建并定义Box2D物体(body)。为了创建一个物体(body),你需要定义一个物体(body)定义,创建一个物体(body)、一个形状(Shape),以及一个夹具(fixture)定义。这些参数没有被硬编码为固定值,主要是考虑到这个方法将在PolygonSprite的子类中使用。

需要注意的是categoryBitsmaskBits这两个变量,这两个变量用来过滤对象之间碰撞事件,实现机制是通过一个对象的类别位(category bit)和另一个对象的掩码位(mask bit)相匹配,反之亦然。你需要首先将它们设置为0,因为在这些对象在初始创建的时候,你肯定不希望任何碰撞发生。

最后,你需要定义两个方法,通过变更categroryBitsmaskBits的取值,实现PolygonSprite对象碰撞功能的激活与反激活。

另外还有一个方法被添加到了PolygonSprite.mm之中:

-(CGAffineTransform) nodeToParentTransform

{

b2Vec2 pos  = _body->GetPosition();

float x = pos.x * PTM_RATIO;

float y = pos.y * PTM_RATIO;

if ( !isRelativeAnchorPoint_ ) {

x += anchorPointInPoints_.x;

y += anchorPointInPoints_.y;

}

// Make matrix

float radians = _body->GetAngle();

float c = cosf(radians);

float s = sinf(radians);

if( ! CGPointEqualToPoint(anchorPointInPoints_, CGPointZero) ){

x += c*-anchorPointInPoints_.x+ -s*-anchorPointInPoints_.y;

y += s*-anchorPointInPoints_.x+ c*-anchorPointInPoints_.y;

}

// Rot, Translate Matrix

transform_ = CGAffineTransformMake( c,  s,

-s,c,

x,y );

return transform_;

}

还记得之前我曾经提到过你还需要PhysicsSprite中的一些东西吗?OK,就是这个。上述代码的作用就是确保在Box2d形状(Shape)在移动的时候,和对应的精灵处在同一个位置上。这是Cocos2D为我们提供的,可以使得效果更棒。

拷贝完上述代码之后,现在就可以从项目中删除PhysicsSprite.hPhysicsSprite.mm,因为我们已经完全不需要他们了。

编译运行,应该没有什么差错了。现在你已经完成了PlygonSprite类。

设计水果

在开始创建我们的水果类之前,你必须搞清楚图片和形状(Shape)所必须遵从的规则。既然需要将我们的纹理映射到单独的Box2D多边形上,你必须遵从Box2D对多边形的一些限制。你需要将下面两点记在心上:

  • 多边形必须是凸多边形,意味着不存在大于180度的内角。
  • 多边形不能超过8个顶点。

你如果允许每个物体(body)包含多个形状(Shape)的话,实际上是可以突破上述限制的。使用三角法将凹多边形分解成多个角度(即:将大于180度的角分解成若干个小于180的角),这样就可以使得Box2D处理凹多边形了,但这些内容超出了本教程要讨论的内容。

为了让事情简单一些,在本教程中,你将遵从1个物体(body)对应1个形状(Shape)的规则。

注意:本教程后面将会讨论的PhysicsEditor工具,实际上内置了自动将你绘制的多边形分解成凸多边形集合的代码。然而,如同我之前描述的一样,在这里我们尽量让事情简单一些,保证使用凸多边形,使得每个物体(body)只需要一个形状(Shape)。

让我们看一下这两个水果:

Concave vs Convex

使用香蕉不是一个好主意,因为它天生就是凹的。相反,西瓜就非常合适,因为你可以定义一个凸多边形,能够与它的形状(Shape)吻合的非常好。

如果你按照Box2D的规则为这两个水果定义多边形形状(Shape),最终将或多或少地做成如下的样子:

Box2D Shape Outline

西瓜的形状(Shape)吻合的非常好,而香蕉的多边形会有一个很大的空白区域,在此之上,相应的图像向内弯曲。Box2D将把这个区域作为我们对象的一部分,当它与其他对象碰撞或者被切的时候,会使得香蕉显得非常不自然。

这并不意味着你不能使用香蕉,而是不推荐使用香蕉。事实上,本教程中你要创建的游戏就会用到它。

创建第一个水果

现在是时候创建第一个水果了:西瓜(至少是一片西瓜)。

回顾我们初始化PolygonSprite的流程,你知道initWithTexture需要一个Box2D刚体,但是之前的一步initWithFile却没有提供这个刚体。

这个问题的原因在于:你需要单独的创建和定义每个水果的刚体,这会是在最初的步骤initWithWorld里进行的,在这个方法里,要创建刚体,并对于每个水果赋予独特的属性。

为了创建Box2D刚体,你必须先知道需要创建的多边形形状的顶点。有一些不同的方法来做这件事情,但是在这个教程中,你会用到一个漂亮的工具,叫做PhysicsEditor。这个工具有很多功能,但你只用它来指导我们获取多边形的顶点坐标。如果你没有这个软件,下载PhysicsEditor,安装并启动。你将会得到一个空白的项目,包括3个面板/栏。

Physics Editor!

使用PhysicsEditor非常直接了当。在左侧,放置所有你需要用到的图片。在中间栏里,能可视化地为图片定义多边形。右边栏中定义刚体的各种属性。

从资源包中的Images文件夹中找到watermelon.png,将其拖拽到左侧面板中。现在你会看到一个西瓜出现在中间的面板上。在面板的底部,放大视图尺寸到一个合适的程度,然后点击面板上方的五角形按钮(Pentagon Button),用它创建一个3个边的多边形。

在创建的多边形上点击鼠标右键,选择“添加顶点”(Add Vertex),直到有了5-8个顶点为止。在西瓜的边缘移动这些顶点,同时,保证以下两点:

  • 你创建的多边形必须是凸多边形。
  • 西瓜的所有像素点要包含在多边形中。

注意:另外一个绘制形状的捷径是使用PhysicsEditor中的魔棒(magic wand)工具。将容差设置为(5-8),你将会得到5-8个顶点,然后再调整这些点。

从资源包的Image文件夹中添加所有其它的水果和炸弹,对它们做同样的处理。

你应该为以下图片定义形状:

  • banana.png
  • bomb.png
  • grapes.png
  • pineapple.png
  • strawberry.png
  • Watermelon.png

在你结束工作后,在右上角处,更改导出(Exporter)设置为“Box2D generic (PLIST)”,最后,你应该得到下图中类似的结果:

Define Shapes Easily with PhysicsEditor!

点击“发布”(Publish),或者是“发布为”(Publish As),将导出一个PLIST文件,包含了顶点信息。将文件保存为fruits.plist

例如,在教程中用到的fruits.plist包含在资源包的Misc文件夹中。

 

你只是想看看包含在PLIST文件中的信息,因此不需要将它添加到你的项目中,只需要用Xcode将其打开,就可以按照一定的组织方式浏览内容了。

点击“bodies”旁边的三角图标,展开这个片段,你可以看到所有定义了形状的图片列表。你需要一直展开到最后一层,才能得到西瓜的多边形顶点信息,如下:

The PhysicsEditor PLIST

Expand watermelon/fixtures/Item 0/polygons and you should now see another Item 0 of Type Array under polygons. This last array is your shape. If you had properly defined a convex shape with 8 or less vertices, you should only see one array under polygons.

展开watermelon/fixtures/Item 0/polygons,然后你可以看到另外一个属性数组Item 0在polygons结点下。这个最后的数组就是形状。如果你正确的定义了一个包含8个或更少顶点的凸多边形,在polygons下面只会看到一个数组。

如果你看到多于一属性个数组,比如Item 0,Item 1等等,这意味着PhysicsEditor制造了一个复杂的形状,你可能定义了过多顶点,或定义了一个凹多边形。如果是这样的话,回到PhysicsEditor中将其修正。

下一步,展开Item 0数组,观察最后的列表。这些是多边形的顶点,你在右侧看到的按照{ 数字, 数字 }格式排列的值,是每个顶点的xy坐标。

现在,你获得了多边形确切的顶点,可以继续创建Watermelon类了。

在Xcode中,用iOS\cocos2d v2.x\CCNode作为模板创建一个新的文件,并且继承PolygonSprite然后命名为Watermelon。打开Watermelon.h ,然后做以下的改变:

// Add to top of file

#import "PolygonSprite.h"

切换到Watermelon.m,重命名为Watermelon.mm,然后添加以下init方法:

// Add inside the @implementation

-(id)initWithWorld:(b2World *)world

{

int32 count = 7;

NSString</a> *file = @"watermelon.png";

b2Vec2 vertices[] = {

b2Vec2(5.0/PTM_RATIO,15.0/PTM_RATIO),

b2Vec2(18.0/PTM_RATIO,7.0/PTM_RATIO),

b2Vec2(32.0/PTM_RATIO,5.0/PTM_RATIO),

b2Vec2(48.0/PTM_RATIO,7.0/PTM_RATIO),

b2Vec2(60.0/PTM_RATIO,14.0/PTM_RATIO),

b2Vec2(34.0/PTM_RATIO,59.0/PTM_RATIO),

b2Vec2(28.0/PTM_RATIO,59.0/PTM_RATIO)

};

CGSize screen = [[CCDirector sharedDirector] winSize];

b2Body *body = [self createBodyForWorld:world position:b2Vec2(screen.width/2/PTM_RATIO,screen.height/2/PTM_RATIO) rotation:0 vertices:vertices vertexCount:count density:5.0 friction:0.2 restitution:0.2];

if ((self = [super initWithFile:file body:body original:YES]))

{

// We will initialize more values for the fruit here later

}

return self;

}

在以上的代码中,你先定义了有几个顶点,那么在这个例子中是7个。接着,你创建了一个顶点数组,它存储了你在PLIST中看到的所有顶点的坐标。你用这些信息和一个你定义在PolygonSprite中的简单的方法,创建了一个body。

你添加了一个小的摩擦数值以至于不会让这个图形无休止的滑动,你也添加了一个小的restitution恢复数值,使模型在反弹的时候不会停下来。

最后,你通过调用父类的初始化方法,并传入图片文件的名字,创建Box2D body对象,同时声明这个对象是一个原始的水果。

你需要一些在资源工具包中的西瓜图片,那么现在是时候去添加这个教程中需要使用到的所有图片资源了。

在你的项目导航面板中,右击Resources,选择“Add Files to CutCutCut”。把资源工具包中的图片文件夹添加到项目中。确保勾选了“Copy items into destination group‘s folder”和“Create groups for any added folders”。

按照相同的步骤添加Banana,Grapes,Pineapple,Strawberry和Bomb。既然这是一个基本的重复添加的过程,你只要解决了如何一步步创建第一个水果就行了。资源工具包的Class文件夹中包含了已经做好的水果和炸弹类。当你需要一些指导的时候可以看看这些例子,或是你可以跳过这一步把他们直接加到你的项目中。

编译运行,确保一切运行正常。

在场景中加入一个水果

到目前为止,屏幕上没有任何反应,显然,你急切地想看到你的水果劳工。

切换到HelloWorldLayer.h ,然后做以下修改:

// Add to top of file

#import "PolygonSprite.h"

// Add inside the @interface

CCArray *_cache;

// Add after the @interface

@property(nonatomic,retain)CCArray *cache;

切换到HelloWorldLayer.mm,然后做这些修改:

// Add to top of file

#import "Watermelon.h"

// Add inside the @implementation

@synthesize cache = _cache;

// Add inside the init method, below [self initPhysics]

[self initSprites];

// Add inside the dealloc method, before calling [super dealloc]

[_cache release];

_cache = nil;

// Add anywhere inside the @implementation and before the @end

-(void)initSprites

{

_cache = [[CCArray alloc] initWithCapacity:53];

// Just create one sprite for now. This whole method will be replaced later.

PolygonSprite *sprite = [[Watermelon alloc] initWithWorld:world];

[self addChild:sprite z:1];

[sprite activateCollisions];

[_cache addObject:sprite];

}

你声明了一个将会保存所有你创建的水果和炸弹的缓存数组。接着,你创建了1个西瓜并把它加到了场景。你调用了activateCollisons方法使西瓜不会穿越围墙。

编译运行,你应该会看到一个西瓜从屏幕的中间掉落,停在底部。

你也许已经注意到西瓜并不在绝对的中心位置。这是因为你定位一个物体是基于它的Box2D body,而Box2D body的起始点是在物体的左下角。西瓜的薄边框可见是因为开了Box2D调试模式。

The Fruit of your Labor

何去何从

这里有一个包含上述教程中的所有代码的样例工程

这就是这个系列中的第一部分了。到目前为止,你有了一个西瓜图案的多边形掉落到屏幕的底部。

不同于平常你在Box2D教程中看到的绘制一个矩形的带有透明区域的精灵,这个教程使用PRKit只绘制了与Box2D的主体顶点相一致的纹理部分。这将很快派上用场。

敬请关注这个教程的第二部分,它会让你实现切水果的功能。同时,如果你有任何问题或建议,请加入下面的论坛。

Related Posts

  1. 我想问下,我把教程的 第三部分源代码下载下来,里面的刀光效果在模拟器上是对的,但是在真机上测试,发现刀光的位置偏了很多,xcode4.5 , ios 5.1 测试的