Game Dev Session – Gamepad Controls Library Updates

Updating the gamepad library

Today, I’m focusing on the controls library. Goal: add right stick capturing without breaking Centipede or Robotron.

Robotron needs the right stick to handle firing. I could just leave controls.js alone and update it’s functionality in the game itself, like this:

var theControls = controls;

Then add stuff to controls like this:

theControls.aNewThing = newStuff;

Or override stuff like this:

theControls.anExistingThing = newStuff;

But I’ll have to move the right stick controls eventually, so I’ll work with controls.js directly, by importing it from canvas-libs via relative path, src="../canvas-libs/controls.js", instead of from GitLab. This way I can test changes locally as I make them.

It also occurs to me that I might have a game at some point (or I might release the controls library as open source) where the right stick will need to do something else. With that in mind, I should be writing capture and execution in separate libraries, but I’ll cross that bridge when I get to it.

Modifying the stick reading from controls. Gird yourself for code.

Commit: d260dfd

Before:

detectControllerMovement : function() {
  this.activeLeftStick = {x : 0, y : 0};
  if (this.controllerEnabled && this.controllerIndex >= 0) {
    movementAxes = navigator.getGamepads()[this.controllerIndex].axes;
    leftStickValues = {
      x : Math.abs(movementAxes[0]) > 0.15 ? movementAxes[0] : 0,
      y : Math.abs(movementAxes[1]) > 0.15 ? movementAxes[1] : 0,
    };
    if (leftStickValues.x || leftStickValues.y) {
      this.activeLeftStick = leftStickValues;
      return;
    };
  };
},

There’s a lot of unnecessary stuff here. The code that’s resetting the left stick each cycle and checking the values before setting them is not necessary.

this.activeLeftStick = {x : 0, y : 0};

Originally I had written it to only update it from origin if the values were non-zero. But since I’m setting the value of the local leftStickValues to 0 if it doesn’t meet criteria anyway, this is pointless. And since this is pointless, we can just update this.activeLeftStick directly, and detectControllerMovement becomes:

detectControllerMovement : function() {
  if (this.controllerEnabled && this.controllerIndex >= 0) {
    movementAxes = navigator.getGamepads()[this.controllerIndex].axes;
    this.activeLeftStick.x = Math.abs(movementAxes[0]) > 0.15 ? movementAxes[0] : 0;
    this.activeLeftStick.y = Math.abs(movementAxes[1]) > 0.15 ? movementAxes[1] : 0;
  };
},

Since I want to include right stick values, I changed the name to captureControllerAxes and added rightStick checks:

captureControllerAxes : function() {
  if (this.controllerEnabled && this.controllerIndex >= 0) {
    movementAxes = navigator.getGamepads()[this.controllerIndex].axes;
    this.leftStick.x = Math.abs(movementAxes[0]) > 0.15 ? movementAxes[0] : 0;
    this.leftStick.y = Math.abs(movementAxes[1]) > 0.15 ? movementAxes[1] : 0;
    this.rightStick.x = Math.abs(movementAxes[2]) > 0.15 ? movementAxes[2] : 0;
    this.rightStick.y = Math.abs(movementAxes[3]) > 0.15 ? movementAxes[3] : 0;
  };
},

Then inverted the controller state check logic to fail fast:

captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  }
  movementAxes = navigator.getGamepads()[this.controllerIndex].axes;
  this.leftStick.x = Math.abs(movementAxes[0]) > 0.15 ? movementAxes[0] : 0;
  this.leftStick.y = Math.abs(movementAxes[1]) > 0.15 ? movementAxes[1] : 0;
  this.rightStick.x = Math.abs(movementAxes[2]) > 0.15 ? movementAxes[2] : 0;
  this.rightStick.y = Math.abs(movementAxes[3]) > 0.15 ? movementAxes[3] : 0;
},

The controller state checks should mean there’s never an array out of bounds error on the movementAxes access, but it would still be prudent to ensure movementAxes has the correct number of values. This becomes more necessary when capturing the dPad, which only appears in the axes on the Dinput mode; otherwise it’s in the buttons (I don’t see the value in switching between locations, but I’m not Mayflash.)

captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  };
  movementAxes = navigator.getGamepads()[this.controllerIndex].axes;
  if (movementAxes.length >= 2) {
    this.leftStick.x = Math.abs(movementAxes[0]) > 0.15 ? movementAxes[0] : 0;
    this.leftStick.y = Math.abs(movementAxes[1]) > 0.15 ? movementAxes[1] : 0;
  };
  if (movementAxes.length >= 4) {
    this.rightStick.x = Math.abs(movementAxes[2]) > 0.15 ? movementAxes[2] : 0;
    this.rightStick.y = Math.abs(movementAxes[3]) > 0.15 ? movementAxes[3] : 0;
  };
  if (movementAxes.length >= 6) {
    this.dPad.x = Math.abs(movementAxes[5]) > 0.15 ? movementAxes[5] : 0;
    this.dPad.y = Math.abs(movementAxes[6]) > 0.15 ? movementAxes[6] : 0;
  };
},

