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

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

【cocos2d】ダミーのスプライトを使った当たり判定

BeeCluster 4面のボスのクマ。
時折腕を振り回して攻撃してきます。
今回は、クマの腕に使用した、ダミースプライトによる当たり判定について書きたいとおもいます。

BeeClusterの当り判定は全てオブジェクト同士の距離を計算して行っています。
つまり、オブジェクトスプライトの絵に相当する大きさの直径の円が別のオブジェクトの円と重なった場合、「当たっている」と判断します。詳しくはこちらのポストをご参照ください。
この方式の欠点は、細長いキャラクターの当たり判定が不正確になる事です。クマの腕はかなり細長い形をしています。
当たり判定の円の直径を大きくするとクマの腕が当たっていないのに当りと判定されたり、逆に小さくすると掌が当たってもすり抜けてしまう現象が発生します。
矩形の当たり判定であればより正確になりますが、クマの腕だけのために特別な判定機能を実装するのは重労働なので別の方法を考えます。
それが、ダミーのスプライトを使ったあたり判定です。
簡単にいうと、クマの掌部分に当たり判定用の見えないスプライトを配置し、掌大の当たり判定円を設定しようという物です。
従いまして、掌以外の腕の部分にはあたり判定はありません。
これは、「実はクマは手を振り上げてパンチしている」という解釈で逃げたいと思います。
ダミーのスプライトを腕の形状に沿って並べれば腕とのコリジョン検出も可能ですが、このゲームではやりません。

これは、イニシャライザーの腕の初期化処理です。

        _armL = [CollisionSprite spriteWithSpriteFrameName:@"bearArm.png"];
        _armR = [CollisionSprite spriteWithSpriteFrameName:@"bearArm.png"];
        _armR.scaleX = -1.0; //左右反転
        [[SpriteBatch sharedSpriteBatch] addChild:_armL];
        [[SpriteBatch sharedSpriteBatch] addChild:_armR];
        _armL.anchorPoint = ccp(0.5, 0.8);
        _armR.anchorPoint = ccp(0.5, 0.8);
        [self resetArmL]; //腕の位置、角度を初期値に
        [self resetArmR]; //腕の位置、角度を初期値に
        _armL.tag = kNoType; //腕はあたり判定なし
        _armR.tag = kNoType; //腕はあたり判定なし
    
        _palmL = [CollisionSprite node]; // 当たり判定用なので絵はいらない
        _palmR = [CollisionSprite node]; // 当たり判定用なので絵はいらない
        [[ParentNode sharedParentNode] addChild:_palmL];
        [[ParentNode sharedParentNode] addChild:_palmR];
        [self updatePalms]; //_palmL, _paLmRのpositionを設定。
        _palmL.radius = 30.0;
        _palmR.radius = 30.0;

テクスチャアトラスの残り面積が厳しくなってきたので右側の腕は逆の腕のテクスチャーを反転して使用しています。_armR.scaleX = -1.0の部分です。
回転の中心が付け根部分になるようにanchorPointのY座標を上にずらしています。
resetArmL, resetArmRというメソッドは単に腕の位置・角度を初期位置にするための物です。

- (void)resetArmL
{
    CGSize winSize = [CCDirector sharedDirector].winSize;
    CGPoint winCenter = ccp(winSize.width/2.0, winSize.height/2.0);
    _armL.position = ccp(winCenter.x - 280, winCenter.y + 50);
    _armL.rotation = 0.0;
}

- (void)resetArmR
{
    CGSize winSize = [CCDirector sharedDirector].winSize;
    CGPoint winCenter = ccp(winSize.width/2.0, winSize.height/2.0);
    _armR.position = ccp(winCenter.x + 280, winCenter.y + 50);
    _armR.rotation = 0.0;
}

280とか50とかいう数値は試行錯誤して決めました。

