← ラボノート

計測が正したジャンプ:直感を実測で修正する

紙上で導いたジャンプは、エンジンが生み出すジャンプと一致しませんでした。5タイルの教習用の隙間は、教えるはずのダッシュを必要としていませんでした。失敗を許すための二つの復帰用足場は階段になり、教訓そのものを消していました。

三つの失敗は、制作したレベルに対して実際のティック順序をシミュレーションして初めて見えました。研究記事は移動やレベルの値を一つも決めないまま終わっていましたが、その最小限の完成版をSkyline Runとして公開しました。

教訓は単純です。離散的なゲームでは、計測は設計の一部です。計算はルールを提案できますが、そのルールが本当に成り立つかを示せるのは、実際に動くシステムだけです。

Skyline Runの白いプレイヤーキャラクターが、シアンの空中ダッシュの軌跡を残して6タイルの屋上の隙間を越え、計測したジャンプとレベルの値がその場面を囲んでいます。
最初のプレイ可能な一部分は、すべての移動値をレベル内の具体的な教訓に結び付けられるほど小さくしています。

このサイトとAI Maker Labチャンネルは私が所有し、制作しています。これは制作記録であり、第三者による独立したレビューではありません。

チェックリストは実行可能な契約になった

A1からC5までの13の問いは、検討を促す項目ではなく、設計記録になりました。A1はゲームを一文に絞りました。走り、ジャンプし、ダッシュして、一つのネオン街区の屋上にある出口へ到達する。危険物に触れるか落下すると、最新のチェックポイントへ戻る。高精度プラットフォーマーが約束の中心にあるため、正確な移動、原因を特定できる失敗、素早い再挑戦を優先します。

A2では、DT = 1/60の60 Hzで実行を固定しました。エンジンは純粋でDOMに依存せず、step(state, input, level)は新しい状態を返し、引数を一切変更しません。A3とA4はtitle → playing ⇄ paused → completeとなり、死亡は独立したシーンへ昇格させず、プレイ中の18ティックの入力ロックと、それに続くチェックポイントからの再開として扱いました。

B1では、位置、速度、接触、能力カウンター、SP、チェックポイントの進行、死亡回数、ティック、前回の入力、カメラの状態、通知を、一つの信頼できる状態へまとめました。B2とB3では、軸平行境界ボックス(AABB)、つまり回転しない衝突判定用の箱を使って軸ごとに分離する衝突判定、上から落下しているときだけ遮る一方向の足場、ゴール、チェックポイント、危険物の順で処理するトリガーの優先順位を定めました。クリア中と死亡中は入力をロックします。リトライしても時間、死亡回数、最新のチェックポイントは維持されます。

B4では、入力を計測可能な契約にしました。ジャンプを押す、長押しする、離すという操作には、それぞれ異なる意味があります。足場を離れた後も数フレームだけジャンプを受け付けるコヨーテタイムと、ジャンプバッファのカウンターは6ティックと7ティックですが、実装された処理順では、実効ウィンドウはいずれも5ティックです。境界をまたぐ1〜5ティックの入力は発動し、6ティック以上では発動しません。Swinkからは操作感覚の言葉を、Pittmanからはデザイナーが理解しやすい移動の目標値を、Thorsonからは猶予を与える仕組みを、Kerenからはジャンプと連携するカメラの仕組みを得ました。

大きなC5の境界を含む13の問いすべての記録は、Skyline Runの設計記録にあります。ここでの目的は、それを繰り返すことではありません。記録した答えのどこが誤っており、計測によってどう修正されたかを示すことです。

実測したジャンプが本当の境界を決めた

ゲームは640×360のキャンバスで動作し、タイルは16 px、プレイヤーのAABBは12×20 pxです。以下は初期の見積もりではなく、公開した調整値です。

物語を左右するのは三つです。JUMP_VELOCITYが跳び出す勢いを決め、GRAVITY_UPが実測される頂点を形作り、RUN_SPEEDが隙間からダッシュを学べるかを決めます。

