为了使大家对ccsprite和各相关类的使用有更加直观的印象,下面我们结合前面的游戏示例,使用精灵表单优化游戏性能,同时在游戏开始和结束时添加菜单,让玩家对游戏有更多控制权。当然,在这个示例小游戏中,这种优化是看不出差别的。但这是最佳实践,建议读者以后编写游戏都以这种方式使用精灵。
**3.8.1 注释draw方法和背景
**
首先,在xcode中打开之前的项目中把draw方法注释掉,同时恢复先前注释掉的添加游戏背景的代码段,编译并运行,如图3-6所示。
注意 这时必须触碰玩家飞机才可以发射子弹,同时,此刻的碰撞检测代码也有所差别,当然,玩家是感觉不到的。
3.8.2 加载游戏资源
首先需要让玩家决定什么时候开始游戏,因此要在游戏正式开始前添加一个菜单用来开始游戏。只有当玩家触碰此菜单时,才会正式开始游戏。为了使游戏开发更加贴近实际开发,这里介绍如何制作一个loadingscreen场景来加载游戏所需要的图片、音效、背景音乐、存档等资源。
1 . 创建loadingscreen
为了简单起见,该loadingscreen只显示一个loading字样,没有使用进度条。后面章节的实例中再考虑使用进度条,这样用户体验会更好一些。虽然界面显示比较简单,但这里向大家介绍的是一种通用的预先加载游戏资源的做法,以后任何cocos2d游戏都可以使用此方法进行加载。
单击file→new→file,从左边选择cocos2d v2.x模板,在右边的模板类中选择ccnode class模板类,选择cclayer作为基类,然后选择next并保存到项目的verticalshootinggame文件夹下。
用代码清单3-8中的代码替换loadingscreen.h的内容。
代码清单3-8 loadingscreen.h文件替换代码
loadingscreen继承自cclayer,提供一个静态scene方法供ccdirector对象调用。接下来定义3个成员变量,其中winsize代表游戏屏幕窗口大小,wincenter代表窗口的中点,assetcount用来保存游戏需要加载的资源总数。
2 . loading方法
重点是接下来一系列的loading方法,每个方法都接收一个nsarray数组作为参数。这些参数都是一些具体资源的文件名,参数的值是从一个配置文件中读出来的,后面我们在介绍实现时会给出来。这些方法及用途如下:
-(void) loadmusic:(nsarray *) musicfiles;(加载背景音乐)
-(void) loadsounds:(nsarray *) soundclips;(加载游戏音效)
-(void) loadspritesheets:(nsarray *) spritesheets;(加载精灵表单)
-(void) loadimages:(nsarray *) images;(加载背景图片等图片资源)
-(void) loadassets:(nsarray *) assets;(加载游戏字体、存档等资源)
-(void) progressupdate;(更新游戏进度条,目的只是计算何时加载完成)
-(void) loadingcomplete;(资源全部加载完成,切换到另一个游戏场景)
3 . loadingscreen的具体实现
打开loadingscreen.m文件,用代码清单3-9替换其中的内容。
代码清单3-9 loadingscreen.m
虽然代码比较多,但是不要害怕,我们会依次解释其中的每个方法。
(1)+(ccscene *) scene
这个类方法的实现代码跟一般的有一些不同,它使用objective-c的动态语言特性反射。首先使用nsstringfromclass得到类名:
此处classname为loadingscreen。然后使用nsclassfromstring得到该类的类型:
这里还会给该类型发送一个node消息。这种反射和任意发送消息的能力使objective-c语言具有强大的灵活性,也为游戏开发提供大量的便利。我们可以把关卡的配置、敌人的类型直接存储为文件,然后反射解析。
(2)init方法
该方法的实现非常简单,就是计算屏幕窗口大小和中点位置,然后初始一个cclabelttf对象,用来显示loading字样。
下面我们来看比较重要的onentertransitiondidfinish方法。
(3)onentertransitiondidfinish方法
该方法首先加载一个配置文件,然后读取配置文件中的游戏资源名字列表并存储在不同的数组中。示例代码如下:
首先使用ccfileutils获得preloadassetmanifest.plist文件的具体路径,调用nsdictionary的dictionarywithcontentsoffile把该文件转换成一个字典对象;然后通过key值取出每种不同类型资源的数组;最后调用数组的count方法得到总共需要加载的资源数量。
继续之前,我们首先需要将资源添加到项目中,打开本章附带示例项目的chapter3/resource/progress目录,将其中的资源全部添加进项目中;然后查看preloadassetmanifest.plist的具体配置情况,在xcode中打开resources分组下的arts文件夹,并展开其中的项目,如图3-7所示。
由图3-7可以看出,加载的音效是bullet.mp3,精灵表单为gamearts.plist,背景音乐是game_music.mp3。
注意 本项目比较简单,资源数量有限,完成加载场景基本体会不到加载的过程,因为速度实在是太快了。
接下来我们看看后面的代码。
这段代码并不复杂,调用performselectoronmainthread在主线程上依次加载每种类型的游戏资源,同时waituntildone的值为yes能保证所有的资源按照序列依次加载。
其他方法这里就不一一介绍了,注意几个方法。比如:
预先加载音效:[[simpleaudioengine sharedengine] preloadeffect:soundclip]
预先加载背景音乐:[[simpleaudioengine sharedengine] preloadbackgroundmusic:music]
预先加载png图片:[[cctexturecache sharedtexturecache] addimage:image]
预先加载精灵表单可以使用下面的方法:
这个方法首先会加载与该plist方法名称相同但后缀为.png的纹理图片,把该plist的所有spirteframe信息读取出来,以后可以通过spriteframewithname获取相应的精灵帧。
(4)progressupdate和loadingcomplete方法
目前,progressupdate方法非常简单,只是更新资源的总数,当资源全部加载完毕时会调用loadingcomplete方法。loadingcomplete方法的作用是:过2秒之后运行一个场景切换特效跳转到游戏主场景,即helloworldlayer。
3.8.3 修改appdelegate.m文件
1)添加所包含的头文件。在文件的顶部添加代码:
2)修改- (bool)application:(uiapplication *)application
didfinishlaunchingwithoptions:(nsdictionary *)launchoptions方法。
在该方法最后,把之前的pushscene方法改成下面代码:
编译并运行游戏,首先会看到一个loading界面,再进入游戏场景。但是我们希望在场景跳转之后,玩家可以看到有一个开始游戏的按钮,只有当玩家选择“开始游戏”时,敌机才出现,游戏才正式开始。因此这里需要对helloworldlayer的实现做一些修改。
3.8.4 修改helloworldlayer
1)打开helloworldlayer.h文件,在其中添加下列变量:
2)打开helloworldlayer.m的init方法,添加如代码清单3-10所示代码。
代码清单3-10 在init方法中添加代码
首先注意,这里把之前的代码清单2-11、2-14和2-21的部分代码移到block方法的内部。这里还需要注意,因为只有当玩家触碰“开始游戏”按钮之后,才会正式允许玩家控制飞机的飞行以及发射子弹,所以一定要把menu的position设置为cgpointzero。
此时编译并运行,加载场景过程效果如图3-8所示,加载完资源以后的游戏画面如3-9所示。
注意 这里所使用的菜单是最简陋的一种,是直接使用系统内置字体创建的。当然,大家还可以通过label、sprite创建更加丰富多彩的按钮,可以参考cocos2d自带的menutest项目。
3.8.5 代码重构
接下来要解决本章最重要的代码重构问题,使用spriteframecache和spriteframe初始化精灵,这里用精灵表单来优化游戏性能。
1 . 添加精灵表单
在init方法的最上面添加代码:
这里给spritebatchnode添加一个新的tag,所以需要在helloworldlayer.m文件最上方定义这个枚举常量:
2 . 修改sprite的初始化方式
把spritewithfile改写成spritewithspriteframename,同时把sprite加到batchnode中而不是加到layer里。这样做的好处是减少opengl call的次数,提高游戏渲染性能。具体如代码清单3-11所示。
代码清单3-11 使用精灵表单减少opengl call的次数
此时运行代码,我们会发现有点问题,游戏行为变得有些古怪,因为之前操作playersprite时是通过[self getchildbytag:ktagplayer]来获取玩家精灵的。现在我们要改成[batchnode getchildbytag:ktagplayer]。
3 . 定义-(ccsprite*) getplayersprite方法
为了减少重复修改,这里需要定义一个新方法-(ccsprite*) getplayersprite,其实现如代码清单3-12所示。
代码清单3-12 -(ccsprite*) getplayersprite实现
要理解上述代码,我们的头脑中需要有一个node的层级关系图,首先通过当前layer找到batchnode,然后通过batchnode找到playersprite。
此时还需要稍微修改其他内容来获得playersprite的方法,修改以下代码:
将其改写成:
共有三处需要修改,大家可以参考随书附赠的源码,这里就不再赘述了。
4 . 修改发射子弹的碰撞检测算法
另外单击玩家发射子弹的碰撞检测算法也要进行修改,如代码清单3-13所示。
代码清单3-13 修改单击玩家就发射子弹的碰撞检测算法
相信通过这段代码大家能更清楚地认识到,bondingbox是相对于节点的父节点来计算的。如果要判断一个点是否在一个矩形区域内,首先要把它们都转化到同一个坐标系中,才能进行相应的判断。本例中把touch点和玩家飞机的矩形区域都转化到batchonode的坐标空间中。
将-(void) updateplayerposition:(cctime)dt方法中计算图片纹理宽度一半的语句:
改成以下代码:
注意 谨慎使用sprite.texture.contentsize,只有当使用精灵图片文件初始化精灵时这种写法才有效,如果使用spritebatchnode则会失效!!!
如果使用ccspritebatchnode创建精灵对象,就不能将精灵对象添加为当前层的子节点,而应添加为精灵表单的子节点。另外,以上方法是为了让大家对ccsprite相关类有更清晰的认识,在实际游戏开发中,并不需要使用上述这么复杂的步骤。
编译并运行到真机上面,好好享受学习的成果吧!