人工知能に関する断創録

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

Javaでゲーム作りますが何か?のGitHubリポジトリをEclipseで読み込む

10年前のプロジェクトなのに、相変わらず本ブログでのアクセス数が最も多いのがこれ。

以前から、ブログ上ではjarファイルしか配布しておらず、解凍しないとソースコードが見られないのは不便という声を頂いていました。全部のソースコードをブログの記事上に載せるのは不可能なので、全ソースコードをGithub上で公開することにしました。

https://github.com/sylvan5/javagame

この記事では、Github上の全ソースコードをEclipseのプロジェクトとして読み込む方法をまとめておこうと思います。Eclipseを使うとソースコードの閲覧、コンパイル、起動が非常に簡単にでき便利です。

1. Eclipseのダウンロード

Eclipse のサイトからEclipse IDE for Java Developersをダウンロードします。2014年9月6日現在、最新版はEclipse Lunaです。OSの種類によって32Bit版か64Bit版をダウンロードします。メニューを日本語に翻訳したPleiadesもあります。こちらもJavaのFull Editionをダウンロードします。

f:id:aidiary:20140906131915p:plain

2. Eclipseの起動

ダウンロードしたeclipse-java-luna-R-win32-x86_64.zipを解凍してeclipse.exeをダブルクリックすると起動します。私は解凍したeclipseフォルダをC:\に置いています。

3. Git Repositoriesビューを開く

Eclipse Lunaには最初からGitの機能が内蔵されています。 Window > Show View > Other... からGitフォルダの下にあるGit Repositoriesを起動します。こんなウィンドウが追加されます。

f:id:aidiary:20140906131906p:plain

4. GitリポジトリからCloneを作成

Javaでゲーム作りますが何か?の全ソースコード(リポジトリ)をGithubというサイトで公開しています。リポジトリの場所は

https://github.com/sylvan5/javagame

です。このリポジトリの全コードを自分のパソコンに持ってくることをCloneといいます。EclipseのGit RepositoriesからClone a Git repositoryを選びます。URIの場所にhttps://github.com/sylvan5/javagame.gitと入力してください。他は自動で埋まります。あとはNextを連打して最後にFinishするとダウンロードが始まります。

f:id:aidiary:20140906131920p:plain

上のURLはGithubのリポジトリの右下からもコピーできます。

f:id:aidiary:20140906131908p:plain

ダウンロードが完了すると下のようにリポジトリリストに追加されます。私は、D:\MyWorks\Documents\gitにダウンロードしましたが、人によって違います。 Cloneするときにダイアログで指定した場所にダウンロードされます。

f:id:aidiary:20140906131923p:plain

5. ダウンロードしたリポジトリをEclipseにプロジェクトとして追加

実はリポジトリをダウンロードしただけではEclipse上から使えません。Eclipseのプロジェクトとして追加する必要があります。リポジトリを右クリックして、Import Projects... を選択してください。

f:id:aidiary:20140906131917p:plain

このダイアログでは、Import existing projects を選択してください。

f:id:aidiary:20140906131912p:plain

あらかじめEclipseのプロジェクトファイルを同梱しているため追加できるプロジェクトのリストが表示されます。今回は全部チェックして読み込みます。もし、RPG編だけ見たいという場合は、該当するプロジェクト(rpg~)のみチェックします。

f:id:aidiary:20140906131935p:plain

Finishを押すとEclipseにプロジェクトが読み込まれます。

f:id:aidiary:20140906131927p:plain

6. ソースコードを見る

たとえば、

hello_world.jarのソースコードが見たい場合は、hello_worldプロジェクトの下にソースコードがあります。

f:id:aidiary:20140906131933p:plain

HelloWorld.javaをダブルクリックするとソースコードが見られます。

f:id:aidiary:20140906131939p:plain

7. プログラムの実行

メニューから Run > Run を選択すると開いているプログラムが自動的にコンパイルされて起動します。Ctrl+F11(Ctrlキーを押しながらF11を押す)でも起動できます。初回はmain()関数があるソースコードを開いてCtrl+F11を押さないとダメです。

