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

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

【cocos2d】親ノードと子ノードのCCActionを同期させる方法

子ノードのアクションを親ノードのアクションにシンクロナイズさせてタイミングを合わせたいときありますよね。
そんなときは、親ノードのCCCallBlockで子ノードに対しrunActionしてやればうまく行きます。
例えば、この動画をご覧ください。

これは、今開発中のBeeClusterというゲームの2面にボスとして登場するカメレオンなのですが、カメレオン本体が親ノードで、子ノードとして目、舌、舌先のスプライトがaddされています。
舌と舌先は本来はカメレオン本体の下に隠れるのですが、動作がわかりやすいように上に配置しています。
cocos2dでは親ノードをCCFadeOutでフェードアウトしても子ノードはフェードアウトしないので、それぞれフェードアウトしてあげる必要があります。
また、舌や舌先がフェードアウト中にカメレオンから透けて見えないように、フェードアウト前にvisibleプロパティをNOにして表示を消し、カメレオンが完全にフェードインしてからvisibleをYESにする必要があります。
各ノードに対してアクションを別々に掛けると、タイミングを合わせるためのウェイトが必要になり、面倒です。
そんなときは、親ノードのタイミングを基準に、子ノードのアクションをCCCallBlockからrunActionすることで、面倒なウェイトを入れずにアクションを同期させる事ができます。
プログラム例です。

- (void)move
{
    id invisible = [CCCallBlock actionWithBlock:^{
        _tongue.visible = NO;
        _tip.visible = NO;
        [_eye runAction:[CCFadeOut actionWithDuration:1.0]];
    }];

    id fadeout = [CCFadeOut actionWithDuration:1.0];
    
    float dice = MYRAND(0, 3);
    CGSize winSize = [CCDirector sharedDirector].winSize;
    CGPoint screenCenter = ccp(winSize.width/2, winSize.height/2);
    CGPoint position;
    if (dice <= 1) {
        position = ccp(screenCenter.x -100, screenCenter.y);
    } else if (dice <= 2) {
        position = ccp(screenCenter.x, screenCenter.y + 100);
    } else {
        position = ccp(screenCenter.x + 100, screenCenter.y);
    }
    id move = [CCMoveTo actionWithDuration:1.0 position:position];
    
    id fadeinEye = [CCCallBlock actionWithBlock:^{
        [_eye runAction:[CCFadeIn actionWithDuration:1.0]];
    }];
    
    id fadein = [CCFadeIn actionWithDuration:1.0];
    id visible = [CCCallBlock actionWithBlock:^{
        _tongue.visible = YES;
        _tip.visible = YES;
    }];
    id sequence = [CCSequence actions:invisible, fadeout, move, fadeinEye, fadein, visible, nil];
    [self runAction:sequence];
}

まず、舌、舌先はvisible = NOとし、表示を消します。目は体と同じようにフェードアウトさせたいので、CCFadeOutアクションを使います。
これらをinvisibleというCCCallBlockアクションに入れます。
本体のフェードアウトは別のアクションとして登録します。CCCallBlockの中に[self runAction:...];のような処理を入れるとうまく行きません。
次は移動のアクションですが、子ノードは親ノードの移動に自動的に追従するので子ノードに対するアクションは必要ありません。
フェードインのアクションは舌、舌先(visible)を一番最後にし、目(fadeinEye)と体(fadein)は同じタイミングとします。
あとは、これらのアクションをCCSequenceで順番にならべ、親ノードに対しrunActionします。
なお、親ノードと子ノードにアクションを掛ける順番は重要です。
たとえば、fadeinとfadeinEyeを逆にするとどうなるでしょうか?
その場合は、体がフェードインしてから目がフェードインします。
なぜなら、親のCCFadeInアクションが完了するまでfadeinEyeのCCCallBlockが実行されないからです。
fadeInEyeが先の場合、親ノードにおけるCCCallBlockは一瞬で終了しますので次のfadeinと同時にスタートします。

逆に、CCCallBlockの中の子ノードのアクションが完了するのを待ちたい場合は、親ノードにウェイトを掛けます。
目を動かす処理では目を動かしている間は親ノードを待たせています。

- (void)moveEye
{
    CGPoint touch = ccpAdd([TouchLayer sharedTouch].location, TOUCH_OFFSET); //狙う場所はタッチ位置の少し上
    CGPoint vector = ccpSub(touch, [self convertToWorldSpace:_eye.position]);
    CGPoint downVector = ccp(0, -100);
    float angle = ccpAngleSigned(vector, downVector);
    id moveEye = [CCCallBlock actionWithBlock:^{ 
        [_eye runAction:[CCRotateTo actionWithDuration:0.3 angle:CC_RADIANS_TO_DEGREES(angle)]];
    }];
    id wait = [CCMoveBy actionWithDuration:0.3 position:ccp(0, 0)];
    id sequence = [CCSequence actions:moveEye, wait, nil];
    [self runAction:sequence];
}