为了使大家对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文件替换代码
#import <Foundation/Foundation.h> #import "cocos2d.h" @interface LoadingScreen : CCLayer { CGSize winSize; CGPoint winCenter; int assetCount; } +(CCScene *) scene; -(void) loadMusic:(NSArray *) musicFiles; -(void) loadSounds:(NSArray *) soundClips; -(void) loadSpriteSheets:(NSArray *) spriteSheets; -(void) loadImages:(NSArray *) images; -(void) loadAssets:(NSArray *) assets; -(void) progressUpdate; -(void) loadingComplete; @endLoadingScreen继承自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
#import "LoadingScreen.h" #import "SimpleAudioEngine.h" //The next scene you wish to transition to #import "HelloWorldLayer.h" @implementation LoadingScreen +(CCScene *) scene { // 'scene' is an autorelease object CCScene *scene = [CCScene node]; NSString *className = NSStringFromClass([self class]); // 'layer' is an autorelease object. id layer = [NSClassFromString(className) node]; // add layer as a child to scene [scene addChild: layer]; // return the scene return scene; } -(id) init { if ( ( self = [ super init] ) ) { winSize = [[CCDirector sharedDirector] winSize]; winCenter = ccp(winSize.width / 2, winSize.height / 2); CCLabelTTF *loadingText = [CCLabelTTF labelWithString:@"Loading..." fontName:@"Arial" fontSize:20]; loadingText.position = ccpAdd(winCenter, ccp(0,50)); [self addChild:loadingText]; } return self; } -(void) onEnterTransitionDidFinish { [super onEnterTransitionDidFinish]; NSString *path = [[CCFileUtils sharedFileUtils] fullPathFromRelativePath:@"preloadAssetManifest.plist"]; NSDictionary *manifest = [NSDictionary dictionaryWithContentsOfFile:path]; NSArray *spriteSheets = [manifest objectForKey:@"SpriteSheets"]; NSArray *images = [manifest objectForKey:@"Images"]; NSArray *soundFX = [manifest objectForKey:@"SoundFX"]; NSArray *music = [manifest objectForKey:@"Music"]; NSArray *assets = [manifest objectForKey:@"Assets"]; assetCount = ([spriteSheets count] + [images count] + [soundFX count] + [music count] + [assets count]); if (soundFX) [self performSelectorOnMainThread:@selector(loadSounds:) withObject:soundFX waitUntilDone:YES]; if (spriteSheets) [self performSelectorOnMainThread:@selector(loadSpriteSheets:) withObject:spriteSheets waitUntilDone:YES]; if (images) [self performSelectorOnMainThread:@selector(loadImages:) withObject:images waitUntilDone:YES]; if (music) [self performSelectorOnMainThread:@selector(loadMusic:) withObject:music waitUntilDone:YES]; if (assets) [self performSelectorOnMainThread:@selector(loadAssets:) withObject:assets waitUntilDone:YES]; } -(void) loadMusic:(NSArray *) musicFiles { CCLOG(@"Start loading music"); for (NSString *music in musicFiles) { [[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:music]; [self progressUpdate]; } } -(void) loadSounds:(NSArray *) soundClips { CCLOG(@"Start loading sounds"); for (NSString *soundClip in soundClips) { [[SimpleAudioEngine sharedEngine] preloadEffect:soundClip]; [self progressUpdate]; } } -(void) loadSpriteSheets:(NSArray *) spriteSheets { for (NSString *spriteSheet in spriteSheets) { [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:spriteSheet]; [self progressUpdate]; } } -(void) loadImages:(NSArray *) images { CCLOG(@"LoadingScreen - loadImages : You need to tell me what to do."); for (NSString *image in images) { //Do something with the images [self progressUpdate]; } } -(void) loadAssets:(NSArray *) assets { //Overwrite me CCLOG(@"LoadingScreen - loadAssets : You need to tell me what to do."); for (NSString *asset in assets) { //Do something with the assets [self progressUpdate]; } [self progressUpdate]; } -(void) progressUpdate { if (--assetCount) { //以备后面显示进度条用 } else { [self loadingComplete]; CCLOG(@"All done loading assets."); } } -(void) loadingComplete { CCDelayTime *delay = [CCDelayTime actionWithDuration:2.0f]; CCCallBlock *swapScene = [CCCallBlock actionWithBlock:^(void) { [[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0f scene:[HelloWorldLayer scene]]]; }]; CCSequence *seq = [CCSequence actions:delay, swapScene, nil]; [self runAction:seq]; } @end虽然代码比较多,但是不要害怕,我们会依次解释其中的每个方法。(1)+(CCScene *) scene这个类方法的实现代码跟一般的有一些不同,它使用Objective-C的动态语言特性反射。首先使用NSStringFromClass得到类名:
NSString *className = NSStringFromClass([self class]);此处className为LoadingScreen。然后使用NSClassFromString得到该类的类型:
id layer = [NSClassFromString(className) node];这里还会给该类型发送一个node消息。这种反射和任意发送消息的能力使Objective-C语言具有强大的灵活性,也为游戏开发提供大量的便利。我们可以把关卡的配置、敌人的类型直接存储为文件,然后反射解析。
(2)init方法该方法的实现非常简单,就是计算屏幕窗口大小和中点位置,然后初始一个CCLabelTTF对象,用来显示Loading字样。下面我们来看比较重要的onEnterTransitionDidFinish方法。(3)onEnterTransitionDidFinish方法该方法首先加载一个配置文件,然后读取配置文件中的游戏资源名字列表并存储在不同的数组中。示例代码如下:
NSString *path = [[CCFileUtils sharedFileUtils] fullPathFromRelativePath:@"preloadAssetManifest.plist"]; NSDictionary *manifest = [NSDictionary dictionaryWithContentsOfFile:path]; NSArray *spriteSheets = [manifest objectForKey:@"SpriteSheets"]; NSArray *images = [manifest objectForKey:@"Images"]; NSArray *soundFX = [manifest objectForKey:@"SoundFX"]; NSArray *music = [manifest objectForKey:@"Music"]; NSArray *assets = [manifest objectForKey:@"Assets"]; assetCount = ([spriteSheets count] + [images count] + [soundFX count] + [music count] + [assets count]);首先使用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。注意 本项目比较简单,资源数量有限,完成加载场景基本体会不到加载的过程,因为速度实在是太快了。接下来我们看看后面的代码。
if (soundFX) [selfperformSelectorOnMainThread:@selector(loadSounds:) withObject:soundFXwaitUntilDone:YES]; if (spriteSheets) [selfperformSelectorOnMainThread:@selector(loadSpriteSheets:) withObject:spriteSheets waitUntilDone:YES]; if (images) [self performSelectorOnMainThread:@selector(loadImages:) withObject:images waitUntilDone:YES]; if (music) [self performSelectorOnMainThread:@selector(loadMusic:) withObject:music waitUntilDone:YES]; if (assets) [self performSelectorOnMainThread:@selector(loadAssets:) withObject:assets waitUntilDone:YES];这段代码并不复杂,调用performSelectorOnMainThread在主线程上依次加载每种类型的游戏资源,同时waitUntilDone的值为YES能保证所有的资源按照序列依次加载。其他方法这里就不一一介绍了,注意几个方法。比如:预先加载音效:[[SimpleAudioEngine sharedEngine] preloadEffect:soundClip]预先加载背景音乐:[[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:music]预先加载PNG图片:[[CCTextureCache sharedTextureCache] addImage:image]预先加载精灵表单可以使用下面的方法:
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:spriteSheet];这个方法首先会加载与该plist方法名称相同但后缀为.png的纹理图片,把该plist的所有spirteFrame信息读取出来,以后可以通过spriteFrameWithName获取相应的精灵帧。(4)progressUpdate和loadingComplete方法目前,progressUpdate方法非常简单,只是更新资源的总数,当资源全部加载完毕时会调用loadingComplete方法。loadingComplete方法的作用是:过2秒之后运行一个场景切换特效跳转到游戏主场景,即HelloWorldLayer。3.8.3 修改AppDelegate.m文件1)添加所包含的头文件。在文件的顶部添加代码:
#import "LoadingScreen.h"2)修改- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法。在该方法最后,把之前的pushScene方法改成下面代码:
[director_ pushScene: [LoadingScreen scene]];编译并运行游戏,首先会看到一个Loading界面,再进入游戏场景。但是我们希望在场景跳转之后,玩家可以看到有一个开始游戏的按钮,只有当玩家选择“开始游戏”时,敌机才出现,游戏才正式开始。因此这里需要对HelloWorldLayer的实现做一些修改。3.8.4 修改HelloWorldLayer1)打开HelloWorldLayer.h文件,在其中添加下列变量:
CCMenu *_startGameMenu; BOOL _isGameStarted;2)打开HelloWorldLayer.m的init方法,添加如代码清单3-10所示代码。代码清单3-10 在init方法中添加代码
//15.add game start menu & relative game logic _isGameStarted = NO; [CCMenuItemFont setFontSize:20]; [CCMenuItemFont setFontName:@"Arial"]; CCMenuItemFont *startItem = [CCMenuItemFont itemWithString:@"开始游戏" block:^(id sender) { _isGameStarted = YES; CCMenuItem *item = (CCMenuItemFont*)sender; item.visible = NO; //6.spawn enemy after 1.0 sec [self performSelector:@selector(spawnEnemy) withObject:nil afterDelay:1.0f]; //7.enable accelerometer self.isAccelerometerEnabled = YES; //9.enable touch self.isTouchEnabled = YES; }]; startItem.position = ccp(winSize.width / 2, winSize.height / 2); _startGameMenu = [CCMenu menuWithItems:startItem, nil]; _startGameMenu.position = CGPointZero; [self addChild:_startGameMenu];首先注意,这里把之前的代码清单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方法的最上面添加代码:
CCSpriteBatchNode *batchNode = [CCSpriteBatchNode batchNodeWithFile:@"gameArts.png"]; batchNode.position = CGPointZero; [self addChild:batchNode z:0 tag:kTagBatchNode];这里给spriteBatchNode添加一个新的tag,所以需要在HelloWorldLayer.m文件最上方定义这个枚举常量:
enum { kTagPalyer = 1, kTagBatchNode = 2, };2 . 修改sprite的初始化方式把spriteWithFile改写成spriteWithSpriteFrameName,同时把sprite加到batchNode中而不是加到layer里。这样做的好处是减少opengl call的次数,提高游戏渲染性能。具体如代码清单3-11所示。
代码清单3-11 使用精灵表单减少opengl call的次数
//2.add background CCSprite *bgSprite = [CCSprite spriteWithSpriteFrameName:@"background_1.jpg"]; bgSprite.position = ccp(winSize.width / 2,winSize.height/2); [batchNode addChild:bgSprite z:-100]; //3.add player's plane CCSprite *playerSprite = [CCSprite spriteWithSpriteFrameName:@"hero_1.png"]; playerSprite.position = CGPointMake(winSize.width / 2, playerSprite.contentSize.height/2 + 20); [batchNode addChild:playerSprite z:4 tag:kTagPalyer]; //5.initialize 10 enemy sprites & add them to _enemySprites array for future useage const int NUM_OF_ENEMIES = 10; for (int i=0; i < NUM_OF_ENEMIES; ++i) { CCSprite *enemySprite = [CCSprite spriteWithSpriteFrameName:@"enemy1.png"]; enemySprite.position = ccp(0,winSize.height + enemySprite.contentSize.height + 10); enemySprite.visible = NO; [batchNode addChild:enemySprite z:4]; [_enemySprites addObject:enemySprite]; } //10.init bullets _bulletSprite = [CCSprite spriteWithSpriteFrameName:@"bullet1.png"]; _bulletSprite.visible = NO; [batchNode addChild:_bulletSprite z:4];此时运行代码,我们会发现有点问题,游戏行为变得有些古怪,因为之前操作playerSprite时是通过[self getChildByTag:kTagPlayer]来获取玩家精灵的。现在我们要改成[batchNode getChildByTag:kTagPlayer]。3 . 定义-(CCSprite*) getPlayerSprite方法为了减少重复修改,这里需要定义一个新方法-(CCSprite*) getPlayerSprite,其实现如代码清单3-12所示。代码清单3-12 -(CCSprite*) getPlayerSprite实现
-(CCSprite*)getPlayerSprite{ CCSpriteBatchNode *batchNode = (CCSpriteBatchNode*)[self getChildByTag:kTagBatchNode]; return (CCSprite*)[batchNode getChildByTag:kTagPalyer]; }要理解上述代码,我们的头脑中需要有一个node的层级关系图,首先通过当前layer找到batchNode,然后通过batchNode找到playerSprite。此时还需要稍微修改其他内容来获得playerSprite的方法,修改以下代码:
CCSprite *playerSprite = (CCSprite)[self getChildByTag:kTagPlayer];将其改写成:
CCSprite *playerSprite = [self getPlayerSprite];共有三处需要修改,大家可以参考随书附赠的源码,这里就不再赘述了。4 . 修改发射子弹的碰撞检测算法另外单击玩家发射子弹的碰撞检测算法也要进行修改,如代码清单3-13所示。代码清单3-13 修改单击玩家就发射子弹的碰撞检测算法
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ //修改成,必须选中playerSprite才能够发射子弹 UITouch *touch = [touches anyObject]; CCSprite *playerSprite = [self getPlayerSprite]; CGPoint pt; //简化为下面的一句代码调用 CCSpriteBatchNode *batchNode = (CCSpriteBatchNode*)[self getChildByTag: kTagBatchNode]; pt = [batchNode convertTouchToNodeSpace:touch]; if (CGRectContainsPoint(playerSprite.boundingBox, pt)) { _isTouchToShoot = YES; CCLOG(@"touched!"); } }相信通过这段代码大家能更清楚地认识到,bondingBox是相对于节点的父节点来计算的。如果要判断一个点是否在一个矩形区域内,首先要把它们都转化到同一个坐标系中,才能进行相应的判断。本例中把touch点和玩家飞机的矩形区域都转化到batchoNode的坐标空间中。将-(void) updatePlayerPosition:(ccTime)dt方法中计算图片纹理宽度一半的语句:
float imageWidthHavled = playerSprite.texture.contentSize.width * 0.5f;改成以下代码:
float imageWidthHavled = playerSprite.textureRect.size.width * 0.5f;注意 谨慎使用sprite.texture.contentSize,只有当使用精灵图片文件初始化精灵时这种写法才有效,如果使用spriteBatchNode则会失效!!!如果使用CCSpriteBatchNode创建精灵对象,就不能将精灵对象添加为当前层的子节点,而应添加为精灵表单的子节点。另外,以上方法是为了让大家对CCSprite相关类有更清晰的认识,在实际游戏开发中,并不需要使用上述这么复杂的步骤。编译并运行到真机上面,好好享受学习的成果吧!
相关资源:cocos2d-x游戏源码