Javaでブロック崩し(解説)⑴ボールを動かす

①はじめに

Javaで定番のブロック崩しを作っていきます。描画ライブラリはJavaFXを使いますが、awtなどを使う場合でも、描画処理以外はやることは同じなので参考になると思います。

②描画の下準備

まずは描画するためのウィンドウを作ります。JavaFXの描画の仕組みは、ウィンドウに当たるStage、ウィンドウ内の描画範囲に当たるScene、描画する各オブジェクト(図形やキャンバスなど)をノードとしてツリー構造にしています。そのため、それらを最初に生成する必要があります。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.scene.paint.Color;

public class Breakout extends Application{
    public static void main(String[] args) throws Exception {
        launch();
    }

    @Override
    public void start(Stage stage) throws Exception {
        //ウィンドウのタイトルをセット
        stage.setTitle("Breakout");
        //親ノードをボーダーパネルで生成
        BorderPane root = new BorderPane();
        //親ノードを引数としてシーンを生成
        Scene scene = new Scene(root);
        //描画するためのキャンバスを生成
        Canvas canvas = new Canvas(1000, 800);
        //キャンバスに描画するGraphicsContextを生成
        GraphicsContext gc = canvas.getGraphicsContext2D();
        //キャンバスをボーダーパネルの中央に配置
        root.setCenter(canvas);
        //シーンをステージにセット
        stage.setScene(scene);
        //ステージを表示
        stage.show();
    }
}

1000×800の真っ白なウィンドウが表示されます。 javafxでウィンドウ作成 試しに、黒い四角を描画してみます。 「stage.show();」のあとにコードを追加します。

        stage.show();
        //黒色にセット
        gc.setFill(Color.BLACK);
        //(0, 0)から(300, 300)までの長方形を描画
        gc.fillRect(0, 0, 300, 300);

左上に黒い四角が描画されます。

javafxでキャンバスの描画

次に、1フレームごとに描画を更新する処理を書いていきます。Timelilneを用いて、KeyFrameを挿入することで、描画することができます。 適宜、必要なものをインポートしていきながら、書いていきます。

        Timeline timeline = new Timeline();
        KeyFrame keyframe = new KeyFrame(Duration.seconds(0), new EventHandler<ActionEvent>() {
            public void handle(ActionEvent e){
                //ここに処理を記述
            }
        });
        timeline.getKeyFrames().addAll(keyframe, new KeyFrame(Duration.seconds(0.1))); //1フレームの間隔
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();

drawメソッドを作り、そこでキャンバスへの描画を行います。キャンバスのサイズをfinalで宣言して定数にしておきます。

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.scene.paint.Color;

public class Breakout extends Application{
    public static void main(String[] args) throws Exception {
        launch();
    }

    //キャンバスの幅と高さ
    final int WIDTH = 1000;
    final int HEIGHT = 800;

    @Override
    public void start(Stage stage) throws Exception {
        //ウィンドウのタイトルをセット
        stage.setTitle("Breakout");
        //親ノードをボーダーパネルで生成
        BorderPane root = new BorderPane();
        //親ノードを引数としてシーンを生成
        Scene scene = new Scene(root);
        //描画するためのキャンバスを生成
        Canvas canvas = new Canvas(WIDTH, HEIGHT);
        //キャンバスに描画するGraphicsContextを生成
        GraphicsContext gc = canvas.getGraphicsContext2D();
        //キャンバスをボーダーパネルの中央に配置
        root.setCenter(canvas);
        //シーンをステージにセット
        stage.setScene(scene);
        //ステージを表示
        stage.show();

        Timeline timeline = new Timeline();
        KeyFrame keyframe = new KeyFrame(Duration.seconds(0), new EventHandler<ActionEvent>() {
            public void handle(ActionEvent e){
                //ここに処理を記述
                draw(gc);
            }
        });
        timeline.getKeyFrames().addAll(keyframe, new KeyFrame(Duration.seconds(0.1))); //1フレームの間隔
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }

    //描画メソッド
    public void draw(GraphicsContext gc){
        //背景を白で塗りつぶす
        gc.setFill(Color.WHITE);
        gc.fillRect(0, 0, WIDTH, HEIGHT);
    }
}

