zoukankan      html  css  js  c++  java
  • Cocos2d开发系列(七)

    Learn IPhoneand iPad Cocos2d Game Delevopment》第8章 。

    这种类型的游戏(shoot’emup游戏)最重要的是什么?射击的目标和需要躲避的子弹。本章,将为游戏添加一些敌人以及一个大 boss。

    敌人和玩家将使用新的BulletCache 类射击不同的子弹,这些子弹来自同一个 pool。这个缓冲类会重用无效的子弹,以避免重复的内存分配和释放动作。同样,敌人会使用EnemyCache 类,因为待会屏幕上会出现成堆的敌人。

    显然玩家可以向敌人射击。我会介绍基于组件编程的概念,用一种模板化的方式扩展游戏角色。除了 shooting 组件和 moving 组件,我们还会为 boss 老怪创建 healthbar 组件(生命值,俗称“血槽”)。毕竟,老怪不应该是一下就能pk 掉的,其生命值总是被一点点减少直至彻底干掉它。

    一、添加 BulletCache 类

    该类在是 “一站式” 的,可以一次性生成许多子弹。原来这些代码是放在 GameScene 类中,但这(指生成子弹)显然不该由 GameScene 来管。下面显示 BulletCache 的头文件,它包括了CCSpriteBatchNode 对象和无效子弹计数器nextInactiveBullet:

    #import "cocos2d.h"

    @interface BulletCache : CCNode {

    CCSpriteBatchNode* batch;

     int nextInactiveBullet;

    }

    -(void) shootBulletAt:

    (CGPoint)startPositionvelocity:(CGPoint)velocity

    frameName:(NSString*)frameName;

    @end

    为了把 代码重构到 GameScene类之外,我需要把 initialization 方法和射击子弹的方法移到 BulletCache 类(代码见后)。接着,我决定使用一个CCSpriteBatchNode 变量,以免在每次需要这个对象时就得调用一次[CCNode CCSpriteBatchNode]方法。这会带来细微的性能优化。由于我会在类 GameScene 中加入 BulletCache 对象,因此很容易就能把 sprite batch node 传给 BulletCache。

    注意,新的 BulletCache有一个问题,增加了scene的层次——一个额外的 CCNode。如果你担心这点,你也可以把 sprite batch node放在GameScene类中,用一个方法从BulletCahce 获取这个 sprite batch node。

    但是,额外的函数调用开销有可能会使性能得以下降。如果你怀疑是不是真的对性能由影响,那就让你的代码可读性更好些。当有必要进行性能优化的时候再重构你的代码。

    #import "BulletCache.h"

    #import "Bullet.h"

    @implementation BulletCache

    -(id) init {

    if ((self = [super init])) {

    // 从当前贴图集中获得角色帧

    CCSpriteFrame* bulletFrame =[[CCSpriteFrameCache sharedSpriteFrameCache]

    spriteFrameByName:@"bullet.png"];

    // 使用角色帧的贴图构建CCSpriteBatchNode

    batch = [CCSpriteBatchNodebatchNodeWithTexture:bulletFrame.texture];

     [self addChild:batch];

    // 创建子弹并加到 batch

    for (int i = 0; i < 200; i++) {

    Bullet* bullet =[Bullet bullet];

    bullet.visible =NO;

    [batchaddChild:bullet];

    }

    return self;

    }}

     -(void) shootBulletAt:(CGPoint)startPositionvelocity:(CGPoint)velocity frameName:(NSString*)frameName{

    CCArray* bullets = [batch children];

    CCNode* node = [bullets objectAtIndex:nextInactiveBullet];

    NSAssert([node isKindOfClass:[Bulletclass]], @"not a Bullet!");

    Bullet* bullet = (Bullet*)node;

    [bullet shootBulletAt:startPositionvelocity:velocity frameName:frameName];

    nextInactiveBullet++;

    if (nextInactiveBullet >= [bulletscount]) {

    nextInactiveBullet= 0;

    }

    }

    @end

    shootBulletAt方法已经完全变了。它有3个参数:startPosition,velocity和frameName——取代 Ship类指针。然后这些参数被传递给 Bullet 类的 shootBulletAt 方法,这个方法现在已经变为:

    -(void) shootBulletAt:(CGPoint)startPositionvelocity:(CGPoint)vel frameName:(NSString*)frameName {

    self.velocity = vel;

    self.position = startPosition;

    self.visible = YES;

    // 改变子弹的贴图,设置一个不同的角色帧去显示

    CCSpriteFrame *frame = [[CCSpriteFrameCachesharedSpriteFrameCache] spriteFrameByName:frameName];

    [self setDisplayFrame:frame];

    [self scheduleUpdate];

    }

    velocity 和position 被直接赋值给 bullet。这意味着调用 shootBulletAt 方法的代码必需自己决定子弹的位置、方向和速度。这出于这样的考虑:子弹射击的动作会适应更多的变化,包括可以改变子弹的角色帧(用setDisplayFrame 方法)。因为子弹使用的是相同的贴图集、相同的贴图,所以需要通过设置相应的贴图帧来改变子弹的显示。实际上,渲染贴图的不同部分很轻松,并不会带来额外的开销。

    在编辑 Bullet 类时,我还修正了一个边界问题——只有子弹移出屏幕右边时,才会设为不可见并被放会重用列表(其实这是一个bug)。通过在update方法中使用 CGRectIntersectsRect 检查子弹的边框和屏幕矩形,任何完全移出屏幕的子弹都会被标记为重用:

    // 子弹离开屏幕后,设为不可见

     if (CGRectIntersectsRect([self boundingBox], screenRect) ==NO) {

    ……

    }

    screenRect变量出于方便和性能的原因,被存储为static 变量,因此它能被其他类访问,并不需要每次使用的时候创建。static 变量在类实现文件中声明并有效,比如 screenRect。它们就像类的全局变量,任何类实例都可以读取和修改。成员变量则不同,它们只存在于每个实例对象中。因为屏幕尺寸在游戏期间永远不会变,所有的子弹都需要用到它,把它存储为所有实例的static变量显然是行得通的。第一个实例负责给 screeenRect 赋值。 CGRectIsEmpty 方法负责检查 screenRect 变量是否未初始化——因为是static变量,只需要初始化一次就行了。

    static CGRect screenRect;

    ......

    // 确保只初始化一次

    if (CGRectIsEmpty(screenRect)) {

    CGSize screenSize = [[CCDirectorsharedDirector] winSize];

    screenRect = CGRectMake(0, 0,screenSize.width, screenSize.height);

    }

    接下来,移除GameScene 类中原有的用于射击子弹的代码。此外,需要用初始化 BulletCache 来替换初始化 CCSpriteBatchNode (在GameScene 的 init 方法中):

    BulletCache* bulletCache = [BulletCache node];

    [self addChild:bulletCache z:1tag:GameSceneNodeTagBulletCache];

    还需要为 bulletCache 添加一个访问方法以便其他类通过GameScene访问BulletCache实例:

    -(BulletCache*) bulletCache {

    CCNode* node = [self getChildByTag:GameSceneNodeTagBulletCache];NSAssert([node isKindOfClass:[BulletCache class]], @"not aBulletCache"); return (BulletCache*)node;

    }

    InputLayer 现在可以用BulletCache 发射子弹了。 子弹的位置、速度和所用的角色帧这些属性, 应当在 InputLayer 的update方法里传递给射击方法:

    if (fireButton.active && totalTime> nextShotTime) {

    nextShotTime = totalTime + 0.5f;

    GameScene* game = [GameScenesharedGameScene];

    Ship* ship = [game defaultShip];

    BulletCache* bulletCache = [gamebulletCache];

    // 射击前设置 position, velocity h和 spriteframe

    CGPoint shotPos = CGPointMake(ship.position.x+ [ship contentSize].width * 0.5f, ship.position.y);

    float spread = (CCRANDOM_0_1() - 0.5f) *0.5f;

    CGPoint velocity = CGPointMake(1, spread);

     [bulletCache shootBulletAt:shotPos velocity:velocityframeName:@"bullet.png"];

    }

    重构后的射击过程添加了一些非常必要的灵活性。你可以设想一下,敌人现在可以使用同样的代码发射它们自己的子弹了。

    二、敌人

    此刻,对于敌人我们仅有一个模糊的概念,它们是干什么的?它们的行为是什么?对于敌人,最重要的是——你永远不知道他们该干什么。

    就游戏而言,这意味着一切都要从头开始,要策划出你想让敌人做的事情,从而分析需要编写的代码。与真实世界不同,你完全控制着你的敌人们。是不是觉得自己很伟大?但在你或者其他人感到好笑之前,你需要为统治世界想出一个计划。

    我创建了3种不同类型的敌人的图片。这里,我只知道其中一个应该是Boss。看一眼下面的图片,然后想象一下这些敌人分别能干些什么:

    在写代码之前,先了解一下这些敌人有哪些行为是共性的,这样有些代码只用编写一次。代码复用是最重要的编码规范。我们先来看看敌人们都有哪些共性:

    ¥  发射子弹

    ¥  何时何地发射子弹的判断逻辑

    ¥  能被玩家的子弹击中

    ¥  不能被其他敌人的子弹击中

    ¥  能被多次击中(有生命值)

    ¥  有固定的行为和移动方式

    ¥  死亡时显示特定的行为或动画

    ¥  从屏幕以外进入屏幕后将会显示

    ¥  当移出屏幕后将不再显示

    你可能注意到,上面有些特性也符合玩家飞船。飞船也可以射击子弹,它也可能经受多次射击;当它被摧毁时也应该呈现某个行为或动画,它给人的感觉类似一个特殊的敌人。

    扫描上述列表,会有3种实现方式。可以创建一个类,把飞船、敌人、Boss都包含在其中。代码将是有选择地执行部分代码,这取决于敌人的类型。例如,射击代码可能为不同的类型提供不同的分支。对于对象有限的游戏,这是不错的办法——但它无法面对大规模的对象。随着游戏中加入越来越多地对象,你的游戏代码必将变得肥大臃肿。对这个类的任何部分进行修改,都会潜在地对敌人或者飞船的行为带来不希望的影响。用一个变量——敌人类型来决定代码执行路径是一种古老的C 编程方式,不符合 O-C 的面向对象特性。

    这种方式至今仍然非常有用,但一定要慎用。

    第二种方式,是创建一个类层次。用一个Entity类作为基类,从它派生出一个飞船类、2个怪物类、1个Boss类。很多程序员常这样干,对于游戏对象不多的情况这种方式也非常好用。但本质上,这和第一种方式没什么不同。基类封装了子类要用到的一些通用代码,但不是全部代码。当Entity类中的代码开始基于某个子类的类型执行某个分支时,情况变得糟糕——跟第一种方法一样了。如果小心一点,你应该确保把针对某种敌人的代码放在某个子类里,但在修改的时候很容易会把很多改动放到Entity类里。

    第3种方式,是使用组件编程。这意味着不同的代码路径从Entity类层次结构中分离出来,这部分代码仅仅加到所需的子类中。比如一个“血槽”组件。基于组件的编程可以单独写成一本书,对于射击游戏这类项目而言,这显得有些杀鸡用牛刀了,因此我会混合后面两种方式一起使用,这里只是给出一个概念:

    如何组合游戏对象而不是各自为政,以及这样做的好处。

    我想说明的是,不存在最好的编码方式。选择某种方式完全是主观的,取决于个人经验和偏好。如果你愿意随着对手上开发的游戏的逐渐理解,不断重构你的代码库,能运行的代码比干净的代码更可取。经验让你不经过计划就能做出这些决定,让你能更快地创建更多复杂游戏。要想达到这个目的,从完成一个小游戏开始,然后慢慢地挑战自己的极限。这是个需要学习的过程,很不幸的,在这个过程中你的学习兴趣也很容易被好高骛远消灭掉。为什么每个老练的游戏编程人员会告诉新人,从简单入手,去重写经典的电玩游戏比如俄罗斯方块、帕克人、小行星。

    三、Entity类

    Entity 类是继承自 CCSprite,只包含了Ship类中的setPosition方法定义,以使所有的Entity 实例始终在屏幕内移动。我只对代码做了一小点改动(其实就是如下面代码所示的if语句,原来的代码是没有if语句的),屏幕外的对象可以移动到屏幕内,但一旦进入屏幕后,它们不能再离开屏幕区域。在这个射击类游戏中,敌人不会从你身边走开,而是站在屏幕中间为了演示一下EnemyCache,进行简单的介绍。屏幕区域检查只是简单检查一下sprite的边框是否完全被屏幕边框所包含,如果是的话,代码将让sprite始终保持在屏幕边框内:

    -(void) {

    }

    setPosition:(CGPoint)pos

    // 如果当前位置在屏幕外,则不需要让位置调整到屏幕内

    // 这会允许对象从屏幕外部移动到屏幕内部

    if (CGRectContainsRect([GameScene screenRect], [selfboundingBox])) {

    ...

     [supersetPosition:pos];

    }

    ShipEntity类取代了Ship类。由于Entity类已经包含了setPosition方法,ShipEntity类只剩下了initWithShipImage方法。该方法的代码没有改变。

    四、EnemyEntity类

    我们需要继续深入到EnemyEntity类,首先是头文件:

    #import <Foundation/Foundation.h>

    #import"Entity.h"

    typedef enum{

    EnemyTypeBreadman = 0,

    EnemyTypeSnake,

    EnemyTypeBoss,

    EnemyType_MAX,

    } EnemyTypes;

    @interface EnemyEntity : Entity {

    EnemyTypes type;

    }

    +(id) enemyWithType:(EnemyTypes)enemyType;

    +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType;

    -(void) spawn;

    @end

    没有什么特别的。EnemyTypes 枚举用于3种不同的敌人类型,EnemyType_MAX用于在遍历时标志结束。EnemyEntity类使用了一个EnemyTypes变量存储类型,因此我可以用switch命令基于敌人的类型构建分支语句。EnemyEntity的实现包含许多代码,我会把它分成几个主题,并只显示相关的代码。首先是initWithType方法:

    -(id) initWithType:(EnemyTypes)enemyType

    {

    type = enemyType;

    NSString* frameName;

    NSString* bulletFrameName;

    int shootFrequency = 300;

    switch (type)

    {

    case EnemyTypeBreadman:

    frameName= @"monster-a.png";

    bulletFrameName= @"candystick.png";

    break;

    case EnemyTypeSnake:

    frameName= @"monster-b.png";

    bulletFrameName= @"redcross.png";

    shootFrequency= 200;

    break;

    case EnemyTypeBoss:

    frameName= @"monster-c.png";

    bulletFrameName= @"blackhole.png";

    shootFrequency= 100;

    break;

    default:

    [NSException exceptionWithName:@"EnemyEntityException" reason:@"unhandled enemytype" userInfo:nil];

    }

    if((self = [super initWithSpriteFrameName:frameName]))

    {

    //Create the game logic components

    [self addChild:[StandardMoveComponent node]];

    StandardShootComponent* shootComponent = [StandardShootComponent node];

    shootComponent.shootFrequency= shootFrequency;

    shootComponent.bulletFrameName= bulletFrameName;

    [self addChild:shootComponent];

    //enemies start invisible

    self.visible = NO;

    [self initSpawnFrequency];

    }

    return self;

    }

    方法一开始是变量赋值,根据敌人的类型,使用switch语句为每种类型提供默认值:敌人的角色帧名以及子弹的角色帧名。switch的default分支抛出异常,因为其他类型在Enemytypes枚举中未定义。这样,如果你定义了一种新的敌人类型,但是如果它不会动,或者发射出了错误的子弹,那么你会得到一个错误警告:哈,你忘记修改某些东西了!

    最后别忘了调用[super init…]方法,否则super无法正确初始化并导致一个奇怪的错误然后崩溃。

    接下来创建了一个组件,并把它加到EnemyEntity中。后面我会访问这个组件,在此你只需要知道StandardMoveComponent 能让敌人移动并射击。

    把注意力放到initSpawnFrequency方法。

    -(void) initSpawnFrequency

    {

    // initialize how frequent the enemies willspawn

    if(spawnFrequency == nil)

    {

    spawnFrequency = [[CCArray alloc] initWithCapacity:EnemyType_MAX];

    [spawnFrequency insertObject:[NSNumber numberWithInt:80] atIndex:EnemyTypeBreadman];

    [spawnFrequency insertObject:[NSNumber numberWithInt:260] atIndex:EnemyTypeSnake];

    [spawnFrequency insertObject:[NSNumber numberWithInt:1500] atIndex:EnemyTypeBoss];

    //spawn one enemy immediately

    [self spawn];

    }

    }

    +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType

    {

    NSAssert(enemyType < EnemyType_MAX, @"invalidenemy type");

    NSNumber* number = [spawnFrequency objectAtIndex:enemyType];

    return [number intValue];

    }

    -(void) dealloc

    {

    [spawnFrequency release];

    spawnFrequency = nil;

    [super dealloc];

    }

    我们把每种类型的敌人的出场频率记录在静态数组spawnFrequency里。第一个EnemyEntity实例负责初始化CCArray数组。CCArray不能存储原始数据类型比如整型,因此使用了NSNumber类。使用insertObject方法而不用addObject方法是为了保证对象加入时的顺序,同时别人看到这个枚举值也映射了对应的敌人类型。

    dealloc方法释放了CCArray对象,并将其设为nil,这点非常重要。作为静态变量,第一个EnemyEntity对象在运行其dealloc方法时会释放spawnFrequency的内存,如果spawnFrequency不被设为nil,下一个EnemyEntity对象的dealloc方法将视图再次释放,这会“过度释放”spawnFrequency对象,导致程序崩溃。如果spawnFrequency为nil,任何发给它的消息都会被忽略,包括release消息。

    spawn方法用于“生成”一个游戏对象:

    -(void) spawn

    {

    CCLOG(@"spawn enemy");

    // Select a spawn location just outside theright side of the screen, with random y position

    CGRect screenRect = [GameScene screenRect];

    CGSize spriteSize = [self contentSize];

    float xPos = screenRect.size.width + spriteSize.width * 0.5f;

    float yPos = CCRANDOM_0_1() * (screenRect.size.height - spriteSize.height)+ spriteSize.height * 0.5f;

    self.position = CGPointMake(xPos, yPos);

    // Finally set yourself to be visible, this alsoflag the enemy as "in use"

    self.visible = YES;

    }

    因为EnemyCache用于统一创建所有的敌人,这里整个spawn 方法只是设定一个随机数的y坐标,x坐标是在右侧屏幕以外。visible属性在其他地方会用到,尤其是在组件类中,用于判断EnemyEntity当前是否已使用。如果visible为NO,它可以被“生出”并显示,如果为YES,它就会按照固定的逻辑运行。

    五、EnemyCache类

    从名字上看,这会让你想到BulletCache类,它也持有了大量已初始化对象,以便快速和简单地重用,减少了游戏时对象的创建、释放动作,而这恰恰是导致游戏流畅性下降的原因之一。尤其是动作游戏,这种不流畅给玩家体验带来了灾难性后果。以下是EnemyCache的头文件。

    #import <Foundation/Foundation.h>

    #import "cocos2d.h"

    @interface EnemyCache : CCNode

    {

    CCSpriteBatchNode* batch;

    CCArray* enemies;

    int updateCount;

    }

    @end

    CCSpriteBatchNode对象包含全部敌人角色(sprite),CCArray则储存了每种敌人的列表。updateCount变量在每帧生成一个敌人时自动增加。init方法与BulletCache的init方法十分类似:

    -(id) init

    {

    if((self = [super init]))

    {

    //从贴图集缓存中得到图片

    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"monster-a.png"];

    batch = [CCSpriteBatchNode batchNodeWithTexture:frame.texture];

    [self addChild:batch];

    [self initEnemies];

    [self scheduleUpdate];

    }

    return self;

    }

    但initEnemies方法就复杂多了:

    -(void) initEnemies

    {

    // 创建enemies 数组,用于存放每种类型的敌人

    enemies = [[CCArray alloc] initWithCapacity:EnemyType_MAX];

    // 有多少种敌人,就创建多少个数组

    for (int i = 0; i < EnemyType_MAX; i++)

    {

    //根据敌人种类的不同,设置不同的数组容量。

    int capacity;

    switch (i)

    {

    case EnemyTypeBreadman:

    capacity = 6;

    break;

    case EnemyTypeSnake:

    capacity = 3;

    break;

    case EnemyTypeBoss:

    capacity = 1;

    break;

    default:

    [NSException exceptionWithName:@"EnemyCacheException" reason:@"unhandled enemytype" userInfo:nil];

    break;

    }

    //不需要alloc数组,当数组被加到enemies数组时会自动retain

    CCArray* enemiesOfType = [CCArray arrayWithCapacity:capacity];

    [enemies addObject:enemiesOfType];

    }

    for (int i = 0; i < EnemyType_MAX; i++)

    {

    CCArray* enemiesOfType = [enemies objectAtIndex:i];

    int numEnemiesOfType = [enemiesOfType capacity];

    for (int j = 0; j < numEnemiesOfType;j++)

    {

    EnemyEntity* enemy = [EnemyEntity enemyWithType:i];

    [batch addChild:enemy z:0 tag:i];

    [enemiesOfTypeaddObject:enemy];

    }

    }

    }

    有意思的是,CCArray* enemies 对象自身包含了多个CCArray*对象,每种类型的敌人使用一个CCArray*。这是一个典型的 2 维数组。enemies 变量需要用alloc 分配内存,否则initEnemies 方法一结束它的内存会被释放。相反,enimies数组中的CCAray 元素对象不需要alloc,因为当它被add 到enemies数组中时会被自动retain。每种敌人所用的CCArray数组,其初始容量为该类型一次允许加到屏幕中的个数。每种敌人的CCArray数组使用addObject方法加到enemies数组。用这种方式可以创建层次深度。事实上,cocos2d结点层次结构也是通过在CCNode 类中定义一个CCArray* children成员变量来构建的。

    我将enimies数组的创建和初始化分别放在在两个单独的循环体中,尽管它们其实也可以在一个循环中进行,但它们明显是属于不同的任务,应该保持分离——至于因此导致的性能上的额外开销,是微乎其微的。

    根据在CCArray初始化时的初始容量,相同数目的敌人被构建出来并加入到CCSpriteBatchNode中,然后又加到对应的某种敌人使用的CCArray中。通过CCSpriteBatchNode也能访问到敌人,但单独把这些敌人放在分开的数组中更方便处理,代码列表如下所示:

    -(void) spawnEnemyOfType:(EnemyTypes)enemyType

    {

    CCArray* enemiesOfType = [enemies objectAtIndex:enemyType];

    EnemyEntity* enemy;

    CCARRAY_FOREACH(enemiesOfType, enemy)

    {

    //查找可重建的敌人,重用

    if (enemy.visible== NO)

    {

    //CCLOG(@"spawn enemy type %i",enemyType);

    [enemy spawn];

    break;

    }

    }

    }

    -(void) update:(ccTime)delta

    {

    updateCount++;

    for (int i = EnemyType_MAX- 1; i >= 0; i--)

    {

    int spawnFrequency = [EnemyEntity getSpawnFrequencyForEnemyType:i];

    if (updateCount % spawnFrequency == 0)

    {

    [self spawnEnemyOfType:i];

    break;

    }

    }

    }

    update方法使计数器updateCount加1。这并不会多花费多少时间,但却是值得的,因为他会使我们接下来更轻松一些。

    For循环比较奇怪,循环变量i从EnemyType_MAX开始递减,一直到i为负值。这个目的是为了让EnemyTypes 更大的怪物更早出生。例如,当boss怪和蛇同时出现时,首先让boss怪出生。否则会导致这样的事情发生,蛇会和boss争抢出生机会,甚至阻塞了Boss的出生。这个出生逻辑有一个副作用,我把它保留给你自己去解决,如果你要写一个自己的射击游戏,你可能不得不自己实现一些东西。

    spawnFrequency被EnemyEntity 的getSpawnFrequncyForEnemyType方法所赋值。

    +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType

    {

    NSAssert(enemyType < EnemyType_MAX, @"invalidenemy type");

    NSNumber* number = [spawnFrequency objectAtIndex:enemyType];

    return [number intValue];

    }

    这个方法首先断言enemyType是否是有效值。然后从spawnFrequency数组中取出指定类型的敌人的NSNumber对象并返回其intValue值。

    回到update方法,接下来使用取模运算%,计算updateCount能否被spawnFrequency所整除,意思是只有updateCount数到指定的数时(updateCount是个计数器),某个怪才会降生。

    spanEnemyOfType方法从enemies数组中取出对应的CCArray,然后只需要遍历指定的类型的CCArray数组,而不用去遍历整个CCSrpiteBatchNode:

    -(void) spawnEnemyOfType:(EnemyTypes)enemyType

    {

    CCArray* enemiesOfType = [enemies objectAtIndex:enemyType];

    EnemyEntity* enemy;

    CCARRAY_FOREACH(enemiesOfType, enemy)

    {

    //find the first free enemy and respawn it

    if (enemy.visible== NO)

    {

    //CCLOG(@"spawn enemy type %i",enemyType);

    [enemy spawn];

    break;

    }

    }

    }

    如果找到一个visible为NO的怪,调用其spawn方法。如果所有的该类怪的visible都是YES,当前屏幕上该类怪的数目已经达到最大,不再产生这种类别的怪,这样就限制了屏幕上同一种怪的数量。

    六、Component类

    Component类在游戏逻辑中被视作插件。如果把一个component(组件)加在一个entity类,则该entity可以执行组件的行为:移动,射击,动画,显示生命值等等。编写组件的好处是它能自动工作,因为它们与父容器(CCNode)交互,并尽可能地不对父容器做出要求。有时候组件要求父容器必须是一个EnemyEntity类,但实际上你可以在任何类型的EnemyEntity(子类)上使用它。组件类可根据使用组件的类来配置。例如,这是一个在EnemyEntity中使用StandarShoortComponent组件的例子:

    StandardShootComponent* shootComponent = [StandardShootComponent node];

    shootComponent.shootFrequency= shootFrequency;

    shootComponent.bulletFrameName= bulletFrameName;

    [self addChild:shootComponent];

    shootFrequency和bulletFrameName变量是根据EnemyType来初始化的。把StandartShootComponent添加到EnemyEntity类,该类将会拥有射击的能力。因为组件类未对父容器做任何限制,你甚至可以把组件加到ShipEntity,使玩家飞船以指定射速进行自动射击。通过简单地激活或失活射击组件,你可以用很少的代码实现给玩家更换武器的效果。你仅仅是把射击代码隔离出来,然后把组建植入游戏对象并设置一些参数而已。

    让武器失效并切换武器的逻辑很简单。甚至,你可以把组件使用到其他游戏。组件在封装可重用代码时非常有用,在许多游戏引擎中组件是一种标准机制。如果你想进一步了解游戏组件,请到我的blog(www.learn-cocos2d.com/2010/06/prefer-composition-inheritance/)。

    StandardShootComponent的头文件如下:

    @interface StandardShootComponent : CCSprite

    {

    int updateCount;

    int shootFrequency;

    NSString* bulletFrameName;

    }

    @property (nonatomic) int shootFrequency;

    @property (nonatomic, copy) NSString* bulletFrameName;

    @end

    有两件事情值得注意。首先StandardShootComponent派生自CCSprite,尽管它没有使用任何贴图纹理。因为CCSpriteBatchNode只能包含CCSprite对象,而所有的EnemyEntity对象都被加到了CCSpriteBatchNode,而且EnemyEntity的子节点,这些都是StandardShotComponent的作用对象。因此StandardShootComponent需要从CCSprite继承以满足CCSpriteBatchNode的要求。

    第2是一个NSString 指针,bulletFrameName,用@property关键字封装成了属性。如果你足够细心,应该发现在@property定义中的copy关键字。这说明只要给这个属性赋值,将产生一个复制操作。这样做对于确保这个字符串始终可用很重要, 因为字符串通常都是autorelease对象。我们也可以用retain对象,问题在于,如果源字符串被改变,这将影响到bulletFrameName,这可能不是我们希望的。

    当然,copy关键字还意味着我们要负责在dealloc中释放它,如下所示。

    @implementation StandardShootComponent

    @synthesize shootFrequency;

    @synthesize bulletFrameName;

    -(id) init

    {

    if((self = [super init]))

    {

    [self scheduleUpdate];

    }

    return self;

    }

    -(void) dealloc

    {

    [bulletFrameName release];

    [super dealloc];

    }

    -(void) update:(ccTime)delta

    {

    if(self.parent.visible)

    {

    updateCount++;

    if (updateCount >= shootFrequency)

    {

    //CCLOG(@"enemy %@ shoots!",self.parent);

    updateCount = 0;

    GameScene* game = [GameScene sharedGameScene];

    CGPoint startPos = ccpSub(self.parent.position, CGPointMake(self.parent.contentSize.width * 0.5f, 0));

    [game.bulletCache shootBulletFrom:startPos velocity:CGPointMake(-2, 0) frameName:bulletFrameName];

    }

    }

    }

    @end

    真正的射击代码首先要检查父对象是否visible为YES,否则射击代码显然不应该被调用。BulletCache发射子弹时使用组件bulletFrameName 属性和固定的速度进行发射。 开始位置startPos并不是指组件自己的位置,而是使用父容器的位置和contentSize计算出来的:子弹位于角色的左边。

    对于常规的怪,一个startPos就足够了,但对于Boss来说,用它的嘴或者鼻子来发射子弹,这才酷呢!我把这个工作也留给了你:为组件增加一个属性,以便子弹的初始位置可以被设置。当然,你也可以创建一种单独的BossShootComponent类,专门给Boss设计一种更复杂的射击模式。StandardMoveComponents 也是一样的, boss怪也可能需要在屏幕右边的某个位置不停盘旋。

    七、击中物体

    几乎忘记了——你其实是想向怪物们开火并击中它们,不是吗?

    BulletCache类是检查子弹击中物体的理想地点。我把方法加在了BulletCache中。实际上是3个方法,2个是public的,1个是private方法,如下所示。使用这两个方法:isPlayerBulletCollidingWithRect和isEnemyBulletCollidingWithRect方法的目的是为了隐藏根据子弹的主类进行碰撞检测的内部细节。

    -(bool) isPlayerBulletCollidingWithRect:(CGRect)rect

    {

    return [self isBulletCollidingWithRect:rect usePlayerBullets:YES];

    }

    -(bool) isEnemyBulletCollidingWithRect:(CGRect)rect

    {

    return [self isBulletCollidingWithRect:rect usePlayerBullets:YES];

    }

    -(bool) isBulletCollidingWithRect:(CGRect)rect usePlayerBullets:(bool)usePlayerBullets

    {

    bool isColliding = NO;

    Bullet* bullet;

    CCARRAY_FOREACH([batch children], bullet)

    {

    if (bullet.visible&& usePlayerBullets == bullet.isPlayerBullet)

    {

    if(CGRectIntersectsRect([bullet boundingBox],rect))

    {

    isColliding = YES;

    //remove the bullet

    bullet.visible= NO;

    break;

    }

    }

    }

    return isColliding;

    }

    你也可以把usePlayerBullets 参数暴露给其他类,但这样把这个参数由bool类型改变为enum类型时只会更难,一旦你想使用第3种子弹怎么办?

    只对看得见的子弹进行检测,同时要检查isPlayerBullet 属性,确保怪物们不会被自己的子弹击中。其实碰撞检测是件简单的事情,你可以使用CGRectIntersectsRect,如果子弹真的击中了什么,子弹自身也应该“消失”。

    EnemyCache类持有所有的EenemyEntity对象,这里也是调用方法去检测是否有怪物被玩家击中的好地方。现在EnemyCache类增加了checkForBulletCollisions方法(会由update方法来调用):

    -(void) checkForBulletCollisions

    {

    EnemyEntity* enemy;

    CCARRAY_FOREACH([batch children], enemy)

    {

    if (enemy.visible)

    {

    BulletCache* bulletCache = [[GameScene sharedGameScene] bulletCache];

    CGRect bbox = [enemy boundingBox];

    if([bulletCache isPlayerBulletCollidingWithRect:bbox])

    {

    //This enemy got hit ...

    [enemy gotHit];

    }

    }

    }

    }

    在这里,很方便遍历所有的怪物,并忽略那些当前不可见的。使用BulletCache的isPlayerBulletCollidingWithRect方法以及怪物的boundingBox属性进行检测,我们能快速地发现一个怪是否被玩家子弹击中;如果击中,就调用EnemyEntity的gotHist方法,该方法只是简单地把怪变为不可见。

    我把飞船被怪物子弹击中的练习留给了你。你必须在ShipEntity方法中调用update方法,然后实现checkForBulletCollisions方法并在update方法中调用它。你还要改变isPlayerBulletCollidingWithRect方法和isEnemyBulletColligingWithRect方法,当子弹击中时播放声效。

    八、Boss的血槽

    作为Boss,不应该一枪毙命。应该向玩家显示boss 的生命值,当boss被击中时血槽中的数值就减少一点。首先,需要在EnemyEntity类中增加一个hitPoints成员变量(即血点),用于表明怪物需要多少次击中才会KO。initialHitPoints变量储存怪物满血状态下的血点值,因为怪物被杀死后我们需要恢复它原来的血点(别忘记,我们的怪都是可以被“重用”的)。对头文件所做的修改如下:

    @interface EnemyEntity : Entity {

    EnemyTypes type;

    int initialHitPoints;

    int hitPoints;

    }

    @property (readonly, nonatomic) int hitPoints;

    为了表现血槽,我们需要一个组件类。很显然这就是HealthbarComponent类:

    @interface HealthbarComponent : CCSprite

    {

    }

    -(void) reset;

    @end

    HealthComponent类的实现则比较有趣。HealthBarComponent 根据怪物的剩余血点更新它的scaleX属性(这个scaleX来自于CCNode)。

    -(id) init

    {

    if((self = [super init]))

    {

    self.visible = NO;

    [self scheduleUpdate];

    }

    return self;

    }

    -(void) reset

    {

    float parentHeight = self.parent.contentSize.height;

    float selfHeight = self.contentSize.height;

    self.position = CGPointMake(self.parent.anchorPointInPixels.x, parentHeight + selfHeight);

    self.scaleX = 1;

    self.visible = YES;

    }

    -(void) update:(ccTime)delta

    {

    if(self.parent.visible)

    {

    NSAssert([self.parent isKindOfClass:[EnemyEntity class]], @"nota EnemyEntity");

    EnemyEntity* parentEntity = (EnemyEntity*)self.parent;

    self.scaleX = parentEntity.hitPoints/ (float)parentEntity.initialHitPoints;

    }

    else if (self.visible)

    {

    self.visible = NO;

    }

    }

    @end

    血槽可以根据父对象的visible属性在可视/不可视之间切换。reset方法把血槽放到怪物角色的顶上。因为血点减少是通过修改scaleX属性来显示的,scaleX也应当被重置。

    update方法中,当血槽的父对象是可视时,首先判断父对象是不是EnemyEntity类,因为血槽组件要使用到在EnemyEntity中才有效的某些属性,我们必须确保它的父类必须是EnemyEntity类。我把scaleX属性修改为百分数值:用当前血点除以满血点。因为不知道什么时候血点会变,我们只有在每一帧都进行这个计算,不管血点到底有没有发生变化。这样做有点性能上的浪费,对于复杂计算而言,最好是从EnemyEntity的onHit方法去调用血槽组件的方法。

    在EnemyEntity的init方法中,如果怪物类型为EnemyTypeBoss,则把组件HealthbarComponent加到EnemyEntity对象。

    注意:parentEntity.initialHitPoints被强制转换为float,否则”/”是进行整数除法,这样的结果永远是0。将除数使用float类型就可以保证除法是小数点除法,以得到非0的小数。

    if (type == EnemyTypeBoss) {

    HealthbarComponent*healthbar = [HealthbarComponent spriteWithSpriteFrameName:

    @"healthbar.png"];

    [self addChild:healthbar];

    }

    spawn方法进行了扩展,包括把血点重置为满血,调用子组件中的所有血槽组件的reset方法(如果由多个的话)。我省略了对怪物类型的判断,因为血槽是很通用的,可以被任何怪物用到。

    -(void) spawn

    {

    //CCLOG(@"spawn enemy");

    // 出生地点选择在屏幕右边,y坐标值为随机数

    CGRect screenRect = [GameScene screenRect];

    CGSize spriteSize = [self contentSize];

    float xPos = screenRect.size.width + spriteSize.width * 0.5f;

    float yPos = CCRANDOM_0_1() * (screenRect.size.height - spriteSize.height)+ spriteSize.height * 0.5f;

    self.position = CGPointMake(xPos, yPos);

    // 出生后就表示看得见了

    self.visible = YES;

    // 重置血点,因为我们重用的对象很可能才被打死

    hitPoints = initialHitPoints;

    // 重置一些组件,如血槽

    CCNode* node;

    CCARRAY_FOREACH([self children], node)

    {

    if ([node isKindOfClass:[HealthbarComponent class]])

    {

    HealthbarComponent* healthbar = (HealthbarComponent*)node;

    [healthbarreset];

    }

    }

    }

    九、结论

    做出一个完整并优雅的游戏是一个很大的成果,包括大量的重构,修改代码改进射击以及允许更多的特性并让它们和谐相处。本章,学习了BulletCache和EnemyCache类的作用,使用它们对某个类的所有实例进行管理,便于在一个地方集中访问这些实例。同时起到一种“实例池”的作用,有助于改善性能。

    Entity类层次示范了如何把你的类分离出来,而不需要每个游戏对象都设计一个类。使用组件类和cocos2d结点这样的层次结构的好处在于,你可以把一些很特别的功能创建为即插即用的类。这有助于用复合的方式而非继承的方式构造你的游戏对象。以这种方式编写游戏逻辑能更“柔性”,同时代码的复用性更好。最后,还学习了如何向怪物射击,以及BulletCache和EnemyCache类如何以一种直接的方式完成这个目的。HealthbarComponent提供了一个组件编程的极好例子。

    这个游戏到这里还有几件事情等你完成。首先最主要的是,玩家从来不会被子弹击中。可能你想为蛇加上一个血槽,或者为boss的行为写一些特殊的移动和射击组件。总之,这是一个开始编写滚屏游戏的绝佳起点,需要的只是不断去改进它。下一章,我将讲如果使用粒子特效为这个射击游戏增加炫目的视觉效果。



  • 相关阅读:
    【模板】并查集
    P1063能量项链
    多维动归第一题
    7.14测试
    7.12测试
    7.10测试
    几种display:table-cell的应用
    instanceof和typeof的区别
    右侧悬浮广告
    JavaScript判断浏览器类型及版本
  • 原文地址:https://www.cnblogs.com/encounter/p/2188476.html
Copyright © 2011-2022 走看看