Javaでブロック崩し(解説)⑵パドルを動かす

①キー入力の処理

ボールを動かすことができたら、ボールを跳ね返すパドルを作ります。今回は、パドルを作り、ボールを跳ね返すところまで作ります。
まず、キーボードから操作できるように、キー入力が行われたときの処理を追加します。Sceneには、setOnKeyPressedというキーが押されたされたときに呼び出される関数と、setOnKeyReleasedというキーが離されたときに呼び出される関数があるので、それらをBreakoutクラスに追加します。

        //キーが押されたときの処理
        scene.setOnKeyPressed(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが押されたときの処理を記述
                }
            }
        );
        //キーが離されたときの処理
        scene.setOnKeyReleased(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが離されたときの処理を記述
                }
            }
        );

同時押しにも対応させたいため、リストを宣言して、キーが押されたときにリストに追加、キーが離されたときにリストから削除を行います。 インポートも忘れず行いましょう。

import java.util.ArrayList;

Breakoutクラスのメンバ変数に追加します。

    //キャンバスの幅と高さ
    final int WIDTH = 1000;
    final int HEIGHT = 800;
    //入力キーを保持するリスト
    ArrayList<String> input = new ArrayList<>();

具体的な処理を記述します。 *1

        //キーが押されたときの処理
        scene.setOnKeyPressed(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが押されたときの処理を記述
                    String key = e.getCode().toString();
                    if(!input.contains(key)){
                        input.add(key);
                    }
                }
            }
        );
        //キーが離されたときの処理
        scene.setOnKeyReleased(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが離されたときの処理を記述
                    String key = e.getCode().toString();
                    input.remove(key);
                }
            }
        );

あとは、リストに特定のキーが存在したら、そのキーが押されたときの処理を行えばよいです。 例えば、リストに"A"が存在したら、パドルを左に動かす処理をするといった具合です。 その前に、パドルクラスを作っておきます。

②パドルクラスの作成

パドルクラスに必要な変数は次の2つです。
・パドルの座標
・パドルの幅と高さ
必要なメソッドは次の3つです。
・変数に初期値を代入するコンストラク
・パドルを移動するメソッド
・パドルを描画するメソッド
ボールを跳ね返すメソッドは、ボール側のクラスに実装します。
(後々作ることになるブロックにも跳ね返りの処理があるので、本当は長方形の親クラスを作ってから、それを継承したパドルクラス、ブロッククラスというように作ったほうがいいですが、今回は分かりやすさ重視で行きます。)

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

public class Paddle{
    //パドルの左上の座標
    int x;
    int y;
    //パドルの幅と高さ
    int w;
    int h;

    //コンストラクタ
    public Paddle(int x, int y, int w, int h){
        //初期値を代入
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }

    //移動するメソッド(移動量と画面サイズを引数で受け取る)
    public void move(int vx, int vy, int width, int height){
        //パドルが左右に画面外に行かないとき左右に移動する
        if(x + vx > 0 && x + vx + w < width){
            x += vx;
        }
        //パドルが上下に画面外に行かないとき上下に移動する
        if(y + vy > 0 && y + vy + h < height){
            y += vy;
        }
    }

    //描画メソッド
    public void draw(GraphicsContext gc){
        //青色
        gc.setFill(Color.BLUE);
        //長方形を描画
        gc.fillRect(x, y, w, h);
    }
}

パドルクラスを作ったので、Breakoutクラスに追加してインスタンスの生成を行います。 ・メンバ変数に追加

    //ボール
    Ball ball;
    //パドル
    Paddle paddle;

インスタンスの生成(画面の高さを0.9倍することで、画面下部に生成することができます。)

        //ボールをインスタンス化
        ball = new Ball(WIDTH / 2, HEIGHT / 2, 5, 1, 1);
        //パドルをインスタンス化
        paddle = new Paddle(WIDTH / 2, (int)(HEIGHT * 0.9), 40, 10);

現時点でのBreakoutクラスのソースコードはこのようになります。

import java.util.ArrayList;
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.input.KeyEvent;
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;
    //入力キーを保持するリスト
    ArrayList<String> input = new ArrayList<>();
    //ボール
    Ball ball;
    //パドル
    Paddle paddle;

    @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);
        //パドルをインスタンス化
        paddle = new Paddle(WIDTH / 2, (int)(HEIGHT * 0.9), 40, 10);

        //キーが押されたときの処理
        scene.setOnKeyPressed(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが押されたときの処理を記述
                    String key = e.getCode().toString();
                    if(!input.contains(key)){
                        input.add(key);
                    }
                }
            }
        );
        //キーが離されたときの処理
        scene.setOnKeyReleased(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが離されたときの処理を記述
                    String key = e.getCode().toString();
                    input.remove(key);
                }
            }
        );

        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);
        //パドルの描画メソッドを呼び出す
        paddle.draw(gc);
    }
}

