2Dプラットフォーマー実装ガイド

このページでは、2Dにおける Platformer (プラットフォーマー) (2D横視点のジャンプアクションゲーム)の作り方を解説します。

分類

2Dプラットフォーマーの実装パターンは、主に以下の4種類です。

  • タイルベース(ピュア)
  • タイルベース(スムース)
  • ビットマスク
  • ベクトリアル

ただし、要求される仕様によっては、複数の手法を組み合わせて作られることも良くあります(斜面を線分で表現するなど)。

手法概要
タイルベース(ピュア)移動をタイルサイズに制限したものプリンス・オブ・ペルシャ、ロードランナー
タイルベース(スムース)8bit / 16bitのアクションゲームを作る最もポピュラーな手法8bit / 16bit時代のスーパーマリオやロックマン、メトロイド
ビットマスク画像のピクセルサイズで衝突判定を行う手法
ベクトリアル衝突判定をベクトルで行うもの、物理エンジンBraid、Limbo

タイルベース(ピュア)とは

キャラクターの動きをタイルサイズ、Grid-based (グリッド制) に制限したものです。そのため、2つのタイルの中間にキャラクターが立つことはできません。下記画像は2Dプラットフォーマーではありませんが、タイルベース(ピュア)の概念がわかりやすい例です(倉庫番)。

倉庫番 (出典:ファルコン株式会社)

また、古典的なダンジョンRPGや Roguelike (ローグライクゲーム) なども、グリッド単位の移動しか許容していないものが多いです。

このシステムでは、プレイヤーキャラクターや、ブロックはグリッド単位での移動しかできません。
ですが、移動をタイルサイズに制限することによって、入力精度を求めるゲームではなくゲームのロジカルな部分に集中させやすいため、パズル的な要素を持つゲームに向いていると言えます(ロードランナーやプリンス・オブ・ペルシャなど)。

タイルベースのマップデータ

タイルベースのマップデータは2次元配列で用意します。例えば、0を通路、1を壁として以下のようなデータを定義します。

// マップデータテーブル
int map[5][5] = {
 { 1,1,1,1,1 },
 { 1,0,0,1,1 },
 { 1,1,0,0,1 },
 { 1,0,0,0,1 },
 { 1,1,1,1,1 },
}

このデータは、以下のような地形(マップ)をイメージしたものです。

タイルベースのマップデータ

問題となるのが、マップデータ上の座標系と実際にゲーム画面に表示する座標系が異なることです。
例えば、マップの1つのマス目のサイズが幅x高さが 32×32 だった場合、マップ上での (x, y) = (2, 3) にいるキャラクターの画面上での座標の計算は、

x = 2 * 32; // プレイヤーのX座標は、マス目座標Xとマスの幅をかけた 64
y = 3 * 32; // プレイヤーのY座標は、マス目座標Yとますの高さをかけた 96

で求まり、(x, y) = (64, 96) となります。

プレイヤーがマップを移動する場合には、こういった計算を相互に行い、そこに壁があったら進めない、という判定が必要になります。なので、「マップ座標系」か「ゲーム画面での座標系」のどちらでデータを扱っているのかを意識して作ることが大切となります。

ここではプログラム上にマップデータを定義しましたが、Tiled Map Editor のようなマップエディタで作成したデータを読み込むことができるようにすると、マップデータの作成が効率化できるようになります。

Tiled Map Editorの使い方

当たり判定の実装方法

まずは移動方向を決めます。たいていのゲームでは上下左右4方向への移動しか許可しません。

移動方向を決めたら、移動先のタイルをチェックします。もし移動先が障害物であれば何もしません。

移動可能であれば、移動元の座標(prev)から移動先の座標(next)へ線形補間で移動します。

座標 = prev + (next - prev) * 補完値[0.0~1.0]

※補完値は「経過フレーム数 ÷ 移動完了までの総フレーム数」
※移動完了後「座標 = next」しておかないと、座標にズレが発生する可能性があることに注意

タイルベース(スムース)とは

