人工知能に関する断創録

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

ブロックを壊す

今回はブロック崩しのキモであるブロックを導入します。そしてボールがブロックに当たったときにブロックが消えるようにします。これでようやく遊べるレベルになりますね。

breakout04.jar

ブロックを並べる

ブロック1つ1つはBlockクラスのオブジェクトとなります。まずは、MainPanelを見てBlockクラスがどのように扱われているか見てみます。

public class MainPanel extends JPanel
                       implements Runnable, MouseMotionListener {
    // ブロックの行数
    private static final int NUM_BLOCK_ROW = 10;
    // ブロックの列数
    private static final int NUM_BLOCK_COL = 7;
    // ブロック数
    private static final int NUM_BLOCK = NUM_BLOCK_ROW * NUM_BLOCK_COL;

    private Block[] block; // ブロック

    public MainPanel() {
        ・・・

        block = new Block[NUM_BLOCK];

        // ブロックを並べる
        for (int i = 0; i < NUM_BLOCK_ROW; i++) {
            for (int j = 0; j < NUM_BLOCK_COL; j++) {
                int x = j * Block.WIDTH + Block.WIDTH;
                int y = i * Block.HEIGHT + Block.HEIGHT;
                block[i * NUM_BLOCK_COL + j] = new Block(x, y);
            }
        }

        ・・・
    }
}

ブロックは下図のように10行7列で合計70個配置しました。1つ1つがBlockクラスのオブジェクトになっています。MainPanelでは Blockの配列blockを用意しています。ブロックを並べる処理でブロック1つ1つについて位置を計算してからBlockオブジェクトを作成し blockに格納しています。

f:id:aidiary:20090830105635g:plain

Blockクラス

次に1つ1つのブロックをあらわすBlockクラスを見てみます。

public class Block {
    public static final int WIDTH = 40;
    public static final int HEIGHT = 16;

    ・・・

    // 位置(左上隅の座標)
    private int x, y;

    // ボールが当たって消されたか
    private boolean isDeleted;

    public Block(int x, int y) {
        this.x = x;
        this.y = y;
        isDeleted = false;
    }

    /**
     * ブロックを描画
     * 
     * @param g
     */
    public void draw(Graphics g) {
        g.setColor(Color.CYAN);
        g.fillRect(x, y, WIDTH, HEIGHT);

        // 枠線を描画
        g.setColor(Color.BLACK);
        g.drawRect(x, y, WIDTH, HEIGHT);
    }

    /**
     * ブロックを消去
     * 
     */
    public void delete() {
        // TODO: ここでブロックが壊れる効果音
        // TODO: ここで派手なアクション

        isDeleted = true;
    }

    ・・・

WIDTHとHEIGHTでブロックの大きさを定義しています。コンストラクタではブロックの位置を受け取っています。draw()はブロックを描画するメソッドです。delete()はブロックを消すメソッドです。deleteを呼び出すとisDeletedがtrueになり、以後このブロックは描画されなくなります。

ブロックとボールの当たり判定

ボールがブロックに当たるとブロックは消えてボールは反射します。MainPanelクラスのrun()メソッドを見てください。

    public void run() {
        while (true) {
            ・・・

            // ブロックとボールの衝突処理
            for (int i = 0; i < NUM_BLOCK; i++) {
                // すでに消えているブロックは無視
                if (block[i].isDeleted())
                    continue;
                // ブロックの当たった位置を計算
                int collidePos = block[i].collideWith(ball);
                // ブロックに当たっていたら
                if (collidePos != Block.NO_COLLISION) {
                    block[i].delete();
                    // ボールの当たった位置からボールの反射方向を計算
                    switch (collidePos) {
                        case Block.DOWN :
                        case Block.UP :
                            ball.boundY();
                            break;
                        case Block.LEFT :
                        case Block.RIGHT :
                            ball.boundX();
                            break;
                        case Block.UP_LEFT :
                        case Block.UP_RIGHT :
                        case Block.DOWN_LEFT :
                        case Block.DOWN_RIGHT :
                            ball.boundXY();
                            break;
                    }
                    break; // 1回に壊せるブロックは1つ
                }
            }

         ・・・
         }
    }

forですべてのブロックについてループをまわしてボールと当たっているか調べています。メインループの中で毎回すべてのブロックがボールと当たっているか調べるのは大変だと思われるかもしれませんがコンピュータではこの程度の処理は一瞬で終わります。

それぞれのブロックについてまずそのブロックがすでに消えているかチェックしています。もしすでにボールが当たって消えていたら isDeleted()はtrueを返すのでcontinueによりすぐに次のブロックを調べます。消えているブロックは当たり判定をする必要がないためです。また消えているブロックは描画もしません。

次にcollideWith()でボールがブロックのどの位置に当たったか調べています。Blockクラスには下の9つの定数を用意し、ボールが当たった位置を定義しています。

