ブラウザのGamepadAPIまたはジョイスティック

こんにちは、Habr!









ますます多くの新しい技術がWebに導入されているのを見て、ゲームがどのようにWebに転送されるのかを見て、「ゲームパッドも接続できるといいな...」と思いました。 そして、検索の最初の結果はGamepadAPIでした

W3C GamepadAPIへの少し下のリンク。 見て、試してみたところ、ブラウザーでのジョイスティックの導入に終止符を打ついくつかの問題、落とし穴が見つかりました。 そして、インターフェイスを作成して修正することにしました。 「箱から出してすぐに」とは何であり、正確に何が完成し、変更され、私の意見では改善されているかは、カットの下で説明されています。







GamepadAPIには何がありますか?





APIは、Firefox、クロム、オペラでサポートされています。

フルバージョンの場合:



navigator.getGamepads();



ジョイスティックの配列、ゲームパッドオブジェクトを返します。

window



オブジェクトでのジョイスティックの接続および切断イベント(つまり、 navigator



からのジョイスティックの受信、およびwindow



イベント): "gamepadconnected", "gamepaddisconnected"





  window.addEventListener("gamepadconnected", function(e) {...})
      
      





イベントオブジェクトが関数に渡されますe.gamepad



プロパティは、接続または切断されたジョイスティックです。



Gamepad



オブジェクト自体には、次のプロパティがあります。





しかし、2つの恐ろしい予約があります。

  1. axes



    (軸)は初期化中に0の値を持ちますが、実際には-1の値になります。 これは、XInput用のLinuxのハンマー(トリガー)に適用されますが、ウィンドウでは、ハンマーは一般に1つの軸を持ちます! 1つだけが正の方向に値を変更し、2つ目は負の方向に変更します。つまり、両方を押すと、再び0になります。
  2. id



    見事です。 自分でジョイスティックを認識するためには、VIDとPIDを知る必要があります。これは、このプロパティを解析する必要があることを意味します。ただし、形式「dances」: 、たとえば、 "092c-12a8-..."



    ですが、最悪の事態は、ゼロのプレフィルがないことがわかったため、Windowsでは文字列が"92c-12a8-..."



    変換されることです"92c-12a8-..."







なぜなら クロムは、ドラフトに焦点を当てて、残りの部分よりも先にサポートを導入しようとしたため、webkitプレフィックスのみを使用するブラウザにはさらに予約があります。



問題の一部は時が経て、標準を完全にサポートした後でも明らかになります(つまり、一般的にジョイスティックがあるクロムのすべてのバージョンに存在します)。







何をどのように栽培しましたか?





私はcoffeescriptに書くことにしました。

私の近くにあり、クラスがあります(また、プロセッサを少し仕上げてレイアウトしました 。今ではほぼ本格的なSシャイプリプロセッサがあります!)。したがって、例はコーヒースクリプトにさらにあります。



プリプロセッサについてもう少し...
PHPに精通していなくてもPHPに精通している人は、プリプロセッサがincludeと同じ方法でファイルをインクルードし、defineと同じ方法で定数を定義します。 KerniganとRichieのsiプリプロセッサに関する通常の説明と、World Wide Webのオープンスペースを参照できます。



慣れ親しんでいる人にとっては、機能的なスタイルでの定義は機能せず、コマンドライン(-DDEBUGなど)で定義を転送することはまだ不可能だと言います。 (包含フォルダーが可能です)。 それ以外の場合、標準は、インクルードフォルダー、置換の置換、条件ステートメントを含む、C ++ 11に非常に近い実装を行いました。 しかし、ソース定数はなく、インクルードはインデントを保持します(ディレクティブが記述されているインデントに等しい行の前にインデントを追加することでファイルをインクルードします。これは言語の構文のために必要です)。







すぐに出た最初の2つの問題:

  1. 要素またはマッピングの関連付け。 Firefoxではそうではなく、クロムではそうです。
  2. 偶然の欠如。 リスナーをボタンまたはスティックに固定することはできません。






要素またはマッピングの関連付け。



便宜上、ジョイスティックのボタンを論理ブロックに分割しました。



これは、要素のグループの変更を追跡するために行われます。





クロムプロジェクトからボタンの関連付けのソースコードをあっという間に取得して、ジョイスティックの関連付けマップを作成しました。 プラットフォームに依存していることがわかります。つまり、Windowsとペンギンでは、Macとは異なります。 しかし、新しいジョイスティックやあまり知られていないジョイスティックの場合はどうでしょうか? この場合、 GamepadMap



