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

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

【cocos2d】1面ボスを作ってみた。CCActionで複雑なパターンを実装

1面のボスキャラとしてクワガタを作りました。
まずは動画をご覧ください。

ボスキャラは特殊な処理が多いので雑魚キャラとはクラスを分けて実装することに決めました。
イニシャライザーの実装です。

- (id)initWithPosition:(CGPoint)position type:(NSString *)type name:(NSString *)name attackPoint:(float)attackPoint life:(float)life
{
    if ((self = [super initWithSpriteFrameName:[NSString stringWithFormat:@"%@0.png", name]])) {
        self.radius = self.contentSize.width/3.0; //デフォールト値
        self.tag =kEnemy;
        self.attackPoint = attackPoint;
        self.life = life;
        self.position = position;
        
        CGPoint center = ccp(self.contentSize.width/2.0, self.contentSize.height/2.0);
        
        _jawLeft = [CollisionSprite spriteWithSpriteFrameName:@"jawLeft.png"];
        _jawRight = [CollisionSprite spriteWithSpriteFrameName:@"jawRight.png"];
        _arms = [CollisionSprite spriteWithSpriteFrameName:@"arms.png"];
        _jawLeft.position = center;
        _jawLeft.anchorPoint = ccp(0.49, 0.5);
        _jawRight.position = center;
        _jawRight.anchorPoint = ccp(0.51, 0.5);
        _arms.position = center;
        [self addChild:_jawLeft z:-10];
        [self addChild:_jawRight z:-10];
        [self addChild:_arms z:-20];
        
        // デフォールトの運動特性
        _maxAccel = 20/FPS;
        _maxSpeed = 300/FPS;
        _brakeDistance = 100.0;
        _blinking = NO;
                
        // ターゲットにアクションを設定
        _target = [CollisionSprite node];
        _target.position = position;
        [[ParentNode sharedParentNode] addChild:_target];
        CCAction* action = [self createAction];
        [_target runAction:action];
        
        // アニメーションの設定
        CCAnimation* animation = [self animationWithName:name frameCount:4 delay:0.1];
        id animate = [CCAnimate actionWithAnimation:animation];
        id repeat = [CCRepeatForever actionWithAction:animate];
        [self runAction:repeat];
        [self scheduleUpdate];
    }
    return self;
}

アゴを開いたり閉じたりさせたいので左右のアゴを本体と分けて描きました。また、角は本体より下に重ねますが、前足はアゴより下に重なるので前足も本体と分離しています。結果として4枚のスプライトを使用しています。
CLIP STUDIO PAINT PROでレイヤーを分けて描き、.pngファイルを書き出すときにレイヤーをON/OFFしています。cocos2d側では親ノードの中心に子ノードを一合わせします。アゴスプライトのアンカーポイントをアゴの根元に移動します。こうしないとCCRotateByで回す時の中心がおかしくなります。なお、アンカーポイントの指定はピクセルではなく0〜1の値となります。私はここでハマりました
アニメーションは羽の動きを表現します。キャラが大きくなるとテクスチャアトラスの大部分を占有してしまうので、将来的には羽を別体スプライトにして動かすことになるかもしれません。現時点のテクスチャアトラスはこんな感じになっています。サイズはRetina用が2002 x 178pixelで、非Retina用は755x127pixelす。
f:id:takujidev:20130506100306j:plain

クワガタの動きの部分はCCActionを組み合わせて実装しています。

