GamepadAPI or joystick in browser

    Hello, Habr!





    Watching how more and more new technologies are being introduced into the web, watching how games are transferred to it, I thought: “It would be cool if the gamepad could also be connected ...”. And in the search, the very first result was GamepadAPI .
    A little lower link to the W3C GamepadAPI . Having looked, tried, I found a number of problems, pitfalls that would put an end to the introduction 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 finalized, changed and, in my opinion, improved, is described under the cut.



    What is in GamepadAPI?



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

    navigator.getGamepads();return an array of joysticks, Gamepad objects.
    Events in the facility to connect and disconnect the joystick window(namely, obtaining of joysticks navigator, and events window) "gamepadconnected", "gamepaddisconnected".
        window.addEventListener("gamepadconnected", function(e) {...})
    

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

    The object itself Gamepadhas the following properties:
    • idcontains vendor id, product id (USB) and description. The recording format is not regulated;
    • index which account is connected;
    • mapping the line in which the remapping was written and if so, which one;
    • connected Is the joystick connected?
    • timestamp DOMHighResTimeStamp when the joystick data was last updated;
    • axes array of axes and values ​​from -1 to 1;
    • buttonsan array of buttons, objects containing pressed (boolean)and value[0; 1] since Since triggers can have a smooth change in value, this should be taken into account sometimes.


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


    Because chromium tried to introduce support ahead of the rest, focusing on drafts, so there are more reservations for browsers with only the webkit prefix:
    • no connected;
    • no connect and disconnect events. Worse: for the array returned navigator.webkitGetGamepads()to appear, the joystick must be active during the call to this function (for example, the button is pressed);
    • maping empty, although remapping is;
    • buttons an array of values, not objects.

    Part of the problems has gone through time and manifests itself even after full support of the standard (i.e., exist in all versions of chromiums, where there are joysticks in general):
    • If the joystick model is unknown, then it does not exist at all. In this case, ff at least gives the interface “as is”, though he doesn’t know all the models ( after looking at the source you can see that he calls the OS API to work with joysticks standardly and without troubles );
    • Objects are Gamepadnot updated until called navigator.webkitGetGamepads()or navigator.getGamepads()(if there is one, and if there is one, but to call the old version, then attention will be thrown and nothing will be updated at all). Those. getting an object from a function does not have to get it again, but it is just necessary to call this function.



    What and how did you cultivate?



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

    A little more about the preprocessor ...
    Whoever is not familiar with it, but is familiar with PHP, the preprocessor includes files in the same way as include and defines constants in the same way as define, then here they are. You can find a normal description about the si preprocessor at Kernigan and Richie, as well as on the open spaces of the world wide web.

    For those who are familiar, I’ll say that define in a functional style will not work, and transferring definitions through the command line (-DDEBUG for example) is not yet possible. (inclusion folders are possible). Otherwise, the standard implemented extremely close to C ++ 11, including inclusion folders, replacements in replacements, conditional statements. But there are no source constants, and include retains the indentation (it includes the file by adding indentation before lines equal to the indentation on which the directive is written. This is necessary due to the syntax of the language).



    The first two problems that got out right away:
    1. Association of elements or mapping. In firefox it is not, in chromium is.
    2. The lack of events. You cannot take and hang the listener on a button or stick.



    Association of elements or mapping.


    For convenience, I divided the joystick buttons into logical blocks.
    • dpad or, in the people, a crosspiece
    • lrtb triggers and bumpers (don't know what to call)
    • menu menu buttons
    • axes sticks and their buttons
    • face main action buttons

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


    Insolently taking the source code of the button associations from the chromium project, I created association maps for joysticks. It turns out that they depend on platforms, which means that for windows and for a penguin they differ from macs. But what if it is a new and / or little-known joystick? In this case, the class GamepadMapdelivered separately. An object created from this class can be passed to the interface constructor.


    But not always so bad! It happens that associations are normal. To distinguish ready-made mapping from raw, 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 the association map by getting from the property"id"VID and PID. This is not safe on the one hand, but on the other, 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 already ready, as I wrote above.


    We introduce events.


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


    And then I learned Zen in 5 steps:



    Getting status.


    Because Since the W3C makes no recommendations at all about changing the state of the Gamepad object depending on the actual state change, the chromium did not bother that the first (in the first couple), and the second time (supporting the standard fully): the properties of the Gamepad object are only updated when polling through navigator.getGamepads()or navigator.webkitGetGamepads(). In firearms, everything is simpler, the state is updated automatically. Therefore, if webkit, then pull this method every time before polling.



    EventTarget interface.


    I wanted to recreate the EventTargetinterface for the elements, but you can not just take and create extends EventTarget. I had to "kneel" my implementation, but observing the standard. Why not get ready-made Emet? There is no close observance of the standard in it, but I wanted to do everything standardly where possible.

    Some useful methods like on, off, emet, chain and voila, class EventTargetEmiter:
    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 property is _subscribeaccessible from the outside, but it does not matter who rules the protective properties (with emphasis) ready to shoot himself in the leg. You can assign a parent object to the object, in which a pop-up event will be transmitted.


    Event and CustomEvent.


    To understand who caused the event, you should create it Event, but they simply Eventdo not allow us to create and set properties for it. It comes to the rescue CustomEvent, in which the property is detailcustomizable. And so that the event is raised in the parent elements, do not forget to set it canBubblein truethe constructor.



    Poll states or pooling.


    In all examples related to GamepadAPI, state polls are used requestAnimationFrame. There is a plus and minus in this: the
    plus is that when the window is not active, then there is no need to interrogate the state.
    But on the other hand, if this is a game, then this call is necessary for rendering, otherwise the smoothness of the animation may suffer.
    Therefore, I decided to go the alternative "old" way: focus/blurfor the window, setIntervalfor the scheduler and a single requestAnimationFramefor the first run (because the window can load in the background). Thus, the browser itself will deal with the list of tasks, perform the necessary between rendering.
    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? Did you forget how we played together?


    The system can be registered with several joysticks. It also navigator.getGamepads()returns an array, so we need an array. But we would have an event. Here dances with a tambourine begin: to inherit, Arrayyou need to add a short line in the constructor:
      constructor: (items...) ->
        @splice 0, 0, items...
    


    But this is not enough for us, we still have to EventTargetEmiterinherit. It did not work out directly in coffee script. Therefore, I was helped by a simple function that passes methods and properties to this:
    _implements = (mixins...) ->
      for mixin in mixins
        @::[key] = value for key, value of mixin::
      @
    


    So we got a simple array class with 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, does not make sense to describe, because there is nothing new or nontrivial in it.



    Total:



    I created Gamepadsto work with joysticks, as well as Gamepad2and GamepadMapfor hand and fine tuning.

    The standard of recommendations and white spots is bad. There are so many not obvious moments.

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

    Chrome is trying to present everything in the best possible way, but reject unknown joysticks, and this, in my opinion, is too much (although logical). Mozilla gives us everything "as is" and "rage as you want."

    Links:

    Tester

    Source code

    Coffeescript width C-preprocessor.

    Also popular now: