やりたかったこと
ゲームに、同じ方向キーを素早く2回押すとその方向にダッシュ(短い瞬間移動)する操作がありました。2つのキーを交互に押すと斜めにダッシュするのに、2つのキーを同時に2回押すと、1つの軸にしかダッシュしませんでした。見た目には同じ操作なのに結果が違うので、バグに見えました。
あらかじめ知っておくと便利な概念を紹介します。
keydownイベント: キーを押したときにブラウザが送る合図です。キー1つにつきイベント1つで、「同時に押した」という概念はありません。WとDを同時に押しても、イベントは別々に届きます。- 固定シミュレーションステップ: ゲームでは、画面を描くループ(
requestAnimationFrame)と、ルールを1コマ進める固定間隔の計算が別々に動いています。入力イベントはこの2つの間のどこにでも割り込んできます。
やったこと
原因はイベントの順番でした。以前のコードは、ダブルタップが成立する最初のキーのイベントで方向をすぐに確定し、次のステップで消費していました。Wのイベントが先にダッシュを予約すると、後から来たDのイベントは、すでに決まった方向に合成されませんでした。交互に押すときは、もう一方のキーがすでに押された状態で残っていたため、たまたま斜めになっていただけでした。
- 最初のキーイベントで50msの収集ウィンドウを開き、ウィンドウ内に入ってきた縦・横の軸だけを補って方向を合成します。ウィンドウは最初のキーでのみ始まり、延長しません。
- ダッシュのバッファは、描画ループではなくシミュレーションのステップで1回だけ消費します。別の軸のキーがなければ、ウィンドウが終わった後に単一の軸で実行します。
- ウィンドウがフォーカスを失ったとき(blur)、一時停止、再開始のときは、バッファと最初のタップの記録を空にします。
- 却下した代替案: 即時確定は、同じchordのキーイベントの間にフレームが挟まるとまた失敗し、もっと長く待つと操作の遅延が増えるため、短く制限しました。
検証は、同時押しの2つ目のkeydownの間にフレームを挟む単体テストで行いました。修正前は失敗し、修正後は通ることを確認しました。また、実際のランタイムで8通りの組み合わせ(4つの斜め × キー順2通り)と、一時停止中のキャンセルも確認しました。
学んだこと
1つのchordに属する入力は、最初のイベントで確定しません。 短く上限のある収集ウィンドウに集め、消費するタイミングとリセット経路を別々に検証します。→ キーボードchord収集ウィンドウ
同じ操作でも、順番が違えば別の経路です。 既存のテストは、片方のキーを押したまま、もう片方のキーを2回押す経路しか確認していなかったため、この回帰を見逃しました。
まだ確認できていないこと: 物理キーボードでの体感と、本番ビルドへの反映の有無です。ブラウザでの検証は、DOMのKeyboardEventをランタイムに送ったもので、1回の経験なので、ほかの入力ポリシーやほかのゲームに一般化する根拠はありません。