サイトのトップへ戻る

libGDX ドキュメント 日本語訳

libgdxを使ったAndroidゲーム開発入門 – チュートリアル Part 3 – ジャンプ、重力、動作の改善

これは、LibGdx を使ったゲーム開発連載の第三回です。内容を理解するためには前回の記事も読むようにしてください。

前回の記事では、ボブの移動をアニメーション化しましたが、ロボットのような動きになっています。今回の記事では、ボブをジャンプさせたりより自然に移動するようにします。 物理学を少し使ってこれを実現します。また、コードを綺麗にして前回の記事で紛れ込んだいくつかの問題を修正します。



ジャンプ – 物理演算

ジャンプとはエンティティ(今回の場合はボブ)によって行われる動作です。自分自身を空中に押し上げ、その後地面(基板)に着地します。 地面が引き付ける力(重力)よりも大きい力をオブジェクトに適用することでこれを行います。
各オブジェクトは以下のように識別します:

  • ボブ – エンティティ
  • 地面 – 基板
  • 重力 (G) – 世界中の全てのエンティティに作用する一定の重力

リアルなジャンプを実装するには、ニュートンの運動法則を適用すればいいだけです。 ボブとゲーム内世界に必要な属性 (質量、重力、摩擦) を追加すれば、ジャンプの実装に必要なものは全て揃います。
以下の図を見て、各要素について調べてみましょう。 左側は‘ジャンプ’ボタンを押した時で、右側はジャンプ中のボブを表します。

Forces in jump
それでは、各状態においてボブに適用される力を調べてみましょう。

1. ボブが待機状態 で地面に立っている(grounded)。
この場合、重力 のみがボブに作用しています。つまり、ボブは一定の力で常に下に引っ張られているということです。
オブジェクトを地面に引っ張る力を計算する公式は以下になります
F=m*a
m質量 (重量ではないが重量と考えてください)で、 a加速度です。
分かりやすくするためにボブは1の質量を持っているものと考え、これで重力と加速度は等しくなります。

オブジェクトに一定の力を適用した場合、 velocity は無限に増加します。
オブジェクトの velocity を計算する公式は以下です:
v=u+a*t
ここでは

  • v – は最終的なvelocity
  • u – は初期の velocity (t 秒前のvelocity )
  • a – は加速度
  • t – は加速度が適用されてから経過した時間

ボブを画面中央の空間に配置した場合、最初の velocity は0です。 地球の重力加速度が9.8でボブの体重(質量)が1と考えると、1秒後の落下速度を計算するのは簡単です。

v = 0 + 9.8 * 1 = 9.8m/s
自由落下時に1秒経過すると、ボブは1秒で9.8メートル加速しました。これは35.28 kph もしくは21.92 mphです。とても速いです。
さら1秒後の velocity を知りたい場合は、同じ公式を使います。
v = 9.8 + 9.8 * 1 = 19.6m/s
これは 70.56 kph もしくは 43.84 mph で、とても速いです。
加速度は直線的に作用し、常に一定の力を受けるオブジェクトは無限に加速し続けるということが分かりました。 これは摩擦や抵抗がない理想的な環境での話です。 空気には摩擦がありそれによって落下するオブジェクトへいくつかの力が働くので、落下するオブジェクトはある時点で終端速度に達し、それ以上は加速しません。 これは多くの要因によって左右されますが、今回はそういった複雑な要因は無視しています。

落下するオブジェクトが地面に着いたら、それは停止され、重力は落下オブジェクトにそれ以上影響を与えません。 これは現実とは違いますが、我々は完璧な物理演算シミュレータを作っているのではなく、ボブが終端速度のまま地面に着地しても死なないゲームを作っているのです。
そのために加速度計算を作り直し、ボブが地面に着地したら重力を無視するようにします。



ボブをジャンプさせる

ボブをジャンプさせるには、重力とは反対向きの力(上向き)が必要です。これは、重力の影響を受けたままでボブを空中に押し上げるようにします。 図で確認すると、その力 (F) はとても強いものと分かります (その大きさ長さ は重力のベクトルよりもかなり大きいです)。 二つのベクトル(G とF)を追加することで、ボブに作用する最終的な力を取得できます。
これを単純化するために、各向きのベクトルを除外してY軸方向のベクトルのみを考慮して計算をします。
地球上では、 G = 9.8m/s^2. これは下向きの力なので、ゲーム内では -9.8 m/s^2となります。 ボブがジャンプした時、重力(G)が彼を地面に引き戻す前に高さ(h)まで到達できる加速度が生み出せるよう、十分な力を発生させます。
ボブは我々と同じ人間なので、少なくともジェットパックでものない限り空中で加速し続けることはできません。 これを再現するため、 ‘ジャンプ’キーを押した時に大きな力を発生させます。 By applying the above formulas, the initial velocity will be high enough so even if gravity will act on Bob he will still climb to a point after which he starts the free falling sequence.
この方法を実装した場合、本当に素晴らしいリアルな見た目のジャンプができます。