クラスGamepadMap



個別GamepadMap



発行されました。 このクラスから作成されたオブジェクトは、インターフェイスコンストラクターに渡すことができます。





しかし、必ずしもそんなに悪いわけではありません! 関連付けが正常であることが起こります。 既製のマッピングと生のマッピングを区別するために、「軸」の数にガイドされています。 それらの4つがない場合(2つのスティックのそれぞれに対して垂直および水平)、 "id"



プロパティからVIDおよびPIDを取得することにより、関連付けマップを見つけようとします。 これは一方では安全ではありませんが、他方では、私はそれをより良く見つけることができませんでした。 「マッピング」パラメーターの値でも何も提供しません。webkitプレフィックスでのみ機能するクロムでは、このパラメーターは空ですが、上で書いたように、関連付けは既に準備ができています。





イベントを紹介します。



GamepadAPIにあるイベントはgamepadconnected



gamepaddisconnected



です。 ボタンを押すこととスティックの変更は、個別に取得する必要があります。 理論的には、これは便利ですが、実際には常に便利とは限りません。 特に「clavamysh」の代替を作成する場合。





そして、5つのステップでZenを学びました。







ステータスを取得します。



なぜなら W3Cは実際の状態変化に応じてGamepadオブジェクトの状態を変更することについてまったく推奨していないため、クロムは1回目(最初のカップル)と2回目(標準を完全にサポート)を気にしませんでした:Gamepadオブジェクトのプロパティは更新されるだけですnavigator.getGamepads()



またはnavigator.webkitGetGamepads()



を介してポーリングする場合。 銃器では、すべてがよりシンプルで、状態は自動的に更新されます。 したがって、webkitの場合は、ポーリングの前に毎回このメソッドをプルします。







EventTargetインターフェース。



要素のEventTarget



インターフェースを再作成したかったのですが、 extends EventTarget



取得して作成することはできません。 私は自分の実装を「整える」必要がありましたが、標準を守っていました。 既製のEmetを入手してみませんか? 標準を厳密に遵守することはありませんが、可能な限り標準的にすべてを実行したかったのです。



EventTargetEmiter



クラスのon、off、emet、chain、voilaなどの便利なメソッド:

EventTargetEmiterクラスコード
 class EventTargetEmiter # implements EventTarget ###* *         . * @protected * @type Object ### _subscribe: null ###* *     * @public * @type EventTargetEmiter ### parent: null ###* *     . * @protected * @method _checkValues * @param String|* type   * @param Handler|* listener - ### _checkValues: (type, listener) -> unless isString type ERR "type not string" return false unless isFunction listener ERR "listener is not a function" return false true ###* *   `list`       * handler- * @constructor * @param Array list   ### constructor: (list...) -> @_subscribe = _length: 0 for e in list @_subscribe[e] = [] @['on' + e] = null ###* * Add function `listener` by `type` with `useCapture` * @public * @method addEventListener * @param String type * @param Handler listener * @param Boolean useCapture = false * @return void ### addEventListener: (type, listener, useCapture = false) -> unless @_checkValues(type, listener) return useCapture = not not useCapture @_subscribe[type].push [listener, useCapture] @_subscribe._length++ return ###* * Remove function `listener` by `type` with `useCapture` * @public * @method removeEventListener * @param String type * @param Handler listener * @param Boolean useCapture = false ### removeEventListener: (type, listener, useCapture = false) -> unless @_checkValues(type, listener) return useCapture = not not useCapture return unless @_subscribe[type]? for fn, i in @_subscribe[type] if fn[0] is listener and fn[1] is useCapture @_subscribe[type].splice i, 1 @_subscribe._length-- return return ###* * Burn, baby, burn! * @public * @method dispatchEvent * @param Event evt * @return Boolean ### dispatchEvent: (evt) -> unless evt instanceof Event ERR "evt is not event." return false t = evt.type unless @_subscribe[t]? throw new EventException "UNSPECIFIED_EVENT_TYPE_ERR" return false @emet t, evt ###* * Alias for addEventListener, but return this * @public * @method on * @param String type * @param Handler listener * @param Boolean useCapture * @return this ### on: (args...) -> @addEventListener args... @ ###* * Alias for removeEventListener * @public * @method off * @param String type * @param Handler listener * @param Boolean useCapture * @return this ### off: (args...) -> @removeEventListener args... @ ###* * Emiter event by `name` and create event or use `evt` if exist * @param String name * @param Event|null evt * @return Boolean ### emet: (name, evt = null) -> # run handled-style listeners r = @['on' + name](evt) if isFunction @['on' + name] return false if r is false # run other for fn in @_subscribe[name] try r = fn[0](evt) break if fn[1] is true or r is false if evt?.bubbles is true try @parent.emet name, evt if evt? then not evt.defaultPrevented else true
      
      







