【JavaFX:横スクロールアクションゲーム】ゲーム画面の上に別のUIを表示、ゲームクリア、ゲームオーバー、全画面連携

2018年8月16日

ゲームオーバー、クリアの実装はゲーム画面を暗くして文字でクリアやゲームオーバーと表示します。

カメラをSceneに設定しているので、これの実装がなかなか面倒だったりします。

そこで、JavaFXのSubSceneクラスを使用します。

SubSceneの利用

SubSceneはSceneと同じく、カメラを設置でき、さらにNodeを継承しているクラスです。

これを利用することによって、カメラが適用されたNode表示上にカメラが適用されないUIの表示が簡単に可能になります。

ではSubSceneについて基本の利用方法をサンプルコードで示します。

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.ParallelCamera;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Test10 extends Application {

	public static void main(String[] args) {
		launch(args);
	}

	@Override
	public void start(Stage primaryStage) throws Exception {
		//通常の表示画面を作成
		Group root = new Group();
		Scene scene = new Scene(root, 400, 300);
		Rectangle rect1 = new Rectangle(220,125,50,50);

		//赤色をSceneのGroupに追加
		rect1.setFill(Color.RED);
		root.getChildren().add(rect1);

		//SubSceneを作成し、Groupをルートにする
		Group grp = new Group();
		SubScene sub = new SubScene(grp,400,300);
		Rectangle rect2 = new Rectangle(220,125,50,50);

		//青色をSubSceneのGroupに追加
		rect2.setFill(Color.BLUE);
		grp.getChildren().add(rect2);

		//SubSceneにカメラをセットし、右へ動かす
		ParallelCamera c = new ParallelCamera();
		c.setTranslateX(100);
		sub.setCamera(c);

		root.getChildren().add(sub);
		primaryStage.setScene(scene);
		primaryStage.show();
	}
}
実行結果

このコードではStage→Scene→Group→SubScene→Groupという具合にノードをはめ込んでいます。SceneはStageにセットするだけでGroup等には追加できませんが、SubSceneであればNodeとして扱えるので、Groupにセットできています。

さらにSubSceneにはカメラをセットし右へスクロールさせています。赤色と青色の四角形は同じ座標に作成したのに、表示位置が違いますね。

これは青色はカメラが適用されたSubScene上に表示され、赤色はカメラが適用されないScene上に表示されたからです。

ライブラリにはGameSceneFXというクラスを追加してゲームで利用していますが、このクラスがSubSceneとして扱えるクラスとなります。

中身はSubSceneにゲームループを実装できるようにしただけの単純なクラスです。

別画面との連携

ゲーム画面はタイトルやゲーム説明画面でも使いまわす予定ですので、今回の主な画面構成を見ておきます。

ゲーム画面はSubSceneノードで作成し、そのノードをタイトル画面やゲーム説明画面、さらにゲーム中のステータスやゲームオーバーなどを表示する画面に埋め込むようにします。

このようにしておけば、設定画面の裏でゲームを動かしながら処理させるのも簡単ですね。

タイトル画面やゲーム説明画面などの画面では普通にいつも通りJavaFXのAPIでのUIを構築をしておけばOKです。

UI側の表示は半透明表示を多めに使います。まあ、せっかく裏でゲームを動かすわけですから完全に見えなくなってしまうと意味がないですからね。

落ちたらゲームオーバーにする

これは単純にキャラのY座標が一定値より大きくなった時に判定して実装します。

カメラからのY座標を計算して実装したほうが場合によってはいい場面もありそうですが、今回のような単純にY座標範囲が0~600の固定であれば、固定値で座標比較するれば良いでしょう。

右端へ着いたらゲームクリアにする

これも単純にキャラのX座標で判定します。

ステージの大きさより少し小さい範囲をカメラの移動範囲にしておけば、キャラが画面外へ移動したときにクリアとかも出来そうですよね。