オリジナルの star guard ゲームをよく確認してみると、ジャンプボタンを押した長さに応じて主人公がジャンプする高さが変わります。 これは、プレイヤーがジャンプキーを押し続けている間に上向きの力を適用し続け、一定の時間が経過したら力の適用を解除することで簡単に実装できます。 一定時間後に強制解除するのは、ボブが飛行できないようにするためです。



ジャンプの実装

ジャンプに必要な物理演算は揃ったと思うので、それではジャンプの実装について見ていきましょう。

We will also do a little housekeeping task and reorganise the code. I want to isolate the jumping and movement so I will ignore the rest of the world. コードの変更内容を知りたい場合は、リファクタリングの項目までスクロールダウンしてください。

BobController.javaを開きます。これは以前WorldController.javaだったものの名前を変更しています。 これをボブを操作するものなので、その名前の方が分かりやすいです。

public class BobController {

	enum Keys {
		LEFT, RIGHT, JUMP, FIRE
	}

	private static final long LONG_JUMP_PRESS 	= 150l;
	private static final float ACCELERATION 	= 20f;
	private static final float GRAVITY 			= -20f;
	private static final float MAX_JUMP_SPEED	= 7f;
	private static final float DAMP 			= 0.90f;
	private static final float MAX_VEL 			= 4f;
	
	// these are temporary
	private static final float WIDTH = 10f;

	private World 	world;
	private Bob 	bob;
	private long	jumpPressedTime;
	private boolean jumpingPressed;
	
	// ... code omitted ... //

	public void jumpReleased() {
		keys.get(keys.put(Keys.JUMP, false));
		jumpingPressed = false;
	}

	// ... code omitted ... //
	/** The main update method **/
	public void update(float delta) {
		processInput();
		
		bob.getAcceleration().y = GRAVITY;
		bob.getAcceleration().mul(delta);
		bob.getVelocity().add(bob.getAcceleration().x, bob.getAcceleration().y);
		if (bob.getAcceleration().x == 0) bob.getVelocity().x *= DAMP;
		if (bob.getVelocity().x > MAX_VEL) {
			bob.getVelocity().x = MAX_VEL;
		}
		if (bob.getVelocity().x < -MAX_VEL) {
			bob.getVelocity().x = -MAX_VEL;
		}
		
		bob.update(delta);
		if (bob.getPosition().y < 0) {
			bob.getPosition().y = 0f;
			bob.setPosition(bob.getPosition());
			if (bob.getState().equals(State.JUMPING)) {
					bob.setState(State.IDLE);
			}
		}
		if (bob.getPosition().x < 0) {
			bob.getPosition().x = 0;
			bob.setPosition(bob.getPosition());
			if (!bob.getState().equals(State.JUMPING)) {
				bob.setState(State.IDLE);
			}
		}
		if (bob.getPosition().x > WIDTH - bob.getBounds().width ) {
			bob.getPosition().x = WIDTH - bob.getBounds().width;
			bob.setPosition(bob.getPosition());
			if (!bob.getState().equals(State.JUMPING)) {
				bob.setState(State.IDLE);
			}
		}
	}

	/** Change Bob's state and parameters based on input controls **/
	private boolean processInput() {
		if (keys.get(Keys.JUMP)) {
			if (!bob.getState().equals(State.JUMPING)) {
				jumpingPressed = true;
				jumpPressedTime = System.currentTimeMillis();
				bob.setState(State.JUMPING);
				bob.getVelocity().y = MAX_JUMP_SPEED; 
			} else {
				if (jumpingPressed && ((System.currentTimeMillis() - jumpPressedTime) >= LONG_JUMP_PRESS)) {
					jumpingPressed = false;
				} else {
					if (jumpingPressed) {
						bob.getVelocity().y = MAX_JUMP_SPEED;
					}
				}
			}
		}
		if (keys.get(Keys.LEFT)) {
			// left is pressed
			bob.setFacingLeft(true);
			if (!bob.getState().equals(State.JUMPING)) {
				bob.setState(State.WALKING);
			}
			bob.getAcceleration().x = -ACCELERATION;
		} else if (keys.get(Keys.RIGHT)) {
			// left is pressed
			bob.setFacingLeft(false);
			if (!bob.getState().equals(State.JUMPING)) {
				bob.setState(State.WALKING);
			}
			bob.getAcceleration().x = ACCELERATION;
		} else {
			if (!bob.getState().equals(State.JUMPING)) {
				bob.setState(State.IDLE);
			}
			bob.getAcceleration().x = 0;
			
		}
		return false;
	}
}

