《Cocos2D权威指南》——3.8 垂直射击游戏—加载游戏数据-阿里云开发者社区

开发者社区> 华章出版社> 正文

《Cocos2D权威指南》——3.8 垂直射击游戏—加载游戏数据

简介: 本节书摘来自华章计算机《Cocos2D权威指南》一书中的第3章,第3.8节,作者:王寒,屈光辉,周雪彬著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.8 垂直射击游戏—加载游戏数据

为了使大家对CCSprite和各相关类的使用有更加直观的印象,下面我们结合前面的游戏示例,使用精灵表单优化游戏性能,同时在游戏开始和结束时添加菜单,让玩家对游戏有更多控制权。当然,在这个示例小游戏中,这种优化是看不出差别的。但这是最佳实践,建议读者以后编写游戏都以这种方式使用精灵。
**3.8.1 注释draw方法和背景
**
首先,在Xcode中打开之前的项目中把draw方法注释掉,同时恢复先前注释掉的添加游戏背景的代码段,编译并运行,如图3-6所示。

image

注意 这时必须触碰玩家飞机才可以发射子弹,同时,此刻的碰撞检测代码也有所差别,当然,玩家是感觉不到的。
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;
@end

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

#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所示。

image

由图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 修改HelloWorldLayer
1)打开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项目。

image

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相关类有更清晰的认识,在实际游戏开发中,并不需要使用上述这么复杂的步骤。
编译并运行到真机上面,好好享受学习的成果吧!

版权声明:如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:developerteam@list.alibaba-inc.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接