夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【cocos2d】敵のキャラクター生成ルーチンを実装してみた

今回は、前回作成した.plistファイルから読み込んだデータを使って敵キャラクターを画面上に発生させてみます。このポストの最後に実行画面の動画が貼付けてありますのでご参照ください。
f:id:takujidev:20130422183102p:plain
一番上の行がDisctionaryのキーになり、2行目以降は各エントリーのそれぞれのキーに対する値になります。このファイルでは4エントリーありますので、敵が4個生成されます。
列Aは敵の種類を示します。この値を見てどの敵を生成するか判定します。列B, Cは敵を発生させる座標です。敵が突然画面内に現れないよう、画面外の座標を指定しています。なお、座標はX軸は画面中央、Y軸は画面下端を原点としています。これは人間(自分)が左右対称の位置を指定しやすくするためです。
列Dは次の敵を発生させるまでの間隔を秒数で指定します。0を指定したときは次のフレーム(1/60秒後)の発生となります。従いまして、全く同じタイミングで複数の敵を発生させる事はできません。これはプログラム簡略化と負荷の分散の両方の意味でそうしています。

では、敵を生成するプログラムの実装を見てみましょう。
MyEnemySpawnというクラスをCCNodeを継承して作りました。インスタンス変数として、.plistファイルから読み込んだArrayを保持する_spawnTableと、次にどのエントリーを読み出すかを保持する_indexがあります。initイニシャライザで初期化すればEnemySpawn.plistというファイルからデータを読み出す指定となります。initWithFileメソッドでの.plistファイルの読み込みについては前回のポストを参考にしてください。ここではNSAssertを適宜使ってファイルが存在する事、ファイルのデータが適切である事をチェックしています。NSAssert関数は、第1引数が真であれば何もしません。偽であれば第2引数で指定したメッセージをログ出力しプログラムを停止します。ファイルが存在しなかったりデータを読み込めなかったりした場合はnilが返るのでNSAssertで引っかかります。
最後にscheduleメソッドでタイマーを掛けます。プログラム開始後2秒経過するとspawnメソッドが呼ばれ最初の敵が生成されます。この2秒というのは適当です。

spawnメソッドではまず_spawnTableのエントリの数を調べます。毎回調べるのは無駄ですがインスタンス変数に保存しておくほどの情報でもないのでとりあえずそうしています。そして、まず次に処理するエントリーがArrayの範囲内にあるかどうか調べ、範囲外であればunscheduleでスケジュールを止め、returnします。uscheduleしないと無駄に何度も呼ばれます。
次にobjectAtIndexメソッドで_spawnTableから1エントリー分のDictionaryデータのオブジェクトを作り、変数elementに入れます。それと同時に_indexの値を次回のためにインクリメントしておきます。読み込んだ位置データのX座標の値に画面幅の半分の値を足しているのは、人間にわかりやすい左右対称の座標からcocos2dの通常の画面左下を原点とする座標に変換するためです。このときX座標とY座標の値をまとめてCGPoint構造体に入れています。ちなみにccpはCGPointMakeと同じで、cocos2dのCGPointExtension.hで定義されています。タイプ量がずいぶん少なくて済むので私はいつもccpを使っています。
次にMyEnemy* enemy = [MyEnemy enemyWithPosition:position type:type];の行ではpositionとtypeの情報を渡して敵キャラクターのオブジェクトを生成します。生成したキャラクターはバッチノードにaddChildします。あとはキャラクターのオブジェクトが勝手に動いてやられて消えてくれます。この、「オブジェクトを生成したら後は放置」というオブジェクト指向の考え方は私は大好きです。
最後にunscheduleでスケジュールを一旦止めて、データから読み込んだintervalの値で掛け直します。ちなみに、scheduleでなくscheduleOnceを使うとハマります。詳しくはこちらのポストをご参照ください。

@implementation MyEnemySpawn
{
    NSArray* _spawnTable;
    NSUInteger _index;
}

- (id)init
{
    return [self initWithFile:@"EnemySpawn"];
}

- (id)initWithFile:(NSString *)filename
{
    if ((self = [super init])) {
        NSBundle* bundle = [NSBundle mainBundle];
        NSString* path = [bundle pathForResource:filename ofType:@"plist"];
        NSAssert(path, @"Enemy spawn data file not found");
        _spawnTable = [NSArray arrayWithContentsOfFile:path];
        NSAssert(_spawnTable, @"Enemy spawn data missing");
        [self schedule:@selector(spawn) interval:2.0];
    }
    return self;
}

- (void)spawn
{
    NSUInteger tableCount = [_spawnTable count];
    if (_index >= tableCount) {
        [self unschedule:@selector(spawn)];
        return;
    }
    NSDictionary* element = [_spawnTable objectAtIndex:_index++];
    NSString* type = [element objectForKey:@"type"];
    float positionX = [[element objectForKey:@"positionX"] floatValue];
    float positionY = [[element objectForKey:@"positionY"] floatValue];
    CGSize winSize = [CCDirector sharedDirector].winSize;
    CGPoint position = ccp(positionX + winSize.width/2, positionY); //xの原点を画面中心から画面左端に変換
    float interval = [[element objectForKey:@"interval"] floatValue];
    MyEnemy* enemy = [MyEnemy enemyWithPosition:position type:type];
    [[MySpriteBatch sharedMyBatch] addChild:enemy];
    [self unschedule:@selector(spawn)];
    [self schedule:@selector(spawn) interval:interval];
}