このクラスに追加したものを理解するために少し時間を取りましょう。
以下の行について説明します。:
#07 – #12 – ゲーム内世界とボブに対して作用する値を保持する定数

  • LONG_JUMP_PRESS – ジャンプに適用される推進力が停止されるまでのミリ秒単位の時間。このゲームでは高ジャンプを実装し、プレイヤーがボタンを長く押すほどボブが高くジャンプすることを忘れないでください。 ボブが飛行してしまうのを防ぐため、150ミリ秒経過したらジャンプ力の適用を停止します。
  • ACCELERATION – これは歩行/走行で使用されます。ジャンプの原理と全く同じですが、水平方向の X 軸上で使用されます。
  • GRAVITY – これは重力加速度です (図の中では下向きのG )
  • MAX_JUMP_SPEED – これは終端速度です。ジャンプ中にこの速度を超えることはできません。
  • DAMP – これはボブが止まる時の動きをスムーズにするためのものです。ボブは突然停止することはありません。詳しくは後述します。ジャンプには関連しない内容なので無視してください。
  • MAX_VEL – MAX_JUMP_SPEED と同じものですが、横軸での移動に使用されます。

#15 – これは仮の定数で、ゲーム内世界の横幅です。これを使って、ボブの移動する範囲を画面内に制限します。
#19 – jumpPressedTime はジャンプボタンが押されている時間を累積する変数です
#20 – ジャンプボタンが押されている場合はtrueになる boolean 型の値
#26jumpReleased()では、 jumpingReleased 変数に falseを設定する必要があります。これは単純な状態変数です

以降は我々に代わってほぼ全ての作業を行う、メインの update メソッドです。
#32 – これまでと同じ様に processInput を呼び出し、任意のキーが押されているかを確認します
processInputについて見てみましょう
#71 – ジャンプボタンが押されているかどうかを確認します
#72 – #76 – この場合はボブは JUMPING 状態ではなく (つまり地面にいます)、ジャンプ処理を初期化します。 ボブはジャンプ状態に設定され、飛び立つ準備をします。 普通に上向きの力を適用するのではなく、ちょっと細工をします。ボブの上向きの速度に彼がジャンプする際の最大速度を設定します (#76行目)。 また、ジャンプ処理を初期化する時にミリ秒単位の時間を保存します。
#77 – #85 – これはボブが空中にいる時に実行されます。ジャンプボタンを押したままだった場合は、ジャンプ処理を初期化してからの経過時間が設定した閾値を越えているかどうか確認し、制限時間内(現時点では150ミリ秒)だった場合はボブの垂直方向の速度を維持します。

#87-107行目は横方向への移動処理なので無視してください。

update メソッドに戻ります:
#34 – ボブの加速度には GRAVITYが設定されます。重力は常に一定に作用しており、これが加速計算の開始点になるためです。
#35 – このサイクルにおける経過時間分の加速度を算出します。 Our initial values are in units/seconds so we need to adjust the values accordingly. 1秒間に updateを60 回呼び出す場合、 delta の値は 1/60になります。これはあなたに代わってlibgdxが全て制御します。
#36 – ボブの現在の速度を両軸上での加速度を使って更新します。ユークリッド空間でベクトルを扱っていることを忘れないでください。
#37 – これはボブの停止を滑らかなものにします。ボブのX軸上の加速度が0の場合、サイクル毎にボブのスピードを10%ずつ減らしていきます。1秒間に多くのサイクルが回れば、ボブは非常に速く、非常に滑らかに停止します。
#38 – #43 – ボブが最大許容速度(終端速度)を越えないようにします。これは、オブジェクトに常に一定の力が作用する場合そのオブジェクトが無限に加速するという法則に対する予防線です。
#45 – ボブのupdate メソッドを呼び出します。このメソッドは、ボブの速度に応じてその位置を更新するだけです。
#46 – #66 – これはとても基本的な衝突判定です。ボブが画面内から出るのを防ぎます。単純ににボブの位置が画面の外かどうかを確認し(世界座標を使って)。画面外にいた場合は画面端までボブを戻すだけです。注目すべきは、ボブが地面に着地したりゲーム内世界の端に着いた時は常に 待機状態にするということです。これにより再度ジャンプができるようになります。

