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

歡迎來到系列教程的第3部分,本系列教程將教你如何製作一款類似Halfbrick Studios公司出品的水果忍者的遊戲。

第1部分中,你學會了如何製作一個紋理多邊形,並基於它製作了一個西瓜。

第2部分中,你學會了如何使用Box2D Ray Casting 和一些數學方法來切割紋理多邊形。

在本篇同時也是最後一部分中,你將把上一篇結束時的工程通過加入gameplay,特效和音效讓其變得羽翼豐滿。

另外,如果你是剛剛接觸Cocos2D 和 Box2D的話,請先學習本網站的Cocos2D入門Box2D入門

準備工作

我們需要使用上一部分結束的工程,所以確保你已經有了第2部分的工程

另外,如果你還沒有本教程的所用資源,請先下載它。你稍後將要在這個工程中添加很cool的特效!

向上拋水果

到目前爲止你只在屏幕上畫了一些靜止的水果。在你加入“向上拋”這個機制前,你必須有不同種類的水果。如果你還沒有準備好這些水果的類,那麼你可以到resources的Classes文件夾裏找到它們。

所以此時你在工程裏應該有:香蕉(Banana),葡萄(Grapes),菠蘿(Pineapple),草莓(Strawberry),和西瓜(Watermelon)等水果了。

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

// Add to top of file
typedef enum _State
{
kStateIdle = 0,
kStateTossed
} State;
 
typedef enum _Type
{
kTypeWatermelon = 0,
kTypeStrawberry,
kTypePineapple,
kTypeGrapes,
kTypeBanana,
kTypeBomb
} Type;
 
// Add inside @interface
State _state;
Type _type;
 
// Add after @interface
@property(nonatomic,readwrite)State state;
@property(nonatomic,readwrite)Type type;

然後切換到PolygonSprite.mm並作如下修改:

// Add inside @implementation
@synthesize state = _state;
@synthesize type = _type;
 
// Add inside the if statement of initWithTexture
_state = kStateIdle;
 
// Add inside createBodyForWorld, right after setting the maskBits of the fixture definition
fixtureDef.isSensor = YES;

你在PolygonSprite中添加一個type屬性用來區分這些子類。接下來,你爲每種水果分別添加了state屬性。一個idle(空閒)的state意味着水果可以被向上拋,另外tossed(被拋)state意味着水果還在屏幕中運動的過程中呢。

PolygonSprites中的body對象設成sensors,這意味着Box2D只會檢測這些body的碰撞而不會實際作用這些碰撞。當你把一個水果從底部拋向空中時,你並不想讓他們在下落時互相碰撞,因爲玩家很有可能還沒看見它們就輸掉了。

接下來,作如下修改:

// Add inside the if statement of Banana.mm
self.type = kTypeBanana;
// Add inside the if statement of Bomb.mm
self.type = kTypeBomb;
// Add inside the if statement of Grapes.mm
self.type = kTypeGrapes;
// Add inside the if statement of Pineapple.mm
self.type = kTypePineapple;
// Add inside the if statement of Strawberry.mm
self.type = kTypeStrawberry;
// Add inside the if statement of Watermelon.mm
self.type = kTypeWatermelon;

切換回HelloWorldLayer.mm,並作如下修改:

// Add to top of file
#import "Strawberry.h"
#import "Pineapple.h"
#import "Grapes.h"
#import "Banana.h"
#import "Bomb.h"
 