    // ボールの当たり位置
    public static final int NO_COLLISION = 0; // 未衝突
    public static final int DOWN = 1;
    public static final int LEFT = 2;
    public static final int RIGHT = 3;
    public static final int UP = 4;
    public static final int DOWN_LEFT = 5;
    public static final int DOWN_RIGHT = 6;
    public static final int UP_LEFT = 7;
    public static final int UP_RIGHT = 8;

ボールが当たった位置を求めるのは、ボールが反射する方向を決定するためです。たとえば、ボールがブロックの真下から当たった場合はボールは下方向に反射します。一方ボールがブロックの真上から当たった場合はボールは上方向に反射します。ボールがどの方向からブロックに当たったかは重要な情報になります。collideWith()はボールがブロックに当たらなかった場合は、NO_COLLISIONを返し、当たった場合はあたった場所を戻り値として返します。ボールが当たった場所は下、左、右、上の4つの他に左下、右下、左上、右上を加えた8箇所になっています。

f:id:aidiary:20090830110700g:plain

もしボールがブロックに当たった場合は、当たったブロックのdelete()を呼び出して消しています。delete()を呼ばれたブロックは描画されなくなるため画面から消えます。

最後にボールが当たった位置に応じてボールをバウンドさせます。ブロックの上か下に当たった場合はY方向のバウンド、左か右に当たった場合はX方向のバウンド、左下、右下、左上、右上に当たった場合はXY方向のバウンドです。

    /**
     * X方向のバウンド
     */
    public void boundX() {
        vx = -vx;
    }

    /**
     * Y方向のバウンド
     */
    public void boundY() {
        vy = -vy;
    }

    /**
     * ななめにバウンド
     */
    public void boundXY() {
        vx = -vx;
        vy = -vy;
    }

バウンドは速度の符号を入れ替えることで実現します。ボールが壁に当たったときに跳ね返るのと同じ理屈です。ボールが跳ね返る処理(2004/9/20)

細かい点ですが、ボールがブロックに当たったときにbreakを呼び出してループを抜けている点に注意してください。こうすると1回の衝突で壊せるブロックが一つになります。このbreakをなくすとボールが2つ以上のブロックに当たったときに一度にブロックが2つ消えてしまいます。

ブロックとボールの当たり判定

最後にブロックとボールの当たり判定を見てみます。

    /**
     * ボールと衝突したか
     * 
     * @param ball ボール
     * @return 衝突位置
     */
    public int collideWith(Ball ball) {
        Rectangle blockRect = new Rectangle(x, y, WIDTH, HEIGHT);

        int ballX = ball.getX();
        int ballY = ball.getY();
        int ballSize = ball.getSize();
        if (blockRect.contains(ballX, ballY)
            && blockRect.contains(ballX + ballSize, ballY)) {
            // ブロックの下から衝突=ボールの左上・右上の点がブロック内
            return DOWN;
        } else if (blockRect.contains(ballX + ballSize, ballY)
            && blockRect.contains(ballX + ballSize, ballY + ballSize)) {
            // ブロックの左から衝突=ボールの右上・右下の点がブロック内
            return LEFT;
        } else if (blockRect.contains(ballX, ballY)
            && blockRect.contains(ballX, ballY + ballSize)) {
            // ブロックの右から衝突=ボールの左上・左下の点がブロック内
            return RIGHT;
        } else if (blockRect.contains(ballX, ballY + ballSize)
            && blockRect.contains(ballX + ballSize, ballY + ballSize)) {
            // ブロックの上から衝突=ボールの左下・右下の点がブロック内
            return UP;
        } else if (blockRect.contains(ballX + ballSize, ballY)) {
            // ブロックの左下から衝突=ボールの右上の点がブロック内
            return DOWN_LEFT;
        } else if (blockRect.contains(ballX, ballY)) {
            // ブロックの右下から衝突=ボールの左上の点がブロック内
            return DOWN_RIGHT;
        } else if (blockRect.contains(ballX + ballSize, ballY + ballSize)) {
            // ブロックの左上から衝突=ボールの右下の点がブロック内
            return UP_LEFT;
        } else if (blockRect.contains(ballX, ballY + ballSize)) {
            // ブロックの右上から衝突=ボールの左下の点がブロック内
            return UP_RIGHT;
        }

        return NO_COLLISION;
    }

先ほどお話したようにボールがブロックのどの位置に当たったかによって異なる値を返しています。ボールがブロックのどの位置に当たったかを計算する必要がありますが、これにはRectangleクラスのcontains()というメソッドを使うと簡単になります。rect.contains(x, y)はrectが点(x,y)を内部に含んでいた場合にtrueを返すメソッドです。下図はボールの当たる位置に応じてボールのどの頂点がブロックの内部に入るかを示しています。たとえば、ボールがブロックの下から当たったとするとボールの上の2つの頂点(図では赤丸)がブロックの中に入ります。逆に言うとこの2つの頂点がブロック内部に含まれていればボールがブロックの下から当たったことになります。ソースコードを見ると

        if (blockRect.contains(ballX, ballY)
                && blockRect.contains(ballX + ballSize, ballY)) {
            // ブロックの下から衝突=ボールの左上・右上の点がブロック内
            return DOWN;
        }

となっており、ボールの上の2つの頂点座標がblockRectに含まれている(contain)ならば下から衝突と判定してDOWNを返しています。他のも同様ですので図とコードと見比べてみてください。

f:id:aidiary:20090830110701g:plain