【JavaFX:横スクロールアクションゲーム】背景スクロールを実装

2018年8月28日

背景のスクロールは通常と同じ比率でのスクロールではなく、遠近感をだしたスクロールを実装します。

前の記事 横スクロールアクションゲームTop 次の記事

今回の場合簡単な方法は単純に背景画像に対してZ座標を指定する方法が最も簡単でしょう。

ですが、私が背景画像を等倍で作ってしまったので、あえてプログラム側でスクロール比率を制御した実装をしてみたいと思います。



カメラ座標の比率を元に遠近感あるスクロールを実現する

それでは、遠近感のあるスクロールの実装を考えていきましょうか。

私たちが普段歩行しているときに、目にする物体の移動量をイメージしてください。

近くにあるものと遠くにあるものの移動量は違いますよね。

近くにあるものは移動量が大きく、遠くにあるのもは移動量が小さく見えるはずです。

それと同じで、近くにあるものは移動量を大きくし、遠くにあるものは移動量を小さくします。

また、私たちが歩いているときに物体を見ていると物体の移動方向は逆ですよね。

それと同じで、キャラが右へ移動すると他の物体は左へ移動します。

今回のプログラムではカメラをキャラの中心に映すカメラを使用していますので、物体の座標は変更せずとも自動で逆へ移動します。

これを考慮するとスクロール量を小さくするためには、カメラの動きに合わせて、同じ方向へ背景の座標を移動させます。

移動させる量は遠くにあるものほどカメラの移動量よりも小さめに移動させます。

近くにあるものほどカメラの移動量よりも大きめに移動させます。

遠くにあるものをスクロールさせるイメージを作りました。

背景スクロールの無限ループ化

背景のスクロールを無限で繰り替えさせるには同じ画像を二つ並べて実装する方法が簡単そうなので、これで実現してみます。

この方法は、横に二つの画像を並べてカメラの表示範囲に収まらなくなったら、カメラの表示領域分横に一気にずらすというものです。

和かりにくいと思うので、イメージを用意しました。

同じ画像を横に並べているので、一気に移動したことはプレイヤーにはわかりません。

遠近感のあるスクロールを実現するサンプル

それでは実際にサンプルを作成し、動かしてみましょうか。

背景を二つ並べる部分は結合して新たな1枚の画像を生成して実現することとしました。

これはJavaFXのWritableImageを利用して画像を生成します。WritableImageは下記の記事で紹介しました。

背景を動かすロジックはこんな感じです。

	public void update(FixedTargetCamera2DFX camera) {
		//自動スクロール指定
		x-=autoMoveX;

		//スクロールの割合を元にカメラの座標で調整し、遠近感のあるスクロールを実現する
		Node node = getViewNode();
		double cx = camera.getLeft();
		double cr = camera.getRight();
		double cy = camera.getTop();
		double rx = Math.round(x + cx * scrollRatio);
		double ry = Math.round(y + cy * scrollRatio);

		//カメラの領域に入らない場合は座標調整
		//ウィンドウ座標分右に同じ画像を用意しておくことを前提としているため、ウィンドウサイズ分ずらしても違和感のない座標変更が可能である
		while ( rx + AppManager.getW() <= cx ) rx += (int)AppManager.getW();
		while ( rx >= cr ) rx -= (int)AppManager.getW();

		//スクロール後の座標をセット
		node.setTranslateX(rx);
		node.setTranslateY(ry);
	}

scrollRatioで移動比率を指定していますので、この値が0より大きいほど遠くにあるもののようにスクロールします。0より小さいほど近くにあるもののようにスクロールします。

基本的に-1.0~1.0の間あたりを指定するのが無難でしょう。

背景オブジェクトの生成プログラムは下記のようになりました。

	//背景オブジェクトを作成するメソッド
	public static BackgroundScrollObject createBackground(String imgName, double scrollRatio, int x, int y, double viewOrder, double autoMoveX) {
		//表示元
		Image img = AppManager.getImage(imgName);

		//BackgroundScrollObjectクラスは元画像からウィンドウ横幅分右側に画像のコピーを貼り付けた状態の物を前提とした処理とし、
		//それに合わせて、元画像からウィンドウ横幅分右側に同じ画像を描画した1枚の画像を生成する
		WritableImage newImage = new WritableImage((int)img.getWidth() + (int)AppManager.getW(), (int)img.getHeight());
		PixelWriter pw = newImage.getPixelWriter();

		//元画像をそのまま描画
		pw.setPixels(0, 0, (int)img.getWidth(), (int)img.getHeight(), img.getPixelReader(), 0, 0);

		//元画像から右側に描画
		pw.setPixels((int)AppManager.getW(), 0, (int)img.getWidth(), (int)img.getHeight(), img.getPixelReader(), 0, 0);

		//画像Nodeの作成
		ImageView viewNode = new ImageView(newImage);//表示ノード
		viewNode.setImage(newImage);
		viewNode.setTranslateX(x);
		viewNode.setTranslateY(y);
		viewNode.setViewOrder(viewOrder);
		BackgroundScrollObject bso = new BackgroundScrollObject(viewNode, scrollRatio);
		bso.setAutoMoveX(autoMoveX);
		return bso;
	}