Then, I got rid of (some of) the magic numbers.

captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  };
  movementAxes = navigator.getGamepads()[this.controllerIndex].axes;
  let x = 0;
  let y = 0;
  if (movementAxes.length >= 2) {
    x = this.axisIndices.leftStick.x;
    y = this.axisIndices.leftStick.y;
    this.leftStick.x = Math.abs(movementAxes[x]) > 0.15 ? movementAxes[x] : 0;
    this.leftStick.y = Math.abs(movementAxes[y]) > 0.15 ? movementAxes[y] : 0;
  };
  if (movementAxes.length >= 4) {
    x = this.axisIndices.rightStick.x;
    y = this.axisIndices.rightStick.y;
    this.rightStick.x = Math.abs(movementAxes[x]) > 0.15 ? movementAxes[x] : 0;
    this.rightStick.y = Math.abs(movementAxes[y]) > 0.15 ? movementAxes[y] : 0;
  };
  if (movementAxes.length >= 6) {
    x = this.axisIndices.dPad.x;
    y = this.axisIndices.dPad.y;
    this.dPad.x = Math.abs(movementAxes[5]) > 0.15 ? movementAxes[5] : 0;
    this.dPad.y = Math.abs(movementAxes[6]) > 0.15 ? movementAxes[6] : 0;
  };
},

That caused it to balloon and now I have a bunch of repeated code, so it’s subfunction time:

captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  };
  inputValues = navigator.getGamepads()[this.controllerIndex].axes;
  if (inputValues.length >= 2) {
    this.getAxis('leftStick', inputValues);
  };
  if (inputValues.length >= 4) {
    this.getAxis('rightStick', inputValues);
  };
  if (inputValues.length >= 6) {
    this.getAxis('dPad', inputValues);
  };
},
getAxis : function(axis, inputValues) {
  x = this.axes[axis].indices.x;
  y = this.axes[axis].indices.y;
  this.axes[axis].values.x = Math.abs(inputValues[x]) > 0.15 ? inputValues[x] : 0;
  this.axes[axis].values.y = Math.abs(inputValues[y]) > 0.15 ? inputValues[y] : 0;
},

After updating all of the references to activeLeftStick, everything works great, but captureControllerAxes is still pretty ugly, so let’s add a requiredArrayLength param to each axis.

axes : {
  leftStick : {
    indices : {x : 0, y : 1},
    values : {x : 0, y : 0},
    requiredArrayLength: 2,
  },
  …
},
captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  };
  inputValues = navigator.getGamepads()[this.controllerIndex].axes;
  if (inputValues.length >= this.axes.leftStick.requiredArrayLength) {
    this.getAxis('leftStick', inputValues);
  };
  if (inputValues.length >= this.axes.rightStick.requiredArrayLength) {
    this.getAxis('rightStick', inputValues);
  };
  if (inputValues.length >= this.axes.dPad.requiredArrayLength) {
    this.getAxis('dPad', inputValues);
  };
},

Now we can take it one step further and loop over the keys from our axes object to capture each axis in turn:

captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  };
  inputValues = navigator.getGamepads()[this.controllerIndex].axes;
  Array.from(Object.keys(this.axes)).forEach(axis => {
    if (inputValues.length >= this.axes[axis].requiredArrayLength) {
      this.getAxis(axis, inputValues);
    };
  });
},

So the final axis capture code is:

axes : {
  leftStick : {
    indices : {x : 0, y : 1},
    values : {x : 0, y : 0},
    requiredArrayLength: 2,
  },
  rightStick : {
    indices : {x : 2, y : 3},
    values : {x : 0, y : 0},
    requiredArrayLength: 4,
  },
  dPad : {
    indices : {x : 4, y : 5},
    values : {x : 0, y : 0},
    requiredArrayLength: 6,
  },
},
captureControllerAxes : function() {
  if (!this.controllerEnabled || this.controllerIndex < 0) {
    return;
  };
  inputValues = navigator.getGamepads()[this.controllerIndex].axes;
  Array.from(Object.keys(this.axes)).forEach(axis => {
    if (inputValues.length >= this.axes[axis].requiredArrayLength) {
      this.getAxis(axis, inputValues);
    };
  });
},
getAxis : function(axis, inputValues) {
  x = this.axes[axis].indices.x;
  y = this.axes[axis].indices.y;
  this.axes[axis].values.x = Math.abs(inputValues[x]) > 0.15 ? inputValues[x] : 0;
  this.axes[axis].values.y = Math.abs(inputValues[y]) > 0.15 ? inputValues[y] : 0;
},

This will safely capture all axis values every frame into the controls.axes object. Hurray!

Just to make sure I didn’t break anything, I also hooked it up to Centipede locally, and it worked!

Post

Next up: mapping right stick to firing for Robotron.

Leave a Reply