こんにちは、Habr!

ますます多くの新しい技術がWebに導入されているのを見て、ゲームがどのようにWebに転送されるのかを見て、「ゲームパッドも接続できるといいな...」と思いました。 そして、検索の最初の結果はGamepadAPIでした 。
W3C GamepadAPIへの少し下のリンク。 見て、試してみたところ、ブラウザーでのジョイスティックの導入に終止符を打ついくつかの問題、落とし穴が見つかりました。 そして、インターフェイスを作成して修正することにしました。 「箱から出してすぐに」とは何であり、正確に何が完成し、変更され、私の意見では改善されているかは、カットの下で説明されています。
GamepadAPIには何がありますか?
APIは、Firefox、クロム、オペラでサポートされています。
フルバージョンの場合:
navigator.getGamepads();
ジョイスティックの配列、ゲームパッドオブジェクトを返します。
window
オブジェクトでのジョイスティックの接続および切断イベント(つまり、
navigator
からのジョイスティックの受信、および
window
イベント):
"gamepadconnected", "gamepaddisconnected"
window.addEventListener("gamepadconnected", function(e) {...})
イベントオブジェクトが関数に渡されます
e.gamepad
プロパティは、接続または切断されたジョイスティックです。
Gamepad
オブジェクト自体には、次のプロパティがあります。
-
id
は、ベンダーID、製品ID(USB)、および説明が含まれます。 記録形式は規制されていません。 - 接続アカウントである
index
。 - 再マッピングが書かれているかどうか、書かれている場合はどの行に
mapping
か。 - ジョイスティックが
connected
かどうかにconnected
; - ジョイスティックデータが最後に更新されたときの
timestamp DOMHighResTimeStamp
。 -
axes
配列と-1〜1の値。 -
buttons
配列、pressed (boolean)
およびvalue
[0;を含むオブジェクト。 1]以来 トリガーは値を滑らかに変化させることができるため、場合によってはこれを考慮する必要があります。
しかし、2つの恐ろしい予約があります。
-
axes
(軸)は初期化中に0の値を持ちますが、実際には-1の値になります。 これは、XInput用のLinuxのハンマー(トリガー)に適用されますが、ウィンドウでは、ハンマーは一般に1つの軸を持ちます! 1つだけが正の方向に値を変更し、2つ目は負の方向に変更します。つまり、両方を押すと、再び0になります。 -
id
見事です。 自分でジョイスティックを認識するためには、VIDとPIDを知る必要があります。これは、このプロパティを解析する必要があることを意味します。ただし、形式「dances」: 、たとえば、"092c-12a8-..."
ですが、最悪の事態は、ゼロのプレフィルがないことがわかったため、Windowsでは文字列が"92c-12a8-..."
変換されることです"92c-12a8-..."
なぜなら クロムは、ドラフトに焦点を当てて、残りの部分よりも先にサポートを導入しようとしたため、webkitプレフィックスのみを使用するブラウザにはさらに予約があります。
-
connected
なし。 - 接続および切断イベントはありません。 さらに悪いことに、
navigator.webkitGetGamepads()
によって返される配列が表示されるためには、この関数の呼び出し中にジョイスティックがアクティブである必要があります(たとえば、ボタンが押されています)。 - 再マッピングは可能ですが、マッピング
maping
空です。 -
buttons
オブジェクトではなく、値の配列です。
問題の一部は時が経て、標準を完全にサポートした後でも明らかになります(つまり、一般的にジョイスティックがあるクロムのすべてのバージョンに存在します)。
- ジョイスティックモデルが不明な場合は、まったく存在しません。 この場合、ffは少なくともインターフェースを「現状のまま」提供しますが、彼はすべてのモデルを知っていません( ソースを確認した後、OS APIを呼び出してジョイスティックを標準で問題なく動作させることがわかります )。
-
Gamepad
オブジェクトは、navigator.webkitGetGamepads()
またはnavigator.getGamepads()
が呼び出されるまで更新されません(存在する場合、および存在する場合、古いバージョンを呼び出すと、「注意」がスローされ、何も更新されません)。 つまり 関数からオブジェクトを取得するために再度取得する必要はありませんが、この関数を呼び出すだけで十分です。
何をどのように栽培しましたか?
私はcoffeescriptに書くことにしました。
私の近くにあり、クラスがあります(また、プロセッサを少し仕上げてレイアウトしました 。今ではほぼ本格的なSシャイプリプロセッサがあります!)。したがって、例はコーヒースクリプトにさらにあります。
プリプロセッサについてもう少し...
PHPに精通していなくてもPHPに精通している人は、プリプロセッサがincludeと同じ方法でファイルをインクルードし、defineと同じ方法で定数を定義します。 KerniganとRichieのsiプリプロセッサに関する通常の説明と、World Wide Webのオープンスペースを参照できます。
慣れ親しんでいる人にとっては、機能的なスタイルでの定義は機能せず、コマンドライン(-DDEBUGなど)で定義を転送することはまだ不可能だと言います。 (包含フォルダーが可能です)。 それ以外の場合、標準は、インクルードフォルダー、置換の置換、条件ステートメントを含む、C ++ 11に非常に近い実装を行いました。 しかし、ソース定数はなく、インクルードはインデントを保持します(ディレクティブが記述されているインデントに等しい行の前にインデントを追加することでファイルをインクルードします。これは言語の構文のために必要です)。
慣れ親しんでいる人にとっては、機能的なスタイルでの定義は機能せず、コマンドライン(-DDEBUGなど)で定義を転送することはまだ不可能だと言います。 (包含フォルダーが可能です)。 それ以外の場合、標準は、インクルードフォルダー、置換の置換、条件ステートメントを含む、C ++ 11に非常に近い実装を行いました。 しかし、ソース定数はなく、インクルードはインデントを保持します(ディレクティブが記述されているインデントに等しい行の前にインデントを追加することでファイルをインクルードします。これは言語の構文のために必要です)。
すぐに出た最初の2つの問題:
- 要素またはマッピングの関連付け。 Firefoxではそうではなく、クロムではそうです。
- 偶然の欠如。 リスナーをボタンまたはスティックに固定することはできません。
要素またはマッピングの関連付け。
便宜上、ジョイスティックのボタンを論理ブロックに分割しました。
- dpadまたは、人々では、クロスピース
- lrtbトリガーとバンパー(何を呼ぶべきかわからない)
- メニューメニューボタン
- 軸スティックとそのボタン
- メインアクションボタンに直面する
これは、要素のグループの変更を追跡するために行われます。
クロムプロジェクトからボタンの関連付けのソースコードをあっという間に取得して、ジョイスティックの関連付けマップを作成しました。 プラットフォームに依存していることがわかります。つまり、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プリプロセッサ。