瘋狂iOS講義之瘋狂打飛機(1)
本次我們將帶領大家手動完成一個簡單但功能完整的打飛機游戲,實現飛機飛行、飛機碰撞、發射子彈、敵機發射大子彈、背景音樂、子彈音效、分數統計、菜單管理等功能。它雖然不會為你贏得什么獎項,但是可以總結前面所學的所有知識,幫助大家更好地掌握cocos2d基本對象的使用,同時體驗cocos2d的強大以及易用性。
一、開始前的準備工作
首先打開Xcode,使用cocos2d iOS模板新建一個項目,命名為“AirfightGame”,然后選擇一個目錄,單擊“Create”按鈕。為cocos2d項目的源代碼添加-fno-objc-arc選項讓項目支持ARC。
接下來,將所需要的資源文件,包括圖片和聲音拖到項目的“Resources”組。在游戲開發當中,通常都會使用精靈表單來優化游戲性能,在這個小游戲當中,雖然這種性能優化并不會有特別明顯的效果,但是建議大家以后開發游戲時都使用精靈表單來提高游戲性能。使用Zwoptex將所有圖片制作成精靈表單,生成對應的airfightSheet.png和airfightSheet.plist文件,并將這兩個文件拖到項目的“Resources”組。
二、添加游戲菜單項功能
現在,我們來為游戲添加一個菜單設置功能,在這里可以完成開始游戲、游戲設置、退出游戲等操作。步驟如下。
① 選擇“AirfightGame”組并單擊右鍵,選擇“New File”,在左邊欄中選擇“cocos2d v2.x”模板,在右邊的模板類中選擇“CCNode class”模板類,“Subclass of”選擇“CCLayer”,然后單擊“Next”按鈕。命名為“MenuLayer”,然后單擊“Create”按鈕。
MenuLayer繼承自CCLayer,提供一個類方法scene供CCDirector對象調用。該類的作用是顯示一個菜單場景,讓用戶選擇。
打開MenuLayer.m文件,實現代碼如下。
程序清單:codes/13/13.14/AirfightGame/AirfightGame/MenuLayer.m
- -(id) init
- {
- if( (self=[super init]) ) {
- CGSize winSize = [[CCDirector sharedDirector] winSize];
- // 創建“開始游戲”標簽,當觸碰該標簽時,調用startGame:方法
- CCMenuItemFont* startItem = [CCMenuItemFont itemWithString:@"開始游戲"
- target:self selector:@selector(startGame:)];
- startItem.position=ccp(winSize.width/2, winSize.height*0.6);
- // 創建“游戲設置”標簽,當觸碰該標簽時,調用setting:方法
- CCMenuItemFont* settingItem = [CCMenuItemFont itemWithString:@"游戲設置"
- target:self selector:@selector(setting:)];
- // 設置“游戲設置”標簽位置
- settingItem.position=ccp(winSize.width/2, winSize.height*0.4);
- // 創建控制菜單,并將兩個標簽添加進去
- CCMenu* menu = [CCMenu menuWithItems:startItem,settingItem, nil];
- menu.position = CGPointZero;
- [self addChild:menu];
- }
- return self;
- }
init方法比較簡單,創建了兩個CCMenuItemFont,選擇標簽時會調用對應的startGame:和setting:方法,并將它們添加到CCMenu當中,再將CCMenu添加為當前層的子節點。
② 添加startGame:和setting:兩個方法,實現代碼如下(程序清單同上):
- -(void) startGame:(id)sender{
- // 切換到PreloadLayer場景
- CCTransitionSlideInL* transitionScene = [CCTransitionSlideInL
- transitionWithDuration:2.0 scene:[PreloadLayer scene]];
- [[CCDirector sharedDirector] replaceScene:transitionScene];
- }
startGame:方法非常簡單,當用戶選擇“開始游戲”標簽時,場景切換到PreloadLayer,在下一節中將重點介紹PreloadLayer(程序清單同上)。
- -(void) setting:(id)sender{
- // 切換到SettingLayer場景
- CCTransitionSlideInL* transitionScene = [CCTransitionSlideInL
- transitionWithDuration:2.0 scene:[SettingLayer scene]];
- [[CCDirector sharedDirector] replaceScene:transitionScene];
- }
當用戶選擇“游戲設置”標簽時,場景切換到SettingLayer,進行游戲設置。
③ 同上面的步驟一樣,使用“cocos2d v2.x”模板創建一個類并命名為“SettingLayer”,繼承自CCLayer。該類的實現代碼如下。
程序清單:codes/13/13.14/AirfightGame/AirfightGame/SettingLayer.m
- -(id) init
- {
- if( (self=[super init]) ) {
- CGSize winSize = [[CCDirector sharedDirector] winSize];
- // 提示菜單項
- CCMenuItemFont* musicItem = [CCMenuItemFont itemWithString:@"背景音樂:"];
- musicItem.position = ccp(winSize.width*0.4, winSize.height*0.6);
- // 創建“開”和“關”菜單項
- CCMenuItemFont* musicOn = [CCMenuItemFont itemWithString:@"開"];
- CCMenuItemFont* musicOff = [CCMenuItemFont itemWithString:@"關"];
- // CCMenuItemToggle,默認顯示“開”。開=0,關=1
- CCMenuItemToggle* musicToggle = [CCMenuItemToggle itemWithTarget:self
- selector:@selector(change:) items:musicOff,musicOn, nil];
- musicToggle.position = ccp(winSize.width*0.6, winSize.height*0.6);
- // 創建“返回主菜單“菜單項
- CCMenuItemFont* returnItem = [CCMenuItemFont itemWithString:@"返回主菜單"
- target:self selector:@selector(backToMainLayer:)];
- returnItem.position = ccp(winSize.width/2, winSize.height*0.4);
- // 創建控制菜單,并將3個標簽添加進去
- CCMenu* menu = [CCMenu menuWithItems:musicItem,musicToggle,returnItem, nil];
- menu.position = CGPointZero;
- [self addChild:menu];
- // NSUserDefaults用戶首選項可以用來保存用戶在操作應用的過程中設置的首選項。
- NSUserDefaults* userDef = [NSUserDefaults standardUserDefaults];
- // 如果Bool為No,則顯示1=關
- if(![userDef boolForKey:@"music"]){
- musicToggle.selectedIndex = 1;
- }
- }
- return self;
- }
- -(void) change:(id)sender{
- // 判斷mute(靜音)屬性,根據屬性狀態進行切換
- if([CDAudioManager sharedManager].mute == TRUE){
- [CDAudioManager sharedManager].mute = FALSE;
- }else{
- [CDAudioManager sharedManager].mute = TRUE;
- }
- NSUserDefaults* userDef = [NSUserDefaults standardUserDefaults];
- CCMenuItemToggle* tooggle = (CCMenuItemToggle*)sender;
- // 關=1,設置Bool為NO
- if(tooggle.selectedIndex == 1){
- [userDef setBool:NO forKey:@"music"];
- }else{
- [userDef setBool:YES forKey:@"music"];
- }
- }
- // 定義一個CCTransitionSlideInL場景切換效果,并使用CCDirector單例對象來切換場景
- -(void) backToMainLayer:(id)sender{
- CCTransitionSlideInL* transitionScene =
- [CCTransitionSlideInL transitionWithDuration:2.0 scene:[MenuLayer scene]];
- [[CCDirector sharedDirector] replaceScene:transitionScene];
- }
SettingLayer類代碼在13.13.2節中已經詳細介紹過,這里不再贅述。
④ 修改IntroLayer.m文件
IntroLayer默認加載HelloWorldLayer,但此時我們不再使用HelloWorldLayer作為應用的第一個場景,而是使用MenuLayer作為應用的第一個場景,因此需要修改IntroLayer,將IntroLayer改為加載MenuLayer場景。修改如下。
在IntroLayer.m文件的頂部添加所包含的頭文件:
- #import "MenuLayer.h"
修改-(void) makeTransition:(ccTime)dt方法,將該方法改成下面的代碼。
程序清單:codes/13/13.14/AirfightGame/AirfightGame/IntroLayer.m
- -(void) makeTransition:(ccTime)dt
- {
- [[CCDirector sharedDirector] replaceScene:
- [CCTransitionFade transitionWithDuration:1.0
- scene:[MenuLayer scene] withColor:ccWHITE]];
- }
編譯并運行游戲,運行時模擬器顯示效果如圖13.58所示。
三、預加載游戲資源
在真實項目當中,在游戲開始前,都會預先加載游戲所需要的圖片、背景音樂、音效等資源,這里介紹如何制作一個PreloadLayer來預加載游戲資源。
1. 創建PreloadLayer
選擇“AirfightGame”組并單擊右鍵,選擇“New File”,在左邊欄中選擇“cocos2d v2.x”模板,在右邊的模板類中選擇“CCNode class”模板類,“Subclass of”選擇“CCLayer”,然后單擊“Next”按鈕。命名為“PreloadLayer”,然后單擊“Create”按鈕。
PreloadLayer繼承自CCLayer,提供一個類方法scene供CCDirector對象調用。該類的作用是預加載游戲資源,在加載過程中會顯示一個進度條,進度條全部顯示完成代表加載完畢,加載完畢后顯示游戲主場景。
首先打開PreloadLayer.m文件,先在文件上方定義一個私有的Category。實現代碼如下。
程序清單:codes/13/13.14/AirfightGame/AirfightGame/PreloadLayer.m
- /**
- 定義一個私有的Category,為了不讓API暴露給客戶端
- 將一些類內部所使用的方法和變量放在私有的擴展里面,而不是直接聲明在頭文件當中
- */
- @interface PreloadLayer ()
- - (void) loadMusics:(NSArray *) musicFiles; // 加載背景音樂
- - (void) loadSounds:(NSArray *) soundClips; // 加載游戲音效
- - (void) loadSpriteSheets:(NSArray *) spriteSheets; // 加載精靈表單
- - (void) loadingComplete; // 資源全部加載完成,切換到另一個游戲場景
- - (void) progressUpdate; // 更新游戲進度條,計算何時加載完成
- @end;
這里定義了一系列的load方法,每個方法接收一個NSArray數組作為參數。這些參數代表一些具體資源的文件名,參數值會從一個配置文件中讀取出來,該配置文件在之后的代碼實現時會給出。
然后定義3個變量,其中sourceCount用來保存游戲需要加載的資源總數;progress用于顯示進度條,CCProgressTimer 類是cocos2d中對進度條的一個封裝,用來實現各種進度條功能,非常方便,之后我們還會使用該類來實現游戲的自定義血條量;progressInterval代表進度條更新的次數。實現代碼如下
- @implementation PreloadLayer
- // 用來保存游戲需要加載的資源總數
- int sourceCount;
- // 顯示進度條的成員變量
- CCProgressTimer* progress;
- // 代表進度條更新的次數
- float progressInterval;
2. PreloadLayer的具體實現
創建一個preloadResources.plist文件,該文件用于保存項目需要的所有資源文件,文件內容如圖13.59所示。
可以看出,加載的音效是b0.mp3,精靈表單是airfightSheet.plist,背景音樂是s3.wav。
在PreloadLayer.m文件中添加代碼。實現代碼如下。
程序清單:codes/13/13.14/AirfightGame/AirfightGame/PreloadLayer.m
- + (CCScene *) scene
- 002 {
- 003 CCScene* scene = [CCScene node];
- 004 PreloadLayer* layer = [PreloadLayer node];
- 005 [scene addChild:layer];
- 006 return scene;
- 007 }
- 008 - (id) init{
- 009 if((self = [super init])){
- 010 // 獲取屏幕大小
- 011 CGSize winSize = [[CCDirector sharedDirector] winSize];
- 012 // 創建一個進度條精靈
- 013 CCSprite* barSprite = [CCSprite spriteWithFile:@"progressbar.png"];
- 014 // 初始化一個CCProgressTimer進度條對象
- 015 progress = [CCProgressTimer progressWithSprite:barSprite];
- 016 // setPercentage:0.0f,表示并未加載任何資源,表現在屏幕上就是什么也看不見
- 017 [progress setPercentage:0.0f];
- 018 // 由于圖片大小關系,把scale設置成0.5,即縮小一半
- 019 progress.scale = 0.5;
- 020 // 設置進度條動畫的起始位置,默認在圖片的中點
- 021 // 如果想要顯示從左到右的一個動畫效果,必須改成(0,y)
- 022 progress.midpoint = ccp(0,0.5);
- 023 // barChangeRate表示是否改變水平或者垂直方向的比例,設置成1表示改變,0表示不改變
- 024 progress.barChangeRate = ccp(1,0);
- 025 // 本例制作一個從左至右的水平進度條,所以midpoint應該是(0,0.5)
- 026 // 因為x方向需要改變,而y方向不需要改變,所以barChangeRate = ccp(1, 0)
- 027 // kCCProgressTimerTypeBar表示為條形進度條
- 028 progress.type = kCCProgressTimerTypeBar;
- 029 // 設置position在中心點
- 030 [progress setPosition:ccp(winSize.width/2,winSize.height/2)];
- 031 // 將進度條添加為當前層的子節點
- 032 [self addChild:progress];
- 033 }
- 034 return self;
- 035 }
- 036 - (void) onEnterTransitionDidFinish{
- 037 [super onEnterTransitionDidFinish];
- 038 // 加載preloadResources.plist配置文件
- 039 NSString* path = [[CCFileUtils sharedFileUtils]
- 040 fullPathFromRelativePath:@"preloadResources.plist"];
- 041 // 讀取配置文件中的游戲資源名稱列表,返回一個NSDictionary對象
- 042 NSDictionary* resources = [NSDictionary dictionaryWithContentsOfFile:path];
- 043 // 通過key值取出每種不同類型資源的數組
- 044 NSArray *spriteSheets = [resources objectForKey:@"SpriteSheets"];
- 045 NSArray *sounds = [resources objectForKey:@"Sounds"];
- 046 NSArray *musics = [resources objectForKey:@"Musics"];
- 047 // 調用數組的count方法得到總共需要加載的資源數量
- 048 sourceCount = [spriteSheets count] + [sounds count] + [musics count];
- 049 // 設置進度條更新次數=100/需要加載的資源數量
- 050 progressInterval = 100.0 / (float) sourceCount;
- 051 // 調用performSelectorOnMainThread在主線程上依次加載每種類型的游戲資源
- 052 // waitUntilDone的值為YES能保證所有的資源按照序列依次加載
- 053 if(sounds){
- 054 [self performSelectorOnMainThread:@selector(loadSounds:)
- 055 withObject:sounds waitUntilDone:YES];
- 056 }
- 057 if(spriteSheets){
- 058 [self performSelectorOnMainThread:@selector(loadSpriteSheets:)
- 059 withObject:spriteSheets waitUntilDone:YES];
- 060 }
- 061 if(musics){
- 062 [self performSelectorOnMainThread:@selector(loadMusic:)
- 063 withObject:musics waitUntilDone:YES];
- 064 }
- 065 }
- 066 // 加載背景音樂
- 067 - (void) loadMusics:(NSArray *)musicFiles{
- 068 for (NSString *music in musicFiles) {
- 069 [[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:music];
- 070 [self progressUpdate];
- 071 }
- 072 }
- 073 // 加載聲音
- 074 - (void) loadSounds:(NSArray *)soundClips{
- 075 for (NSString *soundClip in soundClips) {
- 076 [[SimpleAudioEngine sharedEngine] preloadEffect:soundClip];
- 077 [self progressUpdate];
- 078 }
- 079 }
- 080 // 加載精靈表單
- 081 - (void) loadSpriteSheets:(NSArray *)spriteSheets{
- 082 for (NSString *spriteSheet in spriteSheets) {
- 083 // 該方法會加載與該plist文件名稱相同但后綴為.png的紋理圖片
- 084 // 把該plist的所有spriteFrame信息讀取出來
- 085 // 在之后的代碼中可以通過spriteFrameWithName獲取相應的精靈幀
- 086 // 本例中airfightSheet.plist對應airfightSheet.png
- 087 [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile: spriteSheet];
- 088 [self progressUpdate];
- 089 }
- 090 }
- 091 - (void) progressUpdate{
- 092 // 每次調用該方法說明加載一個資源,自減更新資源總數
- 093 if (--sourceCount) {
- 094 [progress setPercentage:100.0f-(progressInterval * sourceCount)];
- 095 }else{
- 096 // CCProgressFromTo動作用于以漸進的方式顯示圖片
- 097 // actionWithDuration表示持續0.5秒,from表示進度條百分百從開始一直到100
- 098 CCProgressFromTo *ac = [CCProgressFromTo actionWithDuration:0.5
- 099 from:progress.percentage to:100];
- 100 // 當資源全部加載完畢時調用loadingComplete方法
- 101 CCCallBlock *callback = [CCCallBlock actionWithBlock:^() {
- 102 [self loadingComplete];
- 103 }];
- 104 // CCSequence組合動作
- 105 id action = [CCSequence actions:ac.callback.nil];
- 106 // 進度條執行動作
- 107 [progress runAction:action];
- 108 }
- 109 }
- 110 // 延遲2秒之后運行一個場景切換特效跳轉到游戲主場景,即HelloWorldLayer
- 111 - (void) loadingComplete{
- 112 CCDelayTime *delay = [CCDelayTime actionWithDuration:2.0f];
- 113 CCCallBlock *callblock = [CCCallBlock actionWithBlock:^(void) {
- 114 [[CCDirector sharedDirector] replaceScene:
- 115 [CCTransitionFade transitionWithDuration:1.0f scene:[HelloWorldLayer scene]]];
- 116 }];
- 117 CCSequence *sequence = [CCSequence actions:delay.callblock.nil];
- 118 [self runAction:sequence];
- 119 }
- 120 @end
下面依次解釋以上代碼中的每一個方法。
Ø +(CCScene *) scene方法很簡單,和前面的一樣,首先創建了一個scene場景,然后創建了一個PreloadLayer層,將PreloadLayer層作為scene場景的子節點,最后返回scene場景。
Ø init方法首先獲取屏幕窗口大小,然后創建了一個進度條。這里使用progressbar.png圖片初始化一個精靈,再通過該精靈初始化一個CCProgressTimer對象,設置setPercentage屬性為0,表示當前未加載任何資源,表現在屏幕上就是什么也看不見。由于圖片大小關系,把scale設置成0.5,即縮小一半。
接下來設置CCProgressTimer對象最重要的3個參數。
q Ø midpoint:表示進度條動畫的起始位置,默認在圖片的中點,如果想要顯示從左到右的一個動畫效果,則必須改成(0,y)。
q Ø barChangeRate:表示是否改變水平或者垂直方向的比例,設置成1表示改變,0表示不改變。本例制作一個從左至右的水平進度條,所以midpoint應該是(0,0.5)。因為x方向需要改變,而y方向不需要改變,所以設置barChangeRate為ccp(1,0)。
q Ø type:設置為kCCProgressTimerTypeBar,表示條形進度條。
關于CCProgressTimer類的使用可以參考官方文檔,讀者也可以找到cocos2d的示例項目cocos2d-tests-ios.xcodeproj,并運行ActionProgressTest這個TARGET。感興趣的讀者也可以仔細分析該項目中的ActionProgressTest.m源文件(位于項目的tests目錄下)來掌握不同progress的用法示例。
最后設置CCProgressTimer對象的位置,并添加為當前層的子節點。
onEnterTransitionDidFinish方法加載配置文件preloadResources.plist,讀取配置文件中的游戲資源名稱列表并存儲在不同的數組中。首先使用CCFileUtil獲得plist文件的具體路徑,調用NSDictionary的dictionaryWithContentsOfFile方法把該文件轉換成一個字典對象。然后通過字典的key值取出不同類型資源的數組,調用每個數組的count方法累加得到總共需要加載的資源總數量,使用100除以資源總數量獲得進度條需要更新次數用于之后計算進度條顯示的百分比。最后將數組作為參數調用performSelectorOnMainThread: withObject: waitUntilDone:方法在主線程中依次加載每種類型的游戲資源,將waitUntilDone的值設置為“YES”能保證所有的資源按照序列依次加載。
loadMusics:和loadSounds:方法比較簡單,通過循環遍歷數組,預加載背景音樂和音效,加載完后調用progressUpdate方法更新進度條。
這里需要注意的是loadSpriteSheets:方法,該方法循環遍歷數組,數組的每個元素是一個plist文件名稱,調用CCSpriteFrameCache的addSpriteFramesWithFile時,該方法會加載與該plist文件名稱相同但后綴為.png的紋理圖片(本例中airfightSheet.plist對應airfightSheet.png),把該plist的所有spriteFrame信息讀取出來,之后在項目當中就可以通過spriteFrameWithName獲取相應的精靈幀了。
progressUpdate方法比較簡單,每次被調用時自減更新資源總數變量,修改進度條的百分比。當資源全部加載完畢時調用loadingComplete方法。
loadingComplete方法被調用說明資源加載完畢,延遲2秒后運行一個場景切換特效跳轉到游戲主場景HelloWorldLayer。
編譯并運行游戲,選擇“開始游戲”菜單項,模擬器首先會顯示一個進度條,進度條全部顯示完畢切換到HelloWorldLayer顯示經典的Hello World畫面。恭喜你!資源文件加載成功,進度條功能實現。運行時模擬器顯示效果如圖13.60所示。
————本文節選自《瘋狂ios講義(下)》