Multiplayer javascript game input

- 5 mins
Hello1
ViewSource code

Multiplayer input support for a Javascript / Typescript game

In this tutorial we are going to add support for multiple players to use our input system. This tutorial will be fairly simple as we have already laid the complicated input groundwork and now we can reap the rewards by extending mostly our types to include up to four players.

Extending our types to handle 4 player input

Similar to previous tutorials we will start by extending our types to support 4 players.

In our Game specific types.ts file extend the GameState type with the following extra player fields so that it is the same as below.

export type GameState = {
  player1: PlayerState,
  player2: PlayerState,
  player3: PlayerState,
  player4: PlayerState,
  inputs: {
    standardGameInputFourPlayer: StandardGameInputFourPlayer,
    playerInputMappings: PlayerInputs
  }
}

Next in our input types.ts file extend the following types.

Update the PlayerInputs type to include 4 players as below. Remember the PlayerInput is responsible for defining the players current input device and different input control mappings.

export type PlayerInputs = {
  player1: PlayerInput,
  player2: PlayerInput,
  player3: PlayerInput,
  player4: PlayerInput
}

Finally update the StandardGameInputFourPlayer type to include 4 players input as below. The StandardGameInput represents a typical gamepad.

export type StandardGameInputFourPlayer = {
  player1: StandardGameInput,
  player2: StandardGameInput,
  player3: StandardGameInput,
  player4: StandardGameInput
}

That is all the type related changes we need to make in order to support 4 players.

Updating the Gamestate

We will need to update our GameState with default values for the other 3 players to match the new GameState extensions we added in the previous section.

Working in the GameConstants.ts class lets make the following adjustments. Add a new player2 field under the player1 field at the top level and set it to the InitialPlayerState type as below.

player2: InitialPlayerState,
player3: InitialPlayerState,
player4: InitialPlayerState,

Add a player2, player3 and player4 field in the StandardGameInputFourPlayer field set to the StandardGameInputStateNeutral type as below.

standardGameInputFourPlayer: {
  player1: StandardGameInputStateNeutral,
  player2: StandardGameInputStateNeutral,
  player3: StandardGameInputStateNeutral,
  player4: StandardGameInputStateNeutral
}

Finally update the playerInputMappings field to include the new players mapping configuration as below, make sure to set the first player to a current keyboard input and the rest to a gamepad if you have one plugged in, otherwise you can set all the players to a keyboard input instead.

playerInputMappings: {
  player1: {
    current: 'keyboard',
    gamepadMapping: GamepadMappingNeutral,
    keyboardMapping: KeyboardMappingNeutral
  },
  player2: {
    current: {id: 0},
    gamepadMapping: GamepadMappingNeutral,
    keyboardMapping: KeyboardMappingNeutral
  },
  player3: {
    current: {id: 0},
    gamepadMapping: GamepadMappingNeutral,
    keyboardMapping: KeyboardMappingNeutral
  },
  player4: {
    current: {id: 0},
    gamepadMapping: GamepadMappingNeutral,
    keyboardMapping: KeyboardMappingNeutral
  }
}

In the GameRuntime.ts's draw method add the following debug logs for player's 1 and 2 to print their current input state.

context.fillText(`P1: ${JSON.stringify(GameRuntime.gameState.player1.direction)}`, 0, 10)
context.fillText(`P2: ${JSON.stringify(GameRuntime.gameState.player2.direction)}`, 0, 30)
context.fillText(`P1: ${JSON.stringify(GameRuntime.gameState.inputs.standardGameInputFourPlayer.player1)}`, 0, 50)
context.fillText(`P2: ${JSON.stringify(GameRuntime.gameState.inputs.standardGameInputFourPlayer.player2)}`, 0, 70)

Run the game and confirm that you can see the default values set for player 2. Note only player 1's values will update when you use their corresponding input but we'll change that in the next section.

Update InputManager to return input for multiple players

Currently our InputManager's getInputs method takes a single PlayerInput and returns a single StandardGameInput type. This was okay for single player but we now want it to take our PlayerInputs type and return our wrapping StandardGameInputFourPlayer type instead.

In InputManager.ts change the getInputs method signature to take a PlayerInputs type and return a StandardGameInputFourPlayer type instead of a StandardGameInput type. Next retrieve all the individual players inputs via the InputManager.getStandardGameInput method and return them. The getInputs method should match below.