f:id:aidiary:20140906131925p:plain

プログラムにエラーがあった場合は、コンソールに表示されます。

f:id:aidiary:20140906131930p:plain

Eclipseはプログラミングをサポートするさまざまな機能があります。特にJavaプログラミングでは最適なので使いこなせると便利だと思います。

8. プログラムのWarningやErrorについて

私がこのプロジェクトをやっていたのはもう10年近く前なので、最新のJava環境だとWarning(黄色い線)やError(赤い線)が表示され、コンパイルできないケースもありそうです。これは後ほど修正していきたいと思います。

反射方向の調整

ブロックを壊す(2007/7/15)を何度かプレイしてみるとわかりますが毎回同じパターンが再現されます。これはラケットの反射方向が毎回同じだからです。これだとプレイヤーがボールの動きをまったく制御できないのでゲーム性がないです。そんなわけでボールがラケットに当たる位置に応じてボールの反射方向が変わるようにしてみます。

breakout05.jar

まずラケットに当たる位置を定義します。

    // ボールの当たり位置
    public static final int NO_COLLISION = 0;  // 未衝突
    public static final int LEFT = 1;
    public static final int RIGHT = 2;

NO_COLLISIONはラケットにボールが当たらなかったとき、LEFTはラケットの左半分にボールが当たったとき、RIGHTはラケットの右半分にボールが当たったときを表しています。ラケットの左半分に当たったときと右半分に当たったときで反射方向を変えます。

前回までのcollideWith()は、ボールが当たったらtrue、当たらなかったらfalseを返していましたが、ボールが当たった位置を返すように変更します。

    /**
     * ボールが当たった位置を返す
     * 
     * @param ball ボール
     * @return ボールに当たったらtrue
     */
    public int collideWith(Ball ball) {
        // ラケットの矩形
        Rectangle racketRectLeft = new Rectangle(
                centerPos - WIDTH / 2, MainPanel.HEIGHT - HEIGHT,
                WIDTH / 2, HEIGHT);
        Rectangle racketRectRight = new Rectangle(
                centerPos, MainPanel.HEIGHT - HEIGHT,
                WIDTH / 2, HEIGHT);
        // ボールの矩形
        Rectangle ballRect = new Rectangle(
                ball.getX(), ball.getY(),
                ball.getSize(), ball.getSize());

        // ラケットとボールの矩形領域が重なったら当たっている
        if (racketRectLeft.intersects(ballRect)) {
            return LEFT;
        } else if (racketRectRight.intersects(ballRect)) {
            return RIGHT;
        }

        return NO_COLLISION;
    }

ラケットを半分に分割し、左側の矩形と右側の矩形のRectangleを求めています。ボールが左右のどちらのRectangleに当たっているか調べてLEFTかRIGHTを返しています。

MainPanelのrun()内でボールが当たった位置に応じて反射方向を変えています。

    /**
     * ゲームループ
     * 
     */
    public void run() {
        while (true) {
            // ボールの移動
            ball.move();

            // ラケットとボールの衝突処理
            int collidePos = racket.collideWith(ball);
            // ラケットに当たっていたら
            if (collidePos != Racket.NO_COLLISION) {
                // ボールの当たった位置に応じてボールの速度を変える
                switch (collidePos) {
                    case Racket.LEFT:
                        // ラケットの左側に当たったときは左に反射するようにしたい
                        // もしボールが右に進んでいたら反転して左へ
                        // 左に進んでいたらそのまま
                        if (ball.getVX() > 0) ball.boundX();
                        ball.boundY();
                        break;
                    case Racket.RIGHT:
                        // ラケットの右側に当たったときは右に反射するようにしたい
                        // もしボールが左に進んでいたら反転して右へ
                        // 右に進んでいたらそのまま
                        if (ball.getVX() < 0) ball.boundX();
                        ball.boundY();
                        break;
                }
            }

            ・・・
        }
    }

コメントのとおりです。もしラケットの左側に当たったら左に反射するようにし、ラケットの右側に当たったら右に反射するようにしました。これでボールが反射する方向をある程度制御できます。

ブロックを壊す

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

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