ブロックとの衝突
マップ(ステージ)を作ります。マップにはブロックがありプレイヤーとぶつかるようにします。
マップの作成
まずはマップを作ります。基本的にRPG編のお城を建てると同じです。RPGでは上から見たマップ(鳥瞰図)ですが、横スクロールアクションでは横から見たマップです。横スクロールアクションでは横に進んでいきます。
Mapクラスを見てください。mapという配列にマップを格納しています。0が何もない場所、1がブロックです。目をこらして見ると何となくブロックに囲まれた部屋が見えるでしょう?
// マップ private int[][] map = { {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} };
マップを描くメソッドが、draw()です。
/** * マップを描画する * * @param g 描画オブジェクト */ public void draw(Graphics g) { g.setColor(Color.ORANGE); for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { // mapの値に応じて画像を描く switch (map[i][j]) { case 1 : // ブロック g.fillRect(tilesToPixels(j), tilesToPixels(i), TILE_SIZE, TILE_SIZE); break; } } } }
ブロックがある場所のみミカン色のブロックを描画しています。それ以外のところは何も描画しません。tilesToPixels()は文字通りタイルの位置をピクセル単位に直します。お城を建てるではj*CSのように書いていましたがメソッドを使うように変更したわけです。
当たり判定
次にプレイヤーとブロックとの当たり判定(衝突処理)を実装します。この部分は結構悩みました。はじめは、インベーダー編の衝突判定のようにプレイヤーと弾のRectangleが重なっているかを調べるという簡単な方法をとろうとしたのですがこれではなかなかうまくいきませんでした。理由は2つあって
- プレイヤーがブロックのどっち側から衝突したかを知る必要があるから
- ブロックにめり込まないように位置調整をする必要があるから
です。たとえば、プレイヤーがブロックに下から当たった場合と上から当たった場合は区別しないといけません。下から当たった場合はにはジャンプの上向き速度を0にする必要があります。それ以上、上には移動できないからです。また、上からあたった場合は着地した(onGround=true)状態に変更する処理が必要です。インベーダーのプレイヤーと弾との衝突処理のように当たったか当たらないかだけの判定では不十分です。
もう1つは、ブロックにめり込まないように微妙な位置調整が必要になるからです。プログラムでは移動の細かいニュアンスが出せるようにプレイヤーの位置や速度はdouble型で表現しています。こうすると、速度の値によっては移動先が壁の中になってしまう場合があります。あまりにも痛々しいので壁にめり込まないように微妙な位置調整してやる必要があります。
そういうわけで例のごとく『Developing Games in Java』のコード例を参考にしました。Playerクラスのupdate()を見てください。
public void update() { // 重力で下向きに加速度がかかる vy += Map.GRAVITY; // x方向の当たり判定 // 移動先座標を求める double newX = x + vx; // 移動先座標で衝突するタイルの位置を取得 // x方向だけ考えるのでy座標は変化しないと仮定 Point tile = map.getTileCollision(this, newX, y); if (tile == null) { // 衝突するタイルがなければ移動 x = newX; } else { // 衝突するタイルがある場合 if (vx > 0) { // 右へ移動中なので右のブロックと衝突 // ブロックにめりこむ or 隙間がないように位置調整 x = Map.tilesToPixels(tile.x) - WIDTH; } else if (vx < 0) { // 左へ移動中なので左のブロックと衝突 // 位置調整 x = Map.tilesToPixels(tile.x + 1); } vx = 0; } // y方向の当たり判定 // 移動先座標を求める double newY = y + vy; // 移動先座標で衝突するタイルの位置を取得 // y方向だけ考えるのでx座標は変化しないと仮定 tile = map.getTileCollision(this, x, newY); if (tile == null) { // 衝突するタイルがなければ移動 y = newY; // 衝突してないということは空中 onGround = false; } else { // 衝突するタイルがある場合 if (vy > 0) { // 下へ移動中なので下のブロックと衝突(着地) // 位置調整 y = Map.tilesToPixels(tile.y) - HEIGHT; // 着地したのでy方向速度を0に vy = 0; // 着地 onGround = true; } else if (vy < 0) { // 上へ移動中なので上のブロックと衝突(天井ごん!) // 位置調整 y = Map.tilesToPixels(tile.y + 1); // 天井にぶつかったのでy方向速度を0に vy = 0; } } }
基本的な方針をまとめると
- X方向の衝突とY方向の衝突は別々に考える。
- プレイヤー位置に速度を足して移動先の座標を求める。
- 移動先にあるブロックの座標を取得する。
- ブロックがなければ移動する(移動先の座標をプレイヤー位置にセット)。
- ブロックがある場合はプレイヤーの左右上下のどの方向で当たっているか調べる。これは簡単。たとえば、右へ移動している最中にぶつかったのなら右にあるブロックと当たっているとすぐわかる。
- ブロックにめり込まないように位置調整。当たっているブロックの座標はわかっているのでブロックにぴったり接触するようにプレイヤー位置を調整する。
- ブロックに上から接触した場合は着地したことにする。
- ブロックに下から接触した場合は上向き速度を0にする。
図で書くとこうです。右へ移動中なのでX方向の移動と衝突だけ考えてます。ぶつかったブロックの座標は(tile.x, tile.y)とわかっているのでブロックにぴったり接触するようにプレイヤー位置を調整しています。
動先でどのブロックと衝突するかを調べるためにMapクラスのgetTileCollision()があります。これは『Developing Games in Java』のコードをそのまま使ってます。このメソッドはプレイヤーが標準的なタイル(16×16)より大きい場合も想定して作られています。この点を注意して解読してみてください。具体的な数字を入れて処理を追ってみると何をしているかよくわかります。
最後に重力(GRAVITY)はMapクラスに移動しました。マップによって重力を変えられるようにしています。たとえば、海ステージは重力を小さくしたいですよね?