実行すると白で塗りつぶされたウィンドウが生成されます。これで、描画の下準備は完了です。次に、ボールクラスを作成します。

③ボールクラスの作成

まず、ボールのメンバ変数として必要なのは、座標・半径・速度です。これがあれば、ボールの動きを描画できます。必要なメソッドは、初期値を代入するコンストラクタ、描画を行うメソッド、ボールを動かすメソッドの3つです。
円を描く際、fillOvalを使いますが、引数に注意が必要です。fillOval(円の左上のx座標、円の左上のy座標、円の横幅、円の縦幅)と指定するので、ボールの中心座標であるx,yをそのまま代入してはいけません。

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

public class Ball {
    //ボールの中心座標
    int x;
    int y;
    //ボールの半径
    int r;
    //ボールの速度
    int vx;
    int vy;

    //コンストラクタ
    public Ball(int x, int y, int r, int vx, int vy){
        //初期値を代入
        this.x = x;
        this.y = y;
        this.r = r;
        this.vx = vx;
        this.vy = vy;
    }

    //ボールを動かすメソッド
    public void update(){
        //座標に速度を足して動かす
        x += vx;
        y += vy;
    }

    //描画するメソッド
    public void draw(GraphicsContext gc){
        //赤色にする
        gc.setFill(Color.RED);
        //座標を指定して円を描く
        gc.fillOval(x - r, y - r, r * 2, r * 2);
    }
}

Ballクラスを作ったので、インスタンスを生成します。やることは次の4つです。
・Breakoutクラスのメンバ変数にballを宣言
・startメソッドでballをインスタンス化 (ballの初期値は座標をキャンバス中央、半径5、x速度1、y速度1)
・1フレームごとに行う処理にボールの移動を追加
・描画メソッドにballの描画を追加
※1フレームの間隔を0.1秒から0.01秒に変更しました。

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.scene.paint.Color;

public class Breakout extends Application{
    public static void main(String[] args) throws Exception {
        launch();
    }

    //キャンバスの幅と高さ
    final int WIDTH = 1000;
    final int HEIGHT = 800;
    //ボール
    Ball ball;

    @Override
    public void start(Stage stage) throws Exception {
        //ウィンドウのタイトルをセット
        stage.setTitle("Breakout");
        //親ノードをボーダーパネルで生成
        BorderPane root = new BorderPane();
        //親ノードを引数としてシーンを生成
        Scene scene = new Scene(root);
        //描画するためのキャンバスを生成
        Canvas canvas = new Canvas(WIDTH, HEIGHT);
        //キャンバスに描画するGraphicsContextを生成
        GraphicsContext gc = canvas.getGraphicsContext2D();
        //キャンバスをボーダーパネルの中央に配置
        root.setCenter(canvas);
        //シーンをステージにセット
        stage.setScene(scene);
        //ステージを表示
        stage.show();

        //ボールをインスタンス化
        ball = new Ball(WIDTH / 2, HEIGHT / 2, 5, 1, 1);

        Timeline timeline = new Timeline();
        KeyFrame keyframe = new KeyFrame(Duration.seconds(0), new EventHandler<ActionEvent>() {
            public void handle(ActionEvent e){
                //ここに処理を記述
                //ボールを移動
                ball.update();
                //描画メソッドを呼び出す
                draw(gc);
            }
        });
        timeline.getKeyFrames().addAll(keyframe, new KeyFrame(Duration.seconds(0.01))); //1フレームの間隔
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }

    //描画メソッド
    public void draw(GraphicsContext gc){
        //背景を白で塗りつぶす
        gc.setFill(Color.WHITE);
        gc.fillRect(0, 0, WIDTH, HEIGHT);
        //ボールの描画メソッドを呼び出す
        ball.draw(gc);
    }
}