// Replace the initSprites method
-(void)initSprites
{
    _cache = [[CCArray alloc] initWithCapacity:53];
 
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Watermelon alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Strawberry alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Pineapple alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Grapes alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Banana alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
 
    for (int i = 0; i < 3; i++)
    {
        PolygonSprite *sprite = [[Bomb alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
}

你爲每一種PloygonSprite的子類都賦予一種type,在遊戲中預先創建水果(每種10個),另外3個炸彈。你並不想讓它們立刻顯示,所以先把它們放到屏幕之外。

編譯並運行,不會看到有水果顯示出來。

Prepare to Toss

在遊戲中,水果從屏幕下方被拋起來。我們可以採取同時或者一個接一個的向上拋的方式,對每一次拋的間隔,水果的數量,位置,高度和方向都做一些隨機。

這些隨機特性會讓遊戲變得更有趣。

切換回HelloWorldLayer.h並作如下修改:

// Add to top of file, below the calculate_determinant definition
#define frandom (float)arc4random()/UINT64_C(0x100000000)
#define frandom_range(low,high) ((high-low)*frandom)+low
#define random_range(low,high) (arc4random()%(high-low+1))+low
 
typedef enum _TossType
{
kTossConsecutive = 0,
kTossSimultaneous
}TossType;
 
// Add inside the @interface
double _nextTossTime;
double _tossInterval;
int _queuedForToss;
TossType _currentTossType;

然後,切換到HelloWorldLayer.mm並作如下修改:

// Add inside the init method
_nextTossTime = CACurrentMediaTime() + 1;
_queuedForToss = 0;

你定義了方法用來輸出在固定範圍內的隨機float和integer,並對上文提到的兩種拋水果的方式定義了type。

接下來,你定義了以下游戲邏輯的變量,它們是:

  • nextTossTime: 這是下一次水果被拋起的時間,可以是一個或者一組水果。它總是和CACurrentMediaTime()做比較,你將其初始化爲當前時間加1秒,這樣在遊戲開始時不會沒有任何緩衝時間地馬上開始拋水果。
  • tossInterval: 這是兩次拋水果的時間間隔(秒)。在每次拋水果時,你都把這個值加到nextTossTime上。
  • queuedForToss: 此值表示在當前的拋水果類型中,還需要被拋的水果的隨機數量。
  • currentTossType: 當前拋水果的類型。在simultaneous(同時) 和 consecutive(順序)中隨機選一個。

還在HelloWorldLayer.mm中,添加方法:

-(void)tossSprite:(PolygonSprite*)sprite
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
    CGPoint randomPosition = ccp(frandom_range(100, screen.width-164), -64);
    float randomAngularVelocity = frandom_range(-1, 1);
 
    float xModifier = 50*(randomPosition.x - 100)/(screen.width - 264);
    float min = -25.0 - xModifier;
    float max = 75.0 - xModifier;
 
    float randomXVelocity = frandom_range(min,max);
    float randomYVelocity = frandom_range(250, 300);
 
    sprite.state = kStateTossed;
    sprite.position = randomPosition;
    [sprite activateCollisions];
    sprite.body->SetLinearVelocity(b2Vec2(randomXVelocity/PTM_RATIO,randomYVelocity/PTM_RATIO));
    sprite.body->SetAngularVelocity(randomAngularVelocity);
}

這個方式賦予在屏幕下方的sprite一個隨機位置,並計算出一個隨機的速度。min和max根據當前位置限制速度,讓sprite不會太偏左,也不會太偏右。

這些值大多都是試出來的。如果sprite在最左邊,速度的x在-25到75能夠保持sprite仍然在屏幕範圍內。如果sprite在中間,那麼-50到50就可以滿足了,其他情況類似。

Planned Trajectories

在計算過所有的隨機值後,將state設置爲kStateTossed表示sprite已經被拋起了,同時啓動sprite的collision mask並設置初始速度。

我在之前說正被拋到空中的sprite並不和正在下落的sprite碰撞,所以你一定會奇怪爲什麼我們這裏調用activateCollisions。這是因爲這個方法只是設置sprite的body的category和mask bits,並不會改變它是sensor的事實。

改變這些bits很重要,因爲當sprite被切割後,新的形狀就不再是sensor了,同時它們會繼承原sprite的以上這些屬性。

這個方法已經給予了每個水果隨機位置和隨機速度,所以接下來的邏輯就是創建每兩次拋水果的隨機間隔時間了。

添加方法到HelloWorldLayer.mm中:

-(void)spriteLoop
{
    double curTime = CACurrentMediaTime();
 
    //step 1
    if (curTime > _nextTossTime)
    {
        PolygonSprite *sprite;
 
        int random = random_range(0, 4);
        //step 2
        Type type = (Type)random;
        if (_currentTossType == kTossConsecutive && _queuedForToss > 0)
        {
            CCARRAY_FOREACH(_cache, sprite)
            {
                if (sprite.state == kStateIdle && sprite.type == type)
                {
                    [self tossSprite:sprite];
                    _queuedForToss--;
                    break;
                }
            }
        }
        else
        { //step 3
            _queuedForToss = random_range(3, 8);
            int tossType = random_range(0,1);
 
            _currentTossType = (TossType)tossType;
            //step 4
            if (_currentTossType == kTossSimultaneous)
            {
                CCARRAY_FOREACH(_cache, sprite)
                {
                    if (sprite.state == kStateIdle && sprite.type == type)
                    {
                        [self tossSprite:sprite];
                        _queuedForToss--;
                        random = random_range(0, 4);
                        type = (Type)random;
 
                        if (_queuedForToss == 0)
                        {
                            break;
                        }
                    }
                }
            } //step 5
            else if (_currentTossType == kTossConsecutive)
            {
                CCARRAY_FOREACH(_cache, sprite)
                {
                    if (sprite.state == kStateIdle && sprite.type == type)
                    {
                        [self tossSprite:sprite];
                        _queuedForToss--;
                        break;
                    }
                }
            }
        }
        //step 6
        if (_queuedForToss == 0)
        {
            _tossInterval = frandom_range(2,3);
            _nextTossTime = curTime + _tossInterval;
        }
        else 
        {
            _tossInterval = frandom_range(0.3,0.8);
            _nextTossTime = curTime + _tossInterval;
        }
    }
}

這裏發生了很多事,讓我們分解成詳細步驟看看:

  • 階段 1: 通過比較當前時間和nextTossTime,檢查是否到了下一次拋水果的時間。
  • 階段 2: 如果在consecutive模式中還有在隊列中的水果等待被拋起,那麼拋起它並直接進入階段6.
  • 階段 3: 從consecutive和simultaneous拋水果模式中選擇其一,並設置一個被拋棄的水果的數量。
  • 階段 4: 同時拋起隨機數量的水果。注意水果tpye的範圍從0到4因爲你並不想包含Bomb(炸彈)類型。
  • 階段 5: 與階段2類似。檢測如果是consecutive模式,就拋起第一個水果並進入階段6.
  • 階段 6: 設置兩次拋水果的間隔時間。當所有的水果都拋完後,你隨機取一個較長的間隔時間,否則,說明你當前處在consecutive模式,那麼就隨機取一個較短的間隔時間。

把這個方法添加到update方法中來循環執行。在HelloWorldLayer.mm中,添加下邊的一行到update方法中:

[self spriteLoop];

在啓動遊戲之前還需要做一件事。由於我們的sprite從屏幕下方被拋起,並最終落回屏幕,你應該移除被默認創建的牆。仍然在HelloWorldLayer.mm,作如下修改:

// In the initPhysics method, replace gravity.Set(0.0f, -10.0f) with
gravity.Set(0.0f, -4.25f);
 
// Comment out or remove the following code from the initPhysics method
// bottom
groundBox.Set(b2Vec2(0,0), b2Vec2(s.width/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);
 
// top
groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO));
groundBody->CreateFixture(&groundBox,0);
 
// left
groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(0,0));
groundBody->CreateFixture(&groundBox,0);
 