8bit / 16bit のほぼすべての2Dプラットフォーマーが採用している手法です。実装の自由度が高く、要求される技術もそれほど高くありません。またレベルエディタの構築も容易です。

当たり判定の実装方法

基本はタイルベース(ピュア)と同じです。ただしプレイヤーキャラクターの移動がタイルの制限を受けません。

斜面やOne-Way床を実装しないとすると、とてもシンプルです。

境界の判定方法

左上を原点(x, y)として、コリジョンの幅を width 高さを height とした場合に、判定を行うコリジョンの境界は以下のルールとなります。

移動方向キャラクターの境界障害物の境界
右移動右(x + width)左(x)
左移動左(x)右(x + width)
上移動上(y)下(y + height)
下移動下(y + height)上(y)

この衝突判定の方法は、トップビューのゲーム(ゼルダの伝説・ガントレットなど)でも有効です

ゼルダの伝説 (出典:Nintendo Co., Ltd.)

斜め移動を考慮したときの当たり判定

斜め移動を可能とした場合、少しややこしくなるので説明をします。

斜め移動の当たり判定

この問題の解決法として押さえておきたいのが、「X軸とY軸を分離して計算する」ということです。
斜め移動が存在することを前提としますが、斜め移動した後に衝突判定を行うと、壁へのめり込みが発生した場合、どちらに押し返すのか判定することが難しく、問題が起きることが多いです。具体的な問題としては、壁に沿って歩く「壁ズリ」を実装するのが難しくなります。

そのため、移動する前に、まずは「X軸方向に移動させてみて、めり込んだらめり込まない位置まで戻す」次に「Y軸方向に移動させてみて、めり込んだら押し戻す」という方法を使うと、たいていの衝突応答は問題なく処理できます。

斜め移動の当たり判定の実装方法

図では、Y軸方向のみ押し返していますが、X軸移動で衝突があった場合はX軸方向に押し戻しをすることが必要となります。

なお、Unityのように物理エンジンで衝突応答を行う場合はこのような制御は不要ですが、物理エンジン特有の問題に注意する必要があります。

One-Way床

One-Way床(一方向プラットフォーム)は下からすり抜けて登れる床のことです。

One-Way床

One-Way床の実装方法

実装方法は以下のとおりです

  1. X軸方向の移動では衝突判定は不要です
  2. 判定はY軸方向のみ行います
  3. Y軸方向で移動量が正(すなわち落下中)の場合のみ、衝突判定を行います
  4. 衝突していたら、足下の座標が床の上になるように押し戻し処理を行います

基本は上からの衝突のみ有効にするです。つまり、左右からの衝突を判定せず、プレイヤーの移動が上昇中(Y方向の速さがマイナス)の場合は衝突判定をしないという実装となります。

問題は床側のコリジョンの高さを大きくすると、床の下半分の部分で衝突した際に、キャラクターの押し戻し距離が長くなるため、ワープしたかのような挙動となります。

One-Way床の押し戻し

これを回避するには、コリジョンを小さくする(縦方向に抜けが発生しないよう対策が必要)、押し戻し距離を制限する(一定の距離以上は押し返せないようにする)、などの処理が必要となります

余談ですがコリジョン抜けの対策には、移動量のベクトルと地形をベクトルで表現して、線分の交差判定により衝突をチェックする方法があります。

すり抜け防止方法

飛び降りの実装

多くのゲームではOne-Way床に乗っている状態で「下キー」または「下キー+ジャンプボタン」で下へ降りることができます。

One-Way床からの飛び降り

これを実装する方法は以下のとおりです

  1. 飛び降りを開始したときに「飛び降り有効状態」にします
  2. 「飛び降り有効状態」の場合は、床との衝突判定を無効にします
  3. 一定フレーム経過(16フレームなど)後、「飛び降り有効状態」を無効にします

降りる操作(下+ジャンプなど)による落下開始後の一定フレームの間は衝突判定をしない、という方法で実装できます。

移動床

移動床は動く足場で、上に乗っているキャラクターを動かします。

