Better controlling with gamepad

Over the last several years, we have seen a very welcome and robust list of new APIs added to HTML5. These include WebSockets, canvas, local storage, WebGL, and many more. In the context of game development, the next natural step was to add standard support for a gamepad.

Similar to the fullscreen mode, the gamepad API is still in the very early drafting stages. In fact, gamepad support is even more "primitive" than fullscreen. Although you will find browser support to be adequate, working with the API can be buggy and somewhat unpredictable. However, the gamepad API does provide a good enough interface for a great end user experience. As the specification matures, the prospect of adding a gamepad to the browser is very exciting and promising.

The first thing you'll notice about the gamepad API is the way in which it differs from all other input APIs in the DOM as it is not driven by events such as a mouse or keyboard. For example, although every input with a keyboard triggers an event (in other words, a registered callback is invoked), input from a connected gamepad can only be detected by manually polling the hardware. In other words, the browser will fire gamepad-related events to let you know that a gamepad has connected and disconnected. However, beyond these kinds of events, the browser does not fire an event every time a key is pressed on a connected gamepad.

To start using a gamepad in your game, you will first need to wait for one to connect to the game. This is done by registering a callback to listen to the global gamepadconnected event:

/**
 * @type {GamepadEvent} event
 */
function onGamepadConnected(event) {
    var gamepad = event.gamepad;
}

window.addEventListener('gamepadconnected', onGamepadConnected);

The gamepadconnected event will fire any time a gamepad is connected to your computer during the lifetime of your game. If a gamepad is already connected before the script loads, the gamepadconnected event will not fire until the player presses a button on the gamepad. While this may seem weird at first, this restriction was put in place for a very good reason—namely, to protect the player from being fingerprinted by ill-intentioned scripts. However, requiring the user to press a button before the controller is activated is not that big of a deal since the player will need to press a button at some point if he or she wishes to play your game. The only drawback to this, as you can imagine, is that we won't know right off the bat whether the user has a gamepad connected to the computer yet. Still, coming up with creative solutions to work around this limitation is not too difficult of a task.

The GamepadEvent object exposes a gamepad property, which is a reference to the actual Gamepad object, which is what we're after. The interesting thing about this object is that it is not self updating like other objects in JavaScript. In other words, whenever the browser receives input from a connected gamepad, it keeps track of its state internally. Then, once you poll the gamepad state, the browser creates a new Gamepad object with all the updated properties to reflect the current state of the controller.

function update(){
    var gamepads = navigator.getGamepads();
    var gp_1 = gamepads[0];

    if (gp_1.buttons[1].pressed) {
        // Button 1 pressed on first connected gamepad
    }

    if (gp_1.axes[1] < 0) {
        // Left stick held to the left on first connected gamepad
    }

    requestAnimationFrame(update);
}

During each update cycle, you will need to obtain the most recent snapshot of the gamepad object and look up its state.

The Gamepad object interface defines no methods, but its several properties are as follows:

interface Gamepad {
    readonly attribute DOMString id;
    readonly attribute long index;
    readonly attribute boolean connected;
    readonly attribute DOMHighResTimeStamp timestamp;
    readonly attribute GamepadMappingType mapping;
    readonly attribute double[] axes;
    readonly attribute GamepadButton[] buttons;
};

The id attribute describes the actual hardware connected to the application. If you connect a gamepad through some USB adapter, it is likely that the id will reference the adapter device rather than the actual controller that was used.

The index will reference the Gamepad object within the GamepadList object, which is what the browser provides in response to navigator.getGamepads(). Using this index value, we can get a reference to a specific gamepad that we wish to query.

As expected, the boolean connected property indicates whether a particular gamepad is still connected to the application. If a gamepad disconnects prior to a call to navigator.getGamepads(), the corresponding element that is based on a Gamepad.index offset will be null in the GamepadList. However, if a reference to a Gamepad object is obtained, but the hardware disconnects, the object will still have its connected property set to true because those properties are not dynamically updated. In summary, this property is superfluous and will probably be removed from the spec in future updates.

We can check when the browser last updated the gamepad state by looking at the timestamp attribute on a Gamepad object.

A particularly interesting attribute is mapping. The idea behind this is that there can be several standard mappings so as to make it easier to wire up the application corresponding to the way the hardware is laid out.

Better controlling with gamepad