当たり判定用のスプライト_palmL, _palmRはテクスチャーを読み込まずに生成します。CollisionSpriteは当たり判定用のCCSpriteを継承して作ったクラスです。CCSpriteに当たり判定円の半径を入れておくradiusというプロパティを追加しただけです。掌の半径は30ピクセルにしました。
updatePalmsメソッドでスプライトとクマの腕の絵の位置合わせをしています。

- (void)updatePalms
{
    // 腕の先にあたり判定用の手の位置を合わせる。初期化時および毎フレーム呼ばれる。
    _palmL.position = ccpRotateByAngle(ccpAdd(_armL.position, ccp(0, -260)), _armL.position, -CC_DEGREES_TO_RADIANS(_armL.rotation-10));
    _palmR.position = ccpRotateByAngle(ccpAdd(_armR.position, ccp(0, -260)), _armR.position, -CC_DEGREES_TO_RADIANS(_armR.rotation+10));
}

260, 10という数値は試行錯誤して求めますが、このときは目安が欲しいので目に見えるテクスチャーを表示した状態で位置合わせしました。
ccpRotateByAngleという関数は、第1引数が示す点が、第2引数が示す点を中心に第3引数が示す(ラジアン)角度だけ反時計回りに回転するとどの位置に来るかを求めてくれる便利なものです。
cocos2dの角度は「°」(degree)で回転方向は時計回りなので、ラジアンに変換し、値をマイナスにして渡す必要があります。
このupdatePalmsはupdateメソッドから毎フレーム呼ばれます。
したがって、ダミースプライトは掌の位置に正確に追従します。

パンチを繰り出すためのccActionはこんな感じになっています。

- (void)punchL
{
    id rotate = [CCRotateBy actionWithDuration:1.0 angle:-105];
    id move = [CCMoveBy actionWithDuration:1.0 position:ccp(100, 200)];
    id punch = [CCSpawn actions:rotate, move, nil];
    id ease = [CCEaseIn actionWithAction:punch rate:4.0];
    id pull = [CCMoveBy actionWithDuration:0.4 position:ccp(0, 200)];
    id reset = [CCCallBlock actionWithBlock:^ {
        [self resetArmL];
    }];
    id sequence = [CCSequence actions:ease, pull, reset, nil];
    [_armL runAction:sequence];
    
    id wait = [CCRotateBy actionWithDuration:1.6 angle:0.0]; // punch+pullより少し長め
    [self runAction:wait];
    _actionMethod = @selector(shoot);
}

- (void)punchR
{
    id rotate = [CCRotateBy actionWithDuration:1.0 angle:105];
    id move = [CCMoveBy actionWithDuration:1.0 position:ccp(-100, 200)];
    id punch = [CCSpawn actions:rotate, move, nil];
    id ease = [CCEaseIn actionWithAction:punch rate:4.0];
    id pull = [CCMoveBy actionWithDuration:0.4 position:ccp(0, 200)];
    id reset = [CCCallBlock actionWithBlock:^ {
        [self resetArmR];
    }];
    id sequence = [CCSequence actions:ease, pull, reset, nil];
    [_armR runAction:sequence];
    
    id wait = [CCRotateBy actionWithDuration:1.6 angle:0.0]; // punch+pullより少し長め
    [self runAction:wait];
    _actionMethod = @selector(shoot);
}

腕の回転と移動をCCSpawnで同時に行い、CCEaseInを掛ける事でパンチを加速させます。
pullは腕をとりあえず画面から出すために入れています。4インチのRetinaディスプレイでも確実に画面外に出るよう距離は調整しています。
その後、CCCallBlockで腕の位置を初期位置に戻します。
クマ本体の動きと同期させるため本体側にはwaitのアクション(角度を変えないCCRotateBy)を入れて腕のアクションの合計時間より少し長めに待たせます。
待たせない場合はパンチの動作が終わる前に次のアクションが始まってしまうためゲームの難易度が上がります。
あとは実際にプレーしてダミースプライトの位置、大きさを微調整すれば完成です。
私は、当たり判定検出部分にブレークポイントを掛けて、当たり判定が検出したときの絵の重なり具合を見ながら何度も調整しました。