歡迎來到系列教程的第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個炸彈。你並不想讓它們立刻顯示,所以先把它們放到屏幕之外。
編譯並運行,不會看到有水果顯示出來。
在遊戲中,水果從屏幕下方被拋起來。我們可以採取同時或者一個接一個的向上拋的方式,對每一次拋的間隔,水果的數量,位置,高度和方向都做一些隨機。
這些隨機特性會讓遊戲變得更有趣。
切換回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就可以滿足了,其他情況類似。
在計算過所有的隨機值後,將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方法中:
在啓動遊戲之前還需要做一件事。由於我們的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下落的太快。
編譯並運行,你會看到你的水果正在上升和下落!
在遊戲運行的過程中,你會發現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的。
接下來,你得到切割線的中心座標,以此來作爲推力的作用點。
參考下面圖示:
爲了把兩片sprite推開,你對它們分別施加了linear impulse(線性衝量),作用點爲線段中心,方向相反。此衝量基於每個body的質量,所以兩個物體所受的推力基本上是一致的。更大的sprite會得到更大的衝量,更小的sprite會得到更小的衝量。
編譯並運行,這次水果被切割的感覺就很不錯了,同時遊戲可以無盡的玩下去。
添加計分系統/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條命。
編譯並運行,這個遊戲接近完成了!
讓遊戲更有挑戰
爲了讓遊戲更有趣,你要添加一些炸彈到遊戲中。在之前你已經初始化了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條命。
編譯並運行,炸彈就有啦!
使用粒子特效豐富遊戲
遊戲邏輯完成後,你可以集中精力打磨遊戲了。你確實應該爲遊戲添加更多的活力。目前的切割先得很乏味,炸彈不會爆炸,背景也顯得不夠動態。
你可以使用粒子系統豐富場景。粒子系統允許你使用大量的使用一個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方法中把所有的粒子添加到遊戲層中。另外,當水果或者炸彈被切割時,你在切割線的中間位置創建一個粒子特效。
編譯並運行,粒子滿天飛!
免費的音樂和音效
你知道的,作爲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的點子,或者對此遊戲有什麼問題和評論,歡迎到下面的討論區討論!