// right
groundBox.Set(b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);

除了要移除所有的物理牆之外,你還把重力修改的更弱因爲你並不希望sprite下落的太快。

編譯並運行,你會看到你的水果正在上升和下落!

Fruits Galore

在遊戲運行的過程中,你會發現3個問題。

  • 最終由於cache中沒有任何對象,你也沒有重設水果的狀態,拋水果的動作會停下來
  • 你切割的次數越多,遊戲運行效率就越低。這是因爲你沒有及時清除被切割的已經落到屏幕外的水果碎片,Box2D仍然在一直模擬它們。
  • 當你切割水果後,新的碎片是粘在一起的,這是因爲你只是簡單的把水果分隔成兩部分,而沒有強行的將它們分開。

我們這就修正這些問題,在HelloWorldLayer.mm中作如下修改:

// Add inside the splitPolygonSprite method, right before [sprite deactivateCollisions]
sprite.state = kStateIdle; 
 
// Add this method
-(void)cleanSprites
{
    PolygonSprite *sprite;
 
    //we check for all tossed sprites that have dropped offscreen and reset them
    CCARRAY_FOREACH(_cache, sprite)
    {
        if (sprite.state == kStateTossed)
        {
            CGPoint spritePosition = ccp(sprite.body->GetPosition().x*PTM_RATIO,sprite.body->GetPosition().y*PTM_RATIO);
            float yVelocity = sprite.body->GetLinearVelocity().y;
 
            //this means the sprite has dropped offscreen
            if (spritePosition.y < -64 && yVelocity < 0)
            {
                sprite.state = kStateIdle;
                sprite.sliceEntered = NO;
                sprite.sliceExited = NO;
                sprite.entryPoint.SetZero();
                sprite.exitPoint.SetZero();
                sprite.position = ccp(-64,-64);
                sprite.body->SetLinearVelocity(b2Vec2(0.0,0.0));
                sprite.body->SetAngularVelocity(0.0);
                [sprite deactivateCollisions];
            }
        }
    }
 
    //we check for all sliced pieces that have dropped offscreen and remove them
    CGSize screen = [[CCDirector sharedDirector] winSize];
    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
    {
        if (b->GetUserData() != NULL) {
            PolygonSprite *sprite = (PolygonSprite*)b->GetUserData();
            CGPoint position = ccp(b->GetPosition().x*PTM_RATIO,b->GetPosition().y*PTM_RATIO);
            if (position.x < -64 || position.x > screen.width || position.y < -64)
            {
                if (!sprite.original)
                {
                    world->DestroyBody(sprite.body);
                    [self removeChild:sprite cleanup:YES];
                }
            }
        }
    }
}
 