定数意味
RUN_SPEED130px/s単位の水平方向の最高速度。
ACCEL_GROUND1400px/s²単位の地上加速度。
DECEL_GROUND1800px/s²単位の地上減速度。
ACCEL_AIR1100px/s²単位の空中加速度と減速度。
JUMP_VELOCITY-500ジャンプ発動時のpx/s単位の上向き速度。
GRAVITY_UP2080px/s²単位の上昇時の重力。
GRAVITY_DOWN2600px/s²単位の落下時の重力。
APEX_THRESHOLD40ジャンプ長押し時に頂点処理を適用する速度のしきい値。
APEX_GRAVITY_FACTOR0.5ジャンプ長押し中に頂点付近で適用する重力の倍率。
JUMP_CUT_FACTOR0.45上昇中にボタンを離したときの速度の倍率。
MAX_FALL560px/s単位の最大落下速度。
COYOTE_TICKS6保存するコヨーテタイムのカウンター。
BUFFER_TICKS7保存するジャンプバッファのカウンター。
DASH_SPEED320px/s単位で固定した水平方向のダッシュ速度。
DASH_TICKS9ダッシュの持続時間。
DASH_COOLDOWN_TICKS12ダッシュ終了後のクールダウン。
SP_REGEN_TICKS_RUN48SPを1回復するために必要な接地走行中のティック数。
SP_REGEN_TICKS_IDLE96SPを1回復するために必要な接地静止中のティック数。
RESPAWN_TICKS18再開まで死亡状態を保つティック数。
CAM_LOOKAHEAD24px単位の進行方向へのカメラオフセット。
CAM_LERP_X8水平方向のスムージング係数。
CAM_LERP_Y6垂直方向のスムージング係数。
CAM_WINDOW_UP72px単位の上方向のカメラ脱出ウィンドウ。
CAM_WINDOW_DOWN40px単位の下方向のカメラ脱出ウィンドウ。

ジャンプを最後まで長押しすると、14ティック目の0.233 sで、ちょうど56.00 px、つまり3.5タイルの高さに達します。総滞空時間は0.467 sです。1ティック後にボタンを離すと、約1タイルに相当する16.56 pxの小さなジャンプになります。

ジャンプを長押しした最終ティック計測した頂点
116.56 px
222.42 px
327.72 px
432.62 px
537.02 px
640.97 px

この範囲では、長押しを1ティック延ばすたびに頂点が単調に高くなります。ダッシュは垂直速度をゼロに固定したまま、ちょうど9ティックで、ちょうど48 px、つまり3タイル進みます。

ゲームプレイ用のタイマーはすべて、秒を表す浮動小数点数ではなく、整数のティックカウンターです。これによってルールから累積誤差がなくなり、各境界を厳密なアサーションにできます。1ティック前なら操作を利用でき、1ティック後なら利用できません。ページ側ではティックを時間として表示できますが、エンジンが、残りわずかな浮動小数点タイマーを実質的にゼロと見なせるか判断する必要はありません。

解析的に導出したジャンプ軌道と、60 Hzで計測した軌道を比較し、14ティック目の正確な56 pxの頂点と、1ティックでボタンを離したときの短いジャンプを示した図です。
役に立つ曲線は、ボタンを離す処理、頂点付近の重力、衝突判定を含む、エンジンの実際の更新順序から生まれる曲線です。

計算が嘘をついた場所

4タイルと6タイルの食い違いは、当初計画していた定数JUMP_VELOCITY -350GRAVITY_UP 1094GRAVITY_DOWN 1750RUN_SPEED 140で起きたものでした。制作前の表ではジャンプだけで越えられるのは最大4タイルとしていましたが、同じ定数をシミュレーションすると6タイルと計測されました。12 px幅のコライダーは、その左端が足場を離れるまでプレイヤーを接地状態に保つため、実際の踏み切り位置は点質量モデルの想定より12 px先になります。さらにコヨーテタイムによって踏み切りまで最大5ティック増え、140 px/sでは約11.7 px進みます。表自体にも矛盾がありました。表自身の計算は4タイルを超える到達距離をすでに示していたのに、「最大4タイル」と記していました。しかし、4タイルは64 pxにすぎません。

もう一つの別の誤りは、水平到達距離ではなく頂点の高さに関するものでした。公開したJUMP_VELOCITY -500GRAVITY_UP 2080では、閉形式の頂点は500² / (2 × 2080) = 60.10 pxです。半陰的オイラー積分、つまり固定ティックごとに速度を先に更新してから位置を更新する方法では、14ティック目にちょうど56.00 pxと計測され、予測値v × dt / 2 = 500 / 120 = 4.17 pxに対して4.10 px失われます。頂点が低くなれば滞空時間は短くなるため、予測より大きな水平到達範囲の説明にはなりません。

