📜 ⬆️ ⬇️

GamepadAPI or browser joystick

Hello, Habr!





Watching how more and more new technologies are being introduced into the web, looking at how games are transferred to it, I thought: “It would be cool if the gamepad could also be connected ...”. And in the search for the first result was GamepadAPI .
A bit below is the link to the W3C GamepadAPI . Having looked, having tried, I found out a number of problems, reefs which would put an end to implementation of joysticks in the browser. And I decided to fix it by creating an interface. What is "out of the box", and what exactly has been modified, changed and in my opinion improved, described under the cut.
')


What is GamepadAPI?



API is supported in firefox, in chromium, opera.
In the full version:

navigator.getGamepads(); return an array of joysticks, Gamepad objects.
Events of connecting and disconnecting the joystick in the window object (namely, receiving joysticks from the navigator , and events in the window ): "gamepadconnected", "gamepaddisconnected" .
  window.addEventListener("gamepadconnected", function(e) {...}) 

The event object is passed to the function, where the e.gamepad property is the joystick that is connected or disconnected.

There are properties in the Gamepad object itself:


But there are two terrible reservations:
  1. Axes (axes) have a value of 0 at initialization, whereas in fact they can be in a value of -1. This applies to hammers (triggers) in Linux for XInput, in the windows, the hammers generally have one axis! Only one changes the value in a positive direction, and the second - in a negative, which means that pressing both of them will get 0 again.
  2. id self-willed. In order to recognize the joystick yourself, you need to know the VID and PID, it means you need to parse this property, but the format is “dancing”: in the chromium, the line contains a description, and only then "Vendor: 092c Product: 12a8" , in firefox, the line begins with them, separating by minuses , for example "092c-12a8-..." , but the nastiest thing is that in the windows it turned out that prefilling with zeros is simply absent, therefore in Windows the line is transformed into "92c-12a8-..."


Since Chromium attempted to introduce support ahead of the rest of the planet, guided by drafts, so there are more reservations for browsers in which only the webkit prefix is:

Part of the problems has passed through time and manifests itself even after the standard is fully supported (i.e., they exist in all versions of chrome, where joysticks are generally available):



What and how improved?



I decided to write on coffeescript.
It is closer to me, there are classes in it, (I also finished the processor a bit and laid it out , now it has an almost full-fledged C-shny preprocessor!) Therefore, the examples are further on the coffee script.

A little more about the preprocessor ...
Whoever is not familiar with this, but is familiar with PHP, the preprocessor includes files like include and defines constants like define, then they are there. A normal description of the preprocessor C can be found in Kernighan and Richie, as well as in the world wide web.

Those who are familiar will say that define in the functional style will not work, and it is still not possible to transfer definitions via the command line (-DDEBUG for example). (folder inclusion is possible). The rest of the standard was implemented extremely close to C ++ 11, including the include folders, replacements in replacements, conditional statements. But there are no initial constants, and include preserves the indents (includes the file, adding indents before the lines, equal to the indent on which the directive is written. It is necessary because of the syntax of the language).



The first two problems that came out immediately:
  1. Associations of elements or mapping. It is not in firefox, it is in chromium.
  2. Lack of eventfulness. You can not take and hang the listener on the button or stick.



Associations of elements or mapping.


For convenience, I divided the joystick buttons into logical blocks.

This is also done in order to track changes in the group of elements.


Brazenly taking the source codes of the associations of buttons from the chromium project, I created maps of associations for joysticks. It turns out that they depend on the platforms, which means that they differ from the macin for the windows and for the penguin. But what if this is a new and / or little-known joystick? For this case, the GamepadMap class GamepadMap rendered separately. An object created from this class can be passed to the interface constructor.