移動床

移動床は以下のように実装します

キャラクターが移動床の上に乗った際に、キャラクターにその移動床の参照を持たせます
キャラクターの移動ステップの前に、移動床の座標の変化量を求めます
座標の変化量 = 現在の座標 - 前フレームの座標
  1. 変化量をキャラクターの座標に加算します
  2. キャラクターの移動ステップは通常通りに行います
キャラクターが床から離れたときに、移動床の参照を解除します

基本的な機能は、移動床の上に乗っていたら移動床の動きに合わせてプレイヤーも動かす、という方法で実装できますが、特殊な場面の対応が難しいです。

  • 壁と移動床にプレイヤーが挟まれたらどうする?
  • 移動床が地面をすり抜ける際に、プレイヤーが移動床に乗っていたらどうなる?
  • 移動床同士が交差した場合、その上に乗っているプレイヤーはどうなる?

といった場面の衝突応答の解決は難しいため、「移動床に挟まれたら死亡する」「壁の近くには移動床を置かない」「移動床は地面と交差しないようにする」などの仕様や配置上の制約を加えて、問題を未然に防ぐのが楽で良いです。

ハシゴ

ハシゴの実装は、以下の制約で運用すると実装が楽です。

実装が楽なハシゴ

逆に難しくなるのが以下の仕様です。

実装が難しいハシゴ

斜面

斜面は段差をスムーズに登ったり下ったりできる地形です。

斜面

斜面の床」の実装は、X方向から衝突した際に上方向に押し出す、という衝突応答で実装できます。

斜面の実装

問題となるのが、上方向にどれだけ持ち上げるかですが、最初は45度の斜面だけにしてみるのが良いです。

45度の斜面

45度の斜面は、横幅と高さが同じ距離となるため、Y方向への持ち上げも同じ距離となって計算が簡単です。(正確には斜め移動なので、移動距離が√2 倍になってしまいますが、多くの場合は誤差として問題ないです)

例:右上がりの45度の斜面の実装例

// 右上がりの45度の斜面での押し上げ
int right;       // プレイヤーの右側
int right_slope; // 斜面の右側

// プレイヤーの右側から斜面の右側への距離
int dist = right_slope - right;

// 押し出す量を決める (右上がりの45度なので距離の逆)
// 斜面の幅 - 距離
int d = slope.width - dist;

// プレイヤーを押し上げる
player.y += d;

22.5度斜面

45度だと傾斜がキツイのが気になる場合は、22.5度斜面を用意します。

22.5度斜面

45度斜面の半分の傾斜角度の地形を用意することで、X方向に移動に対して、Y方向は半分の値持ち上げるだけとなり、計算が楽です。
(なお、正確には底辺が2で高さが1となる直角三角形は atan2(1, 2) ≒ 26.565度なのですが、わかりやすく22.5度という用語を使うようにしました)

滑らかに斜面を繋ぎたい場合は、図のように22.5度斜面の後半マップチップを用意することで、スムーズに移動できるようにします。

下り坂の対応

下り床はそのまま横移動だけすると、ガクガク降りる動きとなります。これをスムーズに坂を下るようにするには、プレイヤーに斜面に接地しているフラグを保持するようにし、その場合の横移動は斜め下に移動する、というやや強引な方法で対処します。

下方向への移動距離を大きくしすぎると斜面の切れ目から落下した場合、ガクンとプレイヤーが沈みすぎてしまいます。この問題の解消方法としては、下り斜面の最後は平面の地形とつなぐルールで運用して回避するのが楽です。

下り坂の注意点

また急な傾斜の下り坂は、ガクガク降りても仕方ない、と割り切るのも一つの解決法です

スムースタイルの補足

タイルベース(スムース)は、衝突判定をタイルマップにより定義しますが、キャラクターがワールド内を自由に(通常整数値のピクセル単位で)移動できます。これにより、弧を描くジャンプや斜面の実装が可能となります。