// Add inside the update method, after [self checkAndSliceObjects]
[self cleanSprites];

這裏引入了狀態處理。sprite初始爲idle(空閒)狀態,緊接着toss方法會改變其狀態。toss方法只改變idle狀態的sprite,最後你把被切割的原sprite的狀態還原回idle。

在cleanSprites方法中,首先檢查所有的原sprite是否是掉落到屏幕以外,如果是,就在向上拋之前重置它們的狀態。接下來檢查所有的被切割的碎片是否在屏幕以外,如果是,就銷燬它的Box2D body並將其從場景中移除。

切換到HelloWorldLayer.h,在#define random_range(low,high)行之後添加以下內容:

#define midpoint(a,b) (float)(a+b)/2

切換回HelloWorldLayer.mm並對splitPolygonSprite方法作如下修改:

// Add to the top part inside of the if (sprite1VerticesAcceptable && sprite2VerticesAcceptable) statement
b2Vec2 worldEntry = sprite.body->GetWorldPoint(sprite.entryPoint);
b2Vec2 worldExit = sprite.body->GetWorldPoint(sprite.exitPoint);
float angle = ccpToAngle(ccpSub(ccp(worldExit.x,worldExit.y), ccp(worldEntry.x,worldEntry.y)));
CGPoint vector1 = ccpForAngle(angle + 1.570796);
CGPoint vector2 = ccpForAngle(angle - 1.570796);
float midX = midpoint(worldEntry.x, worldExit.x);
float midY = midpoint(worldEntry.y, worldExit.y);
 
// Add after [self addChild:newSprite1 z:1]
newSprite1.body->ApplyLinearImpulse(b2Vec2(2*body1->GetMass()*vector1.x,2*body1->GetMass()*vector1.y), b2Vec2(midX,midY));
 
// Add after [self addChild:newSprite2 z:1]
newSprite2.body->ApplyLinearImpulse(b2Vec2(2*body2->GetMass()*vector2.x,2*body2->GetMass()*vector2.y), b2Vec2(midX,midY));

