Game Dev Session – Canvas Gamepad Support

Today, I’m adding controller support to Centipede. (See the first controller commit.)

Controls Code Cleanup

First, the location of the controls currently doesn’t make a lot of sense, so I’m moving them to a new file, controls.js. On load, controls.addEventListeners will execute. There are no dependencies, so the script load order doesn’t matter. Previously these were on the gameArea object because that’s where the keys tracker was stored, but the keys tracker can just live in the controls object.

I ran into problems when I changed the gameArea qualifiers on the keysDown list to this inside the window.addEventListeners function calls.

Uncaught TypeError: Cannot set property 'LMB' of undefined
    at controls.js:5

This makes sense, since window will have no reference to controls once the event listener is set. I changed the reference back to explicit instead: controls.keysDown.

This is now the setup for mouse/keyboard control:

window.addEventListener('mousedown', function (e) {
  controls.keysDown['LMB'] = (e.type === "mousedown" && event.which === 1);
});
window.addEventListener('mouseup', function (e) {
  controls.keysDown['LMB'] = (e.type === "mousedown" && event.which === 1);
});
window.addEventListener('keydown', function (e) {
  controls.keysDown[e.keyCode] = (e.type == "keydown");
});
window.addEventListener('keyup', function (e) {
  controls.keysDown[e.keyCode] = (e.type == "keydown");
});

Basically, key and mouse presses are tracked by updating a key-value pair of the keysDown object on mouse/keydown (True), or mouse/keyup (False). The controls algorithms then look for specific key codes each cycle, and if they’re True, action happens.

// definition
var controls = {
  fireKeyCodes : [16, 37, 38, 39, 40, 'LMB'],
  isFiring : function() {
    for (let key of this.fireKeyCodes) {
      if (this.keysDown[key]) {
        return true;
      };
    };
  },
};
// each cycle
  if (controls.isFiring())
    // do the thing
)

The above controls.isFiring() can be reduced by using Array.prototype.find, which returns the first match (which would be truthy), or if no matches, undefined (falsey):

isFiring : function() {
  return this.fireKeyCodes.find(key => this.keysDown[key]);
},

To add a firing option for the controller, we just need to identify how to capture the code with an event listener, add it to the keysDown object when pressed, and add it to the fireKeyCodes array.

Exploring the GamePad API

According to MDN, the GamePad API should do everything needed. Throwing on the first listener example for gamepadconnected gets us a readout when the gamepad is turned on.

Listener

window.addEventListener("gamepadconnected", function(e) {
  console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
    e.gamepad.index, e.gamepad.id,
    e.gamepad.buttons.length, e.gamepad.axes.length);
});

Readout when connecting the adapter in X-input mode:

controls.js;37 Gamepad connected at index 0: HJZ Mayflash WiiU Pro Game Controller Adapter (Vendor: 0079 Product: 1800). 12 buttons, 6 axes.
controls.js:37 Gamepad connected at index 1: HJZ Mayflash WiiU Pro Game Controller Adapter (Vendor: 0079 Product: 1800). 12 buttons, 6 axes.
controls.js:37 Gamepad connected at index 3: HJZ Mayflash WiiU Pro Game Controller Adapter (Vendor: 0079 Product: 1800). 12 buttons, 6 axes.
controls.js:37 Gamepad connected at index 2: HJZ Mayflash WiiU Pro Game Controller Adapter (Vendor: 0079 Product: 1800). 12 buttons, 6 axes.

Readout when connecting the adapter in D-input mode:

Gamepad connected at index 0: HJZ Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e). 17 buttons, 4 axes.

With the following, I can read the buttons by looking at the navigator object:

for (let button of navigator.getGamepads()[3].buttons) {
  // console.log(button.pressed)
  console.log(button.value)
}

With the above, button.value prints as 0 by default, 1 when pressed, and button.pressed prints as false by default, true when pressed.