But everything is not always so bad! It happens that associations are normal. In order to distinguish a ready mapping from a raw one, I am guided by the number of “axes”. If there are not 4 of them (vertical and horizontal for each of the two sticks), then I try to find an association map by getting the VID and PID from the "id" property. It is not safe on the one hand, but on the other parameter I could not find it better. Even the value of the mapping parameter does not give anything: in chromium, which only works with the webkit prefix, this parameter is empty, but the associations are ready, as I wrote above.


Introduce the event.


The only events in GamepadAPI are gamepadconnected and gamepaddisconnected . Pressing the buttons and changes in the sticks should be obtained independently. Theoretically, this is useful, but in practice it is not always convenient. Especially if you create an alternative to "clavamyshi".


And here I learned Zen in 5 steps:



Receiving status.


Since W3C does not give any recommendations at all about changing the state of the Gamepad object depending on the actual state change, then chrome did not bother that first (in the first pairs), and the second time (supporting the standard completely): the properties of the Gamepad object are updated only when polling via navigator.getGamepads() or navigator.webkitGetGamepads() . In the firelight, everything is simpler, the state is updated automatically. Therefore, if the webkit, then pull this method every time before the survey.



EventTarget interface.


I wanted to recreate the EventTarget interface for the elements, but you can't just create and create extends EventTarget . I had to “knee” my implementation, but observing the standard. Why not take ready Emet? There is no close compliance with the standard, but I wanted to do everything as standard where it is possible.

Some useful methods, such as on, off, emet, chaining and voila, are the EventTargetEmiter class:
EventTargetEmiter class code
 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 


the _subscribe property _subscribe accessible from the outside, but it doesn’t matter who rules the protective properties (with underlining) is ready to shoot himself in the foot. An object can be assigned a parent object, in which a “pop-up” event is transmitted.


Event and CustomEvent.


To understand who caused the event, you should create an Event , but simply create the Event and set its properties to us is not allowed. CustomEvent comes to the CustomEvent , in which the detail property is customizable. And so that the event is called and in the parent elements, do not forget to set canBubble to true in the constructor.



Poll states or pooling.


In all examples related to the GamepadAPI, the requestAnimationFrame used to poll the state. This is a plus and a minus:
plus the fact that when the window is not active, then there is no need to poll the state.
But on the other hand, if this is a game, then this call is necessary for drawing, otherwise the smoothness of the animation may suffer.
Therefore, I decided to go as an alternative “old” way: focus/blur for the window, setInterval for the scheduler, and a single requestAnimationFrame for the first run (after all, the window can load in the background). Thus, the browser itself will be engaged in the list of tasks, will execute necessary between drawings.
Source
  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 



One gamepad? Have you forgotten how we played together?


Several joysticks can be registered in the system. Yes, and navigator.getGamepads() returns an array, so we need an array. But we would be eventful. This is where dances with a tambourine begin: to inherit an Array you need to add a short line in the constructor:
  constructor: (items...) -> @splice 0, 0, items... 


But this is not enough for us, we would still have EventTargetEmiter inherit. This could not be done directly in the cofescript. Therefore, I was helped by a simple function that passes the methods and properties to this :
 _implements = (mixins...) -> for mixin in mixins @::[key] = value for key, value of mixin:: @ 


So it turned out to be a simple array class of events, only the constructor does not accept the length of the array:
 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... 



Then everything was relatively trivial: blocks, buttons, sticks, creating a structure. This routine, in my opinion it makes no sense to describe, because there is nothing new or non-trivial in it.



Total:



Created Gamepads for working with joysticks, as well as Gamepad2 and GamepadMap for manual and fine settings.

The standard of the recommendations and "white spots" is bad. There are a lot of not obvious moments.

The joystick can not be accessed from the worker. It can be harmful if the main logic is in it.

Chrome tries to present everything in the best possible way, but reject unknown joysticks, and this, in my opinion, is a bust (albeit logical). Mozilla gives us everything “as it is” and “frantic as you want.”

References:

Tester

Source

Coffeescript width C-preprocessor.

Source: https://habr.com/ru/post/242835/


All Articles