static getInputs = (playerInputs: PlayerInputs): StandardGameInputFourPlayer => {

  const inputState = InputStateManager.getInputs()
  const gamepads = navigator.getGamepads()

  const player1StandardGameInput = InputManager.getStandardGameInput(playerInputs.player1, inputState, gamepads)
  const player2StandardGameInput = InputManager.getStandardGameInput(playerInputs.player2, inputState, gamepads)
  const player3StandardGameInput = InputManager.getStandardGameInput(playerInputs.player3, inputState, gamepads)
  const player4StandardGameInput = InputManager.getStandardGameInput(playerInputs.player4, inputState, gamepads)

  return {
    player1: player1StandardGameInput,
    player2: player2StandardGameInput,
    player3: player3StandardGameInput,
    player4: player4StandardGameInput
  }
}

Updating other players state

In GameRuntime.ts we want to update each of the players state based on the retrieved inputs we receive from the InputManager. Update the GameRuntime loop method to be the same as below.

static loop = (context: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number) => {
  const inputs = InputManager.getInputs(GameRuntime.gameState.inputs.playerInputMappings)

  const player1State = Player.getPlayerState(inputs.player1, GameRuntime.gameState.player1)
  const player2State = Player.getPlayerState(inputs.player2, GameRuntime.gameState.player2)
  const player3State = Player.getPlayerState(inputs.player3, GameRuntime.gameState.player3)
  const player4State = Player.getPlayerState(inputs.player4, GameRuntime.gameState.player4)

  GameRuntime.gameState.player1 = player1State
  GameRuntime.gameState.player2 = player2State
  GameRuntime.gameState.player3 = player3State
  GameRuntime.gameState.player4 = player4State

  GameRuntime.gameState.inputs.standardGameInputFourPlayer.player1 = inputs.player1
  GameRuntime.gameState.inputs.standardGameInputFourPlayer.player2 = inputs.player2
  GameRuntime.gameState.inputs.standardGameInputFourPlayer.player3 = inputs.player3
  GameRuntime.gameState.inputs.standardGameInputFourPlayer.player4 = inputs.player4
  
  GameRuntime.draw(context, canvasWidth, canvasHeight, GameRuntime.gameState)
}

Here we are retrieving the inputs constant of type StandardGameInputFourPlayer and retrieving the individual playerState based on the respective player's StandardGameInput. We then update the GameRuntime.gameState individual PlayerState with the updated PlayerState. Finally we update the GameState's StandardGameInput for each player and call the draw method.

Run the app and confirm that player 2's debug logs for direction and input are being updated when their respective input is pressed.

Drawing the other players

If all of the players have the same starting position we won't be able to see them, we need to create a individual InitialPlayerState for each player.

Let's create some new constants in GameContants.ts for the individual player's InitialPlayerState and rename InitialPlayerState to InitialPlayer1State as below.

export const InitialPlayer1State: PlayerState = {
  position: {
    x: 50,
    y: 50
  },
  direction: {
    horizontal: 0,
    vertical: 0
  },
  top: false,
  right: false,
  bottom: false,
  left: false
}

export const InitialPlayer2State: PlayerState = {
  position: {
    x: 200,
    y: 50
  },
  direction: {
    horizontal: 0,
    vertical: 0
  },
  top: false,
  right: false,
  bottom: false,
  left: false
}

export const InitialPlayer3State: PlayerState = {
  position: {
    x: 50,
    y: 200
  },
  direction: {
    horizontal: 0,
    vertical: 0
  },
  top: false,
  right: false,
  bottom: false,
  left: false
}

export const InitialPlayer4State: PlayerState = {
  position: {
    x: 200,
    y: 200
  },
  direction: {
    horizontal: 0,
    vertical: 0
  },
  top: false,
  right: false,
  bottom: false,
  left: false
}

Next lets assign these constants to the players starting state. In GameConstants.ts set InitialGameState's player fields to the individual InitialPlayerState's as below.

player1: InitialPlayer1State,
player2: InitialPlayer2State,
player3: InitialPlayer3State,
player4: InitialPlayer4State

Finally we are printing player 2 debug logs but we aren't actually drawing any of the extra players to the screen yet. Update the GameRuntime's draw method by adding the code below to the end to draw the other players.

Draw.drawPlayer(context, gameState.player2)
Draw.drawPlayer(context, gameState.player3)
Draw.drawPlayer(context, gameState.player4)

Now run the app and you should see 4 black squares representing the different players! Depending on your input configuration you should be able to use the keyboard to control some of the squares and the Gamepad to control others. Try setting multiple players to a keyboard input with different key mappings and see that they only move when their respective mapping key is pressed.

Have a go playing around with the players input settings and confirm the different input types work. If you have two Gamepad's test out a second Gamepad by setting the id to 1.

Summary

That's it we've completed our input system! It can support keyboard and gamepad input, different key mappings and up to 4 players. I'd love to hear in the comments how you found the course and examples to games of where you've used this input system. Lookout for the last post in this course sharing a zipped version of the input code. We will use this input system in future tutorials.