Java14とJavaFXを使い普通のブロック崩しを作る

はじめに

安藤です。
世の中の情勢が厳しい昨今ですがいかがお過ごしでしょうか。私は元気です。
今回は3/17にリリースされたJava14を早速使ってみたという話です。

Java14について

全容はきしださんがまとめてくれています。いつもお世話になっています。
Java 14新機能まとめ
個人的に注目したいのが、ついに正式版になったPackaging Toolです。
およそ1年前にこちらの記事でも触れましたが、ようやく公式のパッケージャが登場しました。

今回作ったもの

本題です。
この記事のためだけにJavaFXを使ったシンプルなブロック崩しを作りました。
javafx-breakout
breakout.zip
実は前職で新卒研修時に暇だったので似たようなものを作ったことがあり、それを思い出しながら作っていたのですが、想像の3倍くらい大変でした。
新しい構文を使うことと、できるだけ内部状態の変更を起こさないことをコンセプトに作りました。

全体の構成

ProcessingのようにCanvasで画面を描画していきます。fxmlの記述はこれだけです。

<?xml version="1.0" encoding="UTF-8"?>


<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.Group?>
<Group xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1"
       fx:controller="jp.imagemagic.breakout.FieldController"
       onMouseMoved="#mouseMove" onMouseClicked="#mouseClick">
    <children>
        <Canvas fx:id="canvas" focusTraversable="true" width="400" height="600"/>
    </children>
</Group>

画面にはブロック、ボール、バーがありますが、統一的な計算のために以下を用意しました。
Recordはイミュータブルな推移をさせるためにうってつけです。

package jp.imagemagic.breakout;

import javafx.scene.paint.Color;

interface Drawable {
    Vector pos();

    Vector size();

    default DrawType drawType() {
        return DrawType.Rect;
    }

    default Color fill() {
        return Color.WHITE;
    }

    default Color stroke() {
        return Color.BLACK;
    }
}


package jp.imagemagic.breakout;

public record Vector(double x, double y) {
    public Vector move(double dx, double dy) {
        return new Vector(x + dx, y + dy);
    }

    public Vector move(Vector v) {
        return move(v.x, v.y);
    }

    public Vector scalar(double n) {
        return scalar(n, n);
    }

    public Vector scalar(double nx, double ny) {
        return new Vector(x * nx, y * ny);
    }
}

クラス構成の大枠は以下の通りです。これはtetrixの影響を受けた構成になっています。
  • 画面からの入力の処理と一定時間ごとに画面の推移を行うFieldController
  • 現在の状態を保持するState
  • Stateの推移ロジックをもつField
  • FieldControllerとStateを仲介するViewUnit

package jp.imagemagic.breakout;

import java.util.function.Consumer;
import java.util.function.UnaryOperator;

final class ViewUnit {
    private final Field field;
    private State state;

    ViewUnit(double w, double h) {
        field = new Field(new Vector(w, h));
        state = field.initState();
    }

    void trans(UnaryOperator<State> trans) {
        state = trans.apply(state);
    }

    UnaryOperator<State> moveBar(double x) {
        return field.moveBar(x);
    }

    UnaryOperator<State> moveBall() {
        return field.moveBall();
    }

    UnaryOperator<State> transStatus() {
        return field.transStatus();
    }

    void drawView(Consumer<Drawable> draw, Consumer<Status> statusDraw) {
        state.view().objects().forEach(draw);
        statusDraw.accept(state.status());
    }
}


// 一部抜粋
public class FieldController implements Initializable {
    @FXML
    private Canvas canvas;
    private GraphicsContext gc;
    private ViewUnit unit;

    public void mouseMove(MouseEvent e) {
        transAndDraw(unit.moveBar(e.getSceneX()));
    }

    public void mouseClick() {
        transAndDraw(unit.transStatus());
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        unit = new ViewUnit(canvas.getWidth(), canvas.getHeight());
        final var timeline = new Timeline(new KeyFrame(new Duration(10), e -> transAndDraw(unit.moveBall())));
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
        drawView();
    }
}

補記: jpackageによるパッケージング

予めjlinkでランタイムの大きさを削っておくのがお勧めです。
インストーラを伴わないものを作ろうと色々と試した結果、このようなコマンドになりました。
jpackage -t app-image -n breakout -d dist -i app --main-class jp.imagemagic.breakout.Main --main-jar jp.imagemagic.breakout.jar --runtime-image jregram.min\ --java-options "--add-exports javafx.base/com.sun.javafx.reflect=ALL-UNNAMED --add-reads javafx.base=ALL-UNNAMED --add-reads javafx.graphics=ALL-UNNAMED --enable-preview"
appにjarファイルがあり、distにbreakoutというディレクトリができます。
jarファイルにしていますが、オプションを変えればクラスファイルでも行けると思います。