1. 程式人生 > 實用技巧 >iOS cocos2d 2遊戲開發實戰(第3版)---你的第一個遊戲!

iOS cocos2d 2遊戲開發實戰(第3版)---你的第一個遊戲!

>>> hot3.png

隨著蘋果公司不斷地創新與發展,新的iPhone 5、iPad 4以及iPad mini產品相繼問世,包括iOS與Xcode在內的開發環境和開發工具也都有了更新和進步。相信有不少開發團隊正緊鑼密鼓地在iPhone 5和iPad mini上部署自己的應用,一切都是那麼令人激動!而隨著iOS 6的推出,cocos2d遊戲引擎又有了新的發展。在IT行業,可謂唯一不變的便是變化。日新月異的技術需要同為開發者的你我保持強盛的好奇心和完美的學習狀態,只有這樣,才能不斷進步,開發出更酷的遊戲或應用。相信這本書的再版,一定能使你受益匪淺,幫助你實現夢想。


你的第一個遊戲 第四章

在本章中,你將編寫專屬於你的第一個完整的遊戲。它不會為你贏得什麼獎項,但從中可以學到cocos2d基本要素的使用方法,而且這個遊戲是很容易修改的。實際上,本書之前版本的讀者已經制作了這個遊戲的幾個修改版併發布到了App Store中。

這個遊戲是著名的“Doodle Jump”遊戲的“倒版”,它被貼切地命名為“DoodleDrop”。玩家通過旋轉螢幕儘可能躲避落下的障礙物。遊戲的最終版本如圖4-1所示,你可以在此提前瞭解一下將在本章創造的作品。

圖 4-1 “DoodleDrop”遊戲的最終版本

4.1 建立DoodleDrop專案
在第2章我們學習瞭如何建立支援ARC的Kobold2D和cocos2d專案。
Kobold2D從執行Kobold2D啟動應用程式開始,然後選擇Empty-Project 模板(見第2章中的圖2-2)。使用DoodleDrop作為專案名稱,基本工作就完成了。剩下的事就是選擇應用程式的目標程式,選擇Supported Device Orientation的portrait mode圖示(見第3章中的圖3-17)。由於DoodleDrop是設計在Portrait模式下玩的,因此不選擇landscape mode圖示。

下面的部分僅適於cocos2d使用者——Kobold2D使用者可以略過。


4.2 從一個支援ARC的cocos2d專案開始
cocos2d使用者要建立支援ARC的cocos2d專案,就應該依照第2章中的指導內容來做。如果已經做過了,就只要把已經建立的專案複製一遍。這樣做可以節省時間:保留一份由原始cocos2d專案模板轉換得來的支援ARC的未修改版本,這樣每次建立新專案時就會比較簡單快捷。
提示:
在這本書的原始碼中,可以在Cocos2D_ARC_Template_Projects資料夾下找到支援ARC的cocos2d模板專案。
在完成第2章中的指導內容之後,就有一個支援ARC的cocos2d專案了。我的專案取名為cocos2d-2.x-ARC-iOS。用Xcode開啟之前先簡單地將包含.xcodeproj檔案的資料夾拷貝一份。但是不要自己重新命名.xcodeproj檔案,因為那樣做會導致檔案不可用。

現在你可以通過Xcode來重新命名專案,這同樣也會重新命名.xcodeproj檔案。在Project Navigator中選擇cocos2d-2.x-ARC-iOS專案(第一個條目,見第2章中的圖2-5),帶延遲地雙擊來編輯該條目。就是單擊一次,停頓兩秒,再單擊一次。這樣專案名稱就可以編輯了。輸入DoodleDrop作為這個專案的名稱。
按下Enter鍵確認修改之後,Xcode會問你是否確認重新命名圖4-2所示的那幾項。確認則單擊Rename。否則Xcode 仍然會重新命名專案,但是其他名稱不變。所以就算你這個時候發現了拼寫錯誤,或者突然不喜歡這個名字了,也都應該單擊Rename。重新命名之後考慮到Prefix.pch檔案,可能還會出現警告:“New name for file can not be the same”。無視這個警告即可,因為不會有什麼問題。
還有最後一個需要手動重新命名的條目:應用程式的計劃方案。應該仍然名為cocos2d- 2.x-ARC-iOS,也就是你命名專案時用的那個名稱。選擇Product Manage Schemes來檢視方案列表。延遲雙擊方案名稱來選擇並編輯,重新命名為DoodleDrop。完成之後,關閉scheme列表。

圖4-2 確認重新命名專案以及相關檔案
由於DoodleDrop是Portrait模式應用程式,因此需要編輯AppDelegate.m 檔案。修改shouldAutorotateToInterfaceOrientation方法,使之返回YES以僅用於Portrait模式:
return UIInterfaceOrientationIsPortrait(interfaceOrientation);
現在,在Run和Stop按鍵右邊的下拉選單中選擇DoodleDrop的專案方案,然後執行,確認一切工作正常,如圖4-3所示。