ゲームオーバー、ゲームクリアの実装サンプル

それでは、実際にゲームオーバーとゲームクリアを実装したサンプルをご覧ください。

import java.util.Iterator;

import com.nompor.gtk.fx.FixedTargetCamera2DFX;
import com.nompor.gtk.fx.GTKManagerFX;
import com.nompor.gtk.fx.GameSceneFX;
import com.nompor.gtk.fx.GameViewGroupFX;

import javafx.animation.Animation;
import javafx.animation.FadeTransition;
import javafx.animation.SequentialTransition;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Test8 extends Application {

	public static void main(String[] args) {
		launch(args);
	}

	@Override
	public void start(Stage primaryStage) throws Exception {
		final int WIDTH = 800, HEIGHT = 600;
		//フラグ保持構造体
		class Flg {
			boolean isGameClear=false;
			boolean isGameOver=false;
			boolean isEndOfView=false;
		}
		Flg flg = new Flg();
		GTKManagerFX.start(primaryStage,WIDTH, HEIGHT);

		//ゲーム画面
		//GameSceneFXはJavaFXのSubSceneを継承したクラスで、processメソッドはゲームループの処理を実装する
		GameSceneFX gameView = new GameSceneFX(WIDTH,HEIGHT) {
			Player p;
			FieldObject[][] data;
			FixedTargetCamera2DFX camera;
			GameField field;
			FieldObjectAppearanceObserverFactory factory;
			FieldObjectManager objMng;

			@Override
			public void start() {

				//ワールド領域3000*600に合わせて二次元配列構築
				final int MAX_W=3000,MAX_H=HEIGHT;
				final int ROW=MAX_H/50,COL=MAX_W/50;
				data = new FieldObject[ROW][COL];

				//画面一番下に地面ブロック配置
				final int UNDER_INDEX = (ROW-1);
				for ( int i = 0;i < COL;i++ ) {
					data[UNDER_INDEX][i] = FieldObject.GROUND;
				}
				data[UNDER_INDEX][20] = FieldObject.NONE;

				//ブロックはテスト用に適当に追加
				for ( int i = 18;i < 25;i++ ) {
					data[ROW-3][i] = FieldObject.BLOCK;
				}

				//オブジェクト出現ファクトリ
				factory = FieldObjectAppearanceObserverFactory.createFactory(this, data);
				factory.init();

				//表示リスト
				objMng = factory.getFieldObjectManager();

				//カメラ
				camera = factory.camera;

				//ステージデータ取得
				field = objMng.getField();

				//プレイヤーの作成
				p = objMng.getPlayer();
			}

			@Override
			public void process() {

				//出現チェック
				factory.execute();

				//プレイヤー移動
				p.update();

				//プレイヤーとフィールド判定
				field.fieldCheck(p);

				//背景の動き
				Iterator<BackgroundScrollObject> backs = objMng.getBackgrounds();
				while(backs.hasNext()) {
					BackgroundScrollObject back = backs.next();
					back.update(camera);
				}

				//ゴール判定
				//プレイヤーがフィールドの右端まで辿り着いたかを判定する
				Bounds b = p.getViewNode().getBoundsInParent();
				if ( b.getMaxX() > objMng.getField().maxX - 50 ) {
					flg.isGameClear = true;
				} else if ( b.getMinY() > objMng.getField().maxY ) {
					//ゲームオーバー判定
					flg.isGameOver = true;
				}
			}
		};

		//ゲームオーバー、クリア画面
		//GameViewGroupFXはJavaFXのGroupを継承したクラスであり、子要素のGameView系オブジェクトに自動でイベント伝番するクラス
		GameViewGroupFX gameOverOrClearView = new GameViewGroupFX();
		gameOverOrClearView.setOnProcess(e -> {
			//ゲームループ

			//ゲームオーバーかゲームクリアを表示したら以降何もしないようにする
			if (flg.isEndOfView) return;

			if ( flg.isGameClear ) {

				//操作無効化
				PlayerControllerManager.setActive(false);

				//画面を暗くする
				Rectangle rect = new Rectangle(0,0,AppManager.getW(),AppManager.getH());
				rect.setFill(Color.BLACK);
				rect.setOpacity(0);
				gameOverOrClearView.getChildren().add(rect);

				//ステージクリアテキストを表示
				Text clearText = new Text("Stage Clear!!");
				clearText.setTranslateX(80);
				clearText.setTranslateY(150);
				clearText.setFont(new Font(30));
				clearText.setFill(Color.YELLOW);
				clearText.setOpacity(0);
				gameOverOrClearView.getChildren().add(clearText);

				//各種フェードアニメーションのセッティング
				FadeTransition fade1 = new FadeTransition(Duration.millis(2000),rect);
				fade1.setToValue(0.8);

				FadeTransition fade2 = new FadeTransition(Duration.millis(100), clearText);
				fade2.setToValue(1);

				Animation anime = new SequentialTransition(fade1, fade2);
				anime.play();
				flg.isEndOfView = true;
			} else if ( flg.isGameOver ) {

				//キー操作無効化
				PlayerControllerManager.setActive(false);

				//画面を白くする
				Rectangle rect = new Rectangle(0,0,AppManager.getW(),AppManager.getH());
				rect.setFill(Color.BLACK);
				rect.setOpacity(0);
				gameOverOrClearView.getChildren().add(rect);

				//ゲームオーバーテキストを表示
				Text clearText = new Text("Game Over");
				clearText.setTranslateX(80);
				clearText.setTranslateY(150);
				clearText.setFont(new Font(30));
				clearText.setFill(Color.RED);
				clearText.setOpacity(0);
				gameOverOrClearView.getChildren().add(clearText);

				//各種フェードアニメーションのセッティング
				FadeTransition fade1 = new FadeTransition(Duration.millis(2000),rect);
				fade1.setToValue(0.8);

				FadeTransition fade2 = new FadeTransition(Duration.millis(100), clearText);
				fade2.setToValue(1);

				Animation anime = new SequentialTransition(fade1, fade2);
				anime.play();
				flg.isEndOfView = true;
			}
		});

		//ゲーム画面をベースのゲームオーバーやクリアを表示する画面に追加
		gameOverOrClearView.getChildren().add(gameView);

		//画面の表示
		GTKManagerFX.changeView(gameOverOrClearView);
	}

}
実行結果