③キー入力でパドルを動かす

入力キーをリストで保持しているため、リストに移動キーが存在するなら、パドルの移動メソッドを呼び出します。操作はWASDで行います。
注意点として、パドルの速度とボールの速度が異なると挙動がおかしくなる場合があります。一応異なる速度に対応させることはできますが、処理が複雑になってしまうのでおすすめしません。

        KeyFrame keyframe = new KeyFrame(Duration.seconds(0), new EventHandler<ActionEvent>() {
            public void handle(ActionEvent e){
                //ここに処理を記述
                //左移動
                if(input.contains("A")){
                    paddle.move(-1, 0, WIDTH, HEIGHT);
                }
                //右移動
                if(input.contains("D")){
                    paddle.move(1, 0, WIDTH, HEIGHT);
                }
                //上移動
                if(input.contains("W")){
                    paddle.move(0, -1, WIDTH, HEIGHT);
                }
                //上移動
                if(input.contains("S")){
                    paddle.move(0, 1, WIDTH, HEIGHT);
                }
                //ボールを移動
                ball.update(WIDTH, HEIGHT);
                //描画メソッドを呼び出す
                draw(gc);
            }
        });

実行すると、パドルが描画され、WASDでパドルを動かすことができます。 javafxでブロック崩し(パドル) 最後に、ボールとパドルの当たり判定を付けます。(当たり判定がブロック崩し最大の難関です。)

④ボールとパドルの当たり判定

ボールとパドルの当たり判定はボールが移動する直前に行います。衝突判定のメソッドはボールクラスに作成します。このメソッドは、パドルを引数として受け取り、パドルのどこ(上下左右)と衝突しているかを返します。
まず、ボールクラスに上下左右の方向を定数として宣言しておきます。ボールのインスタンスごとに値が変わるものではないので、クラス変数(static)の定数(final)で宣言します。

    //方向定数
    static final int TOP = 1;
    static final int BOTTOM = 2;
    static final int RIGHT = 3;
    static final int LEFT = 4;
    //ボールの中心座標
    int x;
    int y;
    //ボールの半径
    int r;
    //ボールの速度
    int vx;
    int vy;

まず、パドルの上側の当たり判定について考えます。衝突しているときのボールとパドルのx座標、y座標の関係は図から次のようにわかります。
x座標は、「ボールの中心座標がパドルの幅に入っている」という条件が成り立ちます。 javafxでブロック崩し(当たり判定) y座標は、「ボールの上側の座標<パドルの上側の座標<ボールの下側の座標」という大小関係が成り立ちます。
javafxでブロック崩し(当たり判定)
この条件から、上側の当たり判定はこのようになります。

    //ボールとパドルの衝突判定
    public int isCollideWithPaddle(Paddle paddle){
        //パドルの座標と幅高さ
        int px = paddle.x;
        int py = paddle.y;
        int pw = paddle.w;
        int ph = paddle.h;
        //パドルの上側衝突判定
        if(px < x && x < px + pw && y - r < py && py < y + r){
            return TOP;
        }
    }

下左右にたいしても同じように考えることで、衝突判定のメソッドはこうなります。また、衝突していないときは0を返します。

    //ボールとパドルの衝突判定
    public int isCollideWithPaddle(Paddle paddle){
        //パドルの座標と幅高さ
        int px = paddle.x;
        int py = paddle.y;
        int pw = paddle.w;
        int ph = paddle.h;
        //パドルの上側衝突判定
        if(px < x && x < px + pw && y - r < py && py < y + r){
            return TOP;
        }
        //パドルの下側衝突判定
        if(px < x && x < px + pw && y - r < py + ph && py + ph < y + r){
            return BOTTOM;
        }
        //パドルの左側衝突判定
        if(py < y && y < py + ph && x - r < px && px < x + r){
            return LEFT;
        }
        //パドルの右側衝突判定
        if(py < y && y < py + ph && x - r < px + pw && px + pw < x + r){
            return RIGHT;
        }
        //衝突していないときは0を返す
        return 0;
    }

衝突判定メソッドができたので、ボールの移動メソッドの中に入れて、switch文で衝突したときの処理を記述します。衝突判定にパドルが引数として必要なので、ボールの移動メソッドにも引数にパドルを追加します。

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

衝突したときは、各速度の絶対値を取って、プラスマイナスを付けています。単にマイナスをつけて反転するだけでは、ボールがパドルにめり込んだ時に速度が反転し続けて、パドルから出てこなくなることがあります。そこで、単に反転させるのではなく絶対値を取るやり方をしています。
最後に、ボールの移動メソッドの引数にパドルを追加したので、ボールの移動メソッドを呼び出しているところにパドルを引数で渡してあげます。

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

