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

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

【cocos2d】当たり判定を実装してみた

ようやく各クラスをCCNodeのサブクラスからCCSpriteのサブクラスに直し終わりました。ずいぶんすっきりとしたように感じます。当たり判定に関する処理や情報を各クラス共通で入れたかったので実際は、CCSpriteを継承するCollisionSpriteというクラスを作り、さらにそれを継承して、MyShip, MyEnemy, MyBulletの各クラスを作っています。今までMySpriteという名前だった自機のクラスはこれを機にMyShipと言う名前に変更しました。
CollisionSpriteのヘッダと実装はこうなっています。radiusプロパティは当たり判定で使用する自分のオブジェクトの大きさです。とりあえずスプライトの幅の半分=スプライトの幅の直径を入れています。2つのオブジェクトの当たり判定の円が重なったときにコリジョン発生と見なします。CGRectInterSectRect()を使って円ではなく矩形で衝突判定する方法と比較して簡単そうなのでこちらにしました。radiusの大きさを調整する事で自機の当たり判定領域を小さくしたりする調整は簡単にできます。collisionDecectedWith:メソッドはサブクラスでオーバーライドして実装します。サブクラスで実装しなければこちらのメソッドが呼ばれますが、ここでは何もしません。コリジョンが発生した事を検知したオブジェクトは相手のオブジェクトに対してこのメッセージを送ります。相手が誰と衝突したかわかるようsenderで自分のオブジェクトを知らせます。

@interface CollisionSprite : CCSprite {
    
}

- (void)collisionDetectedWith:(CollisionSprite *)sender;

@property (assign) float radius;

@end

@implementation CollisionSprite {
    float _radius;
}

- (void)collisionDetectedWith:(CollisionSprite *)sender
{
    // コリジョン発生時に相手より呼ばれるメソッド。
    // 処理は各クラスでoverrideして実装する。
    // オーバーライドしてなければこれが呼ばれる。
}
@end

そして、コリジョンを検出するためのクラスMyCollisionを作ります。こちらは各クラスから何も考えずにアクセスできるようシングルトンの形式で実装しています。
ヘッダファイルは次のようになっています。まず、typedefでenumのObjType型を定義します。画面内の全てのオブジェクト同士のが重なっているかどうかをチェックするのは時間がかかるので、これにより敵同士など衝突を調べる必要のない物をスキップできるようにするのが狙いです。オブジェクトの種類ごとにビットの位置を変え、後からビット演算で衝突判定をするかどうかを求めます。各オブジェクトのイニシャライザでself.tagにオブジェクトの種類に見合った数値を入れておきます。KNoTypeはとりあえずその他のカテゴリーに当てはまらないオブジェクトを意味します。例えば自機のオプションなどです。

typedef enum {
    kNoType         = 1,
    kMyShip         = 1 << 1,
    kMyBullet       = 1 << 2,
    kEnemy          = 1 << 3,
    kEnemyBullet    = 1 << 4
} ObjType;

@interface MyCollision : CCNode {
    
}

+ (MyCollision *)sharedMyCollision;
- (CollisionSprite *)checkCollisionBetween:(CollisionSprite *)sender andType:(ObjType)type inArray:(CCArray *)array;

@end

