【JavaFX:横スクロールアクションゲーム】キャラとステージ(ブロック)の判定、押し戻し処理の実装

2018年8月22日

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

アクションゲーム制作で面倒くさい処理の一つである、キャラとステージの判定処理です。

このゲームのステージは50×50のマップチップを敷き詰めてステージを構成していますのでこの方法を前提に判定処理をやっていきましょう。



キャラ移動前座標を保持する

判定処理をするのに当たって、まずはキャラクターの移動前座標を保持させます。

移動前の座標を保持しておくことによって、キャラクターがブロックに対して、どの方向から突っ込んできたのかを判定するのです。

CharaObjectに、次のような実装をしました。

	double prel;
	double pret;
	double prer;
	double preb;

	//現在の座標を記録しておく
	protected void positionInit() {
		Bounds b = getHitNode().getBoundsInParent();
		prel = b.getMinX();
		pret = b.getMinY();
		prer = b.getMaxX();
		preb = b.getMaxY();
	}

キャラの上端座標、右端座標、下端座標、左端座標の変数をそれぞれ作成し、positionInitメソッドで現在の当たり判定のNodeから座標を取得し、設定しておきます。

positionInitメソッドはupdateメソッドで毎フレーム呼び出します。

moveメソッドはキャラの移動用メソッドなので、この後にmoveメソッドを呼び出すようにしておけば、更新後の座標と更新前の座標を保持することができますね。

キャラクター付近のブロックを特定する

ブロックとの押し戻し処理を行う前に、判定対象のブロックを特定します。これを行うことによって毎フレーム全ブロックとの判定処理を実行しなくて済みます。

50×50マップチップで構成したステージで、2次元配列で保持している場合は、キャラの座標/50でブロックの配列インデックスを取得できます。

キャラの上端座標、右端座標、下端座標、左端座標を50で割って判定対象の開始インデックス、終了インデックスを特定します。

例えば50×50のマップチップを4×4の二次元配列で構成し、キャラの上端座標、右端座標、下端座標、左端座標がそれぞれ73,118,123,68としましょう。

そうした場合それぞれ、上端座標(73)/50で行の開始インデックス、右端座標(118)/50で列の終了インデックス、下端座標(123)/50で行の終了インデックス、左端座標(68)/50で列の開始インデックスが取得できます。

イメージはこんな感じ。

特定したブロックと矩形判定する

開始インデックスと終了インデックスを特定したら、2重ループでその範囲だけ矩形判定を行います。

矩形判定は事前に作成したのでそのメソッドを呼び出すだけで終了です。

下記は特定したブロックと比較し、ブロックと判定するメソッドです。あと画面外に出たときの押し戻し処理もここで実装しています。

	//キャラクタのフィールド位置チェックを行い補正する
	public void fieldCheck(CharaObject chara) {
		//キャラクタがフィールドのブロックオブジェクトと当たっているか判定し、当たっていたら押し戻す処理
		Node node = chara.getHitNode();
		Bounds b = node.getBoundsInParent();

		//判定対象となるブロックインデックスを算出(開始地点から終了地点)
		int sx = (int)b.getMinX() / Block.W;
		int sy = (int)b.getMinY() / Block.H;
		int ex = (int)b.getMaxX() / Block.W;
		int ey = (int)b.getMaxY() / Block.H;

		//インデックスが配列を越えていた場合は端点に修正
		if ( sx < 0 ) sx = 0;
		if ( sy < 0 ) sy = 0;
		if ( ex < 0 ) ex = 0;
		if ( ey < 0 ) ey = 0;
		if ( sx >= fields[0].length ) sx = fields[0].length - 1;
		if ( sy >= fields.length ) sy = fields.length - 1;
		if ( ex >= fields[0].length ) ex = fields[0].length - 1;
		if ( ey >= fields.length ) ey = fields.length - 1;

		//地上、空中フラグを無衝突状態に初期化
		chara.isAir = true;
		chara.isGround = false;

		//画面端である場合は壁に当たったものとし、押し戻し
		if ( b.getMaxX() > maxX ) {
			chara.moveX(maxX - b.getMaxX());
		}
		if ( b.getMinX() < minX ) {
			chara.moveX(minX - b.getMinX());
		}

		for ( int i = sy;i <= ey;i++ ) {
			for ( int j = sx;j <= ex;j++ ) {
				Field block = fields[i][j];
				if ( block != null && block.isHit(chara) ) {
					//ブロックにめり込んだらめり込んだ分戻す
					block.sinkingRevise(chara);
				}
			}
		}
	}

この実装はGameFieldクラスに実装しています。

衝突ブロックからキャラを押し戻す

※2018/07/28・・・改めて考えると、事前に矩形判定をしているので、押し戻しの際、移動後の座標比較をする必要はないですね。修正しておきました。and条件ではなくなった分すっきりしたはずです。