実行すると、画面の真ん中にボールが出現し、右下に移動していきます。しかし、ボールは反射することなく、そのまま画面外に消えて行ってしまいます。 javafxでボールの描画 ボールクラスのupdateメソッドを変更して、画面端に到達したら速度を反転させる必要があります。画面の大きさが分からないと画面端に到達したかどうかわからないので、updateメソッドの引数で画面の幅と高さを受け取ります。

    //ボールを動かすメソッド
    public void update(int width, int height){
        //ボールが画面端の左右に衝突していないか判定
        if(x - r < 0 || x + r > width){
            //速度を反転
            vx = -vx;
        }
        //ボールが画面端の上下に衝突していないか判定
        if(y - r < 0 || y + r > height){
            //速度を反転
            vy = -vy;
        }
        //座標に速度を足して動かす
        x += vx;
        y += vy;
    }

ボールの端はボールの中心座標から半径を(引いた、もしくは足した)値になります。updateメソッドを呼び出すときは、画面の幅と高さを渡すので、そこも軽く書き換えます。

                //ここに処理を記述
                //ボールを移動
                ball.update(WIDTH, HEIGHT);
                //描画メソッドを呼び出す
                draw(gc);

実行すると、ちゃんと跳ね返ってくれています。これで今回の目標は達成です。
次回は、ボールを跳ね返すパドルを作ります。最後に、ソースコードを貼っておきます。

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.scene.paint.Color;

public class Breakout extends Application{
    public static void main(String[] args) throws Exception {
        launch();
    }

    //キャンバスの幅と高さ
    final int WIDTH = 1000;
    final int HEIGHT = 800;
    //ボール
    Ball ball;

    @Override
    public void start(Stage stage) throws Exception {
        //ウィンドウのタイトルをセット
        stage.setTitle("Breakout");
        //親ノードをボーダーパネルで生成
        BorderPane root = new BorderPane();
        //親ノードを引数としてシーンを生成
        Scene scene = new Scene(root);
        //描画するためのキャンバスを生成
        Canvas canvas = new Canvas(WIDTH, HEIGHT);
        //キャンバスに描画するGraphicsContextを生成
        GraphicsContext gc = canvas.getGraphicsContext2D();
        //キャンバスをボーダーパネルの中央に配置
        root.setCenter(canvas);
        //シーンをステージにセット
        stage.setScene(scene);
        //ステージを表示
        stage.show();

        //ボールをインスタンス化
        ball = new Ball(WIDTH / 2, HEIGHT / 2, 5, 1, 1);

        Timeline timeline = new Timeline();
        KeyFrame keyframe = new KeyFrame(Duration.seconds(0), new EventHandler<ActionEvent>() {
            public void handle(ActionEvent e){
                //ここに処理を記述
                //ボールを移動
                ball.update(WIDTH, HEIGHT);
                //描画メソッドを呼び出す
                draw(gc);
            }
        });
        timeline.getKeyFrames().addAll(keyframe, new KeyFrame(Duration.seconds(0.01))); //1フレームの間隔
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }

    //描画メソッド
    public void draw(GraphicsContext gc){
        //背景を白で塗りつぶす
        gc.setFill(Color.WHITE);
        gc.fillRect(0, 0, WIDTH, HEIGHT);
        //ボールの描画メソッドを呼び出す
        ball.draw(gc);
    }
}
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

public class Ball {
    //ボールの中心座標
    int x;
    int y;
    //ボールの半径
    int r;
    //ボールの速度
    int vx;
    int vy;

    //コンストラクタ
    public Ball(int x, int y, int r, int vx, int vy){
        //初期値を代入
        this.x = x;
        this.y = y;
        this.r = r;
        this.vx = vx;
        this.vy = vy;
    }

    //ボールを動かすメソッド
    public void update(int width, int height){
        //ボールが画面端の左右に衝突していないか判定
        if(x - r < 0 || x + r > width){
            //速度を反転
            vx = -vx;
        }
        //ボールが画面端の上下に衝突していないか判定
        if(y - r < 0 || y + r > height){
            //速度を反転
            vy = -vy;
        }
        //座標に速度を足して動かす
        x += vx;
        y += vy;
    }

    //描画するメソッド
    public void draw(GraphicsContext gc){
        //赤色にする
        gc.setFill(Color.RED);
        //座標を指定して円を描く
        gc.fillOval(x - r, y - r, r * 2, r * 2);
    }
}