- (id)createAction
{
    id moveDown = [CCMoveBy actionWithDuration:2.0 position:ccp(0, -300)];
    id openAndCloseJaw = [CCCallBlock actionWithBlock:^{
        [self openAndCloseJaw];
    }];
    id shootLaser = [CCCallBlock actionWithBlock:^{
        [self shootLaser];
    }];
    id shoot7Way = [CCCallBlock actionWithBlock:^{
        [self shoot7Way];
    }];
    id shootSlow = [CCCallBlock actionWithBlock:^{
        [self shootSlow];
    }];

    id pause02 = [CCMoveBy actionWithDuration:0.2 position:CGPointZero];
    id moveLeft = [CCMoveBy actionWithDuration:1.0 position:ccp(-25, 0)];
    id moveRight = [CCMoveBy actionWithDuration:1.0 position:ccp(25, 0)];
    id sequenceL = [CCSequence actions: openAndCloseJaw, moveLeft, shootSlow, shootLaser, shootSlow, shoot7Way,
                    pause02, shootSlow, shootLaser, moveLeft, nil];
    id sequenceR = [CCSequence actions: openAndCloseJaw, moveRight, shootSlow, shootLaser, shootSlow, shoot7Way,
                    pause02, shootSlow, shootLaser, moveRight, nil];
    id sequenceLRL = [CCSequence actions:sequenceL, sequenceL, sequenceL, sequenceR, sequenceR, sequenceR, sequenceR, sequenceR, sequenceR,
                      sequenceL, sequenceL, sequenceL, nil];
    id repeat = [CCRepeat actionWithAction:sequenceLRL times:UINT_MAX];
    id sequence = [CCSequence actions: moveDown, repeat, nil];
    return sequence;
}

クワガタは画面上方から降りてきて、左右に移動しながらアゴを開け閉めし、弾を吐きます。
レーザーを2回連続で撃つのですが、それらが重ならないように時間待ちのためにCGPointZero(=何もしない)のCCMoveByアクションを使っています。もしかするともっといい方法があるのかもしれません。
細切れにしたアクションをCCSequenceでまとめてそれをさらにまとめたりしています。CCRepeatの部分は「ほぼ」永遠に繰り返す処理になっています。ここで完璧な永遠を求めてCCRepeatForeveを使うとハマります。
弾を撃ったりアゴを動かしたりする部分は別メソッドにしてCCCallBlockで呼び出します。
そのうちのいくつかの実装です。

- (void)shoot7Way
{
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(-1.0, -1.73205) speed:200.0] z:30];
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(-1.0, -2.74748) speed:200.0] z:30];
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(-1.0, -5.67128) speed:200.0] z:30];
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(0.0, -1.0) speed:200.0] z:30];
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(1.0, -5.67128) speed:200.0] z:30];
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(1.0, -2.74748) speed:200.0] z:30];
    [[SpriteBatch sharedSpriteBatch]
     addChild:[EnemyBullet bulletWithPosition:self.position name:@"ball" vector:ccp(1.0, -1.73205) speed:200.0] z:30];
}

- (void)openAndCloseJaw
{
    id openLeft = [CCRotateBy actionWithDuration:0.5 angle:-20.0];
    id openRight = [CCRotateBy actionWithDuration:0.5 angle:20.0];
    id closeLeft = [CCRotateBy actionWithDuration:0.5 angle:20.0];
    id closeRight = [CCRotateBy actionWithDuration:0.5 angle:-20.0];
    id pause1 = [CCMoveBy actionWithDuration:1.0 position:CGPointZero];
    id sequenceLeft = [CCSequence actions:openLeft, pause1, closeLeft, nil];
    id sequenceRight = [CCSequence actions:openRight, pause1, closeRight, nil];
    [_jawLeft runAction:sequenceLeft];
    [_jawRight runAction:sequenceRight];
}

7way弾はバカみたいに7個分の処理を並べています。ベクトルはあらかじめ関数電卓タンジェントの値を計算した結果を入れています。これをfor文でサラッとまとめらるようになったらカッコいいですね。
アゴの開け閉めはCCRotateByで20°回して、1秒待って逆方向に20°回しています。
本体の動きのActionとアゴのActionのシーケンスは連動していませんので、注意が必要です。このopenAndCloseJawのアクションは完了までに2秒かかりますので、2秒以内にこのメソッドが呼ばれるとアクションが重なって動きがおかしくなってしまいます。