Currently, there is only one standard mapping, which can be identified by the name standard, as demonstrated previously (for more information refer to, Gamepad W3C Working Draft 29 April 2015. http://www.w3.org/TR/gamepad). If the browser doesn't know how to layout the controller, it will respond with an empty string for the mapping attribute and map the buttons and axes in the best way that it can. In such cases, the application should probably ask the user to manually map the buttons that the application uses. Keep in mind that there are cases where the d-pad buttons are mapped to one of the axes, so handle each case with care:

var btns = {
        arrow_up: document.querySelector('.btn .arrow-up'),
        arrow_down: document.querySelector('.btn .arrow-down'),
        arrow_left: document.querySelector('.btn .arrow-left'),
        arrow_right: document.querySelector('.btn .arrow-right'),

        button_a: document.querySelector('.buttons .btn-y'),
        button_b: document.querySelector('.buttons .btn-x'),
        button_x: document.querySelector('.buttons .btn-b'),
        button_y: document.querySelector('.buttons .btn-a'),

        button_select: document.querySelector('.controls .btn- select'),
        button_start: document.querySelector('.controls .btn- start'),

        keyCodes: {
            37: 'arrow_left',
            38: 'arrow_up',
            39: 'arrow_right',
            40: 'arrow_down',

            32: 'button_a',
            65: 'button_b',
            68: 'button_x',
            83: 'button_y',

            27: 'button_select',
            16: 'button_start'
        },

        keyNames: {
            axe_left: 0,
            axe_left_val: -1,

            axe_right: 0,
            axe_right_val: 1,

            axe_up: 1,
            axe_up_val: -1,

            axe_down: 1,
            axe_down_val: 1
        }
    };

    Object.keys(btns.keyCodes).map(function(index){
        btns.keyNames[btns.keyCodes[index]] = index;
    });

function displayKey(keyCode, pressed) {
    var classAction = pressed ? 'add' : 'remove';

    if (btns.keyCodes[keyCode]) {
        btns[btns.keyCodes[keyCode]].classList[classAction]('active'),
    }
}

function update(now) {
        requestAnimationFrame(update);

        // GamepadList[0] references the first gamepad that connected to the app
        gamepad = navigator.getGamepads().item(0);

        if (gamepad.buttons[0].pressed) {
            displayKey(btns.keyNames.button_x, true);
        } else {
            displayKey(btns.keyNames.button_x, false);
        }

        if (gamepad.buttons[1].pressed) {
            displayKey(btns.keyNames.button_a, true);
        } else {
            displayKey(btns.keyNames.button_a, false);
        }

        if (gamepad.buttons[2].pressed) {
            displayKey(btns.keyNames.button_b, true);
        } else {
            displayKey(btns.keyNames.button_b, false);
        }

        if (gamepad.buttons[3].pressed) {
            displayKey(btns.keyNames.button_y, true);
        } else {
            displayKey(btns.keyNames.button_y, false);
        }

        if (gamepad.buttons[8].pressed) {
            displayKey(btns.keyNames.button_select, true);
        } else {
            displayKey(btns.keyNames.button_select, false);
        }

        if (gamepad.buttons[9].pressed) {
            displayKey(btns.keyNames.button_start, true);
        } else {
            displayKey(btns.keyNames.button_start, false);
        }

        if (gamepad.axes[btns.keyNames.axe_left] === btns.keyNames.axe_left_val){
            displayKey(btns.keyNames.arrow_left, true);
        } else {
            displayKey(btns.keyNames.arrow_left, false);
        }

        if (gamepad.axes[btns.keyNames.axe_down] === btns.keyNames.axe_down_val) {
            displayKey(btns.keyNames.arrow_down, true);
        } else {
            displayKey(btns.keyNames.arrow_down, false);
        }

        if (gamepad.axes[btns.keyNames.axe_up] === btns.keyNames.axe_up_val) {
            displayKey(btns.keyNames.arrow_up, true);
        } else {
            displayKey(btns.keyNames.arrow_up, false);
        }

        if (gamepad.axes[btns.keyNames.axe_right] === btns.keyNames.axe_right_val) {
            displayKey(btns.keyNames.arrow_right, true);
        } else {
            displayKey(btns.keyNames.arrow_right, false);
        }
    }

    window.addEventListener('gamepadconnected', function (e) {
        update(0);
    });

The preceding example connects a gamepad with no recognizable mapping; thus, it assigns each button to a specific layout. Since the d-pad buttons map to the left axis in this particular case, we check for that state when we want to determine whether the d-pad is being used. The output of this demonstration can be seen as follows:

Better controlling with gamepad

Often, you might wish to offer the user the ability to choose the way they would prefer to interact with your game—using a keyboard and mouse, a gamepad, or both. In the previous example, this is precisely the reason why the btns object referenced seemingly random and arbitrary keyCode values. These values are mapped to specific keyboard keys so that the player could use the arrow keys on a standard keyboard or a gamepad.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.18.141