通過施加某種力改變對象的方向和速度,這樣被切割時分成的兩片就不會貼在一起了。

爲了得到方向,你需要計算得到切割線兩端的世界座標和切割線的角度,再計算得到兩個垂直此線的標準向量。所有的Box2D角度單位都是按弧度計算的,所以1.570796正好是角度的90的。

接下來,你得到切割線的中心座標,以此來作爲推力的作用點。

參考下面圖示:

Push the Pieces Away from Each Other

爲了把兩片sprite推開,你對它們分別施加了linear impulse(線性衝量),作用點爲線段中心,方向相反。此衝量基於每個body的質量,所以兩個物體所受的推力基本上是一致的。更大的sprite會得到更大的衝量,更小的sprite會得到更小的衝量。

編譯並運行,這次水果被切割的感覺就很不錯了,同時遊戲可以無盡的玩下去。

Slice with Impulse

添加計分系統/h2>
如果遊戲沒有明確的目標和合理的結束的話,就不能稱之爲遊戲,所以你需要在合適的地方添加一個計分系統。

你需要根據玩家切割水果的數量計算分數。你會跟玩家3條命,或者說3次機會,當沒有被切過的水果飛出屏幕時,就減1條命。

切換到HelloWorldLayer.h並作如下修改:

// Add inside @interface
int _cuts;
int _lives;
CCLabelTTF *_cutsLabel;

切換回HelloWorldLayer.mm並作如下修改:

// Add inside the init method, right after [self initSprites]
[self initHUD];
 
// Add these methods
-(void)initHUD
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
 
    _cuts = 0;
    _lives = 3;
 
    for (int i = 0; i < 3; i++)
    {
        CCSprite *cross = [CCSprite spriteWithFile:@"x_unfilled.png"];
        cross.position = ccp(screen.width - cross.contentSize.width/2 - i*cross.contentSize.width, screen.height - cross.contentSize.height/2);
        [self addChild:cross z:4];
    }
 
    CCSprite *cutsIcon = [CCSprite spriteWithFile:@"fruit_cut.png"];
    cutsIcon.position = ccp(cutsIcon.contentSize.width/2, screen.height - cutsIcon.contentSize.height/2);
    [self addChild:cutsIcon];
 
    _cutsLabel = [CCLabelTTF labelWithString:@"0" fontName:@"Helvetica Neue" fontSize:30];
    _cutsLabel.anchorPoint = ccp(0, 0.5);
    _cutsLabel.position = ccp(cutsIcon.position.x + cutsIcon.contentSize.width/2 +                _cutsLabel.contentSize.width/2,cutsIcon.position.y);
    [self addChild:_cutsLabel z:4];
}
 
-(void)restart
{
    [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
}
 
-(void)endGame
{
    [self unscheduleUpdate];
    CCMenuItemLabel *label = [CCMenuItemLabel itemWithLabel:[CCLabelTTF labelWithString:@"RESTART"fontName:@"Helvetica Neue"fontSize:50] target:self selector:@selector(restart)];
    CCMenu *menu = [CCMenu menuWithItems:label, nil];
    CGSize screen = [[CCDirector sharedDirector] winSize];
    menu.position = ccp(screen.width/2, screen.height/2);
    [self addChild:menu z:4];
}
 
-(void)subtractLife
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
    _lives--;
    CCSprite *lostLife = [CCSprite spriteWithFile:@"x_filled.png"];
    lostLife.position = ccp(screen.width - lostLife.contentSize.width/2 - _lives*lostLife.contentSize.width, screen.height - lostLife.contentSize.height/2);
    [self addChild:lostLife z:4];
 
    if (_lives <= 0)
    {
        [self endGame];
    }
}

在interface部分,你設置了切割次數和命的數量。同時你還聲明瞭一個label用來顯示玩家當前的分數。

initHUD方法在屏幕的左上角創建了3個標記用來顯示玩家的命數。它還放置了一張圖片代表分數,分數本身也顯示在左上角。

subtractLife方法在標記命數的位置疊加一個新的標記圖片,用來代表生命損失,每當此方法被調用,它還檢查玩家當前是否還有足夠的命數,如果沒有,遊戲就應該結束了。

endGame方法首先移除對update的schedule以停止遊戲邏輯,然後在屏幕中添加restart按鈕,如果此按鈕被點擊,那麼遊戲就會重新開始。

restart方法只是簡單地重新加載場景,並回到遊戲的最初的狀態。

現在已經創建了所有的方法和變量,是時候把它們加到遊戲邏輯中了。

還是在HelloWorldLayer.mm中,作如下修改:

// Add to the splitPolygonSprite method, inside the if (sprite1VerticesAcceptable && sprite2VerticesAcceptable) statement
_cuts++;
[_cutsLabel setString:[NSString stringWithFormat:@"%d",_cuts]];
 
// Add to the cleanSprites method, inside the if (spritePosition.y < -64 && yVelocity < 0) statement
if (sprite.type != kTypeBomb)
{
    [self subtractLife];
}

當一個多邊形被成功切割後,分數會增加,顯示分數的label會更新。如果有沒被切割過的原sprite落到屏幕下,就減少玩家的1條命。

編譯並運行,這個遊戲接近完成了!

Game Over!

讓遊戲更有挑戰

爲了讓遊戲更有趣,你要添加一些炸彈到遊戲中。在之前你已經初始化了3顆炸彈了,但目前還沒有用到它們。

炸彈是獨立的,它們可以在任何時間被拋起。如果一個玩家不小心劃到了一顆炸彈,它會爆炸並且減少玩家1條命。

HelloWorldLayer.mm作如下修改:

// Add to the spriteLoop method, inside if (curTime > _nextTossTime), right after PolygonSprite *sprite;
int chance = arc4random()%8;
if (chance == 0)
{
    CCARRAY_FOREACH(_cache, sprite)
    {
        if (sprite.state == kStateIdle && sprite.type == kTypeBomb)
        {
            [self tossSprite:sprite];
            break;
        }
    }
}
 
// Add to the splitPolygonSprite method, inside the if (sprite.original) statement
if (sprite.type == kTypeBomb)
{
    [self subtractLife];
}
else
{
//placeholder
}

這段代碼直截了當,和之前和對水果所做的類似。首先爲炸彈添加拋的機制,但是這次並不計算拋的類型和已經有多少炸彈正在空中。

炸彈隨機被拋起。這裏使用一個隨機值模8,如果結果等於0,就拋之,這裏只有1/8的機會在每次間隔時拋起炸彈。

然後在splitPolygonSprite方法檢查sprite是否被切割的位置同樣檢查炸彈,如果劃到了一顆炸彈,那麼就調用subtractLife減少玩家1條命。

編譯並運行,炸彈就有啦!

Bombs Away!

使用粒子特效豐富遊戲

遊戲邏輯完成後,你可以集中精力打磨遊戲了。你確實應該爲遊戲添加更多的活力。目前的切割先得很乏味,炸彈不會爆炸,背景也顯得不夠動態。

你可以使用粒子系統豐富場景。粒子系統允許你使用大量的使用一個sprite的小對象。Cocos2D已經包含了可自定義的粒子系統,配合Particle Designer工具,可以可視化的創建粒子。

在Particle Designer中創建粒子很簡單,簡單到甚至不用在本教程中提及。作爲替代,我已經爲你創建好了你需要用到的粒子。Particle Designer將粒子導出爲PLIST格式,你所需要做的就是在Cocos2D中加載它們。

如果你還沒有,請先下載本教程的資源,在Xcode的Project Navigator中,右鍵點擊Resources並選擇“Add Files To CutCutCut”。添加Particles文件夾到項目中。你在做這一步操作時,同樣添加Sounds文件夾到工程中。確保“Copy items into destination group’s folder”和“Create groups for any added folders”是選中的。

以下是你需要添加到項目中的粒子文件:

  • banana_splurt.plist
  • blade_sparkle.plist
  • explosion.plist
  • grapes_splurt.plist
  • pineapple_splurt.plist
  • strawberry_splurt.plist
  • sun_pollen.plist
  • watermelon_splurt.plist

以上其中的5個是噴射的,可以稱其爲“splurt”,它們針對每一種水果被切割時的特效。一種炸彈爆炸時的爆炸粒子。一個跟隨刀刃移動的閃光效果,和一個背景上的微塵花粉效果。

切換到HelloWorldLayer.h並在@interface中加入以下內容:

CCParticleSystemQuad *_bladeSparkle;

接下來,再切換到HelloWorldLayer.mm,並作如下修改:

// Add inside the init method
_bladeSparkle = [CCParticleSystemQuad particleWithFile:@"blade_sparkle.plist"];
[_bladeSparkle stopSystem];
[self addChild:_bladeSparkle z:3];
 
// Add inside the initBackground method
CCParticleSystemQuad *sunPollen = [CCParticleSystemQuad particleWithFile:@"sun_pollen.plist"];
[self addChild:sunPollen];
 
//Add inside ccTouchesBegan
_bladeSparkle.position = location;
[_bladeSparkle resetSystem];
 
//Add inside ccTouchesMoved
_bladeSparkle.position = location;
 
// Add inside ccTouchesEnded
[_bladeSparkle stopSystem];

你添加了微塵花粉特效到背景中,並跟隨玩家的觸摸添加閃光特效。

調用stopSystem會停止粒子系統繼續噴射粒子,調用resetSystem可以重新讓粒子系統噴射粒子。所有的這些粒子都是無盡的,直到你調用stopSystem爲止,它們都不會停止。

接下來是噴射和爆炸特效,對PolygonSprite.h作如下修改:

// Add inside the @interface
CCParticleSystemQuad *_splurt;
 
// Add after the @interface
@property(nonatomic,assign)CCParticleSystemQuad *splurt;

切換到PolygonSprite.mm,在@implementation中添加以下內容:

@synthesize splurt = _splurt;

接下來,對PolygonSprite的子類作如下修改(水果和炸彈):

// Add inside Banana.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"banana_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Bomb.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"explosion.plist"];
[self.splurt stopSystem];
 
// Add inside Grapes.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"grapes_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Pineapple.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"pineapple_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Strawberry.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"strawberry_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Watermelon.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"watermelon_splurt.plist"];
[self.splurt stopSystem];

你爲每一種類型的PolygonSprite添加對應的粒子系統。

切換回HelloWorldLayer.mm並作如下修改:

// Add this line per fruit and bomb in the initSprites method
[self addChild:sprite.splurt z:3];
 
// Add inside the splitPolygonSprite method, inside the if (sprite.original) statement
b2Vec2 convertedWorldEntry = b2Vec2(worldEntry.x*PTM_RATIO,worldEntry.y*PTM_RATIO);
b2Vec2 convertedWorldExit = b2Vec2(worldExit.x*PTM_RATIO,worldExit.y*PTM_RATIO);
float midX = midpoint(convertedWorldEntry.x, convertedWorldExit.x);
float midY = midpoint(convertedWorldEntry.y, convertedWorldExit.y);
sprite.splurt.position = ccp(midX,midY);
[sprite.splurt resetSystem];

在initSprite方法中把所有的粒子添加到遊戲層中。另外,當水果或者炸彈被切割時,你在切割線的中間位置創建一個粒子特效。

編譯並運行,粒子滿天飛!

Particles Everywhere

免費的音樂和音效

你知道的,作爲raywenderlich.com的遊戲,沒有豐富的音樂和音效是不行的! :]

我們的聲音特效不僅僅有助於愉悅心情,同時還能讓玩家用來區分遊戲裏的各種事件。