With this I can determine which buttons map to which indices in the buttons Array. Here are the mappings I discovered, and how I plan to use them.

buttonMappings = {
  'Y' : 0,
  'B' : 1,
  'A' : 2,
  'X' : 3,
  'L1' : 4,
  'R1' : 5,
  'L2' : 6,
  'R2' : 7,
  '-' : 8,
  '+' : 9,
  'L3' : 10,
  'R3' : 11,
};
fireButtonIndices : [0, 1, 2, 3, 4, 5, 6, 7],
pausedButtonIndices : [9],

Investigating the Mayflash Controller Adapter

I’m using a Wii U pro controller with a Mayflash wireless controller adaptor for PC USB on Xinput mode. I get four instances of the controller, which is really interesting. This means the adaptor is capable of pairing up to 4 controllers at a time (I’ll test this out later with a second controller). I had thought that D-input was the multi-input, but this makes more sense. X: cross, D: direct.

When testing, I’m only seeing activity on one of the gamepads (as expected). Unfortunately, with the Mayflash adapter, there’s no way to tell which index has a connected controller, since it fires up all 4 connectors, regardless of whether a controller is actually connected to the adapter.

...
connected: true
id: "HJZ Mayflash WiiU Pro Game Controller Adapter (Vendor: 0079 Product: 1800)"
index : 0
...
connected: true
id: "HJZ Mayflash WiiU Pro Game Controller Adapter (Vendor: 0079 Product: 1800)"
index : 1
...
And so on

I was only able to discover the correct index for the active controller by trying all of them. During gameplay, I’ll have to check all four gamepad indices and every button on those gamepads every game loop to see which one is actually accepting inputs, since Mayflash is connecting all 4. This just seems insane. Worst case scenario, I can find the correct controller once, once buttons are pressed, then stop checking, but since I can’t tie an event listener directly to each button, I’ll have to check periodically, or have a toggle in the UI for controller activation.

Working Fire Button Detection!

I added the toggle checkbox to the UI. Each gameloop, the checkbox state will be determined. If the checkbox is checked, gamepads will be read from the navigator, and the buttons of each gamepad will be checked until input is received. Once it is received, the gamepads index will be stored in a controllerIndex variable, and this check will not occur again until the checkbox state changes. It’s a bit messy, and I’ll make another pass at it later. It could be modified to allow me to register players in order. First detected button push would be player 1, then that index will get ignored on the next check cycle, second detected push would be player 2, etc.

gameHandler.checkControllerState : function() {
  controllerEnabled = document.getElementById("controllerToggle").checked;
  if (!controllerEnabled) {
    controllerIndex = -1;
    return
  };
  let gamepads = navigator.getGamepads();
  if (!controllerIndex) {
    for (let i = 0; i < gamepads.length; i++) {
      if (!gamepads[i]) {
        return;
      };
      buttons = gamepads[i].buttons;
      for (let j = 0; j < buttons.length; j++) {
        if (buttons[j].pressed) {
          controllerIndex = i;
            break;
        };
      };
    };
  };
}

To actually detect fire inputs, a similar approach is used. If controllerEnabled is true (checkbox checked) and the controllerIndex has been detected, only then will the buttons be read. It gets the buttons array from the active controller, checks them against the allowed firing buttons, and if a match is found, returns true. If no match is found, it moves on to check the keysDown array, as before.

controls.isFiring : function() {
  if (controllerEnabled && controllerIndex >= 0) {
    buttons = navigator.getGamepads()[controllerIndex].buttons;
    for (let i = 0; i < buttons.length; i++) {
      if (buttons[i].pressed && this.fireButtonIndices.includes(i)) {
        return true;
      }
    };
  }
  // previous keyboard/mouse detection
  return this.fireKeyCodes.find(key => this.keysDown[key]);
},

Next time I’ll tackle movement, then once it’s working, take time to reconsider the gratuitous use of for loops and see if there’s a more elegant approach.

Leave a Reply