圖4-3 開始遊戲吧!DoodleDrop專案基於第2章中的cocos2d ARC
專案,Kobold2D的Empty-Project模板也不會有太大區別
4.3 建立DoodleDrop場景
下一步你要作如下決定:是使用已有的HelloWorldLayer作為起點,之後再把名字改成現在的專案名稱呢?還是建立自己的場景?是我就選擇後者。因為你遲早需要新增新的場景,所以還不如現在就學習如何從頭建立新的場景。
請確定已選擇你準備新增新場景類的組,然後選擇File | New | New File或者右擊Project Navigator樹中的合適位置,在彈出的選單中選擇New File,開啟如圖4-4所示的New File 對話方塊。

圖4-4 新增新的CCNode派生類的最好方式是通過使用cocos2d或Kobold2D提供的類模板。
在這個例子中,因為我們要建立新的場景,所以選擇的CCNode類是CCLayer的子類
cocos2d和Kobold2D為大多數重要的節點和類都提供了類模板,不使用它們太浪費了!另外,Xcode自帶的Objective-C類也是很好的模板——只需要手動將基類由NSObject 改為CCLayer即可。在cocos2d v2.x的模板部分選擇CCNode類,單擊Next按鈕,再次單擊Next 按鈕會彈出如圖4-5所示的Save File對話方塊,在此之前,確認將該類設定為CCLayer 的子類。
我是將新建的檔案命名為GamLayer.m。整個DoodleDrop遊戲邏輯都在這個檔案中實現,所以這個名字還十分合理。確保DoodleDrop目標程式複選框被選中(見圖4-5)。
注意:
不檢查目標設定的話可能會導致檔案沒有被新增到正確的目標程式裡。這會引起一系列問題——其中編譯錯誤和“file not found”錯誤是常見的典型錯誤。這種情況有時還會導致遊戲執行時崩潰。把檔案放進完全不需要這些檔案的目標程式裡,只會浪費空間。


圖4-5 給新場景命名,並且確保它被新增到正確的目標程式中
目前,我們的GameLayer類是空的,為了將它設定為場景,我們要做的第一件事是在裡面新增+(id) scene方法。我們在這裡插入的程式碼和第3章的基本上一樣,只是層的類名不同而已。幾乎在任何一個類中都需要-(id) init方法。新增-(void) dealloc方法也無傷大雅,要是能輸出物件被正確銷燬的日誌就更好了。監視dealloc方法能有效提前做出系統預警,防止記憶體洩漏。
我也是一位很謹慎的程式設計師,決定將第3章中介紹的日誌語句新增進來。程式清單4-1是完成後的GameLayer.h,程式清單4-2是完成後的 GameLayer.m。
程式清單4-1 帶場景方法的GameLayer.h
#import < Foundation/Foundation.h>
#import "cocos2d.h"

@interface GameLayer : CCLayer
{
}

+(id) scene;

@end
程式清單4-2 帶場景方法以及一些標準方法的GameLayer.m
#import "GameLayer.h"
@implementation GameLayer
+(id) scene
{
CCScene *scene=[CCScene node];
CCLayer* layer=[GameLayer node];
[scene addChild:layer]; return scene;
}

-(id) init
{
if ((self=[super init]))
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
}
return self;
}
-(void) dealloc
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
}
@end
現在可以安全地刪除HelloWorldLayer類。在彈出的對話方塊中選擇Move to Trash選項,將檔案徹底刪除。選擇HelloWorldLayer.h和HelloWorldLayer.m兩個檔案,在頂部選單中選擇Edit | Delete,或者右擊選中的檔案,在彈出的選單中選擇Delete選項。
Kobold2D使用者現在只需要開啟Resources組中的config.lua檔案,修改FirstSceneClassName即可,如下所示:
FirstSceneClassName = "GameLayer",
但是在純cocos2d應用程式中,必須修改AppDelegate.m,將檔案中所有的HelloWorldLayer修改成GameLayer。程式清單4-3中已經突出顯示了要對#import和pushScene語句進行的必要修改。
程式清單4-3 修改AppDelegate.m 檔案,用GameLayer類代替HelloWorldLayer
// replace the line #import "HelloWorldLayer.h" with this one:

#import "GameLayer.h" - (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
// replace HelloWorldLayer with GameLayer
[director_ pushScene:[GameLayer scene]];
}
編譯並執行,應該能看到空白場景。成功了!如果碰到什麼問題,請將你的專案與本書附帶的DoodleDrop01專案進行比較。
提示:
應用程式成功生成但是執行失敗?別忘了,一個Xcode 專案中有多個目標程式,甚至一個目標程式也會有多個方案。檢查Xcode 工具欄中Run 和Stop 按鈕右邊的方案選擇/部署目標程式下拉選單(見圖2-6)。下拉選單的左邊部分用於選擇當前方案。要確保選擇的是DoodleDrop。大多其他方案,比如cocos2d-library是靜態庫。你可以生成靜態庫,但是不能執行它們。刪除、隱藏和選擇方案這些都只能由使用者自己設定完成。其他的問題我都已經為你解釋清楚了。
4.4 新增Player Sprite
接下來將新增Player Sprite(玩家精靈)並使用加速計控制它們的動作。要新增玩家影象,請在Xcode中選擇Resources組,然後選擇File | Add Files to“DoodleDrop”,或者右擊並從選單中選擇Add Files to“DoodleDrop”來開啟選擇檔案的對話方塊。如果不小心把檔案新增到了錯誤的組中,那麼可以在Project Navigator中把它重新拖動到正確的組中。Resources組並沒有什麼特別的地方,它就是按照定義應該儲存不是原始碼的檔案的地方。
玩家影象alien.png和alien-hd.png就在隨書附帶的DoodleDrop專案的Resources資料夾中。也可以挑選你自己的影象,只要影象的尺寸是64×64畫素或128×128畫素(HD格式,帶有-hd字尾)即可。cocos2d自動為帶有Retina顯示屏的iPhone和iPod Touch裝置使用HD檔案,標準解析度(SD)的檔案只用於iPhone 3GS。cocos2d還能識別另外兩種檔案字尾:-ipad用於iPad和iPad 2,-ipadhd用於帶有Retina顯示屏的第3代(或更新的)iPad。
對於專用於Retina裝置和iPad的資源,cocos2d預設使用帶有-hd、-ipad和ipadhd字尾的檔案。普通的iOS應用程式不會使用它們。這些應用程式要想使用高解析度影象,必須使用蘋果公司的@2x副檔名。雖然在cocos2d應用程式中也可以使用@2x副檔名,但是cocos2d的文件警告使用者不要使用該副檔名。
提示:
一個常被問起的問題是在非Retina裝置上簡單地縮小HD影象是否合適。這麼做是不合適的,原因有兩個。一個原因是記憶體限制。即使最早的Retina裝置的記憶體也是非Retina裝置的2倍以上。讓非Retina裝置載入HD影象,佔用的記憶體將是已被縮小並打包的SD影象的4倍。另一個原因是,載入Retina影象的時間要比載入SD影象長得多,在較老、較慢的裝置(比如沒有Retina顯示屏的那些裝置)上這個問題更加明顯。
反過來,在應用程式中只使用標準解析度的資源也不是十分合適。這樣將無法利用Retina顯示屏的高解析度,而且應用程式的影象質量在Retina裝置上不如高解析度的影象。放大和影象處理演算法再好,也不能讓標準解析度影象在Retina裝置上呈現清晰而鮮明的效果。正因如此,應該將遊戲的所有資源設計為使用高解析度,然後在需要的時候再縮小其大小。唯一需要注意的是,尺寸大小應該能夠被2整除。
Xcode隨後會詢問新增檔案的方式和位置細節,如圖4-6所示。確保在Add To Targets區域選中了所有會使用該檔案的目標程式,當然在這裡只有DoodleDrop,不過在Kobold2D專案中最好也把該檔案新增到Mac OS X目標中。如果檔案還沒有儲存到專案的資料夾中,就應該選中“Copy items into destination group’s folder(if needed)”複選框。如果無法確定,就選中該複選框,最壞的情況也就只是有相同檔案的副本。如果不選中該複選框,那麼最壞的情況會是把專案新增到原始碼管理程式或者在壓縮並分享專案時發生檔案丟失錯誤。
提示:
iOS遊戲首選的影象檔案格式是PNG(Portable Network Graphic,行動式網路影象)。這是一種壓縮檔案格式,然而與JPEG檔案不同的是,PNG採用了無損壓縮,保留了原始影象的所有畫素。你也可以儲存不經壓縮的JPEG檔案,不過對於同一影象,PNG檔案的大小明顯要比未經壓縮的JPEG檔案小。但是這隻會影響應用的大小,而不會影響紋理對記憶體(RAM)的使用。不使用JPEG檔案的另一個原因是,cocos2d在iOS上載入這些檔案的速度很慢。我上次測量的結果是它們比PNG檔案慢8倍。第6章將介紹TexturePacker,一個用於管理影象的工具。它允許將影象轉換為各種壓縮格式或減少影象的色深,同時通過抖色和其他技術保留儘可能高的影象質量。

圖4-6 每次新增資原始檔時都會出現這個對話方塊,大多數情況下你都應該使用預設設定
現在我們要向遊戲場景中加入玩家精靈了。我會將它們作為CCSprite*型別的成員變數加入到GameLayer類中。就目前來看,這樣做比較容易,而且遊戲也足夠簡單,可以將所有元件都加入到相同的類中。通常不推薦這種方法,在之後的專案中我們將會建立單獨的類來儲存每個遊戲元件,以符合好的程式碼設計要求。
程式清單4-4展示瞭如何在GameLayer的標頭檔案中新增CCSprite*型別的成員變數。
程式清單4-4 將CCSprite*型別的成員變數新增到GameLayer類中
#import < Foundation/Foundation.h>
#import "cocos2d.h"

