【JavaFX】ゲーム用画面遷移(暗転機能付)

本稿は、JavaFXアプリケーションでゲーム制作するにあたり、画面遷移を実装する方法について考えてみましょう。

JavaFXでは下記のような親子関係でオブジェクトを配置しています。

もちろんif文で分割するのも非常に簡単でいいと思いますが、せっかくなのでこの親子構成を考慮した画面遷移を考えてみることにします。

ぱっと見たところ、Sceneクラスを差し替えて実装するかParentクラスを差し替えて実装するのが簡単そうです。

どっちでもいいのですが今回は実装が楽そうなParentクラスの変更で画面遷移の実装をやってみます。

まずは、ゲーム画面クラスのベースを作りたいと思いますが、どこからでも画面遷移の命令を呼び出したいので、すべてstaticメソッドでアクセスできるようにしました。

下記のGameWindowFXクラスはinitメソッドで初期化パラメータを渡し、showメソッドでウィンドウを表示出来るように定義しました。

GameWindowFX.java
import javafx.animation.FadeTransition;
import javafx.animation.SequentialTransition;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class GameWindowFX {

	private static final GameWindowFX gw = new GameWindowFX();
	private Stage wnd;
	private Scene scene;
	private GameViewFX current;
	private boolean isChangeAnimation;
	private boolean isInit;

	private GameWindowFX() {}

	/**
	 * ゲーム用ウィンドウとして初期化します。
	 * @param stage ウィンドウオブジェクト
	 * @param width ウィンドウの表示領域の横幅
	 * @param height ウィンドウの表示領域の縦幅
	 * @param initView 初期表示対象の画面
	 * @return
	 */
	public static synchronized void init(Stage stage, double width, double height, GameViewFX initView) {
		if ( gw.isInit ) throw new RuntimeException("既に初期化されています。");
		gw.scene = new Scene(initView, width, height);
		stage.setScene(gw.scene);
		gw.wnd = stage;
		gw.current = initView;
		gw.scene.setOnMouseClicked(e -> gw.current.mouseClicked(e));
		gw.isInit = true;
	}

	/**
	 * ウィンドウを表示します。
	 */
	public static synchronized void show() {
		if ( !gw.wnd.isShowing() ) {
			gw.wnd.show();
		}
	}

	/**
	 * ウィンドウを閉じます。
	 */
	public static synchronized void close() {
		if ( gw.wnd.isShowing() ) {
			gw.wnd.close();
		}
	}

	/**
	 * 指定した引数の画面へ暗転処理とともに遷移します。
	 * 画面遷移命令を実行できた場合はtrueを返します。
	 * @param view
	 */
	public static synchronized boolean changeWithAnimation(GameViewFX view) {
		if ( gw.isChangeAnimation ) return false;
		Rectangle rect = new Rectangle(0,0,gw.scene.getWidth(),gw.scene.getHeight());
		rect.setFill(Color.BLACK);
		rect.setViewOrder(Long.MIN_VALUE);
		rect.setOpacity(0);
		gw.current.getChildren().add(rect);

		//フェードインフェードアウトアニメーションを実行
		FadeTransition fadeIn = new FadeTransition(Duration.millis(400), rect);
		fadeIn.setFromValue(0);
		fadeIn.setToValue(1);
		fadeIn.setOnFinished(e -> {
			gw.current.getChildren().remove(rect);
			view.getChildren().add(rect);
			GameWindowFX._change(view);
		});
		FadeTransition fadeOut = new FadeTransition(Duration.millis(400), rect);
		fadeOut.setFromValue(1);
		fadeOut.setToValue(0);
		fadeOut.setOnFinished(e -> {
			view.getChildren().remove(rect);
			gw.isChangeAnimation = false;
		});
		SequentialTransition animation = new SequentialTransition(fadeIn, fadeOut);
		gw.isChangeAnimation = true;
		animation.play();
		return true;
	}

	/**
	 * 指定した引数の画面へ遷移します。
	 * 画面遷移に成功した場合はtrueを返します。
	 * @param view
	 */
	public static synchronized boolean change(GameViewFX view) {
		if ( gw.isChangeAnimation ) return false;
		_change(view);
		return true;
	}

	private static void _change(GameViewFX view) {
		gw.scene.setRoot(view);
		gw.current = view;
	}
}

ゲームの画面用の抽象クラスをParentクラスのサブクラスであるGroupクラスを継承したGameViewFXクラスとして定義します。

GameViewFXにはサブクラスで利用したいメソッドを定義しておきます。今回はユーザー操作で画面遷移するものを作りたいのでmouseClickイベントが実行できるようにGameWindowFXからイベント伝番処理を実行し、GameViewFXにはmouseClickイベント用メソッドを空で定義しておきます。

GameViewFX.java
import javafx.scene.Group;
import javafx.scene.input.MouseEvent;

public abstract class GameViewFX extends Group{
	/**
	 * マウスをクリックしたときのイベント処理
	 * 使用する場合はサブクラスでオーバーライドしてください。
	 * @param e
	 */
	public void mouseClicked(MouseEvent e) {}
}

マウスイベントしか定義していませんが、キーイベントやゲームループのメソッド等も定義しておけばもっと便利になりそうです。

