ジャンプが二段になってしまう:着地判定の見直し
遊びをつくる執筆にClaudeを使用しています。

ジャンプボタンを素早く2回押したら、地面を離れた直後なのにもう一度跳べてしまう。二段ジャンプを作った覚えがないのに、そうなる場面を考えてみます。ジャンプ処理そのものを疑って「ジャンプ回数」の変数を足しても、直ったり再発したりします。原因がジャンプ側ではなく、「いま地面にいるか」という判定側にあるからです。 先に結論を述べます。意図しない二段ジャンプの多くは、「接地している」という状態が、跳んだ後も残る、あるいは空中で誤って立ち上がることで起こります。接地の寿命の管理が崩れているわけです。この記事では、崩れ方の典型パターンを整理し、判定の作り直し方を示し、手元の作品で原因を切り分ける手順までを扱います。 作品の構成は人によって違うため、ここでは特定の作品の内部仕様には触れません。ブラウザで動く2Dジャンプアクションを想定した、一般的な仕組みの解説です。
二段ジャンプの正体は「接地フラグの寿命」
多くのジャンプ実装は、次の二つだけでできています。
- ジャンプ入力があり、かつ接地していれば、上向きの速度を与える。
- 毎フレーム重力で速度を変え、地面に着いたら接地にする。
二段ジャンプは、この2番目の「接地にする」条件が緩いときに起こります。跳んだ直後の数フレームも接地と判定されれば、その間に入力が入るだけで2回目のジャンプが成立します。
具体的な数字で考えてみます。この記事のサンプルでは、初速を毎秒600ピクセル、重力を毎秒1800ピクセルの二乗、1フレームを60分の1秒としています。跳んだ最初の1フレームで進む距離は、計算上およそ9.5ピクセルです。このとき接地の許容幅(地面からこの距離以内なら接地とみなす幅)を12ピクセルにしていると、跳んだ直後も「まだ地面の近く」と判定されます。これは実測ではなくサンプルの数値から出した計算です。ですが、許容幅が1フレームの移動量より大きいと何が起こるかを示すには十分です。
許容幅を設けること自体は悪くありません。段差のふちで足元がわずかに浮いて見えるときの救済になります。問題は、許容幅を「上昇中」にも適用してしまうことです。
典型的な崩れ方と見分け方
二段ジャンプに見える症状は、原因ごとに現れ方が違います。症状から原因を逆引きできるよう、表にまとめます。
- 跳んだ直後だけ、もう一度跳べる、起きやすい原因:接地の許容幅が1フレームの移動量より大きい、見分け方:接地フラグを画面に表示し、ジャンプ直後に true のままか見る
- キーを押しっぱなしで跳ね続ける、連打で2回跳ぶ、起きやすい原因:keydown の自動リピートを入力として数えている、見分け方:押しっぱなしにして、ジャンプ回数のログを見る
- 壁に押し付けて跳ぶと登れる、起きやすい原因:接地判定が側面の接触も拾っている(向きを見ていない)、見分け方:壁に体を押し付けた状態で接地フラグを見る
- 下から薄い足場を抜けると空中でジャンプ回数が戻る、起きやすい原因:上昇中の接触を着地として扱っている、見分け方:足場の真下から上昇して、通過の瞬間のフラグを見る
- 猶予時間の実装後に二段になった、起きやすい原因:猶予(コヨーテタイムや入力バッファ)をジャンプ時に消費していない、見分け方:ジャンプ実行の直後に猶予の残り時間が0になっているか見る
- 端末によって二段になる、ならない、起きやすい原因:時間の刻みが端末のフレームレートに依存している、見分け方:固定ステップに変えて再現するか見る
用語を補足します。コヨーテタイムは、足場から離れた直後のごく短い間もジャンプを許す猶予のことです。入力バッファは、着地の少し前に押したジャンプ入力を短時間とっておき、着地した瞬間に実行する仕組みです。どちらも操作感を良くする工夫ですが、使った後に消さないと、そのまま二段ジャンプの入口になります。
直し方:接地を毎フレーム作り直す
直し方の方針は三つです。
- 接地は「前のフレームの結果を引き継ぐ」のではなく、毎フレームの衝突解決の最後に決め直します。
- 着地として扱うのは、下向きに動いている(速度が0以上の)ときだけにします。
- ジャンプを実行したら、猶予や入力の記憶を同じ瞬間に消します。
次のコードは、これらを入れた最小構成です。1つのHTMLファイルとして単独で動き、外部ライブラリは使いません。スペースキーでジャンプします。
<canvas id="c" width="320" height="180"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
const GROUND = 150, G = 1800, JUMP = -600, STEP = 1 / 60;
const p = { y: GROUND, vy: 0, grounded: true, coyote: 0.08, buffer: 0 };
let want = false;
addEventListener('keydown', e => {
if (e.code === 'Space') { e.preventDefault(); if (!e.repeat) want = true; }
});
function update(dt) {
p.buffer = want ? 0.1 : Math.max(0, p.buffer - dt);
want = false;
if (p.buffer > 0 && p.coyote > 0) { // ジャンプ実行
p.vy = JUMP; p.buffer = 0; p.coyote = 0; p.grounded = false;
}
p.vy += G * dt;
p.y += p.vy * dt;
if (p.vy >= 0 && p.y >= GROUND) { // 下向きのときだけ着地
p.y = GROUND; p.vy = 0; p.grounded = true;
} else {
p.grounded = false;
}
p.coyote = p.grounded ? 0.08 : Math.max(0, p.coyote - dt);
}
function draw() {
ctx.clearRect(0, 0, 320, 180);
ctx.fillRect(0, GROUND, 320, 2);
ctx.fillRect(150, p.y - 20, 20, 20);
ctx.fillText('grounded: ' + p.grounded, 8, 14);
}
let last = performance.now(), acc = 0;
function loop(t) {
acc += Math.min((t - last) / 1000, 0.1); last = t;
while (acc >= STEP) { update(STEP); acc -= STEP; }
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>直しのポイントは4か所です。
- e.repeat の確認で、キーの自動リピートを入力から外しています。
- p.vy >= 0 の条件で、上昇中の接触を着地として扱わないようにしています。
- ジャンプ実行の行で、入力の記憶(buffer)と猶予(coyote)を同時に0にしています。
- 固定ステップの繰り返しで、端末のフレームレートに左右されにくくしています。
キーが効かないときは、先に画面を一度クリックしてフォーカスを渡してください。ゲームはiframeの中で動くため、フォーカスがページ側にあるとキー入力が届かないことがあります。
自分の作品で原因を切り分ける手順
作品が大きくなると、どの原因か見当がつきにくくなります。次の順で、一度に一つだけ条件を変えて確かめてください。
- 状態を見えるようにする。 画面の隅に、接地フラグ、縦方向の速度、猶予の残り時間、ジャンプ回数を出します。ログより画面表示のほうが、跳んだ瞬間の変化を目で追えます。
- 再現手順を固定する。 「ジャンプを2回、何フレーム空けて押すと起きるか」を決めます。最初の押下から2回目までの間隔を、短いほうから少しずつ延ばして、二段が成立する範囲を探します。範囲が数フレームなら接地の許容幅や猶予、広く長いなら回数カウンタのリセット位置が怪しくなります。
- コマ送りか低速にする。 更新を1フレームずつ進められる仕組みを足すと、「跳んだ次のフレームで接地フラグが何になっているか」を直接確認できます。ここで true が見えたら、先の表の1行目です。
- 入力経路を分ける。 キーボード、タッチ、ボタンの経路ごとに、同じ操作で再現するか見ます。特定の経路だけなら、入力側の押下と離上の扱いに原因があります。
- フレームレートを変えて見る。 開発者ツールなどで動作を重くして、再現の有無が変わるか確認します。変わるなら時間の刻みが原因なので、固定ステップへの置き換えを検討します。
結果の読み方は単純です。ジャンプ直後に接地フラグが true なら判定側、false なのに跳べるなら入力や回数カウンタ側を探します。どちらも違うなら、別のコードがジャンプを二重に呼んでいます。
起こりやすい誤りと、投稿前の確認
直したつもりで再発する例も挙げておきます。
- 接地フラグを複数の場所で書き換えている。 衝突処理と足場の処理が別々に書き込むと、後から実行されたほうが勝ちます。接地を決める場所は1か所にまとめてください。
- 許容幅をなくして、ふちで落ちる感触が悪くなる。 許容幅は「下向きのときだけ」使うか、猶予時間として別に持てば、二段ジャンプを避けたまま救済できます。
- ジャンプの高さが可変の作品で、ボタンを離した処理が速度を上書きする。 離したときの速度の切り詰めは、上昇中のみ適用します。
アソバースに投稿する作品では、ブラウザ上の見え方で最終確認することになります。投稿は入力、表示の確認、公開の3段階で、下書き保存もできます。表示の確認の段階で、画面表示にしたデバッグ用の接地フラグを残していないか、キーボードとタッチの両方で跳んでみてください。サーバー用コードに頼る作りは対象外ですが、ここで扱った接地判定はブラウザの中だけで完結するため、問題なく確かめられます。
次の行動
まず、手元のジャンプ処理に接地フラグの表示を足し、跳んだ直後のフラグが何になっているかを確かめてください。true のままなら、着地を「下向きに動いているときだけ」に絞り、ジャンプ実行時に猶予と入力の記憶を消す2点から直すのが近道です。サンプルのコードを単独のHTMLとして開き、自作のコードと見比べてみてください。

