人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

衝突判定

プレイヤーが撃った弾がエイリアンにあたるとエイリアンが消滅するようにします。

invader05.jar

intersects()を用いた衝突判定

エイリアンと弾の衝突判定を実装するわけですが、エイリアンの外周はかなり複雑で弾があたったかどうか判断するのは難しいですね。こういう場合は、下図のように矩形と矩形の衝突を判定するのが一般的です。ちなみに矩形は「くけい」と読みます。長方形のことです。

f:id:aidiary:20090901212059g:plain

このように矩形で衝突判定すると問題もあります。一番右のように弾がエイリアンにあたっていなくても衝突したことになってしまうからです。ただこの誤差は目で見てもほとんどわからないので今回は考えないことにします。もっと厳密に判定したい場合はエイリアンの衝突判定矩形を小さくするなどの方法があります。

矩形同士の衝突を判定しているのがAlienクラスのcollideWith()です。弾と衝突判定するメソッドはエイリアン側に持たせています。各エイリアンが引数で指定された弾(Shot)と衝突しているか判定させたかったからです。衝突判定メソッドを弾に持たせるという方法も考えられますね。

    /**
     * エイリアンと弾の衝突を判定する
     * 
     * @param shot 衝突しているか調べる弾オブジェクト
     * @return 衝突していたらtrueを返す
     */
    public boolean collideWith(Shot shot) {
        // エイリアンの矩形を求める
        Rectangle rectAlien = new Rectangle(x, y, width, height);
        // 弾の矩形を求める
        Point pos = shot.getPos();
        Rectangle rectShot = new Rectangle(pos.x, pos.y,
                shot.getWidth(), shot.getHeight());
        
        // 矩形同士が重なっているか調べる
        // 重なっていたら衝突している
        return rectAlien.intersects(rectShot);
    }

まずエイリアンと弾の矩形を求める必要があります。矩形はJavaのRectangleクラスを使います。Rectangleクラスは矩形の左上の座標と幅、高さを指定して作ります。

次に矩形同士の衝突処理です。矩形同士が衝突している=矩形が重なっているということです。矩形同士が重なっているかを調べるメソッドはなんとJavaにはすでに用意されています。Rectangleクラスのintersects()メソッドです。

   矩形1.intersects(矩形2)

という形式です。矩形1と矩形2が重なっている、つまり衝突していたらtrueを返します。プログラムではエイリアンの矩形(rectAlien)と弾の矩形(rectShot)が重なっているか調べています。

Javaはこういうちょっとしたメソッドがライブラリとして用意されているのですごく便利ですね。intersects()がないJava以外で矩形同士の衝突処理はどうするかっていう話はまた後で取り上げます。

エイリアンと弾の衝突判定

エイリアンと弾の衝突判定を行うメソッドはできました。次にMainPanelクラスにこの機能を組み込みます。MainPanelクラスのcollisionDetection()です。

    /**
     * エイリアンと弾の衝突検出
     *
     */
    private void collisionDetection() {
        // エイリアンと弾の衝突検出
        for (int i=0; i<NUM_ALIEN; i++) {
            for (int j=0; j<NUM_SHOT; j++) {
                if (aliens[i].collideWith(shots[j])) {
                    // i番目のエイリアンとj番目の弾が衝突
                    // エイリアンは死ぬ
                    aliens[i].die();
                    // 弾は保管庫へ(保管庫へ送らなければ貫通弾になる)
                    shots[j].store();
                    // エイリアンが死んだらもうループまわす必要なし
                    break;
                }
            }
        }
    }

今回はエイリアンが50体、弾が5つあります。この全部の衝突を判定するには、エイリアン0が弾1〜弾5と衝突しているか、エイリアン1が弾1〜弾 5と衝突しているか、・・・エイリアン49が弾1〜弾5と衝突しているかを全部判定しないとだめです。気が遠くなるほど大変そうですがコンピュータでは一瞬で終わるのでご安心を。上のように2重のforループをまわすと簡単にできます。

エイリアンiと弾jが衝突した場合は、エイリアンも弾も消滅するようにします。弾は保管庫に移動させればいいですね。store()で保管庫に入ります。エイリアンの場合も弾の保管庫のように画面外に移動させることで消滅させます。それがdie()です。

    // エイリアンの墓(画面に表示されない場所)
    private static final Point TOMB = new Point(-50, -50);
    
    /**
     * エイリアンが死ぬ、墓へ移動
     *
     */
    public void die() {
        setPos(TOMB.x, TOMB.y);
    }

画面外に移動させています。こうすると消滅したように見えますね。

エイリアンと弾が消滅したらforループをぬけるためbreakを呼び出しています。弾があたったらこれ以上ループをまわす必要がないからです。このbreakはなくても動作は変わらないのですがループを無駄にまわさないために書いておいたほうがいいと思います。

コメントにも書きましたが弾を保管庫へ送る処理をコメントアウトすると貫通弾になります。コメントアウトして試してみてください。いろんな弾を追加すると面白そうです。

最後に今作ったcollisionDetection()をゲームループで呼び出せば終わりです。毎ターン衝突検出すると処理が重そうですがたいしたことないです。もっとよい方法あるんですかね・・・

    public void run() {

        while (true) {
            ・・・

            // 衝突判定
            collisionDetection();

            ・・・
        }
    }

これでエイリアンを攻撃できるようになりました。エイリアンだって攻撃されて黙ってるわけにはいきませんよね・・・次回は反撃されるようにします。