添加resources文件夾中的Sounds文件夾到你的Xcode工程中。這裏邊包含了以下幾個事件的聲音:

  • 炸彈爆炸
  • 炸彈被拋起
  • 水果按順序的被拋起
  • 水果同時被拋起
  • 玩家損失一條命
  • 玩家切割水果分隔成小塊兒時
  • 玩家重複的切割一個水果
  • 玩家做出輕掃手勢時
  • 背景自然音效

切換到HelloWorldLayer.h並作如下修改:

// Add to top of file
#import "SimpleAudioEngine.h"
 
// Add inside the @interface
float _timeCurrent;
float _timePrevious;
CDSoundSource *_swoosh;
 
// Add after the @interface
@property(nonatomic,retain)CDSoundSource *swoosh;

再切換回HelloWorldLayer.mm,並作如下修改:

// Add inside @implementation
@synthesize swoosh = _swoosh;
 
// Add inside the dealloc method, before [super dealloc]
[_swoosh release];
 
// Add inside the init method
[[SimpleAudioEngine sharedEngine] preloadEffect:@"swoosh.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"squash.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_consecutive.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_simultaneous.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_bomb.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"lose_life.caf"];
_swoosh = [[[SimpleAudioEngine sharedEngine] soundSourceForFile:@"swoosh.caf"] retain];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"nature_bgm.aifc"];
_timeCurrent = 0;
_timePrevious = 0;
 
// Add inside the update method
_timeCurrent += dt;
 
// Add inside the spriteLoop method, after tossing the bomb
[[SimpleAudioEngine sharedEngine] playEffect:@"toss_bomb.caf"];
 
// Add inside the spriteLoop method, for both the consecutive tosses
[[SimpleAudioEngine sharedEngine] playEffect:@"toss_consecutive.caf"];
 
// Add inside the spriteLoop method, for the simultaneous toss
[[SimpleAudioEngine sharedEngine] playEffect:@"toss_simultaneous.caf"];
 
// Add inside splitPolygon if sprite is a bomb
[[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"];
 
// Add inside splitPolygon if sprite is not a bomb
[[SimpleAudioEngine sharedEngine] playEffect:@"squash.caf"];
 
// Add before destroying the body in the splitPolygonSprite method
[[SimpleAudioEngine sharedEngine] playEffect:@"smallcut.caf"];
 
// Add inside the subtractLife method
[[SimpleAudioEngine sharedEngine] playEffect:@"lose_life.caf"];
 
// Add inside ccTouchesMoved before setting _bladeSparkle.position = location
ccTime deltaTime = _timeCurrent - _timePrevious;
_timePrevious = _timeCurrent;
CGPoint oldPosition = _bladeSparkle.position;
 
// Add inside ccTouchesMoved after setting _bladeSparkle.position = location
if (ccpDistance(_bladeSparkle.position, oldPosition) / deltaTime > 1000)
{
    if (!_swoosh.isPlaying)
    {
        [_swoosh play];
    }
}

除了普通的遊戲聲音的代碼外,你還考慮了時間因素,基於距離/時間的公式,我們只在玩家手指很快滑動的時候才播放swoosh的音效。同時,你保存了一個swoosh音效的指針,只在它沒有播放的時候播放它。

你勝利了!恭喜,你已經制作了一款完整的iphone版切水果遊戲!

何去何從?

這是到本系列教程完整的示例工程

當然,你還可以再改進此遊戲。以下是一些能讓遊戲更好玩兒更有趣的改進點:

  • 支持凹多邊形,你需要使用三角計算法(把一個凹多邊形分成多個凸多邊形)。
  • 讓多邊形的頂點支持多餘8個。
  • 讓PolygonSprite支持使用batch nodes提升效率。
  • 支持多點觸摸和滑動。
  • 支持iPad。
  • 爲polygon做緩存,這樣可以使所用東西被重用。這能夠有效提升效率。
  • 爲切割添加追尾彗星效果,並在連續切割時給予玩家額外獎勵分數。
  • 當特殊水果被切割時觸發事件。
  • 更好的隨機拋水果的機制,比如從側面拋出水果。

如果你讓遊戲更cool的點子,或者對此遊戲有什麼問題和評論,歡迎到下面的討論區討論!


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