これは Star Guardをモデルとした2Dプラットフォーム・ゲームのプロトタイプを作成するlibgdx チュートリアルの第四回目です。
これまでの記事にも興味がある場合は前回の記事を読むことができます。
これまでのチュートリアルに従って、ブロックで構成された画面内の世界とその世界の中で動き回るボブという名前の主人公を上手く作成できましたが、ボブとゲーム内世界とが相互作用していないという問題があります。 タイルを描写した場合、ボブはブロックが存在しないかのように自由に歩いたりジャンプしたりするでしょう。 全てのブロックが無視されています。 ボブとブロックが衝突しているかどうかを確認していないためにこういったことが発生しています。
衝突判定とは、二つ以上の オブジェクトが衝突 した時にそれを 検知する ことです。 今回は、ボブが ブロックと 衝突した時にそれを検知する必要があります。 ボブの境界ボックスと各ブロックの境界ボックスが交差しているかどうかを正確に確認します。 交差していた場合、衝突を検知します。 オブジェクト(ボブとブロック)に注意し、それに従って処理を行います。 今回の場合、衝突したブロックがどちら側にあるかに応じて、ボブの前進や落下やジャンプを止める必要があります。
簡単に速くやる方法は、ゲーム内世界にある全てのブロックに対して反復処理を行い、ボブの現在の境界ボックスと衝突しているかを確認することです。 この方法は10×7の小さな世界では問題なく動作しますが、ブロックが数千個もあるような広大な世界の場合では、検知処理を毎フレーム行うとパフォーマンスに影響が出てしまいます。
上記のやり方を最適化するには、ボブと衝突している可能性のある候補のタイルをいくつかピックアップします。
設計上の意図により、ゲーム内世界を構成するブロックは各軸上で整列された境界ボックスを持ち、その横幅と高さ両方とも1単位となっています。
今回の場合、ゲーム内世界は以下画像のようになります (全てのブロックとタイルは、単位ブロック内に収まっています):
赤い四角はブロックが設置される場合の境界を表します。黄色い四角は、実際に設置されたブロックです。
ゲーム内世界を現す二次元配列(マトリクス)を使用し、各セルはBlock
を持つか、ない場合はnull
を持ちます。
これは mapコンテナです。
常にボブのいる位置は分かっているので、処理対象となるセルを割り出すのは簡単です。
ボブが衝突しているブロックの座標を取得するための簡単で楽な方法は、ボブの周りのセルを全て取得して、ブロックを持つタイルとボブの現在の境界ボックスが重なっているかを確認することです。
我々はボブの移動の制御も行っているので、ボブの向かっている方向や移動速度といった情報を参照できます。これを使って選択肢をさらに絞り込みます。
例えば、ボブが左に向かっている場合は以下のような状況が想定されます。
上記画像では、ボブと衝突しているかどうかを確認すべきオブジェクトの候補が二つあります。
重力は常にボブを下方向へ引っ張っているので、Y軸上のタイルは常に確認する必要があるということを忘れないでください。
垂直方向の速度の値が正か負かを基にして、ボブがジャンプ中なのか落下中なのかが分かります。
ボブがジャンプしている場合、上にあるタイル(セル)が衝突判定の候補になります。
垂直方向の速度が負の値の場合はボブが落下しているということなので、ボブの下にあるタイルを衝突判定の候補として選びます。
ボブが左を向いている場合(速度が0未満の場合)は、ボブの左側にあるタイルが衝突判定の候補となります。
ボブが右を向いている場合(速度が0より大きい場合)は、ボブの右側にあるタイルが衝突判定の候補となります。
水平方向の速度が0というのは、水平方向の衝突判定については気にする必要がないということです。
衝突判定と言うのは毎フレーム行い、全ての敵と弾丸とゲーム内で衝突し得るあらゆるエンティティに対して行う必要があるので、最適化する必要があるのです。
今回の場合はとても単純で、ボブは各軸上での動きが止められます。ボブの各軸上での速度に0を設定します。 これは、X軸とY軸を別々にチェックしている場合にのみ行えます。 水平方向の衝突を確認し、ボブが衝突していた場合は水平方向の移動を止めます。 垂直(Y)軸上でも全く同じことを行います。ただそれだけです。
衝突を確認する時は注意する必要があります。我々人間は行動する前に考える傾向にあります。
壁に直面した場合、そこに向かって歩いたりしません。壁を見て距離を測り、壁にぶつかる前に止まります。
あなたの目が見えない場合を想像してください。目以外の別のセンサーが必要です。
腕を伸ばして壁に触れた場合は、あなたは壁にめり込む前に止まるでしょう。
この動きをボブにも適用できますが、腕ではなく境界ボックスを使います。
まずはボブの速度に応じてボブの境界ボックスを移動させ、移動させた位置が壁に当たっているか(ボブの境界ボックスとブロックの境界ボックスが交差しているか)を確認します。
当たっている場合は、衝突が検知されます。
Bob might have been some distance away from the wall and in that frame he would have covered the distance to the wall and some more.
そうした場合は単純にボブを壁の隣の位置に置き、ボブの境界ボックスと現在の位置を揃えます。
また該当の軸上でのボブの速度を0に設定します。
以下の上記の説明を図に表したものです。
緑の四角は、現在ボブが立っている場所です。少しずれた位置にある青の四角は、次フレームでボブがいるであろう場所です。
紫の範囲はボブがどれだけ壁にめり込むかを表します。
ボブを壁の隣に立たせるには、この距離分ボブを戻す必要があります。
単純にボブの位置を壁の隣に設定すれば、煩わしい計算をしなくて済みます。
衝突判定のコードは、実際にとても簡単です。
このコードは全て BobController.java
に記述します。このコントローラについて説明する前に、他に説明しておく変更がいくつかあります。
World.java
は以下のように変更しています
public class World { /** Our player controlled hero **/ Bob bob; /** A world has a level through which Bob needs to go through **/ Level level; /** The collision boxes **/ Array<Rectangle> collisionRects = new Array<Rectangle>(); // Getters ----------- public Array<Rectangle> getCollisionRects() { return collisionRects; } public Bob getBob() { return bob; } public Level getLevel() { return level; } /** Return only the blocks that need to be drawn **/ public List<Block> getDrawableBlocks(int width, int height) { int x = (int)bob.getPosition().x - width; int y = (int)bob.getPosition().y - height; if (x < 0) { x = 0; } if (y < 0) { y = 0; } int x2 = x + 2 * width; int y2 = y + 2 * height; if (x2 > level.getWidth()) { x2 = level.getWidth() - 1; } if (y2 > level.getHeight()) { y2 = level.getHeight() - 1; } List<Block> blocks = new ArrayList<Block>(); Block block; for (int col = x; col <= x2; col++) { for (int row = y; row <= y2; row++) { block = level.getBlocks()[col][row]; if (block != null) { blocks.add(block); } } } return blocks; } // -------------------- public World() { createDemoWorld(); } private void createDemoWorld() { bob = new Bob(new Vector2(7, 2)); level = new Level(); } }
#09 – collisionRects
は、特定のフレームでボブが衝突しているrectangleを置くだけの単純な配列です。
これはデバッグ目的のためだけのもので、画面にボックスを表示します。削除できるものなので、ゲームが完成したら削除します。
#13 – 衝突判定用ボックスへのアクセスを提供するだけです
#23 – getDrawableBlocks(int width, int height)
は、カメラの枠内にあって画面に描写するBlock
オブジェクトの一覧を戻り値として返すメソッドです。
このメソッドでは、アプリケーションがパフォーマンスを落とすことなく大規模なゲーム内世界を描写できるように準備をします。
これは非常にシンプルなアルゴリズムです。ボブの周りの一定距離内にあるブロックを取得し、これらを戻り値として返して描写します。これは最適化と呼ばれる処理です。
#61 –#06行目で宣言したLevel
を作成します。
ゲームには複数の難易度を持たせたいので、level をworld クラスから外に出すと良いでしょう。
This is the obvious first step.
Level.java
は ここにあります。
以前に述べたように、実際の衝突判定は BobController.java
で行っています。
public class BobController {
// ... code omitted ... //
private Array<Block> collidable = new Array<Block>();
// ... code omitted ... //
public void update(float delta) {
processInput();
if (grounded && bob.getState().equals(State.JUMPING)) {
bob.setState(State.IDLE);
}
bob.getAcceleration().y = GRAVITY;
bob.getAcceleration().mul(delta);
bob.getVelocity().add(bob.getAcceleration().x, bob.getAcceleration().y);
checkCollisionWithBlocks(delta);
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);
}
private void checkCollisionWithBlocks(float delta) {
bob.getVelocity().mul(delta);
Rectangle bobRect = rectPool.obtain();
bobRect.set(bob.getBounds().x, bob.getBounds().y, bob.getBounds().width, bob.getBounds().height);
int startX, endX;
int startY = (int) bob.getBounds().y;
int endY = (int) (bob.getBounds().y + bob.getBounds().height);
if (bob.getVelocity().x < 0) {
startX = endX = (int) Math.floor(bob.getBounds().x + bob.getVelocity().x);
} else {
startX = endX = (int) Math.floor(bob.getBounds().x + bob.getBounds().width + bob.getVelocity().x);
}
populateCollidableBlocks(startX, startY, endX, endY);
bobRect.x += bob.getVelocity().x;
world.getCollisionRects().clear();
for (Block block : collidable) {
if (block == null) continue;
if (bobRect.overlaps(block.getBounds())) {
bob.getVelocity().x = 0;
world.getCollisionRects().add(block.getBounds());
break;
}
}
bobRect.x = bob.getPosition().x;
startX = (int) bob.getBounds().x;
endX = (int) (bob.getBounds().x + bob.getBounds().width);
if (bob.getVelocity().y < 0) {
startY = endY = (int) Math.floor(bob.getBounds().y + bob.getVelocity().y);
} else {
startY = endY = (int) Math.floor(bob.getBounds().y + bob.getBounds().height + bob.getVelocity().y);
}
populateCollidableBlocks(startX, startY, endX, endY);
bobRect.y += bob.getVelocity().y;
for (Block block : collidable) {
if (block == null) continue;
if (bobRect.overlaps(block.getBounds())) {
if (bob.getVelocity().y < 0) {
grounded = true;
}
bob.getVelocity().y = 0;
world.getCollisionRects().add(block.getBounds());
break;
}
}
bobRect.y = bob.getPosition().y;
bob.getPosition().add(bob.getVelocity());
bob.getBounds().x = bob.getPosition().x;
bob.getBounds().y = bob.getPosition().y;
bob.getVelocity().mul(1 / delta);
}
private void populateCollidableBlocks(int startX, int startY, int endX, int endY) {
collidable.clear();
for (int x = startX; x <= endX; x++) {
for (int y = startY; y <= endY; y++) {
if (x >= 0 && x < world.getLevel().getWidth() && y >=0 && y < world.getLevel().getHeight()) {
collidable.add(world.getLevel().get(x, y));
}
}
}
}
// ... code omitted ... //
}
これの全ソースコードは github 上にあり、私はそのドキュメント化を試みました、 but I will go through the important bits here.
#03 – collidable
配列では、各フレームでボブが衝突する可能性のあるブロックを保持します。
update
メソッドはさらに簡潔です。
#07 – 通常通り入力処理をし、ここでは何も変更していません。
#08 – #09 – ボブが空中にいない場合は、ボブの状態をリセットします。
#12 – ボブの加速度をフレーム時間に変換します。フレームはとても短く (通常は 1/60 秒)、フレーム内で1回この変換を行いたいので、この処理は重要です。
#13 – フレーム時間内での速度を計算します。
#14 – ここは衝突判定を行っている箇所なので強調しています。 I’ll go through that method in a bit.
#15 - #22 – このDAMPをボブに適用して停止させ、ボブが最大速度を超過しないようにします。
#25 – checkCollisionWithBlocks(float delta)
メソッドでは、このステージ内でボブがブロックに衝突しているかいないかに基づいて、ボブの状態と位置とその他パラメータを設定します。
#26 – 速度をフレーム時間に変換します
#27 – #28 – Pool を使用してボブの現在の境界ボックスであるRectangle を取得します。この rectangle はフレーム内でボブが移動する予定の位置に動かされ、衝突候補ブロックとの衝突確認を行います。
#29 – #36 – These lines identify the start and end coordinates in the level matrix that are to be checked for collision. The level matrix is just a 2 dimensional array and each cell represents one unit so can hold one block. Level.java
を確認してください
#31 – The Y coordinate is set since we only look for the horizontal for now.
#32 – ボブが左を向いているかどうかを確認し、向いている場合は左にあるタイルを判定対象とします。 The math is straight forward and I used this approach so if I decide that I need some other measurements for cells, this will still work.
#37 – 渡された範囲内のブロックをcollidable
配列に追加します。この場合、ボブの向きに応じて左にあるタイルか右にあるタイルのどちらかが追加されます。 また、その位置のブロックがない場合は結果はnullになるので注意してください。
#38 – ここではボブの境界ボックスのコピーを移動させます。 bobRec
の新しい位置は、通常の状況下においてボブがいるあろう位置になります。しかしX軸上の位置情報のみです。
#39 – world クラス内にデバッグ用のcollisionRects を定義したことを覚えていますか? これをクリアして、ボブが衝突しているrectangleを格納します。
#40 – #47 – ここは、X軸上で発生している衝突を実際に判定している箇所です。全ての衝突候補ブロック(今回の場合は1つ)に対して反復処理を行い、ブロックの境界ボックスとボブの移動後境界ボックスが交差するかを確認します。これには bobRect.overlaps
メソッドを使用します。このメソッドはlibgdxのRectangleクラスに実装されており、二つのRectangleが重なっている場合はt戻り値としてtrueを返します。重なって異な場合は、衝突が起こっているのでボブの速度を0に設定します (#43行目)。
そして 衝突している rectangle を world.collisionRects
に追加して、判定を break で抜けます。
#48 – X軸に関しては一旦置いておきY軸の衝突確認に移るので、境界ボックスの位置をリセットします。
#49 – #68 – 前と全く同じですが、この衝突判定はY軸上で行っています。#61 – #63行目に命令が一つ追加されており、ボブが落下中に衝突を検知した場合は grounded
に true
を設定します。
#69 – ボブの rectangle コピーをリセットします
#70 – ボブの新しい速度が設定され、これはボブの新しい位置を計算するのに使われます。
#71 – #72 – ボブの実際の境界位置を更新します。
#73 – 速度をユニットベースの尺度に戻します。これはとても重要です。
これでボブとタイルの衝突に関する説明は全て完了です。もちろん、さらにエンティティが追加されればこれは改良するでしょうが、今のところは問題ないでしょう。
false
WorldRenderer.java
にも少し追加した箇所があります。
public class WorldRenderer { // ... code omitted ... // public void render() { spriteBatch.begin(); drawBlocks(); drawBob(); spriteBatch.end(); drawCollisionBlocks(); if (debug) drawDebug(); } private void drawCollisionBlocks() { debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.FilledRectangle); debugRenderer.setColor(new Color(1, 1, 1, 1)); for (Rectangle rect : world.getCollisionRects()) { debugRenderer.filledRect(rect.x, rect.y, rect.width, rect.height); } debugRenderer.end(); } // ... code omitted ... // }
衝突が発生している時に白いボックスを描写するdrawCollisionBlocks()
メソッドを追加します。
これは見た目を面白くするためです。
これまでの作業結果は以下動画のようになります:
この記事では基本的な衝突判定について説明しました。次はゲーム内世界の拡張やカメラ移動や敵の作成や武器の使用や音声の追加をやりましょう。 全部重要なものなので、どれを先にやるかあなたの考えを教えてください。
このプロジェクトのソースコードはここ: https://github.com/obviam/star-assaultにあります
part4のブランチをチェックアウトする必要があります。
gitを使ってチェックアウトするには:
git clone -b part4 git@github.com:obviam/star-assault.git
また zip ファイルとしてダウンロードすることもできます。