Game Dev Session – Completed Canvas Gamepad Movement

Planning

Yesterday, I left off with the gamepad controls working, but only in a way that basically broke all of the challenge of the game. The change allowed the player to move outside the defined player boundaries, which meant they could shoot from below the canvas with impunity. Hurray! Today, I’m going to fix that.

Adding a Layer of Abstraction

First, directional control functions were moved from gamePiece to controls. Another layer of abstraction was added to handle getActiveDirection checking. Previously, it called movementKeys[targetDirection] to get the state of all directions (true/false). Now it calls checkDirection, which in turn calls movementKeys[direction] to handle the keys, and ORs that with checkDirectionOfLeftStick, which does some Math.ceil/floor logic to get the current direction.

Before:

getActiveDirection : function() {
  movementKeys = this.movementCodes;
  directionResults = {
    upRight : this.keysPressed(movementKeys['upRight']) && movementKeys.belowTop && movementKeys.insideRight,
    ...
  };
  return Array.from(Object.keys(directionResults)).find(direction => directionResults[direction]);
},

After:

getActiveDirection : function() {
  boundaries = this.boundaries;
  directionResults = {
    upRight : this.checkDirection('upRight') && boundaries.belowTop && boundaries.insideRight,
    ...
  };
  return Array.from(Object.keys(directionResults)).find(direction => directionResults[direction]);
},

The movementKeys object was renamed to boundaries, and this.checkDirection(direction) is called instead of returning directly from the keysPressed map.

The checkDirection function will check both the controller and keyboard states. See the gamepad values conversion in action.

checkDirection : function(direction) {
  return this.checkDirectionOfKeysPressed(this.movementCodes[direction]) || this.checkDirectionOfLeftStick(direction);
},

And checkDirectionOfLeftStick compares the x,y values of the controls.activeLeftStick object, which contains the values detected by the detectControllerMovement function, with the predefined movementCoordinates object, which just maps a direction to coordinate values.

checkDirectionOfLeftStick : function(direction) {
  stickValues = this.activeLeftStick;
  if (!stickValues) {
    return false;
  };
  compareObj = this.movementCoordinates[direction];
  return
    compareObj.x == (stickValues.x >= 0 ? Math.ceil(stickValues.x) : Math.floor(stickValues.x))
      &&
    compareObj.y == (stickValues.y >= 0 ? Math.ceil(stickValues.y) : Math.floor(stickValues.y))
  ;
},

controls.movementCoordinates: {
  upRight : {x : 1, y : -1},
  downRight : {x : 1, y : 1},
  downLeft : {x : -1, y : 1},
  upLeft : {x : -1, y : -1},
  up : {x : 0, y : -1},
  right : {x : 1, y : 0},
  down : {x : 0, y : 1},
  left : {x : -1, y : 0},
},

Commits:
35732f9
4a87dca
9a85375

Sticky Boundaries, Yuck

Once the direction logic was straightened out (though perhaps still in need of some refactor treatment), I could control the player with both WASD and the gamepad’s left stick, but the gamePiece sticks on the boundaries of the play area. This is because the direction boolean check is still triggering, say, upLeft when the piece cannot move in the left direction, when left should be ignored. So it’s being moved, then moved back, making it appear to stick.

At first, I couldn’t figure out why this was happening, since the booleans in the directionResults object should be ignoring an upLeft if boundaries.insideLeft is false. Then I realized that the speed update to the gamePiece was completely ignoring these checks and using the original values of the activeLeftStick object, regardless of the direction filter. This meant that at least one of upLeft/downLeft/upRight/downRight was almost always active, since this value was being retained even when the boundary was checked.

I think another layer of control is needed on the activeLeftStick object.

The below solution partially solves the problem, but the piece still gets stuck in the corners. This makes sense, since it disallows movement in the target direction of those conditions are met, but doesn’t account for those conditions being met when attempting to move in the opposite direction.

Before:

leftStickValues = {
  x : Math.abs(movementAxes[0]) > 0.15 ? movementAxes[0] : 0,
  y : Math.abs(movementAxes[1]) > 0.15 ? movementAxes[1] : 0,
};

After:

leftStickValues = {
  x : Math.abs(movementAxes[0]) > 0.15 && this.boundaries.insideLeft && this.boundaries.insideRight ? movementAxes[0] : 0,
  y : Math.abs(movementAxes[1]) > 0.15 && this.boundaries.aboveBottom && this.boundaries.belowTop ? movementAxes[1] : 0,
};

To properly handle the corner cases, the leftStickValues will need to be modified after the direction is determined. This can be done in a new checkDirectionOfLeftStick function, after the compareResult is obtained:

if (compareResult) {
  this.alignLeftStickValuesToBoundaries(direction);
};

controls.alignLeftStickValuesToBoundaries:

alignLeftStickValuesToBoundaries : function(direction) {
  if (direction == "upRight") {
    if (!this.boundaries.insideRight) {
      this.activeLeftStick.x = 0;
    }
    if (!this.boundaries.belowTop) {
      this.activeLeftStick.y = 0;
    }
  };
  if (direction == "upLeft") {
    if (!this.boundaries.insideLeft) {
      this.activeLeftStick.x = 0;
    }
    if (!this.boundaries.belowTop) {
      this.activeLeftStick.y = 0;
    }
  };
  if (direction == "downRight") {
    if (!this.boundaries.insideRight) {
      this.activeLeftStick.x = 0;
    }
    if (!this.boundaries.aboveBottom) {
      this.activeLeftStick.y = 0;
    }
  };
  if (direction == "downLeft") {
    if (!this.boundaries.insideLeft) {
      this.activeLeftStick.x = 0;
    }
    if (!this.boundaries.aboveBottom) {
      this.activeLeftStick.y = 0;
    }
  };
},

This is not the easiest to follow. Fortunately, there is some commonality between the edge cases, so it can be cleaned up a bit:

controls.alignLeftStickValuesToBoundaries refactor:

alignLeftStickValuesToBoundaries : function(direction) {
  let watchDirections = {
    'up' : ['upRight', 'upLeft'],
    'down' : ['downRight', 'downLeft'],
    'left' : ['upLeft', 'downLeft'],
    'right' : ['upRight', 'downRight'],
  };
  this.activeLeftStick.y = watchDirections.up.includes(direction) && !this.boundaries.belowTop ? 0 : this.activeLeftStick.y;
  this.activeLeftStick.y = watchDirections.down.includes(direction) && !this.boundaries.aboveBottom ? 0 : this.activeLeftStick.y;
  this.activeLeftStick.x = watchDirections.left.includes(direction) && !this.boundaries.insideLeft ? 0 : this.activeLeftStick.x;
  this.activeLeftStick.x = watchDirections.right.includes(direction) && !this.boundaries.insideRight ? 0 : this.activeLeftStick.x;
},

No more sticky mess!

Commit: 980c34b

Weighting the Movement

Finally, the values of the controls.activeLeftStick object need to be applied to controls.getPositionModifiers function to get the appropriate weighted movement.

The activeLeftStick values are checked for non-zero, and if the check passes, the left stick’s weighted value is multiplied by the default gamePieceSpeed. Math.abs is used to accommodate the existing logic for the keyboard controls, where the direction is applied through logic rather than the input (as the controller does). Since we already know the direction, we no longer need the sign of the stick coordinate.

Before:

getPositionModifiers : function() {
  baseSpeed = knobsAndLevers.gamePieceSpeed;
  return {
    upRight: {x : baseSpeed, y : -baseSpeed},
    upLeft: {x : -baseSpeed, y : -baseSpeed},
    downRight: {x : baseSpeed, y : baseSpeed},
    downLeft: {x : -baseSpeed, y : baseSpeed},
    right: {x : baseSpeed},
    down: {y : baseSpeed},
    left: {x : -baseSpeed},
    up: {y : -baseSpeed},
  };
},

After:

getPositionModifiers : function() {
  baseSpeed = knobsAndLevers.gamePieceSpeed;
  objSpeed = {
    x : this.activeLeftStick.x ? baseSpeed * Math.abs(this.activeLeftStick.x) : baseSpeed,
    y : this.activeLeftStick.y ? baseSpeed * Math.abs(this.activeLeftStick.y) : baseSpeed,
  };
  return {
    upRight: {x : objSpeed.x, y : -objSpeed.y},
    upLeft: {x : -objSpeed.x, y : -objSpeed.y},
    downRight: {x : objSpeed.x, y : objSpeed.y},
    downLeft: {x : -objSpeed.x, y : objSpeed.y},
    right: {x : objSpeed.x},
    down: {y : objSpeed.y},
    left: {x : -objSpeed.x},
    up: {y : -objSpeed.y},
  };
},

Commit: 899d5e0

Post Session

Finally! Gamepad controls are done (for a single player).

I think at this point, some additional unit tests are needed, and the falling bug needs the add-a-mushroom feature. I’m targeting the end of the week to be feature complete.

Also, as I was working on this, I felt the urge to add a second player, so I’ve added a new issue for that. That should be fun, and will probably trigger a host of refactors to what I did today with the gamepad controls.

Leave a Reply