@interface GameLayer : CCLayer
{
CCSprite* player;
}
+(id) scene;

@end
程式清單4-5是加入到init方法中的程式碼,它的功能是初始化精靈,將精靈賦給成員變數,並設定精靈到螢幕底部中間的位置,同時還啟用了加速計輸入功能。
程式清單4-5 啟用加速計輸入,建立並定位玩家精靈
-(id) init
{
if ((self = [super init]))
{
CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

self.isAccelerometerEnabled = YES;

player = [CCSprite spriteWithFile:@"alien.png"];
[self addChild:player z:0 tag:1];

CGSize screenSize = [CCDirector sharedDirector].winSize;
float imageHeight = player.texture.contentSize.height;
player.position = CGPointMake(screenSize.width / 2, imageHeight / 2);
}

return self;
}
玩家精靈已新增為層的子節點,它的標記值是1,隨後我們將使用這個標記值識別它並把它與其他的精靈區分開。這裡的檔名使用了標準解析度影象的檔名,即alien.png。在Retina裝置上,cocos2d會自動載入alien-hd.png。如果沒有對應的-hd檔案,cocos2d會載入標準解析度影象。此時,影象在Retina裝置上看起來比在非Retina裝置上小。提供所有影象資源的-hd版本是一種很好的做法。
警告:
記住,在iOS裝置上檔名是區分大小寫的。如果你試圖載入Alien.png或ALIEN.PNG,在模擬器上會成功載入檔案而在iOS裝置上則不會,因為檔案真正的名字是alien.png,字母全部小寫。這就是堅持統一的命名約定——例如強制所有檔名使用小寫字母——的意義所在。為什麼要用小寫?因為全部大寫的檔名很難辨認。
我們將position屬性的x值設為螢幕寬度的一半,從而使玩家精靈的初始位置水平居中。在垂直位置上,我們想使玩家精靈紋理的底端與螢幕底端對齊。如果你記得前一章的內容,就會知道精靈紋理是以其中心點作為位置座標值的。將精靈的垂直位置設為0會導致紋理的下半邊陷入螢幕底端。這不是我們想要的,我們要把它往上挪半個紋理的高度。
可以通過呼叫player.texture.contentSize.height返回精靈紋理的內容尺寸。什麼是內容尺寸?在第3章中,我講到iOS中紋理尺寸的大小隻能是2的方冪。但是實際的影象尺寸可能會比紋理尺寸小。例如,如果原始影象的尺寸為100×100畫素,那麼紋理尺寸就是128×128畫素。紋理的contentSize屬性會返回原始影象的尺寸,也就是100×100畫素。大部分情況下,處理的都是內容尺寸而不是紋理尺寸。即使影象是2的冪,也應該使用contentSize,因為紋理可能是包含多個影象的紋理圖冊。第6章將詳細討論紋理圖冊。
將影象高度的一半設為position屬性的y值後,精靈影象恰好能與螢幕底端對齊。
提示:
無論何時都要儘量避免使用固定的位置值。如果你只是把玩家位置設為(160,32),此時你就做了兩個本該避免的假定。第一,你假定了螢幕寬度為320畫素,但並不是每個iOS裝置都是這樣的。第二,你假定了影象高度是64畫素,然而那也是可能會改變的。一旦你開始像這樣做假定,程式碼就會喪失一部分靈活性,修改起來需要很多時間。
我用了很多程式碼來定位物件,但長遠來看這樣做會節約大量時間。可以將專案部署到不同的裝置,也可以使用不同尺寸的影象,無論如何專案都會正常執行。你不再需要修改這段特別的程式碼了。程式設計師面對的最費時的壞事情之一就是——不得不修改那些依據假定而編寫的程式碼。
4.5 加速計輸入
最後一步,我們來使玩家精靈能夠左右傾斜。在第3章中我已經說明,應該為接收加速計輸入的層新增accelerometer方法。這裡我使用了acceleration.x引數,將它乘以10後加到玩家精靈的位置值上,從而加速玩家精靈的運動:
-(void) accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
CGPoint pos = player.position;
pos.x + = acceleration.x * 10;
player.position = pos;
}
注意到奇怪之處了嗎?上面的三行程式碼用一行就能寫好:
// ERROR: lvalue required as left operand of assignment
player.position.x += acceleration.x * 10;
然而,與其他程式語言(如Java、C++或C#等)不同,像player.position.x += value這樣的語句對Objective-C中的屬性是不起作用的。position屬性的型別是CGPoint,這是一個普通的C結構體。Objective-C中的屬性不能直接向結構體的域賦值。問題出在Objective-C中屬性的工作方式,以及Objective-C所基於的C語言的賦值機制。
player.position.x語句實際上是呼叫了position的getter方法[player position],這意味著你實際上獲得了一個臨時的position值,並試圖改變這個臨時的CGPoint物件的x成員變數。但之後這個臨時的CGPoint物件會被丟棄。於是position的setter方法[player setPosition]就不會自動被呼叫了。所以只能直接對player.position進行賦值,在本例中就是賦給它一個新的CGPoint物件。在使用Objective-C時,你必須接受這個令人遺憾的問題,而且如果你有過編寫Java、C++或C#的經歷,沒準還得改變你的程式設計習慣。
因此,前面的程式碼必須建立一個臨時的CGPoint物件,修改position的x域,然後把臨時的CGPoint賦給player.position。在Objective-C中是必須這麼做的。
4.6 首次測試執行
現在你的專案應與本章附帶的DoodleDrop02專案不相上下。馬上測試一下吧。你要確認已選擇在裝置上執行應用,因為模擬器不會獲得加速計輸入。檢驗當前版本中的加速計輸入表現如何。
如果尚未在Xcode中為這個專案安裝你的開發授權,將會產生“code sign”錯誤。在iOS裝置上執行程式時需要程式碼簽名證書。請查閱蘋果公司的文件以瞭解如何建立和安裝必要的開發授權文件(http://developer.apple.com/ios/manage/provisioningprofiles/howto.action)。
4.7 玩家速度
注意到加速計輸入哪裡不正常了嗎?是的,它反應遲緩,移動不暢。這是因為玩家精靈並沒執行真實的加速和減速。讓我們修改一下它。修改後的程式碼在DoodleDrop03專案裡。
實現加速與減速的概念不在於直接改變玩家的位置值,而是使用單獨的CGPoint變數作為速度向量。每次接收到加速計事件時,速度向量就加上從加速計得到的輸入。當然,這意味著我們必須把速度限制在一個任意的最大值內,否則減速時就要花點時間了。不管有沒有接收到加速計輸入,在每一幀都把速度加到玩家位置上。
注意:
為什麼不使用動作來移動玩家精靈呢?無論何時你需要頻繁地——如每秒數次——改變物件的速度或方向,使用move動作都不是一個好的選擇。動作適用於相對使用期較長的一次性物件,所以頻繁建立新物件在分配和釋放記憶體上增加了額外開銷,這會使遊戲效能大幅下降。
更糟糕的是,如果不為動作留出一點時間,動作是不會執行的。這就是在每幀新增新動作來替換前一個卻沒有任何效果的原因。很多cocos2d開發者都曾偶然發現過這個看似古怪的現象。
例如,如果在每幀都停止所有動作併為物件新增一個新的名為MoveBy的動作,物件不會有一丁點的移動!MoveBy動作只會在下一幀改變物件的位置。但是在下一幀你已經停止所有動作,並加入另一個新的MoveBy動作了。這樣做下去只會讓物件寸步不移。這就像老生常談的關於驢的那一套故事:推得越使勁它就越犟,並且在原地不動彈。
讓我們來看一下對程式碼所做的修改。在標頭檔案中加入了playerVelocity變數:
@interface GameLayer : CCLayer
{
CCSprite* player;

CGPoint playerVelocity;
}
你可能想知道為何使用CGPoint替代float。這是考慮到你以後可能會加速或減速一點點。為今後的擴充套件做些準備總沒有壞處。
程式清單4-6是加速計的程式碼,其中使用速度代替了對玩家位置的直接修改。這段程式碼採用了三個設計引數:減速值、加速計靈敏度和最大速度。這些引數沒有最優值;你需要調整數值,找到最適合你遊戲的設定,因此得名“設計引數”。
減速是指減少當前速度,之後速度將加上新的加速計值與靈敏度相乘後的數值。減速值越低,玩家精靈操作外星人改變方向的速度就越快。靈敏度越高,玩家精靈對加速計輸入的反應就越敏感。由於這些數值是對同一數值進行修改,它們相互作用且相互影響,因此一定記得每次只調整一個值。
程式清單4-6 通過GameLayer實現得到playerVelocity
-(void) accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
// controls how quickly velocity decelerates (lower = quicker to change direction)
float deceleration = 0.4f;
// determines how sensitive the accelerometer reacts (higher = more sensitive)
float sensitivity = 6.0f;
// how fast the velocity can be at most
float maxVelocity = 100;

// adjust velocity based on current accelerometer acceleration
playerVelocity.x = playerVelocity.x * deceleration + acceleration.x * 
sensitivity;

// we must limit the maximum velocity of the player sprite, in both directions
if (playerVelocity.x > maxVelocity)
{
playerVelocity.x = maxVelocity;
}
else if (playerVelocity.x - maxVelocity)
{
playerVelocity.x = - maxVelocity;
}
}
現在playerVelocity能夠改變了,但如何把速度加到玩家精靈的位置值上呢?可以在GameLayer的init方法中指定如下update方法:
// schedules the –(void) update:(ccTime)delta method to be called every frame
[self scheduleUpdate];
同樣需要新增–(void) update:(ccTime)delta方法,如程式清單4-7所示。已指定的update方法在每一幀都會被呼叫,而那就是我們要在玩家精靈的位置值上加上速度的地方。這樣我們就可以做到:無論加速計頻率的大小如何,都能產生流暢平滑的運動。
程式清單4-7 用當前速度更新玩家精靈的位置
-(void) update:(ccTime)delta
{
// Keep adding up the playerVelocity to the player's position
CGPoint pos = player.position;
pos.x + = playerVelocity.x;

// The Player should also be stopped from going outside the screen
CGSize screenSize = [CCDirector sharedDirector].winSize;
float imageWidthHalved = player.texture.contentSize.width * 0.5f;
float leftBorderLimit = imageWidthHalved;
float rightBorderLimit = screenSize.width - imageWidthHalved;

// preventing the player sprite from moving outside the screen
if (pos.x < leftBorderLimit)
{
pos.x = leftBorderLimit;
playerVelocity = CGPointZero;
}

else if (pos.x > rightBorderLimit)
{
pos.x = rightBorderLimit;
playerVelocity = CGPointZero;
}

// assigning the modified position back
player.position = pos;
}
邊界檢查可以防止玩家精靈顯示在螢幕外。我們不得不再一次把玩家精靈紋理的contentSize(內容尺寸)考慮在內,因為玩家精靈是以精靈影象的中心為準計算位置的,我們不希望影象的任何一邊離開螢幕。為此,計算imageWidthHalved的值,然後用它檢查剛剛更新的玩家位置是否在左右邊界之內。上面的程式碼稍顯冗長,但這樣更容易理解。現在構建並執行專案,體驗能夠控制玩家精靈的感覺。
提示:
如果玩過Tilt to Live之類的遊戲,可能會注意到這裡實現的簡單的加速計控制無法像在那些遊戲中一樣有一種動態的感覺。這是因為要實現流暢、動態的加速計控制,需要對加速計輸入進行過濾。在Kobolds2D中,使用KKInput類的屬性可以獲得高通(瞬時)和低通(平滑)過濾後的加速計輸入:
float smoothed = [KKInput sharedInput].acceleration.smoothedX;
利用加速計控制的遊戲通常會使用低通過濾器。“低通”意味著過濾掉加速度突然且極端的變化,從而使得到的結果值十分平滑。下面是由加速計輸入值(rawX/rawY)和常量filterFactor(範圍為0.0~1.0)得到新的smoothedX/smoothedY值(示例變數)的低通過濾器。0.1是一個不錯的過濾因子,表示在新的平滑後的值中只考慮了10%的當前原始加速計值:
smoothedX = (rawX * filterFactor) + (smoothedX * (1.0 - filterFactor));
smoothedY = (rawY * filterFactor) + (smoothedY * (1.0 - filterFactor));
4.8 新增障礙物
在往遊戲中加入一些讓玩家躲避的東西之前,這個遊戲還沒有什麼可玩性。接下來在專案里加入一些令人憎惡的東西:六足人造蜘蛛。有誰不想躲著它們嗎?
與玩家精靈一樣,需要把spider.png和spider-hd.png加入到Resources組中。然後在GameLayer.h的介面中加入3個新的成員變數:在程式清單4-9中出現的NSMutableArray類引用spiders,以及在程式清單4-12中使用的spiderMoveDuration和numSpidersMoved:
@interface GameLayer : CCLayer
{
CCSprite* player;
CGPoint playerVelocity;

NSMutableArray* spiders;
float spiderMoveDuration;
int numSpidersMoved;
}
警告:
在程式碼中應該避免使用CCArray。CCArray是NSArray和NSMutableArray的快速替代版本,但是它只是快了一點點,使用它幾乎不會對幀率產生影響。而它的一些方法(如insertAtIndex或removeObjects)要比NSMutableArray的相同方法慢得多。CCArray最大的問題是過去已經存在一些嚴重的bug,比如與ARC的相容性問題。它也不能支援NSArray/NSMutableArray的全部功能。例如,無法列舉帶有block物件的CCArray,這使得它不適合用於並行處理(例如,通過Grand Central Dispatch來完成)。整體上看,NSMutableArray要比CCArray更可靠,相容性更好,並且缺陷更少。任何時候,我都願意用降低一些效能來換取高可靠性。cocos2d在內部使用了CCArray,不過就內部使用來看,CCArray經過了大量測試,證明沒有問題,所以這種用法我是可以接受的。
與此同時,在GameLayer的init方法中,在scheduleUpdate後面加上對initSpiders方法的呼叫,我們將在後面討論它:
-(id) init
{
if ((self = [super init]))
{
...
[self scheduleUpdate];
[self initSpiders];
}
return self;
}
隨後,我們會向GameLayer類中加入一大段程式碼,先從initSpiders方法開始,它建立了蜘蛛精靈,如程式清單4-8所示。
程式清單4-8 為了更好地進入,蜘蛛精靈被初始化並新增到NSMutableArray中
-(void) initSpiders
{
CGSize screenSize = [CCDirector sharedDirector].winSize;

// using a temporary spider sprite is the easiest way to get the image's size
CCSprite* tempSpider = [CCSprite spriteWithFile:@"spider.png"];

float imageWidth = tempSpider.texture.contentSize.width;

// Use as many spiders as can fit next to each other over the whole screen width.
int numSpiders = screenSize.width / imageWidth;

// Initialize the spiders array using alloc.
spiders = [NSMutableArray arrayWithCapacity:numSpiders];
for (int i = 0; i < numSpiders; i++)
{
CCSprite* spider = [CCSprite spriteWithFile:@"spider.png"];
[self addChild:spider z:0 tag:2];

// Also add the spider to the spiders array.
[spiders addObject:spider];
}

// call the method to reposition all spiders
[self resetSpiders];
}
這裡要說明的是,建立名為tempSpider的CCSprite物件只是為了獲得精靈影象的寬度,然後用它決定蜘蛛精靈的數量。獲得影象尺寸最簡單的方法就是建立臨時的CCSprite物件。注意,我沒有把這個tempSpider物件作為子節點加到任何其他節點上,也沒有把它分配給例項變數。這意味著當程式執行離開initSpiders方法後,ARC會知道tempSpider物件已經不再使用,並自動釋放其記憶體。
與其形成對比的是名為spiders的陣列,我用它來儲存對所有蜘蛛精靈的引用。這個陣列被分配給例項變數spiders,因此,在GameLayer物件本身被釋放以前,ARC不會釋放該陣列物件。使用ARC時,不需要自己以任何方式釋放spiders陣列。
在程式清單4-8的結尾,呼叫了[self resetSpiders]方法,這個方法的程式碼在程式清單4-9中。將精靈的初始化和定位分開處理的原因在於:遊戲總會結束,之後遊戲將被重置。最為高效的做法就是將所有遊戲物件移動到它們的初始位置。然而,一旦遊戲趨於複雜,這種做法將不具備可行性。最終,最簡單的做法就是以玩家的等待為代價重新載入全部場景。
警告:
在重新載入場景時,你可能想使用[[CCDirector sharedDirector] replaceScene: self];來重新載入同一場景。但是這會導致程式崩潰,因為self是當前正在執行的場景。在cocos2d中嘗試用正在執行的場景替換其自身會導致程式崩潰。實際上,必須建立GameLayer類的新例項:[[CCDirector sharedDirector] replaceScene:[GameLayer scene]];。
程式清單4-9 重設蜘蛛精靈的位置
-(void) resetSpiders
{
CGSize screenSize = [CCDirector sharedDirector].winSize;

// Get any spider to get its image width
CCSprite* tempSpider = [spiders lastObject];
CGSize size = tempSpider.texture.contentSize;
int numSpiders = [spiders count];
for (int i = 0; i < numSpiders; i++)
{
// Put each spider at its designated position outside the screen
CCSprite* spider = [spiders objectAtIndex:i];
spider.position = CGPointMake(size.width * i + size.width * 0.5f,
screenSize.height + size.height);

[spider stopAllActions];
}

// Schedule the spider update logic to run at the given interval.
[self schedule:@selector(spidersUpdate:) interval:0.7f];

// reset the moved spiders counter and spider move duration (affects speed)
numSpidersMoved = 0;
spiderMoveDuration = 4.0f;
}
我再一次臨時獲取了某個已有的蜘蛛精靈,然後通過紋理的contentsize屬性獲得它的影象尺寸。這裡我沒有建立新的精靈,因為已有同類精靈存在了,並且由於所有的蜘蛛都使用相同尺寸大小的同一影象,我甚至不用關心獲取的是哪個蜘蛛,因此我只是簡單地獲取了陣列的最後一項。
接下來修改每個蜘蛛的位置,使它們整體橫跨整個螢幕的寬度。還是同樣的原因,蜘蛛精靈的紋理以其中心點作為位置,position屬性的x值加上了影象寬度的一半。至於高度,每個蜘蛛也被設為高於螢幕頂端一個影象高度。這個數值是任意的,這裡我要使影象不可見,能達此目的者均可。由於重置後蜘蛛仍然可能在移動,因此要停止它的全部動作。
提示:
如果不是絕對必要的話,為了節約CPU資源,最好不要在for或其他迴圈語句中使用方法呼叫作為迴圈條件。本例中建立numSpiders變數來儲存[spiders count]的呼叫結果,然後將其用作for迴圈的迴圈條件。由於在迴圈過程中陣列本身並未被修改,因此陣列的計數值保持不變。這就是為何我能儲存這個值並在for迴圈中省去對[spiders count]的重複呼叫。
我還指定spidersUpdate:選擇器每0.7秒執行一次——這是另一個蜘蛛從螢幕頂端落下的時間間隔。如果選擇器已被指定,cocos2d會用一條日誌訊息指出這一點,你可以忽略這條訊息。cocos2d並不會再次指定選擇器,而是會更新已指定選擇器的時間間隔。如程式清單4-10所示,spidersUpdate:方法會隨機挑選一個已經存在的蜘蛛,檢查它是否空閒,然後使用一系列動作操作它從螢幕上方落下。
程式清單4-10 spridersUpdate:——讓蜘蛛頻繁下落的方法
-(void) spidersUpdate:(ccTime)delta
{
// Try to find a spider which isn't currently moving.
for (int i = 0; i < 10; i++)
{
int randomSpiderIndex = CCRANDOM_0_1() * spiders.count;
CCSprite* spider = [spiders objectAtIndex:randomSpiderIndex];

// If the spider isn't moving it won't have any running actions.
if (spider.numberOfRunningActions == 0)
{
// This is the sequence which controls the spiders' movement
[self runSpiderMoveSequence:spider];

// Only one spider should start moving at a time.
break;
}
}
}
我還從未對任何程式清單置之不理,是吧?你也許想知道為什麼這裡我要迴圈迭代10次來得到一個隨機的蜘蛛。原因在於,我不知道隨機生成的索引值對應的蜘蛛是不是活動的,所以要確認最終隨機選出的蜘蛛當前是空閒的。如果10次之後——當然,這個數字是任意的——仍然沒有隨機選出一個空閒的蜘蛛,就會跳過這次更新,然後等待下一次。
也可以使用do/while迴圈進行不斷嘗試,直到找到空閒的蜘蛛為止。但有一種可能,即此刻所有的蜘蛛都在移動——這取決於設計引數,如新蜘蛛落下的頻率。遊戲會為嘗試尋找空閒的蜘蛛而無限迴圈,從而鎖死。此外,我並不喜歡太賣力;對於這個遊戲而言,另一個蜘蛛等個幾秒再落下也並無大礙。雖然如此,如果檢視DoodleDrop03專案,就會發現我加入了日誌記錄語句,輸出找到空閒蜘蛛的重試次數。
由於蜘蛛執行的唯一動作就是一系列的運動,我只要檢查蜘蛛當前是否執行任何動作即可。如果沒有執行動作,我就假定它是空閒的。然後執行程式清單4-11所示的runSpiderMoveSequence方法。
程式清單4-11 通過動作序列控制蜘蛛的運動
-(void) runSpiderMoveSequence:(CCSprite*)spider
{
// Slowly increase the spider speed over time.
numSpidersMoved++;
if (numSpidersMoved % 8 == 0 && spiderMoveDuration > 2.0f)
{
spiderMoveDuration - = 0.1f;
}

// This is the sequence which controls the spiders' movement.
CGPoint belowScreenPosition = CGPointMake(spider.position.x,
-spider.texture.contentSize.height);
CCMoveTo* move = [CCMoveTo actionWithDuration:spiderMoveDuration
position:belowScreenPosition];

CCCallBlock* callDidDrop = [CCCallBlock actionWithBlock:^void(){
// move the droppedSpider back up outside the top of the screen
CGPoint pos = spider.position;
CGSize screenSize = [CCDirector sharedDirector].winSize;
pos.y = screenSize.height + spider.texture.contentSize.height;
spider.position = pos;
}];

CCSequence* sequence = [CCSequence actions:move, callDidDrop, nil];
[spider runAction:sequence];
}
runSpiderMoveSequence方法記錄下了落下的蜘蛛數目。每落下8個蜘蛛,spiderMove- Duration就降低,從而增加所有蜘蛛的速度。你也許不熟悉%,它被稱為求模操作符。求模運算的結果是左運算元除以右運算元的餘數,即如果numSpidersMoved可以被8整除,那麼求模結果為0。
動作序列只包含了一個CCMoveTo動作和一個CCCallBlock動作。動作還有改進的空間,可以使它就像真正的六足蜘蛛人那樣,下落一點點,停住,然後一路下到底。這個任務就留給你了,不過在最終版的DoodleDrop專案中,你可以找到一個示例實現。
到目前為止,唯一重要的是知道我選擇了在傳遞給CCCallBlock動作的block函式中重置了蜘蛛的位置。這個block函式可以簡單地使用與runSpiderMoveSequence方法相同的spider變數。它在蜘蛛的運動完成後呼叫,即蜘蛛已經掉到了玩家角色的下面。通過使用這個block函式,你就不需要花大力氣找出正確的蜘蛛。而後,將蜘蛛的位置重置為螢幕上方。程式清單4-12將程式清單4-11中的block函式單獨列了出來。
程式清單4-12 在CCCallBlock中重設蜘蛛位置,使之可以從螢幕上方重新落下
CCCallBlock* callDidDrop = [CCCallBlock actionWithBlock:^void(){
// move the droppedSpider back up outside the top of the screen
CGPoint pos = spider.position;
CGSize screenSize = [CCDirector sharedDirector].winSize;
pos.y = screenSize.height + spider.texture.contentSize.height;
spider.position = pos;
}];
到目前為止,一切順利。我猜你一定迫不及待地想試玩一下。我想你會很快注意到遊戲還是缺了些東西。小提示:看一下下面的標題。

《ios cocos2d 2 遊戲開發實戰(第三版)》試讀電子書免費提供,有需要的留下郵箱。

轉載於:https://my.oschina.net/cjkall/blog/195970