この二つのクラスを定義したおかげでGameViewFXを継承した画面を1画面として定義できるようになりました。

画面クラスの定義例

class 任意の画面 extends GameViewFX{
	コンストラクタ(){
		//任意のオブジェクトを追加
	}
	//必要であればマウスイベントメソッドをオーバーライドする
	public void mouseClicked(MouseEvent e){
		//内容を判断して画面遷移
	}
}

今回のメインである画面遷移はGameWindowFX.change(GameViewFXオブジェクト)またはGameWindowFX.changeWithAnimation(GameViewFXオブジェクト)で実行できるようにしています。changeWithAnimationメソッドのほうを利用すると画面遷移時に暗転アニメーションを付与できます。(Java9以降のみ)

それでは作成したクラスを利用し、ユーザー操作により画面遷移するプログラムを作成します。

画面はタイトル画面ゲーム画面ゲームオーバー画面ゲームクリア画面を想定します。

それではサンプルをご覧ください。

import javafx.application.Application;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Test extends Application{
	@Override
	public void start(Stage primaryStage) throws Exception {
		//ゲームウィンドウ初期化処理
		GameWindowFX.init(primaryStage, 400, 300, new Title());
		GameWindowFX.show();
	}
}


//タイトル画面
class Title extends GameViewFX{
	Text text = new Text(0, 150, "クリックでゲームを開始します。");
	public Title() {
		text.setFont(new Font(30));
		text.setFill(Color.BLUE);
		text.setStroke(Color.BLACK);
		text.setX(200 - text.getBoundsInLocal().getWidth() / 2);
		getChildren().add(text);
	}

	public void mouseClicked(MouseEvent e) {
		if ( e.getButton() == MouseButton.PRIMARY ) {
			//ゲーム画面へ遷移
			GameWindowFX.changeWithAnimation(new Game());
		}
	}
}

//ゲーム画面
class Game extends GameViewFX{
	Rectangle rect = new Rectangle(50, 175, 50, 50);
	Ellipse el = new Ellipse(325, 200, 25, 25);
	public Game() {
		Text text = new Text(0, 100, "正方形だと思うほうをクリックしよう。");
		text.setFont(new Font(20));
		text.setFill(Color.BLUE);
		text.setStroke(Color.BLACK);
		text.setX(200 - text.getBoundsInLocal().getWidth() / 2);

		getChildren().add(text);
		getChildren().add(rect);
		getChildren().add(el);

	}


	public void mouseClicked(MouseEvent e) {
		if ( e.getButton() == MouseButton.PRIMARY ) {
			if ( rect.contains(e.getX(), e.getY()) ) {
				//正方形をクリックしたらゲームクリア画面に遷移
				GameWindowFX.changeWithAnimation(new GameClear());
			} else if ( el.contains(e.getX(), e.getY()) ) {
				//円をクリックしたらゲームオーバー画面に遷移
				GameWindowFX.changeWithAnimation(new GameOver());
			}
		}
	}
}

//ゲームオーバー画面
class GameOver extends GameViewFX{
	public GameOver() {
		Text text = new Text(0, 100, "不正解・・・");
		text.setFont(new Font(50));
		text.setFill(Color.BLUE);
		text.setStroke(Color.BLACK);
		text.setX(200 - text.getBoundsInLocal().getWidth() / 2);
		getChildren().add(text);

		Text text2 = new Text(0, 200, "クリックでタイトルに戻ります。");
		text2.setFont(new Font(20));
		text2.setX(200 - text2.getBoundsInLocal().getWidth() / 2);
		getChildren().add(text2);
	}

	public void mouseClicked(MouseEvent e) {
		if ( e.getButton() == MouseButton.PRIMARY ) {
			//タイトル画面に遷移
			GameWindowFX.changeWithAnimation(new Title());
		}
	}
}

//ゲームクリア画面
class GameClear extends GameViewFX{
	public GameClear() {
		Text text = new Text(0, 100, "正解!!");
		text.setFont(new Font(50));
		text.setFill(Color.ORANGE);
		text.setStroke(Color.RED);
		text.setX(200 - text.getBoundsInLocal().getWidth() / 2);
		getChildren().add(text);

		Text text2 = new Text(0, 200, "クリックでタイトルに戻ります。");
		text2.setFont(new Font(20));
		text2.setX(200 - text2.getBoundsInLocal().getWidth() / 2);
		getChildren().add(text2);
	}

	public void mouseClicked(MouseEvent e) {
		if ( e.getButton() == MouseButton.PRIMARY ) {
			//タイトル画面に遷移
			GameWindowFX.changeWithAnimation(new Title());
		}
	}
}
実行結果

タイトル画面ではマウスクリックでゲーム画面へ遷移。

ゲーム画面では正方形か、円かをユーザーが判断し、正方形がクリックできればゲームクリア画面へ。クリックできなければゲームオーバー画面へ遷移します。

ゲームクリア画面とゲームオーバー画面ではクリックでタイトル画面へと遷移します。

今回はソースが少し長くなってしまいましたが画面遷移の方法の一つとして参考になれば嬉しいです。

Java

Posted by nompor