ファミコンの横スクロールマリオの挙動をUnityで作ってみました。Physicsに全ておまかせ・・・というわけにはいかず、思っていたよりも大変です(笑)ということで、今回はそのレポートを書いてみます!
今回の記事では、Unityでマリオの挙動を作るのに必要な項目を「ジャンプ編」「衝突判定編」「アニメーション編」「横スクロール編」「入力デバイス編」の5つに分けて紹介していきます。
ジャンプの挙動編
マリオのジャンプは普通のジャンプとは異なる点が3つあります。
- ジャンプボタンを押し続けると、ジャンプの高さが変わる
- ジャンプの軌跡は放物線ではない
- 空中で左右キーを押すと移動できる
なんか、これだけでもう大変そう・・・(笑)1つずつ、見ていくことにしましょう。
ジャンプボタンを押しっぱなしにしたときの挙動
2Dマリオでは、ジャンプボタンを押しっぱなしにしたときジャンプの高さが変化します。といっても、ボタンを押しているあいだ、常に上方向に加速度がかかり続けるわけではなく、放物線の頂点が変わるイメージです。
ジャンプに使う放物線の方程式は次のものを使いました。横軸(x軸)はフレーム、縦軸(y軸)はピクセルです。
図にするとこんな感じ。要するに最大37フレームで62ピクセル上昇するということです。
ジャンプボタンが押されているあいだは、上の方程式に従ってマリオを移動します。
ジャンプ後、落下の軌跡
マリオのジャンプは物理法則に従った挙動にはなっていません(ボタンを押している間、ジャンプし続ける時点で超人ですが。笑)特にジャンプしたときの軌跡が放物線にならないのが特徴的です。
次の図のように上昇速度と下降速度が異なります。ジャンプするときの重力と落下するときの重力が変化する、といったほうが分かりやすいかもしれません。
そこで、ジャンプボタンが離されたら、通常の重力計算に切り替えます。落下速度の計算は次の式を使います。
落下速度 += 重力加速度 マリオのy座標 += 落下速度
重力加速度はで、速度は以下はクリップします。この辺の値は、見た目で微調整しています。
空中で移動できる
マリオは空中で十字キーを押すと、矢印方向に移動できます。かなり意味不明ですね(笑)ただ空中移動の実装は簡単で、マリオの速度ベクトルに左右方向の加速度を加えるだけです。
当たり判定編
ジャンプにPhysicsを使っていないので、当たり判定も全てPhysicsにおまかせ!という訳にはいきません。といっても矩形同士の当たり判定を全て自前でやるのは結構大変です。
ということで、ここではフレーム毎に上下左右の4方向にRayを飛ばして当たり判定をすることにします。
UnityでRayを飛ばすにはRaycastメソッドを使います。使い方は次のような感じになります。
RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up, 0.5f); if (hit.collider != null) { //上方向に障害物があった場合 }
上方向の衝突判定
上方向に当たった場合、ブロックに当たったので、ジャンプをやめて落下に切り替えます。落下に切り替える際にはy方向の速度は0にします。
横方向の衝突判定
左右方向に当たった場合は、プレイヤのX方向の速度ベクトルを0にしています。これでジャンプ中に横からブロックに当たった場合は、壁に沿って滑りながら進むことになります。
めり込み対策
また、ブロックに当たった場合はめり込みを押し戻しています。めり込みを押し戻す方法は、全てのブロックが1m単位のグリッド上に乗っていることを利用して、次の式で計算しています(かなり手抜きです。笑)
プレイヤの位置=ブロックの位置±1
この方法で大体うまくいくのですが、土管だけは2m x 2mの大きさなので、この大きさのBox Colliderを使うと押し戻しの計算が正しくできません。
タグを使って処理を分けてもよいのですが、今回は1m x 1mの大きさのColliderを4つ配置して対応しています。これだとスクリプトで処理を分けなくてもよくて、様々な形の障害物にも対応できるので便利です!
真面目にPhysicsを使わずに当たり判定をしたい場合は「ゲームプログラミングのためのリアルタイム衝突判定」が役に立ちます。
アニメーション編
プレイヤのステートは次の3つを実装しています。本当は急に走る方向を変えた場合のスリップ状態もあるのですが・・・勝手に省略しました(笑)
- Idle
- Run
- Jump
次のようにIdleとJumpは1枚絵、Runは3枚のパラパラ漫画で作ります。
アニメーションの切り替えはAnimatorを利用して制御しています。次のように「Idle」「Run」「Jump」のステートを作り、プレイヤの状態によってアニメーションを切り替えます。
「Idle」と「Run」の状態はプレイヤの横方向のスピードによって切り替えます。スピードが0.005ピクセル/フレーム以上だったら「Run」に切り替え、0.01以下だったら「Idle」に切り替えています。
このように、2つの閾値を変えてヒステリシスを持たせることで、アニメーションがパタつくのを防ぐことができます。
また、ジャンプ制御はbool値で行います。ジャンプボタンが押されたときにフラグを立てて、地面に着地したときにフラグを折ります。これで、ジャンプ中はジャンプ用のスプライトが表示されます。
Animatorの考え方や詳しい使い方は「Unityの教科書」でも紹介しているので、ぜひ参考にしてみて下さい!
横スクロール編
プレイヤの移動にあわせてカメラを動かすとき、単純に「カメラのX座標=プレイヤのX座標+offset」のように実装するとカメラがスムーズにスクロールせず、ガタガタしてしまいます。
これはプレイヤを動かすスクリプトとカメラを動かすスクリプトの実行順序が保証されていないことが原因です。順番に呼ばれないと、プレイヤが移動する前にカメラを移動してしまい、カメラの移動速度が一定にならないため、ガタガタして見えてしまいます。
Unityではメニューバーから「Edit」→「Project Settings」→「Script Execution Order」という機能を使ってスクリプトを実行する順序を指定できます。
今回は、プレイヤの移動→カメラの移動という順番になるようにスクリプトの実行順序を指定しました。
コントローラ入力編
最後に、作成したマリオをキーボードだけではなく、ゲームパッドでも操作できるようにします。ゲームパッドの入力を取得するにはInput Managerを使います。
Input Managerは簡単に言うと、キー入力やゲームパッドの入力、タッチパネルからの入力に名前をつける仕組みです。次の例では「Aキー」と「十字キーの左ボタン」のどちらにも「Left」という名前をつけています。
Input Managerを使うことで入力デバイスを仮想化できるため、異なる入力デバイスでもスクリプトからは同じ名前で扱うことが出来るようになるのです。
今回のゲームでは次のようにキーを割り当てました。
- 十字キー:左右の移動
- Aボタン:ジャンプ
- Bボタン:ダッシュ
Input Managerからの入力は、GetButtonDownメソッドやGetAxisメソッドなどを使って取得できます。
void Update () { if(Input.GetButtonDown("Left")){ Debug.Log("Left Key Pressed"); } }
Input Mnagerに関しても、次の記事で使い方を紹介しているので、参考にしてみて下さい。
まとめ
Unityを使って2Dマリオの挙動を目コピしてみました。敵の挙動やキノコによるパワーアップなど、まだまだ未完成ではありますが、ジャンプの挙動などマリオをマリオたらしめている重要な要素は網羅できたかと思います。