@end

次にEnemySpawnクラスから呼ばれる側のMyEnemyクラスの実装をご覧ください。
今回修正した部分だけですが次のようになっています。MyEnemySpawnクラスから呼ばれるのがenemyWithPositionコンビニエンスコンストラクタです。敵のタイプ毎にif〜else文で分岐してそれに対応するイニシャライザで自分のオブジェクトを初期化します。
各イニシャライザではアクションを設定し、動きを滑らかにするのに必要なターゲットオブジェクトを生成し、最終的な初期化処理をします。
そして、updateメソッドには新たに画面外判定を導入し、打ち損ねて画面外に出た敵オブジェクトを密かに削除する処理を入れました。多少は画面外にはみ出てもまた画面内に戻ってこられるよう、実際の画面の2倍の縦横サイズの仮想画面を設定し、そこから外れる場合を画面外と判定します。注意しなくてはいけないのは、画面外に出た場合にこの仮想画面から外れるようアクションの最終座標を設定する事です。そうしないとオブジェクトが削除されずに残ってしまいます。

+ (id)enemyWithPosition:(CGPoint)position type:(NSString *)type
{
    if ([type isEqualToString:@"fly"]) {
        return [[self alloc] initFlyWithPosition:position];
    } else if ([type isEqualToString:@"ladybugL"]) {
        return [[self alloc] initLadybugLWithPosition:position];
    } else if ([type isEqualToString:@"ladybugR"]) {
        return [[self alloc] initLadybugRWithPosition:position];
    } else {
        return nil;
    }
}

- (id)initFlyWithPosition:(CGPoint)position
{
    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-72-red.png"];
    id moveBy = [CCMoveBy actionWithDuration:3.0 position:ccp(0, -1200)];
    CollisionSprite* target = [CollisionSprite node];
    target.position = position;
    return [self initWithPosition:position frame:frame action:moveBy target:target maxAccel:60/60.0 maxSpeed:600/60.0 brakeDistance:100.0];
}

- (id)initLadybugLWithPosition:(CGPoint)position
{
    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-72-red.png"];
    id moveBy = [CCMoveBy actionWithDuration:0.6 position:ccp(250, 100)];
    id moveBy2 = [CCMoveBy actionWithDuration:0.3 position:ccp(0, -200)];
    id moveByRev = [moveBy reverse];
    id moveBy2Rev = [moveBy2 reverse];
    id sequence = [CCSequence actions:moveBy, moveBy2, moveByRev, moveBy2Rev, nil];
    id repeat = [CCRepeatForever actionWithAction:sequence];
    CollisionSprite* target = [CollisionSprite node];
    target.position = position;
    return [self initWithPosition:position frame:frame action:repeat target:target maxAccel:60/60.0 maxSpeed:600/60.0 brakeDistance:100.0];
}

- (id)initLadybugRWithPosition:(CGPoint)position
{
    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-72-red.png"];
    id moveBy = [CCMoveBy actionWithDuration:0.6 position:ccp(-250, 100)];
    id moveBy2 = [CCMoveBy actionWithDuration:0.3 position:ccp(0, -200)];
    id moveByRev = [moveBy reverse];
    id moveBy2Rev = [moveBy2 reverse];
    id sequence = [CCSequence actions:moveBy, moveBy2, moveByRev, moveBy2Rev, nil];
    id repeat = [CCRepeatForever actionWithAction:sequence];
    CollisionSprite* target = [CollisionSprite node];
    target.position = position;
    return [self initWithPosition:position frame:frame action:repeat target:target maxAccel:60/60.0 maxSpeed:600/60.0 brakeDistance:100.0];
}

- (void)update:(ccTime)delta
{
    // 当たり判定
    [super update:delta];
    CCArray* array = [MySpriteBatch sharedMyBatch].children;
    CollisionSprite* hitObject = [[MyCollision sharedMyCollision]
                                  checkCollisionBetween:self andType:kMyShip | kMyBullet inArray:array];
    if (hitObject != nil) {
        [hitObject collisionDetectedWith:self];
        [self unscheduleUpdate];   // 連続して当たり判定しないようupdateを止める
        [self gotHit];
    }
    
    // 画面外判定
    CGSize screenSize = [CCDirector sharedDirector].winSize;
    CGRect screenRect = CGRectMake(-screenSize.width/2, -screenSize.height/2, screenSize.width*2, screenSize.height*2);
    if (CGRectIntersectsRect(self.boundingBox, screenRect) == NO) {     // 画面の中にいない?
        [self removeFromParentAndCleanup:YES];                          // 親ノードから外れてオブジェクト解放
    }
    
}

それでは、今回実装した敵生成ルーチンを組み込んだプログラムの実行画面をご覧ください。

そろそろcocos2dのアイコンにも飽きてきたのでキャラクターを描かなくてはと思っています。初めはカッコいいキャラクターの本格派シューティングゲームを目指していたのですが、想像だけでスペースシップやそれっぽい敵キャラを描くのは容易でない事にいまさらながら気がつきました。なので、実物が存在するものを題材にしたいと思います。