どちらの誤りも、書き出してみれば難解ではありません。問題は、連続系での導出と内部に矛盾のあるスケッチによって、入力タイミング、コライダー、衝突判定の順序を式に含まない離散システムを保証しようとしたことでした。どちらの導出も最終値として扱うのをやめ、実装したティック順序をシミュレーションするようにしました。

離散シミュレーションにとって、閉形式の解は仕様ではなく仮説です。開始範囲を選ぶためには、今も役に立ちます。しかし、合格基準となる値は、実際に動くシステムから得なければなりません。

何も教えなかった隙間

最初の部屋の中央には、「ここではダッシュが必要だ」と伝えるため、同じ高さの5タイル、80 pxの隙間を置いていました。しかし、エンジンで再現可能な境界は異なります。同じ高さの5タイルの隙間はジャンプだけで越えられますが、6タイルの隙間は越えられません。RUN_SPEED 130では、プレイヤーは1ティックごとに水平方向へ130 / 60 = 2.167 px進みます。そのため、以前のゲートは1ティック分の移動の内側で結果が決まっていました。コヨーテタイムのどのティックでジャンプが発動したかと、接近時のサブピクセル位相、つまりフレーム間で持ち越す1ピクセル未満の端数によって、結果が変わっていたのです。

決定的な点は、プレイヤーのボックスの幅が12 pxあることです。ボックスの左端がまだ足場に支えられている間、プレイヤーは接地していると判定されるため、本当の踏み切り位置は足場の右端です。最初の計算では、間違った地点から距離を測っていました。この誤差は、もっともらしいスケッチの中に隠れるほど小さく、部屋を成立させなくするには十分な大きさでした。

水平方向のわずか1フレーム分の移動で成否が決まる教訓は、設計ではなく偶然です。隙間を6タイル、つまり96 pxへ広げ、ジャンプだけなら確実に失敗し、ダッシュなら越えられるようにしました。これで数値が、部屋の伝える内容と一致します。

最初の復帰設計も、同じ理由で、より大きな規模の失敗になりました。ダッシュに失敗してもやり直しやすいよう、一方向の足場を二つ追加しました。しかし二つを合わせるとゲートを回り込む階段になり、プレイヤーは教わるはずの操作を行わずに目的地へ登れてしまいました。レベルに持たせた寛容さが、自らの主張を消していたのです。

それらの足場は、隙間の下にある一つの復帰用足場へ置き換えました。プレイヤーは踏み切り側へ戻れますが、その足場から目的地までは4タイル上にあり、56 pxの頂点より高いため、復帰ルートが近道になることはありません。

誤っていた5タイルの教習用ゲートと修正後の6タイルの隙間を計測値で比較し、以前のゲートを不安定にしていた1ティックの境界を示した図です。
操作を教えるゲートには、タイルのスケッチ上でもっともらしく見えるだけでなく、正確なコライダーとタイミングのルールに耐えられるだけの余裕が必要です。

制作可能範囲から地上ダッシュのゲートが消えた

着地する高さごとにエンジンを計測すると、実測したジャンプで越えられるレベル形状の範囲、つまりレベル制作範囲が得られました。各値は、越えられる何もない隙間の最大幅をタイル単位で表しています。

踏み切りに対する着地の高さジャンプのみジャンプ + 空中ダッシュ1回ジャンプ + 空中ダッシュ2回
同じ高さ5811
1タイル高い488
2タイル高い477
1タイル低い5912
2タイル低い5912

この表が示すのは、エンジンのバグではなく構造的な限界です。地上ダッシュは3タイル進む一方、ジャンプだけで同じ高さの5タイルの隙間を越えられます。地上ダッシュで越えられるほど狭い隙間は、すでにジャンプだけで届く範囲にあります。地上ダッシュは、より速いリズムや、よりすっきりした軌道といった表現には使えますが、形状によって厳密に要求するゲートには決してできません。

