マガジン

ゲームの物理を自作する:重力・ジャンプ・摩擦の基本式

遊びをつくる

執筆にClaudeを使用しています。

プリズムの光、紙ひこうき、植物の芽を並べた実験机。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

ゲームの物理エンジンは「難しそう」と後回しにされがちですが、プラットフォーマーやアクションゲームで必要な動きのほとんどは、3つの要素で構成されています。重力・ジャンプ・摩擦、この3つさえ自分で書けるようになれば、既製のライブラリに依存せず、自分のゲームに合った「気持ちいい動き」を調整できるようになります。 この記事では、ブラウザゲームをJavaScriptで作る前提で、各要素の計算式と実装の考え方を順に説明します。物理の専門知識は必要ありません。「なぜそう書くか」がわかれば、数値をいじるだけで自分のゲームに合わせられます。

この記事の目次
  1. 重力は「毎フレーム速度に足す加速度」で表現します
  2. ジャンプは「速度に一度だけ負の値を与える」操作です
  3. 摩擦は「速度に係数を掛けて減衰させる」処理です
  4. 地面判定:3つの要素をつなぐ土台
  5. よくある失敗とその対処
  6. 次の行動
  7. 参考資料

重力は「毎フレーム速度に足す加速度」で表現します

物理の教科書では重力は「9.8 m/s²」ですが、ゲームにそのまま使う必要はありません。ゲームの重力は「毎フレーム、落下速度をどれだけ増やすか」という加速度の数値として定義します。

基本的な考え方はこうです。キャラクターには「縦方向の速度(velocityY)」という変数を持たせます。毎フレームの更新処理でこの変数に重力加速度を加算し、その速度をキャラクターのY座標に加算します。

velocityY += gravity;   // 加速度を速度に累積
y += velocityY;         // 速度を位置に反映

gravity の値はゲームによって異なります。一般的なピクセル単位の2Dゲームでは 0.3 から 0.8 程度が自然に見えることが多いです。値が大きいほど「重くてズシンとした」落下になり、小さいほど「ふわっとした」落下になります。

注意点は「落下速度に上限(ターミナルベロシティ)を設けること」です。フレームをまたいで加速し続けると、長い落下で壁や床を突き抜ける問題(トンネリング)が起きます。velocityY = Math.min(velocityY, maxFallSpeed) のように上限を設定しておきましょう。

ジャンプは「速度に一度だけ負の値を与える」操作です

ジャンプの仕組みは、重力を理解すれば自然に導けます。Y軸の正方向を「下」とする座標系では、上方向への移動は負の速度です。ジャンプボタンを押した瞬間に velocityY に大きな負の値を代入するだけで、ジャンプが実現します。

if (jumpPressed && isOnGround) {
    velocityY = -jumpForce;   // 上方向への初速を与える
}

jumpForce の値が大きいほど高く飛びます。その後は毎フレームの重力加算が働くため、自動的に弧を描いて落下します。

ジャンプの調整で使われる2つのテクニック

可変ジャンプ高さ(ボタンを押し続けると高く飛ぶ)

ジャンプボタンを離した瞬間、velocityY が一定値より小さければ(上昇中であれば)速度を小さな値に切り詰めます。これにより、短押しで小ジャンプ、長押しで大ジャンプが実現します。

if (!jumpPressed && velocityY < -minJumpVelocity) {
    velocityY = -minJumpVelocity;
}

コヨーテタイム

足場の端を歩き過ぎても、一定フレーム内(たとえば6〜10フレーム)はジャンプを受け付ける猶予です。isOnGround の代わりに「最後に地面にいたフレームからの経過フレーム数」を使って判定します。これだけでプレイ感が大きく改善されます。

摩擦は「速度に係数を掛けて減衰させる」処理です

横方向の動きに摩擦を実装する最も単純な方法は、毎フレーム水平速度(velocityX)に1未満の係数を掛けることです。

velocityX *= friction;   // frictionは0.8〜0.95程度

係数が 0.8 なら毎フレーム20%ずつ速度が落ちます。0.95 に近いほど滑りが長く続きます。地面の種類によって係数を変えると(氷の上では0.98、砂地では0.7など)、表面ごとの質感を出せます。

地面と空中で摩擦を切り替える

空中でも同じ摩擦係数を使うと操作感が不自然になります。地面にいるときは摩擦大、空中では摩擦小(または0)にするのが基本です。

if (isOnGround) {
    velocityX *= groundFriction;   // 例:0.85
} else {
    velocityX *= airFriction;      // 例:0.98
}

入力がないときだけ摩擦を適用し、入力中は摩擦を切る実装もあります。これは「止まるのは速いが、動き続けるときは軽快」という感触を作りたいときに有効です。

地面判定:3つの要素をつなぐ土台

重力・ジャンプ・摩擦の切り替えはすべて「今地面の上にいるか」という判定(isOnGround)に依存しています。実装が雑だと、空中でジャンプできたり、壁に吸い付いたりする問題が出ます。

最も単純な方法は「キャラクターのY座標が地面のY座標以上になったらフラグを立て、速度をゼロにする」ことです。

if (y + height >= groundY) {
    y = groundY - height;   // めり込みを補正
    velocityY = 0;
    isOnGround = true;
} else {
    isOnGround = false;
}

タイルマップや複数の床オブジェクトがある場合は、各オブジェクトとの衝突判定ループの中でこの処理を行います。「前フレームで地面の上にいたか」「どの方向から衝突したか」を管理すると、より堅牢になります。

数値調整の順番

実装後に「なんかおかしい」と感じたときは、次の順番で調整すると効率的です。

  1. gravity を調整して落下の「重さ」を決める
  2. jumpForce を調整してジャンプの高さを合わせる
  3. groundFriction で止まるときの滑り感を調整する
  4. maxFallSpeed で天井落下時のトンネリングが出ないか確認する
  5. コヨーテタイムと可変ジャンプを加えてプレイ感を仕上げる

この順番を守ると、一つの変数が他の変数の見た目に影響を与えるループを避けられます。

よくある失敗とその対処

フレームレートで動きが変わる

velocityY += gravity をそのまま書くと、60fpsと30fpsでゲームの動きが変わります。gravity に経過時間(デルタタイム)を掛けて補正するか、固定タイムステップ(requestAnimationFrameを60fps固定で動かす)を使うと再現性が上がります。

ジャンプ中に壁に触れると貼り付く

水平方向の衝突判定と垂直方向の衝突判定を分けて処理していないと起きます。X方向の移動・衝突解消を先に行い、次にY方向を処理する「軸分離法」で対応できます。

摩擦で完全に止まらない

係数を掛け続けると速度が0に限りなく近づきますが、数学的には0になりません。Math.abs(velocityX) < 0.01 のような閾値を設けて、そこで強制的に0にすると処理もきれいになります。

次の行動

まずは重力と地面判定だけを実装した最小限のキャンバスを作り、gravity と jumpForce の数値を変えて動きの違いを体感してみてください。物理の「感触」は数値を動かして初めてわかります。動きが整ったら、完成したゲームをHTMLまたはZIPファイルにまとめてアソバース(https://aso-verse.com)に投稿すると、他のプレイヤーに実際に試してもらえます。

参考資料

  • MDN Web Docs「requestAnimationFrame」
  • MDN Web Docs「Canvas API」