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

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

【cocos2d】CCAction実行中の時間の経過にご注意!

cocos2dのCCAction実行中は時間が経過し、プログラムの他の部分が実行されるという事に注意する必要があります。
これに気づかずに私は昨日のポストのプログラムにバグを仕込んでしまいました。

例えば、このlandメソッドのCCScaleToアクションとCCCallBlockアクションはCCSequenceで連続して実行されますが、CCScaleToが始まった時点でプログラムの制御はイベントループに戻ります。
そして、scheduleUpdateを設定している場合は毎フレームupdate:メソッドが呼ばれます。
そこでうっかり変な状態遷移をさせてしまうと非常に見つけにくいバグとなってしまうのです。

- (void)land
{
    self.tag = kNoType;
    id scaleDown = [CCScaleTo actionWithDuration:0.2 scale:0.8];
    id stopAnimation = [CCCallBlock actionWithBlock:^ {
        [self stopAllActions]; //アニメーションを止める
        CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"locustSit0.png"];
        [self setDisplayFrame:frame]; //飛んでる絵から止まってる絵に変える
    }];
    [self runAction:[CCSequence actions:scaleDown, stopAnimation, nil]];
    id wait = [CCRotateBy actionWithDuration:0.2 angle:0.0];
    [_target runAction:wait]; // selfのアクションが終わるまで時間待ちselfは惰性で_targetまで移動する
    _actionMethod = @selector(sit);
}

このプログラムの場合、CCScaleToのあとCCCallBlockでアニメーションを止める処理をしているのですが、CCCallBlockが呼ばれる前にupdate:メソッドの中でアニメーションを開始する条件にヒットしてしまったために、アニメーション開始>即終了という現象が起こっていました。
update:の中では、self.tagの値を見て、kNoTypeであればアニメーション開始の条件をチェックしています。
landメソッドの処理が完全に完了するまではkNoTypeに移行するべきではなかったので、landの次に実行されるsitというメソッドで移す事で不具合を解消する事ができました

- (void)update:(ccTime)delta
{
    if (self.tag == kNoType) { //地面にいる場合
        CGPoint touch = ccpAdd([TouchLayer sharedTouch].location, TOUCH_OFFSET);
        CGPoint vector = ccpSub(touch, self.position);
        float distance = ccpLength(vector);
        if (distance <= 200.0 && distance >= 180.0) { // タッチ位置との距離がこの間なら飛び跳ねる
            self.tag = kEnemy;
            [_target stopAllActions];
            _actionMethod = @selector(jump);
        } else {
            CGSize screenSize = [CCDirector sharedDirector].winSize;
            CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
            if (CGRectIntersectsRect(self.boundingBox, screenRect) == YES) {     // 画面の中にいる?
                float difficulty = [DifficultyManager sharedDifficulty].difficulty;
                if (MYRAND(0, 30) <= difficulty) {
                    // 弾を撃つ
                    EnemyBullet* bullet = [EnemyBullet bulletWithPosition:self.position name:@"ball" vector:vector speed:(difficulty*20)+200];
                    [[SpriteBatch sharedSpriteBatch] addChild:bullet z:30]; //他のオブジェクトより手前に表示されるようにする
                }
            }
        }
        self.rotation = 90.0 + CC_RADIANS_TO_DEGREES(-ccpToAngle(vector)); //タッチ方向を向く
    }
    
    if ([_target numberOfRunningActions] == 0) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:_actionMethod];
#pragma clang diagnostic pop
    }
    if (!_blinking) {
        [self collision]; //点滅中、消えているときはあたり判定しない
    }
    [self steering];
    [self outOfScreen];
    
}

以下はデバッグ後の完全版プログラムです。

- (void)update:(ccTime)delta
{
    if (self.tag == kNoType) { //地面にいる場合
        CGPoint touch = ccpAdd([TouchLayer sharedTouch].location, TOUCH_OFFSET);
        CGPoint vector = ccpSub(touch, self.position);
        float distance = ccpLength(vector);
        if (distance <= 250.0 && distance >= 230.0) { // タッチ位置との距離がこの間なら飛び跳ねる
            self.tag = kEnemy;
            [_target stopAllActions]; //地面と一緒に動くのをやめ、nuberOfRunningActionsを0にする
            _actionMethod = @selector(jump);
        } else {
            CGSize screenSize = [CCDirector sharedDirector].winSize;
            CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
            if (CGRectIntersectsRect(self.boundingBox, screenRect) == YES) {     // 画面の中にいる?
                float difficulty = [DifficultyManager sharedDifficulty].difficulty;
                if (MYRAND(0, 30) <= difficulty) {
                    // 弾を撃つ
                    EnemyBullet* bullet = [EnemyBullet bulletWithPosition:self.position name:@"ball" vector:vector speed:(difficulty*20)+200];
                    [[SpriteBatch sharedSpriteBatch] addChild:bullet z:30]; //他のオブジェクトより手前に表示されるようにする
                }
            }
        }
        self.rotation = 90.0 + CC_RADIANS_TO_DEGREES(-ccpToAngle(vector)); //タッチ方向を向く
    }
    
    if ([_target numberOfRunningActions] == 0) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:_actionMethod];
#pragma clang diagnostic pop
    }
    if (!_blinking) {
        [self collision]; //点滅中、消えているときはあたり判定しない
    }
    [self steering];
    [self outOfScreen];
    
}

- (void)sit
{
    self.tag = kNoType; // 飛び跳ねたあとここで状態を戻す。
    float speed = 100.0;
    float distance = 2000;
    CGPoint vector = ccp(0, -distance);
    [_target runAction:[CCMoveBy actionWithDuration:distance/speed position:vector]];
}

- (void)jump
{
    float speed = 400.0;
    CGPoint touch = ccpAdd([TouchLayer sharedTouch].location, TOUCH_OFFSET);
    CGPoint vector = ccpSub(touch, self.position);
    vector = ccpMult(vector, 2.0); //通り過ぎるくらい飛ぶ
    float distance = ccpLength(vector);
    
    // アニメーション・運動特性の設定
    CCAnimation* animation = [self animationWithName:@"locustFly" frameCount:4 delay:0.02];
    id animate = [CCAnimate actionWithAnimation:animation];
    id repeat = [CCRepeatForever actionWithAction:animate];
    [self runAction:repeat];
    
    id scaleUp = [CCScaleTo actionWithDuration:0.2 scale:1.0];
    [self runAction:scaleUp];
    id move = [CCMoveBy actionWithDuration:distance/speed position:vector];
    [_target runAction:move];

    _actionMethod = @selector(land);
}

- (void)land
{
    // scaleDown中にjumpに状態遷移しないようここではtagをいじらない
    id scaleDown = [CCScaleTo actionWithDuration:0.2 scale:0.8];
    id stopAnimation = [CCCallBlock actionWithBlock:^ {
        [self stopAllActions]; //アニメーションを止める
        CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"locustSit0.png"];
        [self setDisplayFrame:frame]; //飛んでる絵から止まってる絵に変える
    }];
    [self runAction:[CCSequence actions:scaleDown, stopAnimation, nil]];
    id wait = [CCRotateBy actionWithDuration:0.3 angle:0.0];
    [_target runAction:wait]; // selfのアクションが終わるまで時間待ちselfは惰性で_targetまで移動する
    _actionMethod = @selector(sit);
}