読者です 読者をやめる 読者になる 読者になる

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

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

【偽スペースハリアー】.plistに従い敵キャラを生成

cocos2d Objective-C スペースハリアー TravelShooting JP

これまではランダムな位置に敵キャラを生成していましたが、今回からあらかじめ決めた位置・タイミングで敵を生成するようにしました。

敵キャラは「名前」、「x座標」、「インターバル」をキーとした.plistに記述し、そこから読み込むようにしました。.plistファイルを自分で作るのは大変なので、.csvファイルを表計算ソフトで作成し、それを変換します。
.csvファイルを.plistファイルに変換してプログラムから読み込む方法はBeeCluster開発時の
【Objective-C】.plistファイルの作り方と読み込み方 - 夏までにiPhone アプリつくってみっか!
をご参照ください。

「.plistを読み込んだNSArrayから1レコード取り出し、待ち時間分のインターバルタイマーをスケジュールし、スケジュールで呼び出されたら改めてそのレコードの内容に従って敵を生成する」という流れになっています。
タイミングをインターバルとして表現するか絶対時刻として表現するか悩ましいですが、両方試した結果インターバルの方がデータの修正が簡単なのでこちらを採用しました。

また、左右のスクロールに合わせて敵を生成する位置を左右にオフセットすることで敵が左右に流れて行ってしまわないようにしています。

- (id)initWithFile:(NSString *)filename
{
    if ((self = [super init])) {
        [[Scene3D sharedScene3D] registerZScrollNotification:self]; // zスクロール通知を受け取る登録
        NSBundle* bundle = [NSBundle mainBundle];
        NSString* path = [bundle pathForResource:filename ofType:@"plist"];
        NSAssert(path, @"StillObject spawn data file not found");
        _spawnTable = [NSArray arrayWithContentsOfFile:path];
        NSAssert(_spawnTable, @"StillObject spawn data missing");

        // 最初のレコードを読んでタイマーを掛ける
        _index = 0;
        NSDictionary* record = [_spawnTable objectAtIndex:_index];
        CGFloat interval = [[record objectForKey:@"interval"] floatValue];
        [self schedule:@selector(spawn) interval:interval];
    }
    return self;
}

- (void)spawn
{
    NSDictionary* record = [_spawnTable objectAtIndex:_index++];
    StillObjSpawnData* spawnData = [[StillObjSpawnData alloc] init];
    spawnData.name = [record objectForKey:@"name"];
    spawnData.x = [[record objectForKey:@"x"] floatValue];
    
    // 左右スクロールにより生成x位置をオフセットする
    spawnData.x += [Scene3D sharedScene3D].xSpeed * -SCROLL_FACTOR;
    
    
    // 敵キャラの生成
    StillObject* stillObj = [[StillObject alloc] initWithSpawnData:spawnData];
    [[BatchNode sharedBatch] addChild:stillObj];
    
    if (_index >= _spawnTable.count) {
            // 最終レコードならこの面は終了
        [self unschedule:@selector(spawn)];
    } else {
        // 次のレコードを先読みしてタイマーを掛ける
        record = [_spawnTable objectAtIndex:_index];
        CGFloat interval = [[record objectForKey:@"interval"] floatValue];
        [self unschedule:@selector(spawn)];
        [self schedule:@selector(spawn) interval:interval];
    }
}

- (void)onExit
{
    [[Scene3D sharedScene3D] unRegisterZScrollNotification:self]; // zスクロール停止通知を解除
    [super onExit];
}

- (void)zScrollPauseNotification:(BOOL)paused
{
    if (paused) {
        [self pauseSchedulerAndActions];
    } else {
        [self resumeSchedulerAndActions];
    }
}

主人公キャラがやられたときはz方向のスクロールを止めるので、同時に敵生成スケジュールのタイマーも[self pauseSchedulerAndActions];で一時停止しています。
そうしないと、スクロールが再開したときに一気に敵が押し寄せてきてしまいます。

スクロールの停止、再開のタイミングはScend3Dというオブジェクトから通知を受けるようにしています。
(正確にはScene3Dオブジェクトのプロパティをほかのオブジェクト操作する過程で通知が発行されます。)
Scene3Dはシングルトンパターンで実装しており、シーンに関する様々な情報を集中して管理しており、他のクラスのオブジェクトはこのオブジェクト経由で3Dシーンに関する情報をやり取りします。
プレイヤーキャラクターがやられたときは、このオブジェクトのzScrollFixedプロパティにYESを書き込みます。
このプロパティのセッターは次のようにオーバーライドされており、通知を受け取りたいと登録して来たオブジェクトに対し、自動的に通知が行くようにしています。

// セッターをオーバーライド
- (void)setZScrollFixed:(BOOL)zScrollFixed
{
    _zScrollFixed = zScrollFixed; // セッターなのでプロパティでなくインスタンス変数にアクセスすること
    // zスクロールの停止・再開を通知する
    for (id recipient in _recipients) {
        [recipient zScrollPauseNotification:zScrollFixed];
    }
}

- (void)registerZScrollNotification:(id)object
{
    [_recipients addObject:object];
}

- (void)unRegisterZScrollNotification:(id)object
{
    [_recipients removeObjectIdenticalTo:object];
}

セッターの上書きなので、self.zScrollFixed = zScrollFixed;のようにプロパティにアクセスするとクラッシュします。
上記のようにインスタンス変数に直接アクセスするようにします。

ちなみに、スクロールの状態が変化したら通知する機能はObjective-Cプロトコルとして宣言しています。

@protocol ZScrollNotifying <NSObject>

@required
- (void)zScrollPauseNotification:(BOOL)paused;

@end

すみません、今回は内容が難しかったかもしれません。
では、最後になりましたが今回の動画です。

次回はいよいよ自分の意志で動く敵を実装したいと思います。