キャラクターの当たり判定は、AABB(軸並行の矩形)で行います。キャラクターは1つか複数のタイルで構成されます。

  • 1タイル: 小さいマリオ・モーフボール状態のサムス
  • 2タイル: 大きなマリオ・ロックマン・しゃがみ状態のサムス
  • 3タイル: 立ち状態のサムス
ロックマンX (出典:CAPCOM CO., LTD.)

快適なゲームプレイを行うため、キャラクターの当たり判定はタイルにぴったりのサイズではないこともよくあります。例えば、上記のX(ロックマンXのプレイヤーキャラクタ)のように見た目よりも当たり判定を小さくすることが良くあります。

ビットマスクとは

画像のピクセルで衝突判定をおこなう手法。ダイナミックに変化する地形を表現することができます。
しかし、ピクセル単位の当たり判定を行うので処理負荷が高く、それを高速化するには高い技術を要求されます。またレベルエディタの構築も難しいです。

ベクトリアルとは

衝突判定の境界にベクトルデータ(ポリゴン・ライン)を使う方法。ビットマスクと同様にダイナミックな地形を表現することが可能です。ただし高い技術を要求され、自作する場合はレベルエディタを作るのも大変です。

実装方法

線分の交差判定や、ポリゴンの内外判定などを使用して実装する。もしくはBox2Dなど2D物理エンジンを使います。

Unityの衝突システムが、PhysX(フィジックス)という物理エンジンを採用していて、簡単に物理エンジンを使えるようになっています。そのため、2Dプラットフォーマーを物理エンジンで実装するためには最適な環境です。

物理エンジンで作る場合の注意点

物理エンジンを使った2Dプラットフォーマーはダイナミックな挙動を簡単に作ることができます。しかし、いくつかの問題点があります

移動床

移動床の上にトリガーコリジョン(Sensor)を配置して、上にプレイヤーが乗っているかどうかをチェックします。

上に乗っていたらプレイヤーの参照を保持し、更新ステップで移動量の増分をプレイヤーの座標に足し込みます(試していないですが、プレイヤーの下にSensorを配置しても実装できるかもしれません)。

タイルベースのマップを物理エンジンで動かす場合

タイルベースで作る場合、矩形のコリジョンをそのまま並べれば良いかと思うかもしれませんが、大量のコリジョンが作られるためパフォーマンス的に問題があり、かつ計算誤差によるがたつきが発生します。具体的には、矩形コリジョンが並んだ上をプレイヤーが歩くと、並んだコリジョンのスキマにひっかかりが発生し動かなくなります。

その問題を回避するには、以下のようにプレイヤーの足下に円コリジョンを配置します(左右も同様の問題が発生するため、円を配置します)。

プレイヤーコリジョンの設定例

もしくは実行時にシェイプを統合してつなぎ目をなくすのも良いかと思います。

シェイプを統合してつなぎ目をなくす

まとめ

それぞれのメリットと実装難易度、レベルエディタとの連携についてまとめました。

手法メリット技術力レベルエディタ
タイルベース(ピュア)パズルゲーム向きかんたんフリーのツールでも流用化
タイルベース(スムース)アクション全般で有効。自由度が高いふつう流用化だが実装するギミックによる
ビットマスク柔軟なデザインが可能難しいほぼ自作するしかない
ベクトリアル柔軟なデザインが可能難しい(物理エンジンの適用範囲なら「ふつう」)ほぼ自作するしかない (※)

Unityの場合は、エディタの機能やアセットを使うことでレベルエディタの手間を減らすことができます。

2022.8.14 追記:「コヨーテタイム」と「ジャンプバッファリング」

2Dプラットフォーマーを遊びやすくする工夫を Celeste の開発者が解説しているツイートがあります。

こちらゲーム用語集にまとめておいたので、参考にしても良いかもしれません

参考資料

スーパーマリオメーカー2

スーパーマリオメーカー2には大量のアップグレードやギミックが実装されていて、ギミックの資料や実装例のサンプルとなります。なので、ギミックやアップグレードの仕様に悩んだときにかなりおすすめです。