ダッシュを必須にできるのは空中だけです。それでも、高い着地点に対しては、2回目のダッシュで到達距離を伸ばせません。12ティックのクールダウンが残りの滞空時間を使い切るためです。これはレベル制作を明確に変えます。その展開が欲しいという理由で隙間に「地上ダッシュ必須」と名付けることはできず、SPの数だけを見て2回目のダッシュを見込むこともできません。すべてのゲートは、着地の高さに対応する計測済みの行から選び、実装されたクールダウンに対して検証する必要があります。

ジャンプのみ、空中ダッシュ1回、空中ダッシュ2回について、同じ高さ、高い位置、低い位置への着地ごとに、隙間の最大幅を示す能力範囲の図です。
制作に役立つ道具は、一つの代表的な距離ではなく、着地の高さ、ダッシュ回数、能力の間に消費する時間を含む表です。

レベルの約束を守らせるテスト

最も興味深い確認項目は、加速度や重力を個別にテストするものではありません。4部屋からなる200×30タイルのレベルdistrict-01が、今も主張どおりの内容を教えているかをテストします。

level.spec.tsは、立てる足場の完全な一覧を固定し、開始地点、すべてのチェックポイント、ゴールが地面の上にあることを検証します。主要ルートの各ジャンプをダッシュの予算と照合し、ダッシュ必須の二つの隙間がジャンプだけでは越えられない状態を保ち、復帰用の足場でゲートを回避できないことを証明し、二つのくぼんだ穴のどちらでもプレイヤーが進行不能にならないことを証明します。

level.spec.tsには、五つすべてのチェックポイントを有効にし、死亡0回のまま、1304ティック、つまり21.7 sでクリアすることを固定するリプレイテストがリポジトリに追加されました。さらに、新しい状態からのリプレイが同一の結果になることも検証します。リポジトリにあるエンジン、レベル、ルートの各スイートはすべて合格し、bun run checkにも問題はありません。

ブラウザでは、レンダリングされたルートに対する最終確認を行いました。最大ジャンプだけでは教習用の穴の向こう側の壁に突き当たって終わりました。プレイヤーは穴の右壁まで滑ったため、その静止位置はジャンプの到達距離ではなく、壁の位置を計測したものです。空中ダッシュを1回使うと、26.26列目に着地しました。実行時に行った1304ティックの完全なリプレイは「地区をクリアしました」のオーバーレイへ到達し、統計行には0:21 · 落下 0回と表示されました。

レベルが何かを教えるという主張は、テスト可能な契約です。「この隙間はダッシュを教える」は、何では越えられないか、何なら越えられるか、失敗後に何が起きるか、復帰ルートが教訓を保っているかに分解できます。これらの記述を実行可能にすれば、後の調整によって、気づかないうちに部屋が再び嘘をつくことはありません。

計測がレベルの約束を事実に変えた

計測は設計の道具であり、最後に行うQA工程ではありません。ABCチェックリストは、どの問いを立て、答えをどこへ記録するべきかを教えてくれました。その答えが、実際の更新、入力、コライダー、レベルのルールの下で本当に成り立つかを教えたのは、シミュレーションだけでした。

最初のプレイ可能な一部分は、今も一つのレベル、一つの仕組みの教訓、キーボード入力に加えて粗いポインターを備えた端末で表示する画面上の移動、ジャンプ、ダッシュ、ポーズのタッチ操作、そしてプログラムで描いたグレーボックスのアートだけです。先送りした境界は大きいままです。その他の移動能力、戦闘とアイテム、敵、ランと探索の構造、作者が制作するアート、サウンド、ゲームパッド入力、永続化、制作者向けツール、調整可能な難易度は、クリアのオーバーレイの裏に隠れているわけではありません。まだ作られていません。

また、確定的なクリアによって、指導を受けていないプレイヤーが教訓を読み取り、復帰ルートを見つけ、移動を楽しめると証明されたわけでもありません。これは未解決の問いであり、次の手順は、計測を残したままプレイテストを観察し、プレイヤーが別の誤った仮定を明らかにしたときに何が変わったかを確かめることです。

この一部分で証明できたのは、より狭い事実です。実際のエンジンのルールでは、形状が教えるべき内容と矛盾しなくなりました。次に、この決定論的なエンジンを訓練環境へ変え、問いかけます。決定論的なゲームはネットワークに屋上の走り方を教えられるか?

出典

このサイトとAI Maker Labチャンネルは私が所有し、制作しています。これは制作記録であり、第三者による独立したレビューではありません。

あわせて読む