_subscribe



プロパティ_subscribe



外部からアクセスできますが、誰が脚部で自分自身を撃つ準備ができている保護プロパティを(強調して)修正するかは重要ではありません。 親オブジェクトをオブジェクトに割り当てると、ポップアップイベントが送信されます。





イベントとCustomEvent。



誰がイベントを引き起こしたかを理解するには、 Event



を作成する必要がありますが、 Event



を作成してプロパティを設定しないでください。 CustomEvent



は、 detail



プロパティをカスタマイズできるCustomEvent



になります。 そして、イベントが親要素でcanBubble



するtrue



に、コンストラクタでcanBubble



true



に設定することを忘れないでください。







投票状態またはプーリング。



GamepadAPI関連のすべての例でrequestAnimationFrame



requestAnimationFrame



使用して状態を照会します。 これにはプラスとマイナスがあります:

プラスは、ウィンドウがアクティブでない場合、状態を調べる必要がないことです。

しかし一方で、これがゲームの場合、この呼び出しはレンダリングに必要です。そうしないと、アニメーションの滑らかさが損なわれる可能性があります。

したがって、別の「古い」方法を採用することにしました。ウィンドウのfocus/blur



、スケジューラのsetInterval



、および最初の実行の単一のrequestAnimationFrame



です(結局、ウィンドウはバックグラウンドでロードされる可能性があります)。 したがって、ブラウザ自体がタスクのリストを処理し、レンダリングの間に必要な処理を実行します。

出所
  tick = (time, fn) -> #    setInterval fn, time stopTick = (tickId) -> clearInterval tickId _startShedule: (Hz = 60) -> requestAnimationFrame = top.requestAnimationFrame or top.mozRequestAnimationFrame or top.webkitRequestAnimationFrame requestAnimationFrame => #     t = null startTimers = -> t is null and t = tick (1000 / Hz |0), -> #  ,    body() return stopTimers = -> if t isnt null #   ,     stopTick(t) t = null return window.addEventListener 'focus', -> startTimers() window.addEventListener 'blur', -> stopTimers() startTimers() return return
      
      









ゲームパッドは1つですか? 一緒にプレイしたことを忘れましたか?



システムは複数のジョイスティックで登録できます。 navigator.getGamepads()



は配列を返すため、配列が必要です。 しかし、イベントがあります。 ここでタンバリンと踊り始めます: Array



を継承するには、コンストラクタに短い行を追加する必要があります:

  constructor: (items...) -> @splice 0, 0, items...
      
      







しかし、これでは十分ではありませんEventTargetEmiter



継承するEventTargetEmiter



ます。 コーヒーのスクリプトでは直接解決しませんでした。 したがって、メソッドとプロパティをthis



渡す単純な関数に助けられました:

 _implements = (mixins...) -> for mixin in mixins @::[key] = value for key, value of mixin:: @
      
      







そこで、イベントを持つ単純な配列クラスを取得しました。コンストラクターのみが配列の長さを受け入れません。

 class EventedArray extends Array # implements EventTarget _implements.call(@, EventTargetEmiter) ###* * @constructor * @param items array-style constructor without single item as length. ### constructor: (items...) -> @splice 0, 0, items...
      
      









それから、ブロック、ボタン、スティック、構造の作成など、すべてが比較的簡単でした。 私の意見では、このルーチンは新しいものや重要なものは何もないため、説明する意味がありません。







合計:





ジョイスティックを操作するためのGamepad2



と、手動および微調整用のGamepad2



およびGamepadMap



を作成しGamepads







推奨事項とホワイトスポットの標準は悪いです。 明らかではない瞬間がたくさんあります。



ジョイスティックに作業者からアクセスしないでください。 メインロジックが含まれていると、有害な場合があります。



Chromeは可能な限り最高の方法ですべてを表示しようとしていますが、未知のジョイスティックを拒否します。これは、(論理的ではありますが)多すぎると思います。 Mozillaは、「現状のまま」と「あなたが望む怒り」をすべて与えてくれます。



参照:



テスター



ソースコード



Coffeescript幅のCプリプロセッサ。



All Articles