タイトル画面やゲーム説明画面を制作し、全画面遷移の実装サンプル

上記のような感じで、タイトル画面やゲーム説明画面も作って、今まで作成した各種画面を遷移する簡単な実装をしてみます。

TestGameViewクラスがゲーム画面、TestGameUIViewクラスがゲームのUI画面、TestTitleViewクラスがタイトル画面、TestDescriptionViewクラスがゲーム説明画面として定義してみます。

import java.util.Iterator;

import com.nompor.gtk.fx.FixedTargetCamera2DFX;
import com.nompor.gtk.fx.GTKManagerFX;
import com.nompor.gtk.fx.GameSceneFX;
import com.nompor.gtk.fx.GameViewGroupFX;
import com.nompor.gtk.fx.animation.PagingTextAnimationView;
import com.nompor.gtk.fx.animation.TextAnimationView;

import javafx.animation.Animation;
import javafx.animation.FadeTransition;
import javafx.animation.SequentialTransition;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Test9 extends Application {
	final int WIDTH = 800, HEIGHT = 600;

	public static void main(String[] args) {
		launch(args);
	}

	@Override
	public void start(Stage primaryStage) throws Exception {
		GTKManagerFX.start(primaryStage,WIDTH, HEIGHT);

		//画面の表示
		GTKManagerFX.changeView(new TestTitleView());
	}

	//タイトル画面
	@SuppressWarnings("deprecation")
	class TestTitleView extends GameViewGroupFX{

		TestGameView gameView = new TestGameView();
		{
			getChildren().add(gameView);

			Rectangle rect = new Rectangle(800,600);
			rect.setFill(Color.rgb(0, 0, 0, 0.5));
			getChildren().add(rect);

			//ゲーム画面へ遷移
			Text game = new Text("ゲーム画面へ");
			game.setFont(new Font(50));
			game.setOnMouseClicked(e->{
				GTKManagerFX.animationClear();
				GTKManagerFX.changeViewDefaultAnimation(new TestGameUIView());
			});
			game.setCursor(Cursor.HAND);
			game.setOnMouseEntered(e->game.setFill(Color.ORANGE));
			game.setOnMouseExited(e->game.setFill(Color.WHITE));
			game.setWrappingWidth(AppManager.getW());
			game.setTextAlignment(TextAlignment.CENTER);
			game.setTranslateY(100);
			game.setFill(Color.WHITE);
			getChildren().add(game);

			//説明画面へ遷移
			Text description = new Text("説明画面へ");
			description.setFont(new Font(50));
			description.setOnMouseClicked(e->{
				GTKManagerFX.animationClear();
				GTKManagerFX.changeViewDefaultAnimation(new TestDescriptionView());
			});
			description.setCursor(Cursor.HAND);
			description.setOnMouseEntered(e->description.setFill(Color.ORANGE));
			description.setOnMouseExited(e->description.setFill(Color.WHITE));
			description.setWrappingWidth(AppManager.getW());
			description.setTextAlignment(TextAlignment.CENTER);
			description.setTranslateY(300);
			description.setFill(Color.WHITE);
			getChildren().add(description);

			//フルスクリーンへ移行
			Text fullsc = new Text("フルスクリーン");
			fullsc.setFont(new Font(50));
			fullsc.setOnMouseClicked(e->{
				GTKManagerFX.setFullScreenWithResolution(!GTKManagerFX.isFullScreen());
			});
			fullsc.setCursor(Cursor.HAND);
			fullsc.setOnMouseEntered(e->fullsc.setFill(Color.ORANGE));
			fullsc.setOnMouseExited(e->fullsc.setFill(Color.WHITE));
			fullsc.setWrappingWidth(AppManager.getW());
			fullsc.setTextAlignment(TextAlignment.CENTER);
			fullsc.setTranslateY(500);
			fullsc.setFill(Color.WHITE);
			getChildren().add(fullsc);
		}
	}

	//ゲーム説明画面
	class TestDescriptionView extends GameViewGroupFX{
		TestGameView gameView = new TestGameView();
		{

			//ゲーム画面の追加
			getChildren().add(gameView);

			Font f = new Font(20);
			Color clr = new Color(1, 1, 1, 1);

			//テキスト枠
			Rectangle rect = new Rectangle(70, 60, 400, 200);
			rect.setArcWidth(10);
			rect.setArcHeight(10);
			rect.setFill(Color.rgb(0,0,80));
			rect.setStroke(Color.LIME);
			getChildren().add(rect);

			//ボタンを追加
			Button btn = new Button();
			btn.setText("タイトルへ");
			btn.setTranslateX(650);
			btn.setTranslateY(20);
			btn.setCursor(Cursor.HAND);
			btn.setOnAction(e -> {
				GTKManagerFX.animationClear();
				GTKManagerFX.changeViewDefaultAnimation(new TestTitleView());
			});
			btn.setFocusTraversable(false);//ボタンにフォーカスが当たるとキーイベントが正常に動かない
			getChildren().add(btn);

			//1ページずつメッセージの追加
			PagingTextAnimationView pager = new PagingTextAnimationView(90,95);
			int dur = 80;
			pager.add(new TextAnimationView(dur, "ゲームの説明をテストだよ。\n矢印をクリックしてページを進めてね。", clr, f));
			pager.add(new TextAnimationView(dur, "あいうえお。\n", clr, f));
			pager.add(new TextAnimationView(dur, "てすとてすと。\n", clr, f));
			pager.add(new TextAnimationView(dur, "ふんがー。\n", clr, f));
			pager.add(new TextAnimationView(dur, "働きたくないでござる。", clr, f));
			pager.doPlayNowPage();
			getChildren().add(pager);

			//三角形の作成(次のページと前のページへのボタン)
			Polygon nextTri = new Polygon(
					510,160,
					480,120,
					480,200
			);
			Polygon prevTri = new Polygon(
					30,160,
					60,120,
					60,200
			);
			nextTri.setCursor(Cursor.HAND);
			prevTri.setCursor(Cursor.HAND);
			nextTri.setFill(Color.gray(0.2));
			prevTri.setFill(Color.gray(0.2));
			nextTri.setStroke(Color.LIME);
			prevTri.setStroke(Color.LIME);
			nextTri.setOnMouseEntered(e->nextTri.setFill(Color.ORANGE));
			nextTri.setOnMouseExited(e->nextTri.setFill(Color.gray(0.2)));
			prevTri.setOnMouseEntered(e->prevTri.setFill(Color.ORANGE));
			prevTri.setOnMouseExited(e->prevTri.setFill(Color.gray(0.2)));
			nextTri.setOnMouseClicked(e->pager.nextPage());
			prevTri.setOnMouseClicked(e->{
				pager.prevPage();
				pager.doFinalNowPage();
			});
			getChildren().add(prevTri);
			getChildren().add(nextTri);
		}
	}

	//ゲーム画面(UI部分表示)
	class TestGameUIView extends GameViewGroupFX{
		boolean isEndOfView=false;
		TestGameView gameView = new TestGameView();
		{
			getChildren().add(gameView);
			setOnProcess(e ->{
				//ゲームループ

				//ゲームオーバーかゲームクリアを表示したら以降何もしないようにする
				if (isEndOfView) return;

				if ( gameView.isGameClear ) {

					//画面を暗くする
					Rectangle rect = new Rectangle(0,0,AppManager.getW(),AppManager.getH());
					rect.setFill(Color.BLACK);
					rect.setOpacity(0);
					getChildren().add(rect);

					//ステージクリアテキストを表示
					Text clearText = new Text("Stage Clear!!");
					clearText.setTranslateX(80);
					clearText.setTranslateY(150);
					clearText.setFont(new Font(30));
					clearText.setFill(Color.YELLOW);
					clearText.setOpacity(0);
					getChildren().add(clearText);

					//各種フェードアニメーションのセッティング
					FadeTransition fade1 = new FadeTransition(Duration.millis(2000),rect);
					fade1.setToValue(0.8);

					FadeTransition fade2 = new FadeTransition(Duration.millis(100), clearText);
					fade2.setToValue(1);

					Animation anime = new SequentialTransition(fade1, fade2);
					anime.play();
					isEndOfView = true;
				} else if ( gameView.isGameOver ) {

					//画面を白くする
					Rectangle rect = new Rectangle(0,0,AppManager.getW(),AppManager.getH());
					rect.setFill(Color.BLACK);
					rect.setOpacity(0);
					getChildren().add(rect);

					//ゲームオーバーテキストを表示
					Text clearText = new Text("Game Over");
					clearText.setTranslateX(80);
					clearText.setTranslateY(150);
					clearText.setFont(new Font(30));
					clearText.setFill(Color.RED);
					clearText.setOpacity(0);
					getChildren().add(clearText);

					//各種フェードアニメーションのセッティング
					FadeTransition fade1 = new FadeTransition(Duration.millis(2000),rect);
					fade1.setToValue(0.8);

					FadeTransition fade2 = new FadeTransition(Duration.millis(100), clearText);
					fade2.setToValue(1);

					Animation anime = new SequentialTransition(fade1, fade2);
					anime.play();
					isEndOfView = true;
				}
			});

			//画面上部の黒い領域を描画
			LinearGradient grad = new LinearGradient(0, 0, 0, 40, false, CycleMethod.NO_CYCLE, new Stop(0, Color.BLACK), new Stop(1, new Color(0, 0, 0, 0.5)));
			Rectangle rect = new Rectangle(0,0,AppManager.getW(),40);
			rect.setFill(grad);
			getChildren().add(rect);

			//タイトルに戻るはゲーム開始後に表示
			Text titleRet = new Text("タイトルに戻る");
			titleRet.setFont(new Font(18));
			titleRet.setFill(Color.WHITE);
			titleRet.setTranslateY(30);
			titleRet.setTranslateX(50);
			titleRet.setOnMouseClicked(e->{
				GTKManagerFX.animationClear();
				GTKManagerFX.changeViewDefaultAnimation(new TestTitleView());
			});
			titleRet.setCursor(Cursor.HAND);
			titleRet.setOnMouseEntered(e->titleRet.setFill(Color.ORANGE));
			titleRet.setOnMouseExited(e->titleRet.setFill(Color.WHITE));
			getChildren().add(titleRet);
		}

		@Override
		public void mousePressed(MouseEvent e) {
			if ( isEndOfView ) {
				GTKManagerFX.animationClear();
				GTKManagerFX.changeViewDefaultAnimation(new TestTitleView());
			}
		}
	}

	//ゲーム画面
	class TestGameView extends GameSceneFX{
		Player p;
		FieldObject[][] data;
		FixedTargetCamera2DFX camera;
		GameField field;
		FieldObjectAppearanceObserverFactory factory;
		FieldObjectManager objMng;
		boolean isGameClear=false;
		boolean isGameOver=false;
		TestGameView(){
			super(WIDTH,HEIGHT);
		}

		@Override
		public void start() {

			//ワールド領域1200*600に合わせて二次元配列構築
			final int MAX_W=1200,MAX_H=HEIGHT;
			final int ROW=MAX_H/50,COL=MAX_W/50;
			data = new FieldObject[ROW][COL];

			//画面一番下に地面ブロック配置
			final int UNDER_INDEX = (ROW-1);
			for ( int i = 0;i < COL;i++ ) {
				data[UNDER_INDEX][i] = FieldObject.GROUND;
			}
			data[UNDER_INDEX][10] = FieldObject.NONE;

			//オブジェクト出現ファクトリ
			factory = FieldObjectAppearanceObserverFactory.createFactory(this, data);
			factory.init();

			//表示リスト
			objMng = factory.getFieldObjectManager();

			//カメラ
			camera = factory.camera;

			//ステージデータ取得
			field = objMng.getField();

			//プレイヤーの作成
			p = objMng.getPlayer();
		}

		@Override
		public void process() {
			//ゲームループ

			//出現チェック
			factory.execute();

			//プレイヤー移動
			p.update();

			//プレイヤーとフィールド判定
			field.fieldCheck(p);

			//背景の動き
			Iterator<BackgroundScrollObject> backs = objMng.getBackgrounds();
			while(backs.hasNext()) {
				BackgroundScrollObject back = backs.next();
				back.update(camera);
			}

			//ゴール判定
			//プレイヤーがフィールドの右端まで辿り着いたかを判定する
			Bounds b = p.getViewNode().getBoundsInParent();
			if ( b.getMaxX() > objMng.getField().maxX - 50 ) {
				isGameClear = true;
			} else if ( b.getMinY() > objMng.getField().maxY ) {
				isGameOver = true;
			}
		}
	}
}

こんな感じでタイトルや説明書画面にTestGameViewオブジェクトを持たせて、各画面でゲーム画面を表示します。

実行結果

今回でVer1.00の制作範囲は網羅できました。

次回は番外編でVer1.10の実装範囲もやっていきます。

JavaJavaFX

Posted by nompor