実装ファイルは次の通りです。sharedMyCollisionは前にもやったシングルトンのぱたーんです。static変数でインスタンスがあるかないかを覚えておき、なければ生成、あればそれを返します。checkCollisionBetween:andThpe:inArrayは今回メインの衝突判定部分です。引数として、sender, type, arrayを受けます。senderはこのメソッドを呼んでいるオブジェクトです。typeはsenderが誰と衝突判定すべきか知らせるもので、ヘッダで定義したObjTypeが入ります。複数の種類との衝突を調べたいときは、例えば kMyShip | kMybulletのようにビット演算のORで値を重ねます。arrayはオブジェクトが入っている配列ですが、ここにはスプライトバッチノードのchildrenプロパティを入れてあげます。オブジェクトは全てここにはいっているはずです。for...in文の高速列挙でarrayに入っているオブジェクトを順に取り出します。childにはarray = バッチノードのchildrenから取り出されたオブジェクトが入ります。まず取り出したオブジェクトのtagとtypeのビット演算のANDを取ります。結果が0でなければ一致しているビットがあるという事なので衝突を判定する処理に進みます。ANDを取った結果が0であれば衝突判定をする必要はないので次のオブジェクトに進みます。intersecDistanceは2つのオブジェクトの当たり判定円の半径を足した値です。この2つのオブジェクト間の距離がintersectDistanceより小さければ、当たり判定円が重なっていることになるので、オブジェクトは衝突していると判定されます。オブジェクト間の距離はccpDistanceで求めます。矩形の当たり判定領域を使う場合は矩形の角同士が重なっときの距離の方が辺同士が重なったときの距離より長いので角同士が当たるとスプライトの絵同士は重なっていないのに当たっていると判定されてしまいます。円の場合はどの方向から当たっても同じ距離で衝突見なされるのでより自然な当たり判定方法だと思います。当たっていると判定された場合は、当たった相手のオブジェクトをメソッドの呼び出し元にリターンして終了します。例えばある敵にたいし見方の弾2発が同時に当たったとしても、当たった処理がされるのはそのうちのどちらか(for...inで先に取り出された方。多分先にバッチノードにaddされた方?)です。仮に敵に当たった弾は消えるという処理があったとして、実際に消えるのは1つだけでもう一方はすり抜けます。まあ、多分大きな問題にはならないと思います。どのオブジェクトにも当たらずのfor...in文が終了した場合は当たっていない印としてnilを返します。
ちなみに、2点間の距離はx座標の差の2乗+y座標の差の2乗のルートで求められます。ccpDistanceも内部的にはそのような計算をしていると思いますが、ルートの計算は時間がかかるので、ルートを取る代わりにintersectDistanceを2乗と比較しても同様に大小の比較が可能です。こうすれば処理の高速化がでるはずですが、どの程度効果があるかは謎なので現時点ではわかりやすさ重視で普通の距離の比較で実装しました。

@implementation MyCollision

+ (MyCollision *)sharedMyCollision
{
    static MyCollision* sharedInstance = nil;
    if (sharedInstance == nil) {
        sharedInstance = [[self alloc] init];
    }
    return sharedInstance;
}

- (CollisionSprite *)checkCollisionBetween:(CollisionSprite *)sender andType:(ObjType)type inArray:(CCArray *)array
{
    for (CollisionSprite* child in array) {
        if ((child.tag & type) != 0) {
            float intersectDistance = sender.radius + child.radius;
            if (ccpDistance(sender.position, child.position) < intersectDistance){
                return child;
            }
        }
    }
    return nil;
}

@end

最後に、checkCollisionBetweenメソッドを呼び出している側の実装です。これはMyEnemyのupdate部分ですが、このように毎フレーム当たり判定をしてあげる必要があります。敵は、自機と自機の弾に当たると破壊されるという設定で、kMyShip およびkMyBulletとの当たり判定を要求します。arrayにはバッチノードのchildrenプロパティを入れます。checkCollisionBetweenメッセージの結果がnilであれば何もしません。nilでなければまず衝突した相手にcollisionDetectedWithメッセージを送ってあげます。その後、自分の衝突処理をします。ここではとりあえず自分のオブジェクトを親ノードから取り外しています。これにより、オブジェクトは画面から消え、他のオブジェクトから保持されていなければメモリから解放されます。collisionDetetedWithメソッドは他のオブジェクトがこのオブジェクトと当たったときに呼んでもらうよう用意しておきます。敵と自機の弾のあたり判定を見たい場合、敵側から見れば十分で、弾側から見る必要はないので自機の弾からはcheckCollisionBetweenメソッドは呼んでいません。敵の数が自機の弾の数より少ないと仮定すれば、敵から見た方が効率が良いはずです。例えばオブジェクトの総数が100個で敵の数が20個、自機弾の数が50個と仮定すると、実際のあたり判定の処理はどちらの場合も20x50回ですが、for...inループでチェックする数は敵側からチェックする場合は最大100x20回、弾側からチェックする場合は最大100x50回と敵側からチェックする方が処理の総量が少なくて済みます。実際には衝突が見つかった時点でfor...inループを抜けるので一定ではありませんが、衝突はそう頻繁には起こらないので最大数に近い値にはなると思います。

- (void)update:(ccTime)delta
{
    // 当たり判定
    CCArray* array = [MyBatch sharedMyBatch].children;
    CollisionSprite* hitObject = [[MyCollision sharedMyCollision]
                                  checkCollisionBetween:self andType:kMyShip | kMyBullet inArray:array];
    if (hitObject != nil) {
        [hitObject collisionDetectedWith:self];
        [self removeFromParentAndCleanup:YES];
    }
}

- (void)collisionDetectedWith:(CollisionSprite *)sender
{
    [self removeFromParentAndCleanup:YES];
}

今回は弾にあたると敵が消え二度と復活しないという寂しい処理になってしまったので、次回はパーティクルエフェクトを使って敵の撃破で画面をにぎやかにしたいと思います。