本次我们将带领大家手动完成一个简单但功能完整的打飞机游戏,实现飞机飞行、飞机碰撞、发射子弹、敌机发射大子弹、背景音乐、子弹音效、分数统计、菜单管理等功能。它虽然不会为你赢得什么奖项,但是可以总结前面所学的所有知识,帮助大家更好地掌握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讲义(下)》