これで実行すると、パドルでボールを跳ね返すことができていることが分かります。
次回はいよいよブロック崩しのブロックを作成します。最後に現時点のソースコードを貼っておきます。

import java.util.ArrayList;
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.input.KeyEvent;
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 / 2;
    final int HEIGHT = 800 / 2;
    //入力キーを保持するリスト
    ArrayList<String> input = new ArrayList<>();
    //ボール
    Ball ball;
    //パドル
    Paddle paddle;

    @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);
        //パドルをインスタンス化
        paddle = new Paddle(WIDTH / 2, (int)(HEIGHT * 0.9), 40, 10);

        //キーが押されたときの処理
        scene.setOnKeyPressed(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが押されたときの処理を記述
                    String key = e.getCode().toString();
                    if(!input.contains(key)){
                        input.add(key);
                    }
                }
            }
        );
        //キーが離されたときの処理
        scene.setOnKeyReleased(
            new EventHandler<KeyEvent>(){
                public void handle(KeyEvent e){
                    //ここにキーが離されたときの処理を記述
                    String key = e.getCode().toString();
                    input.remove(key);
                }
            }
        );

        Timeline timeline = new Timeline();
        KeyFrame keyframe = new KeyFrame(Duration.seconds(0), new EventHandler<ActionEvent>() {
            public void handle(ActionEvent e){
                //ここに処理を記述
                //左移動
                if(input.contains("A")){
                    paddle.move(-1, 0, WIDTH, HEIGHT);
                }
                //右移動
                if(input.contains("D")){
                    paddle.move(1, 0, WIDTH, HEIGHT);
                }
                //上移動
                if(input.contains("W")){
                    paddle.move(0, -1, WIDTH, HEIGHT);
                }
                //上移動
                if(input.contains("S")){
                    paddle.move(0, 1, WIDTH, HEIGHT);
                }
                //ボールを移動
                ball.update(WIDTH, HEIGHT, paddle);
                //描画メソッドを呼び出す
                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);
        //パドルの描画メソッドを呼び出す
        paddle.draw(gc);
    }
}
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

public class Ball {
    //方向定数
    static final int TOP = 1;
    static final int BOTTOM = 2;
    static final int RIGHT = 3;
    static final int LEFT = 4;
    //ボールの中心座標
    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, Paddle paddle){
        int result = isCollideWithPaddle(paddle);
        switch(result){
            case TOP:
                vy = -Math.abs(vy);
                break;
            case BOTTOM:
                vy = Math.abs(vy);
                break;
            case LEFT:
                vx = -Math.abs(vx);
                break;
            case RIGHT:
                vx = Math.abs(vx);
                break;
        }
        //ボールが画面端の左右に衝突していないか判定
        if(x - r < 0 || x + r > width){
            //速度を反転
            vx = -vx;
        }
        //ボールが画面端の上下に衝突していないか判定
        if(y - r < 0 || y + r > height){
            //速度を反転
            vy = -vy;
        }
        //座標に速度を足して動かす
        x += vx;
        y += vy;
    }

    //ボールとパドルの衝突判定
    public int isCollideWithPaddle(Paddle paddle){
        //パドルの座標と幅高さ
        int px = paddle.x;
        int py = paddle.y;
        int pw = paddle.w;
        int ph = paddle.h;
        //パドルの上側衝突判定
        if(px < x && x < px + pw && y - r < py && py < y + r){
            return TOP;
        }
        //パドルの下側衝突判定
        if(px < x && x < px + pw && y - r < py + ph && py + ph < y + r){
            return BOTTOM;
        }
        //パドルの左側衝突判定
        if(py < y && y < py + ph && x - r < px && px < x + r){
            return LEFT;
        }
        //パドルの右側衝突判定
        if(py < y && y < py + ph && x - r < px + pw && px + pw < x + r){
            return RIGHT;
        }
        //衝突していないときは0を返す
        return 0;
    }

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

public class Paddle{
    //パドルの左上の座標
    int x;
    int y;
    //パドルの幅と高さ
    int w;
    int h;

    //コンストラクタ
    public Paddle(int x, int y, int w, int h){
        //初期値を代入
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }

    //移動するメソッド(移動量と画面サイズを引数で受け取る)
    public void move(int vx, int vy, int width, int height){
        //パドルが左右に画面外に行かないとき左右に移動する
        if(x + vx > 0 && x + vx + w < width){
            x += vx;
        }
        //パドルが上下に画面外に行かないとき上下に移動する
        if(y + vy > 0 && y + vy + h < height){
            y += vy;
        }
    }

    //描画メソッド
    public void draw(GraphicsContext gc){
        //青色
        gc.setFill(Color.BLUE);
        //長方形を描画
        gc.fillRect(x, y, w, h);
    }
}

*1:以前にインターネットから引用したコードですが、ソースが見つけられなかったので、見つけ次第出典を記します。