上記の変更を加えてアプリケーションを実行した場合は、以下のような挙動になります:



コードを整理する– リファクタリング

完成したアプリケーションを見ての通り、タイルが無くなってボブは画面端でしか移動制約を受けていません。
ボブが空中にいる時の画像も違います。一つの画像はボブがジャンプしている時のもので、もう一つの画像はボブが落下している時のものです。
我々は以下の変更を行いました:

  • WorldControllerの名前を BobControllerに変更。これをボブを操作するものなので、意味が通るようにする。
  • WorldRendererrender() メソッド内のdrawBlocks()をコメントアウト。今回はタイルは不要なので描写しません。
  • WorldRenderedsetDebug()メソッドを追加し、GameScreen.java内で切り替え機能に対応しました。 これで、デスクトップではDキーを押せばデバッグ描写モードに切り替えることができます。
  • ボブのジャンプ状態と落下状態を表すための新しい texture regionをWorldRenderer に追加しました。 We still maintain just one state though. world rendererはボブの縦方向の(Y軸上での)速度を確認することでどちらの状態のボブを表示するかを判別します。 縦方向の速度が正の値の場合ボブはジャンプ中です。 縦方向の速度が負の値の場合ボブは落下中です。
    public class WorldRenderer {
    
    	// ... omitted ... //
    
    	private TextureRegion bobJumpLeft;
    	private TextureRegion bobFallLeft;
    	private TextureRegion bobJumpRight;
    	private TextureRegion bobFallRight;
    
    	private void loadTextures() {
    		TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("images/textures/textures.pack"));
    
    		// ... omitted ... //
    
    		bobJumpLeft = atlas.findRegion("bob-up");
    		bobJumpRight = new TextureRegion(bobJumpLeft);
    		bobJumpRight.flip(true, false);
    		bobFallLeft = atlas.findRegion("bob-down");
    		bobFallRight = new TextureRegion(bobFallLeft);
    		bobFallRight.flip(true, false);
    	}
    
    	private void drawBob() {
    		Bob bob = world.getBob();
    		bobFrame = bob.isFacingLeft() ? bobIdleLeft : bobIdleRight;
    		if(bob.getState().equals(State.WALKING)) {
    			bobFrame = bob.isFacingLeft() ? walkLeftAnimation.getKeyFrame(bob.getStateTime(), true) : walkRightAnimation.getKeyFrame(bob.getStateTime(), true);
    		} else if (bob.getState().equals(State.JUMPING)) {
    			if (bob.getVelocity().y > 0) {
    				bobFrame = bob.isFacingLeft() ? bobJumpLeft : bobJumpRight;
    			} else {
    				bobFrame = bob.isFacingLeft() ? bobFallLeft : bobFallRight;
    			}
    		}
    		spriteBatch.draw(bobFrame, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.SIZE * ppuX, Bob.SIZE * ppuY);
    	}
    }
    

    上記抜粋のコードでは、重要な追加部分を示しています。
    #5-#8 – ジャンプ用の新しいtexture region。左向き用と右向き用が必要です。
    #15-#20 – ゲーム素材の準備。プロジェクトにさらにいくつかのpng画像を追加する必要があります。 star-assault-android/images/ ディレクトリを確認してください。すると、 bob-down.png ファイルとbob-up.pngファイルがあるでしょう。 これらが追加され、さらにImagePacker2ツール を使って texture atlasが再作成されています。 作成方法については Part 2 を参照してください。
    #28-#33 – ボブが空中にいる時にどちらのtexture regionを描写するかを決める部分です。

  • Bob.javaではいくつかのバグを修正しています。 The bounding box now has the same position as bob and the update takes care of that. Also the setPosition method updates the bounding boxes’ position. This had an impact on the drawDebug() method inside the WorldRenderer. Now we don’t need to worry about calculating the bounding boxes based on the tiles’ position as the boxes now have the same position as the entity. これは私がうっかり見過ごしてしまった本当にばかげたバグでした。これは衝突判定をする時にとても重要になります。

This list pretty much sums up all the changes but it should be very easy to follow through.

このプロジェクトのソースコードはここ: https://github.com/obviam/star-assaultにあります。
ブランチpart3をチェックアウトする必要があります。


gitを使ってチェックアウトするには:
git clone -b part3 git@github.com:obviam/star-assault.git


また zip ファイルとしてダウンロードすることもできます。

次の記事では、 あらためてタイルを追加し、適切な衝突判定を行ってボブを画面内で動き回らせます。ここからコードをチェックアウトできます。