如何使用Box2D和Cocos2D製作一款像Fruit Ninja一樣的遊戲-第1部分

在本篇教程中,你將學到如何製作一個切圖片的遊戲,像Halfbrick Studios製作的Fruit Ninja一樣,我們使用的工具是強大的Cocos2D和Box2D,以及一些預先做好的工具。

在大多數切東西的遊戲中,當你畫一條線劃過一個圖片精靈時,他們的做法基本上是把圖片精靈轉變爲兩個預先畫好的被從中間切開的圖片精靈,而並不會依照你劃過的實際位置。

但是本篇教程將演示一個更cool的技術。我們的水果可以被切割多次,並且它們會根據實際切割的路線動態的分割!

正如你所想的,這是一種高級的技術,所以本篇教程面向的是高階的Cocos2D和Box2D開發者。如果你剛剛接觸Cocos2D和Box2D,你可以先跟隨Cocos2D入門Box2D入門這兩篇教程,再繼續本篇教程的學習。

本篇教程一共分爲3部分:

  • 在第1部分中,你會爲遊戲打基礎,並學習如何創建textured polygons(紋理多邊形)。
  • 第2部分會教你如何切和分割這些textured polygons。
  • 第3部分會教你添加遊戲性和特效並將之前的內容變成一個完整的遊戲。

特別感謝Rick Smorawski爲本篇教程中的工程所做的基礎性工作。他負責把flash-based slicing demo移植到了Cocos2D上,並把CCBlade和PRKit轉換到了Cocos2D 2.0。

查看下面的視頻,你就知道我們將要學習多麼cool的新技術了!

遊戲Demo

這是一個demo視頻,它會演示給你在接下來的教學中馬上要做的:

正如我之前提到的,你看到的切水果的效果動感十足。水果會根據你劃的位置被切割,並且由於你可以多次的切割它們,你甚至可以把它們切成碎塊兒!

視頻中你還能看到有一個很cool的刀光效果、一些粒子特效、遊戲的邏輯和一些爲遊戲添彩的音效。

好多的內容啊,那麼,讓我們開始吧!

項目準備工作

你將要在本項目中使用Cocos2D 2.X,如果你還沒有它請前往這裏下載。注意你完全可以使用Cocos2D 1.X來代替Cocos2D 2.X,你可以略過本篇教學中轉換PRKit和CCBlade到Cocos2.X的部分,但是請留意那些類中其他的小改動。

下載完成後,雙擊tar文件解壓,然後在終端中的以下命令安裝它:

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

啓動Xcode並使用iOScocos2d v2.xcocos2d iOS with Box2d模板創建一個新工程,將其命名爲CutCutCut。

新的工程看起來應該像這樣:

Project Start

首當其衝的,你應該對模板生成的工程做一些清理,已讓其有一個好的起點。

打開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.mm中拷貝一個方法,所以我們目前先留着它。

使用快捷鍵Command+R編譯並運行你的項目,你將會看到一個被綠色邊框包圍的黑色屏幕:

Clean Slate

剩下的模板代碼設置了Box2D的debug drawing,這一功能可以把所有Box2D物體都畫在屏幕上。看到屏幕周圍綠色的線了吧?它們就是使用項目默認的initPhysics方法生成的牆。

瀏覽一下項目中其餘的代碼,確保你明白了所有的邏輯,它初始化了Box2D world,設置了了地面(綠色的邊界),設置了debug drawing等等。

資源包

接下來請下載項目需要的資源並解壓文件。

請先不要把所有東西都加入工程,其中一些文件實際上是可選的。讓這個文件夾一直在手邊,隨着本篇教程的進行,我會指導你添加一些文件到項目中去。

以下是你在這個包中能找到的:

  • 一個背景圖片和一系列水果的圖片,它們都是由Vicki製作的,Images文件夾中還有其他的各種各樣的圖片。
  • 背景音樂是使用gomix.it混合製作的,它們在Sounds文件夾中。
  • 音效一部分是使用bfxr製作的,另一部分是從freesound下載的,他們在Sounds文件夾中。
  • 所有的粒子系統都是由Particle Designer製作的,它們在Particles文件夾中。
  • 一個由PhysicsEditor生成的PLIST文件,它包含了Fruits和Bomb類的頂點信息,在Misc文件夾中。
  • Fruits和Bomb類,在Classes文件夾中。
  • PRKit 和 CCBlade,教程中你將會用到它們,在Classes文件夾中。
  • 聲音資源的作者列表,在Misc文件夾中。

使用PRKit繪製紋理多邊形

我們的目標是把精靈切成很多份。通常一個CCSprite包含一個texture(紋理)和一個無視圖形真正形狀的碰撞框。這個矩形的碰撞框並不適合我們的遊戲,因爲知道精靈的實際形狀對於切割它是重要的一個步驟。