これを使ったテストプログラムは下記です。

import com.nompor.gtk.fx.FixedTargetCamera2DFX;
import com.nompor.gtk.fx.GTKManagerFX;
import com.nompor.gtk.fx.GameViewFX;

import javafx.application.Application;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Test6 extends Application {

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

	@Override
	public void start(Stage primaryStage) throws Exception {
		final int WIDTH = 800, HEIGHT = 600;
		GTKManagerFX.start(primaryStage,WIDTH, HEIGHT);

		//GameViewFXはGroupを継承したクラスで、processメソッドはゲームループの処理を実装する
		GTKManagerFX.changeView(new GameViewFX() {
			Player p;
			FieldObject[][] data;
			FixedTargetCamera2DFX camera;
			Rectangle rect;
			GameField field;
			BackgroundScrollObject sora;
			BackgroundScrollObject ki;

			@Override
			public void start() {

				//ワールド領域2000*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;
				}

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

				//ブロック系オブジェクトの作成
				Field[][] fields = new Field[data.length][data[0].length];
				for ( int i = 0;i < data.length;i++ ) {
					int y=i*FieldObject.H;
					for ( int j = 0;j < data[i].length;j++ ) {
						int x=j*FieldObject.W;
						if ( data[i][j] == null ) continue;
						switch(data[i][j].TYPE) {
						case BLOCK:
							fields[i][j] = FieldObjectAppearanceObserverFactory.createBlock(data[i][j], x, y);
							break;
						case FIELD:
							fields[i][j] = FieldObjectAppearanceObserverFactory.createField(data[i][j], x, y);
							break;
						default:
							break;
						}
						if ( fields[i][j] != null ) {
							getChildren().add(fields[i][j].getViewNode());
						}
					}
				}
				field = new GameField(fields);

				//プレイヤーの作成
				p = FieldObjectAppearanceObserverFactory.createPlayer();

				//プレイヤーの表示
				getChildren().add(p.getViewNode());

				//カメラのセッティング
				GTKManagerFX.setGameCamera(camera =
						FixedTargetCamera2DFX.createRangeCamera(
								WIDTH, HEIGHT
								,p
								, 0, 0, MAX_W, MAX_H
						)
				);

				//実際の表示領域
				rect = new Rectangle(WIDTH, HEIGHT);
				rect.setFill(Color.GREEN);
				rect.setOpacity(0.3);
				getChildren().add(rect);

				//背景の作成
				sora = FieldObjectAppearanceObserverFactory.createBackground("sora", 0.7, -100, 0, 1000, 0.1);
				ki = FieldObjectAppearanceObserverFactory.createBackground("ki", 0.2, 000, 350, 10,0);
				getChildren().add(sora.getViewNode());
				getChildren().add(ki.getViewNode());

				//ズームアウトしておく
				camera.setTranslateZ(-1500);
			}

			@Override
			public void process() {

				p.update();

				field.fieldCheck(p);

				sora.update(camera);
				ki.update(camera);

				//カメラ座標に緑領域を移動
				rect.setTranslateX(camera.getTranslateX());
				rect.setTranslateY(camera.getTranslateY());
			}
		});
	}

}
実行結果

サンプルでは画像を一気に移動させていることがわかるようにカメラをズームアウトさせて見てみます。

実際に表示される領域はわかりやすいように緑色にしています。

今思うとSceneを分けてカメラを無視したほうが実装内容が単純になったかもしれませんね。

本来ならば画面に映ってない画像の一部は描画しないのが理想です。JavaFXのclipメソッドやviewPortメソッドを利用することで、この辺も実現可能です。

viewportは下記の記事でも紹介しています。

JavaJavaFX

Posted by nompor