矩形判定で当たっていると判断された、ブロックからキャラを押し戻します。

押し戻しの方法は何種類か考えられますが、移動前座標を利用した方法が私の中で簡単な方法だと思うのでこれを採用しました。

まずは押し戻す方向を特定したいと思いますが、これには事前に保持しておいたキャラの移動前座標を使用します。

方法は単純で、移動前座標がブロックの座標の外側であるとき、キャラが突っ込んだと判定できます。これを上下左右で判定し、trueになった方向が、押し戻す方向になります。

わかりにくいと思うのでブロックの上や左から突っ込んだかどうかを判定して押し戻すイメージを作りました。

このイメージは上から突っ込んだときと左から突っ込んだときです。オレンジ色の線同士の座標を比較して外側にいるか内側にいるかを判定していけばOKなのです。

実際はこんなにわかりやすく上から・・・横から・・・とはいかず、左上から斜めに突っ込まれるときもあるでしょう。この場合は問答無用で上から来たことにします。

さて、下記が実際の押し戻しの実装になります。

	//ブロックにめり込んだキャラクタを押し出すメソッド
	@Override
	public void sinkingRevise(CharaObject mv) {

		//当たり判定オブジェクトの取得
		Node node = getHitNode();
		Node mvNode = mv.getHitNode();
		Bounds b = node.getBoundsInParent();
		Bounds mvb = mvNode.getBoundsInParent();

		if ( mv.preb <= b.getMinY() ) {
			//キャラがブロックの上側から突っ込んだ場合めり込んだ分キャラを上にずらす
			mv.moveY(-(mvb.getMaxY() - b.getMinY()));
			mv.isGround = true;
		} else if ( mv.pret >= b.getMaxY() ) {
			//キャラがブロックの下側から突っ込んだ場合めり込んだ分キャラを下にずらす
			mv.moveY(b.getMaxY() - mvb.getMinY());
		} else if ( mv.prer <= b.getMinX() ) {
			//キャラがブロックの左側から突っ込んだ場合めり込んだ分キャラを左にずらす
			mv.moveX(-(mvb.getMaxX() - b.getMinX()));
		}

		//地上ではない場合は空中フラグオン
		mv.isAir=!mv.isGround;
	}

上への押し上げ処理を優先的にしたいので一番上のifを上押し上げ判定処理にしています。それ以降も優先順位ごとにelse if 入れてく感じです。

ブロックの右の押し戻し処理は実装していません。現時点では右移動のキャラしか存在しないからです。

ついでに押し戻し判定ができるということは、今地面に立っているか、空中なのかもわかりますので、キャラクターに地上、空中フラグをセットしておきましょう。

この実装はBlockクラスに実装しています。



テストサンプル

それではうまく判定できているかテストしてみます。

面倒なのでオブジェクト作成はファクトリから作成するようにしました。

これが今回のテストサンプルです。

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

import javafx.application.Application;
import javafx.stage.Stage;

public class Test3 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;
			Enemy e;
			GameField fields;

			@Override
			public void start() {

				//ウィンドウ表示領域800*600に合わせて二次元配列構築
				final int ROW=HEIGHT/50,COL=WIDTH/50;
				Field[][] field = new Field[ROW][COL];

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

				{
					field[10][5] = FieldObjectAppearanceObserverFactory.createBlock(FieldObject.BLOCK, 5 * 50,10*50);
					field[10][12] = FieldObjectAppearanceObserverFactory.createBlock(FieldObject.BLOCK, 12 * 50,10*50);
					field[9][12] = FieldObjectAppearanceObserverFactory.createBlock(FieldObject.BLOCK, 12 * 50,9*50);
				}

				//GameFieldオブジェクトの構築
				fields = new GameField(field);
				Field[][] fieldList = fields.getFieldList();
				for ( int i = 0;i < fieldList.length;i++ ) {
					for ( int j = 0;j < fieldList[i].length;j++ ) {
						Field fld = fieldList[i][j];
						if ( fld != null ) {
							getChildren().add(fld.getViewNode());
						}
					}
				}

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

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

				//敵の作成
				e = FieldObjectAppearanceObserverFactory.createEnemy(FieldObject.SLIME,300,50);

				//敵表示
				getChildren().add(e.getViewNode());
			}

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

				//キャラの移動
				p.update();
				e.update();

				//キャラとブロック判定
				fields.fieldCheck(p);
				fields.fieldCheck(e);
			}
		});
	}

}
実行結果

よっしゃーうまくいった―・・・と思いきや・・・ブロックを縦に並べたときにひっかかってジャンプができません。よし、縦には並べないようにするかー・・・

疲れたので、今回のところはひとまずここまで。

番外編で、もう一工夫して縦に並べても大丈夫なようにすることにしましょうか。ついでに右から突っ込んだ時の処理もそのときに実装しましょう。

本稿の全ソースはこちらです。

JavaJavaFX

Posted by nompor