你需要創建一個紋理多邊形,它會完成如下功能:

  • 創建一個多邊形/形狀和一個相應形狀的圖片(紋理映射)
  • 只顯示在多邊形範圍之內的圖片部分(紋理填充)

Cocos2D和Box2D都沒有提供可以完成此功能的類,通常來說,這需要使用一些三角變換,外加自定義的OpenGL繪製模式。

聽起來很困難是嗎?

幸運的是,所有複雜的計算和完成這些的繪製代碼都已經被Precognitive Research這裏的人實現了。他們基於Cocos2D創建了一個叫做PRKit的庫,這個庫可以處理紋理映射和紋理填充。

我們來進一步研究紋理多邊形,先從這裏下載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版本的,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 shader program(着色程序)。爲了畫出PRFilledPolygon,你需要請求一個內建的的”Position/Texture” shader,你在init方法中指定它。

接下來,你需要爲紋理正確地計算每一個多邊形座標。爲了達成此目標,你需要對calculateTextureCoordinates方法做兩處修改:

  • 比率: 因爲此類會計算它自己的紋理座標,它並不會自動處理高清模式。解決它的方法僅僅需要將texture.pixelsWide乘上CC_CONTENT_SCALE_FACTOR,CC_CONTENT_SCALE_FACTOR是一個Cocos2D提供的方便的低清高清座標轉換的係數。
  • 翻轉Y: 由於某些原因,PRFilledPolygon會把紋理繪製成倒的,所以你要把y值翻轉。

最後,draw函數裏邊的代碼被更新爲了OpenGL ES 2.0的,這一處理方式與CCSprite的draw從Cocos2D 1.X到2.X的變化雷同:

  • 首先調用CC_NODE_DRAW_SETUP()爲繪製作準備。
  • glDisableClientState() 和 glEnableClientState()方法是廢棄的,不必再調用了。
  • glVertexPointer() 和 glTexCoordPointer()都被glVertexAttribPointer()方法替代了,glVertexAttribPointer()方法使用Vertex Position 和 Texture Coordinate作爲它的參數。
  • 以前需要配置glTexEnvf()用來處理多邊形比紋理大的情況,以讓紋理重複繪製,現在使用glTexParameteri()代替之。

如果你對以上任意一點感到困惑的話,你可以去查閱OpenGL ES 2.0 for iPhoneCustom Cocos2D 2.X Shaders兩篇教程來獲取更多的背景知識。但是你並不需要產生的困惑擔心過多,因爲到目前爲止我們所做的就是把這個類轉換到能在Cocos2D 2.X使用而已 :]

編譯並運行,所有的PRKit的錯誤都消失了!

是時候實際應用PRKit了。創建一個負責繪製水果的基礎類PolygonSprite,它繼承自PRKit中的PRFilledPolygon類。

PolygonSprite在PRFilledPolygon基礎上創建,引入了一個關聯的Box2D body到sprite上,同時它還包含了其他爲了完成水果的遊戲邏輯的自定義的變量和方法。

我們這就創建它。按Command+N創建一個新文件,選擇iOScocos2d v2.xCCNode Class template,使他成爲PRFilledPolygon的子類並將其命名爲PolygonSprite.m

切換到PolygonSprite.h並做如下修改:

// Add to top of file
#import "Box2D.h"
#import "PRFilledPolygon.h"
#define PTM_RATIO 32
 
// Add inside @interface
b2Body *_body;
BOOL _original;
b2Vec2 _centroid;
 
// Add after the @interface
@property(nonatomic,assign)b2Body *body;
@property(nonatomic,readwrite)BOOL original;
@property(nonatomic,readwrite)b2Vec2 centroid;
 
// Add before the @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: 這是一個關聯到sprite的Box2D body。它用來模擬物理特性。
  • original: 完整的和被切割過的sprite都使用相同的PolygonSprite類,正因如此,這兩者之間的差異性非常重要。如果此變量爲YES,說明它是沒被切割的,原始的物體,如果爲NO,說明它只是整體的一小部分。
  • centroid: 圖片多邊形的中心往往並不是圖片的中心,所以很有必要保存此值。
  • properties: 把所有的變量都設置屬性,已讓其他類能訪問它們。
  • init/spriteWith*: 我們主要的init方法遵循Cocos2D的命名習慣。
  • 其他方法: 還有一些處理Box2D body及其屬性的方法。
  • 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開頭的方法相比,都只是增加了autorelease。雖然initWithWorld在此類中沒有實際作用,但是在PolygonSprite的子類中會實現。

主要的內容是在initWithFile 和 initWithTexture方法中。爲了更加直觀的說明,創建一個水果的流程如下:

Init Sequence

  • initWithWorld: 這是繼承PolygonSprite的子類需要實現的方法,目前只需return nil,稍微我們會再處理它。
  • initWithFile: 這個方法添加從文件獲取的紋理並把所有的需要的參數傳給initWithTexture。
  • initWithTexture: 我們主要的初始化方法。PRFilledPolygon需要一個紋理和這個紋理的對應的實際多邊形的頂點。由於上一步已經處理了紋理本身了,這一步只需要從sprite的Box2D body中獲取相應的頂點即可。在向PRFilledPolygon傳入這些頂點後,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);
}

在上面這段代碼中,首先你重載了CCNode中的setPostion方法,當你更新sprite的座標時,關聯在其上的Box2D body的座標也會被一同更新。

然後你創建了一個輔助方法來創建和定義Box2D body。要創建body,你需要先定義一個body definition,一個a body object,a shape和一個fixture definition。這個方法沒有什麼具體的數值,稍後會由子類再具體化此方法。

有一點需要注意的是categoryBits和maskBits。它們是用來過濾物體之間的碰撞的,如果一個物體的category bit與另一個物體的mask bit匹配,他們之間就有碰撞。你把他們設置爲0,因爲你希望它在初始化的時候沒有任何碰撞。

最後,你定義了兩個方法簡單的操縱categoryBits 和 maskBits,以此來控制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中拷貝一個方法嗎?是的,就是現在。這個方法所做的是保證Box2D shape和我們的sprite在移動時座標保持同步。只需重寫此方法,Cocos2D會爲我們處理其他的轉換,非常棒。

拷貝完以上代碼之後,你現在可以把PhysicsSprite.h和PhysicsSprite.mm都從你的項目中刪除,它們已經沒有利用價值了。

編譯並運行,一切都應該是完善的,現在你已經完成了PolygonSprite。

爲水果做計劃

在爲我們的水果創建類之前,你需要先了解圖片和它的圖形需要遵循的規則。由於你需要把紋理映射到一個Box2D的多邊形上,你必須受到Box2D的多邊形的限制,有兩點需要牢記在心:

  • 多邊形必須是convex(凸的), 這意味着多邊形沒有內角大於180度。
  • 多邊形的邊數不得超過8個

實際上,如果你允許每一個body包含多個shape,你完全可以超越這些限制。Box2D可以通過一些三角變換讓凹多邊形包含多個三角形來處理凹多邊形,但是這超過了本篇教學的範圍。

爲了保持簡單性,本篇教學裏我們會遵循1個Body對應1個Shape的原則。

Note: PhysicsEditor, the tool we will be covering later in this tutorial, actually contains code to automatically triangulate polygons you draw into a set of convex shapes. However, like I said we’re trying to keep things simple here so will make sure to draw our shapes convex so we only have one shape per body.

看一下這兩個水果的區別:

Concave vs Convex

使用香蕉並不是很好的注意,因爲它天生就是凹的形狀。而西瓜則是非常好的,因爲凸多邊形非常完美的描述了它的外形。

如果你遵循以上我們的原則定義這兩個水果,你大致會得到如下的形狀:

Box2D Shape Outline

西瓜的多邊形很完美,但是香蕉的多邊形有一塊兒很大的空隙,位置就是香蕉凹的那一塊兒。Box2D會把那一部分也當作物體區域的一部分,這樣會讓香蕉的碰撞或切割多少看起來有些不自然。

但這並不意味着你不使用香蕉,只是不建議使用香蕉而已。事實上,在之後的教學中你還是會用的這個香蕉。

創建第一個水果 Creating the First Fruit

是時候創建第一個水果了,就從西瓜開始吧(要想吃它至少要切上一刀)。

回顧一下PolygonSprite的初始化流程,記得initWithTexture方法需要一個Box2D body參數,但是在這之前,initWithFile方法則不需要。

這樣做的原因是你需要爲每一種水果都單獨創建和定義body,所以initWithWorld方法不得不成爲第一步,它會創建body並設置其他的每一種水果獨有的值。

創建Box2D body之前,你必須先知道此形狀對應的多邊形的各個頂點。有很多方法可以它,本教學中,你將會用到一個非常好的工具,PhysicsEditor,這個工具功能齊全,我們只用它一個功能即可,就是通過它幫我們獲取多邊形每個頂點的座標。

如果你還沒有它,請前往download PhysicsEditor下載,完成後啓動它,你會得到一個空的工程,它有3個面板/行。

使用PhysicsEditor相當的直觀。在左側,你放置所有的你需要的圖片。在中間,你可以清楚的看到你爲圖片定義的每一個頂點。在右側,你可以調節body的各種參數。

Physics Editor!

把resource kit裏Images文件夾中的watermelon.png拖拽進左側的面板,西瓜就出現在了中間的面板。

