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.