🌊

Three.js Cannon.es 調査資料 - 複数キー押下時のイベントリスナー

2024/11/09に公開

この記事のスナップショット

(スナップショットは特にありません..)

関連するソースは以前の記事「ギア(MT)の導入/トルク曲線(エンジン性能曲線)にそった挙動」のものになります。

ソース

https://github.com/fnamuoo/webgl/blob/main/032

概要

キー押下時のイベント処理を誤解していました。
キーを押しっぱなしで、さらに他のキーをプッシュ・リリースしたときに、押しっぱなしにしているキーの挙動を正しく実施します。

やったこと

今回の車のアクセルとハンドル操作がちょうどそうなのですが、アクセルボタン(矢印上)を押したまま、ハンドル操作(矢印右や矢印左)を押すことがよくあります。
今までの(一見正しい)コードでは、キーイベントが発生したときに、「その場で」そのアクションに応じた挙動を紐づけていました。

今までの(一見正しい)コード
  document.addEventListener('keydown', (event) => {
    switch (event.key) {
      case 'ArrowUp':
        vehicle.keydown_ArrowUp();    // アクセル操作
        break
      case 'ArrowDown':
        vehicle.keydown_ArrowDown();  // バック操作
        break
      case 'ArrowLeft':
        vehicle.keydown_ArrowLeft();  // ハンドルを左にきる
        break
      case 'ArrowRight':
        vehicle.keydown_ArrowRight();  // ハンドルを右にきる
        break
    ...
  }

この場合に上記のようなキー操作をすると、押しっぱなしにしていたキー操作(アクセル操作)が呼ばれなくなります。こちらとしては、(他のボタンを割り込みで押したとしても)いま加速を押しているのだから、アクセル操作の関数を読んで欲しいのですがそうはならず。
一応、加速ボタンを一旦releaseして、再度pushすると再びアクセル操作が呼ばれます。

シーケンス図っぽいもの
[ユーザ操作]             [イベントリスナー]
     | (加速ボタンpush)      |
     |]-------------------->|
     |]                     | (アクセル操作)
     |]                     |------------------->
     |]                     | (アクセル操作)  .. ここは押しっぱなしで何度も呼ばれるのに
     |]                     |------------------->
     |](ハンドル左push)      |
     |]]------------------->| (ハンドル左操作)
     |]]                    |------------------->
     |]]                    | (ハンドル左操作)
     |]]                    |------------------->
     |]](ハンドル左release)  |
     |]]------------------->| (ハンドル戻す操作)
     |]                     |------------------->
     |]                     |
     |]                     | (アクセル操作)が呼ばれない!!
     |]                     |- - - - - - - - - ->
     |]                     |
     |](加速ボタンrelease)   |
     |]-------------------->| (アクセルを戻す操作)
     |                      |------------------->
     |                      |
     | (加速ボタンpush)      |
     |]-------------------->|
     |]                     | (アクセル操作)
     |]                     |------------------->
     |]                     | (アクセル操作)
     |]                     |------------------->

イベントリスナーはどうやら、このような挙動を示す(スタック機能は期待できない)仕様のようです。

物理モデルの内部状態に依存せずに定数で変更する場合は、今ままでのコードでも問題なかったのですが、前回の記事「ギア(MT)の導入/トルク曲線(エンジン性能曲線)にそった挙動」のときのように、「ボタンを押下している間は常に関数を呼び出したい場合」には今までと違う実装が必要になります。

具体的には、イベントリスナ―ではキー押下のフラグを取得しておいて、物理モデル更新・レンダリング時に「押下フラグに応じた挙動」を施す必要があります。

今回はイベントリスナーのところでフラグを設定しておいて、..

修正コード(イベントリスナー部分)
  var keyEvnt = {
    forwards:false,
    backwards:false,
    left:false,
    right:false,
    brake:false,
    sidebrake:false
  };

  document.addEventListener('keydown', (event) => {
    switch (event.key) {
      case 'ArrowUp':
        keyEvnt.forwards = true;   // アクセル操作のフラグ
        break
      case 'ArrowDown':
        keyEvnt.backwards = true;  // バック操作のフラグ
        break
      case 'ArrowLeft':
        keyEvnt.left = true;       // ハンドルを左にきるフラグ
        break
      case 'ArrowRight':
        keyEvnt.right = true;      // ハンドルを右にきるフラグ
        break
      ...
  }

物理モデル更新・レンダリング時に「押下フラグに応じた挙動(関数)」を呼び出します。

修正コード(レンダリング部分)
  function animate() {
    // 車関連(押しっぱなし/同時押しの key-event処理)
    {
      if (keyEvnt.forwards) {
        vehicle.keydown_ArrowUp();    // アクセル操作
      } else if (keyEvnt.backwards) {
        vehicle.keydown_ArrowDown();  // バック操作
      } else {
        vehicle.keyup_ArrowUp();      // アクセル・バックを戻す操作
      }

      if (keyEvnt.left) {
        vehicle.keydown_ArrowLeft();   // ハンドルを左にきる
      } else if (keyEvnt.right) {
        vehicle.keydown_ArrowRight();  // ハンドルを右にきる
      } else {
        vehicle.keyup_ArrowLeft();     // ハンドルを戻す
      }
    }

    // 物理エンジンの系を進める
    world.step(timeStep)
    ...

感想

サンプルのような簡素な例だとなかなか遭遇しない不具合でした。
以前にも同じようなコードを見かけて「なんでこのような面倒なことを..」と思ってましたが、奥が深いです。

Discussion