增加magnification值,在面板的底部能找到它,將其調節到一個合適的級別,然後點擊面板左上角的五角星按鈕創建一個3個頂點的多邊形。

右鍵點擊多邊形,選擇“Add Vertex”直到一共有5-8個頂點。移動這些頂點讓其符合西瓜的邊緣,另外確保以下兩點:

  • 你創建的多邊形是凸的。
  • 西瓜圖片上所有的有效像素都在多邊形之內。

提示: 還有一種繪製形狀的快捷方式,就是使用PhysicsEditor的magic wand(魔術棒)工具。只需設置tolerance high爲 5-8,就可以自動地生成頂點。

把其他所有的水果以及炸彈的圖片從Images文件夾拖拽進來並執行同樣的操作。

你將會爲以下圖片定義圖形:

  • 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在resource kit的Misc文件夾中。

你僅僅需要查看一下PLIST文件的信息,所以不要把此文件加入到項目中,用Xcode打開它,用它有組織的方式查看該plist。

點擊“bodies”旁邊的小三角展開它,你會看到你剛剛定義的那些形狀。你需要展開到最深層級來查看西瓜的多邊形頂點,如下所示:

The PhysicsEditor PLIST

展開watermelon/fixtures/Item 0/polygons節點,你會看到在polygons下又有另外的一個Item 0開頭的數組。最後的這個數組就是你的形狀。如果你確實是定義的凸多邊形並且頂點數小於等於8個的話,形狀的數組將只有一項。

如果你發現你的不只一項,例如是Item 0 Array, Item 1 Array,等等,那麼這意味着PhysicsEditor製作了一個具有複雜的多邊形的形狀,原因是形成了一個凹多邊形或者頂點數太多了。如果是這樣,請返回PhysicsEditor修復那個形狀。

接下來,展開Item 0這一項,觀察最終的數值,它們就是你需要的頂點了。右側的值是以{ number, number }格式顯示的每個頂點的座標。

現在已經有了每個多邊形準確的頂點值了,來繼續創建西瓜類。

在Xcode中,創建一個新文件,模版選擇iOScocos2d v2.xCCNode Class。讓其繼承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 *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。

你設置了一個小的friction(摩擦力)來讓形狀不會永無止境的滑動,你還設置了一個小的restitution(回覆力)來讓形狀互相碰撞時不會突然停下來。

最後,你通過傳入圖片的文件名,Box2D body,使用基類的初始化方法創建對象。此時,對象的狀態是original(原始的)水果。

你需要resource kit中的watermelon圖片,現在不如就把所有的圖片資源都加入到工程中,以後的教學中也會用到。

在你的Project Navigator panel(項目導航面板)中,右鍵點擊Resources並選擇“Add Files to CutCutCut”。添加resource kit中的Images文件夾到項目中。確保“Copy items into destination group’s folder”和“Create groups for any added folders”是選中的。

Follow the same steps to create Banana, Grapes, Pineapple, Strawberry, and Bomb.
對香蕉,葡萄,菠蘿,草莓和炸彈也用同樣的步驟生成。

你雖然只一步一步的完成了第一個水果的創建任務,但是其他的水果都是大同小異的重複性工作。在resource kit的Classes文件夾中包含了預先製作的水果和炸彈的類,你可以選擇在你製作的過程中參考它們,或者乾脆直接把它們加入到項目中來避免重複性工作。

編譯並運行,確保一切正常。

添加水果到場景

到目前位置,屏幕上還沒有任何的東西呢,有點無聊吧?你肯定迫不及待的想來看看你的勞動成果了 :]

切換到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];
}

你聲明瞭一個緩存數組,它會保存你將來創建的所有的水果和炸彈。接下來,你創建了一個西瓜並把它加入到場景中。通過調用activateCollisions,說過就不會越過屏幕中的圍牆了。

編譯並運行,你將會看到一個西瓜從屏幕中心掉落,最後落在屏幕底。

The Fruit of your Labor

你可能察覺到了,西瓜並不是在屏幕的正中心。導致此的原因是,對象的座標是基於Box2D body改變的,而我們的Box2D body的origin(起始點)是在物體的左下角上。另外,西瓜周圍的細線是因爲我們開啓了Box2D的調試繪製模式。

何去何從?

這是本篇教學到目前爲止的樣本工程

這就是第1部分的全部了,到此爲止,你已經得到了一個具有紋理多邊形的西瓜,滑落到屏幕底端。

但是與Box2D常用的繪製sprite矩形中的透明區域不同,我們使用PRKit繪製Box2D body的多邊形覆蓋的區域。這一點再未來將會派上大用場!

你已經在第一篇中打好基礎了,在第2部分中,你將會添加切割水果的功能!


發佈了55 篇原創文章 · 獲贊 5 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章