Create a keyboard input mapping for a 2D javascript typescript game

- 5 mins
Hello1
ViewSource code

Keyboard game input map

In the previous tutorial we setup a basic 2D keyboard player controller. The result was us being able to control a square witht the w, a, s and d keys and fire four actions via the f, g, h and j keys.

In this tutorial we will extend our Javascript / Typescript html game by allowing custom keyboard input key mappings for the player controls.

This means that pressing the same key could fire two different actions or one action could be fired by multiple keys. This is a standard feature of most games input systems allowing the player to customise their controls to suit their style.

Power of a game input framework

I mentioned by the end of the course we would have an isolated framework for handling input but what does that actually mean?

Handling input is a general concern for most games. The key is to separate and generalise the logic and responsibility for handling input from the logic that decides what effect should occur within the game given that input. If this can be achieved then the input handling logic can be reused across many different games.

In order to help make this distinction clear we are going to store all the input specific code in its own directory.

Add a new input directory to the src directory.

Updating the game input types

Lets also add a types.ts file inside the input directory to store our input related types.

Add the following code to the input types.ts file.

export type SupportedKeys = 'w' | 'a' | 's' | 'd' | 'f' | 'g' | 'h' | 'j'

export type GameKeyboardMapping = {
  direction: {
    up: SupportedKeys[],
    down: SupportedKeys[],
    left: SupportedKeys[],
    right: SupportedKeys[]
  },
  buttons: {
    primary: SupportedKeys[],
    secondary: SupportedKeys[],
    button3: SupportedKeys[],
    button4: SupportedKeys[],
  }
}

export type PlayerInput = {
  keyboardMapping: GameKeyboardMapping
}

export type PlayerInputs = {
  player1: PlayerInput
}

The SupportedKeys type represents all the different HTML keyboard event key codes our game will support.

The GameKeyboardMapping type connects the games different actions to an array of SupportedKeys, it has a key mapping for each of the 4 directions and actions.

The PlayerInput type holds a GameKeyboardMapping and the PlayerInputs sets a player1 field to PlayerInput, this will be extended later to support multiple players.

Updating the gamestate

Add a Constants.ts file to the input directory to hold the input related variables.

Add a KeyboardMappingNeutral type to the constants file to set a default GameKeyboardMapping. The direction values are set to the standard w, a, s, d keys and the actions to f, g, h, j.

export const KeyboardMappingNeutral: GameKeyboardMapping = {
  direction: {
    up: ['w'],
    down: ['s'],
    right: ['d'],
    left: ['a']
  },
  buttons: {
    primary: ['f'],
    secondary: ['g'],
    button3: ['h'],
    button4: ['j']
  }
}

We want to use the new mapping type in the GameState. Modify the GameState type as below.

export type GameState = {
  player1: PlayerState,
  inputs: {
    playerInputMappings: PlayerInputs
  }
}

Be careful to add the PlayerInputs import and ensure it is importing the .js file suffix as below.

import { PlayerInputs } from "../input/types.js"

If we miss the .js suffix in the import after building and running the app we will encounter a similar error as below if we try and refresh the page.

"GET /input/types" Error (404): "Not found"

In GameConstants.ts update the GameState as below by adding the inputs field.

export const InitialGameState: GameState = {
  player1: InitialPlayerState,
  inputs: {
    playerInputMappings: {
      player1: {
        keyboardMapping: KeyboardMappingNeutral
      }
    }
  }
}

Updating the keyboard controller

The KeyboardManager code is specific to our input framework, lets move it to the input folder and update the imports.

Now we can use the player keyboard mappings to check if certain directions or actions are being pressed or un-pressed when a keyboard event occurs.

Modify the keyDown and keyUp methods of the KeyboardManager as below.

import GameRuntime from "../GameRuntime.js"

export class KeyboardManager {

  private static keyDown = (event: KeyboardEvent) => {

    const player1KeyboardMapping = GameRuntime.gameState.inputs.playerInputMappings.player1.keyboardMapping
   
    const up = player1KeyboardMapping.direction.up.find(key => key === event.key)
    if (up) {
      GameRuntime.gameState.player1.direction.vertical = -1
    }

    const down = player1KeyboardMapping.direction.down.find(key => key === event.key)
    if (down) {
      GameRuntime.gameState.player1.direction.vertical = 1
    }

    const left = player1KeyboardMapping.direction.left.find(key => key === event.key)
    if (left) {
      GameRuntime.gameState.player1.direction.horizontal = -1
    }

    const right = player1KeyboardMapping.direction.right.find(key => key === event.key)
    if (right) {
      GameRuntime.gameState.player1.direction.horizontal = 1
    }

    const bottomAction = player1KeyboardMapping.buttons.primary.find(key => key === event.key)
    if (bottomAction) {
      GameRuntime.gameState.player1.bottom = true
    }

    const rightAction = player1KeyboardMapping.buttons.secondary.find(key => key === event.key)
    if (rightAction) {
      GameRuntime.gameState.player1.right = true
    }

    const leftAction = player1KeyboardMapping.buttons.button3.find(key => key === event.key)
    if (leftAction) {
      GameRuntime.gameState.player1.left = true
    }

    const topAction = player1KeyboardMapping.buttons.button4.find(key => key === event.key)
    if (topAction) {
      GameRuntime.gameState.player1.top = true
    }
  }

  private static keyUp = (event: KeyboardEvent) => {
    const player1KeyboardMapping = GameRuntime.gameState.inputs.playerInputMappings.player1.keyboardMapping
   
    const up = player1KeyboardMapping.direction.up.find(key => key === event.key)
    if (up) {
      GameRuntime.gameState.player1.direction.vertical = 0
    }

    const down = player1KeyboardMapping.direction.down.find(key => key === event.key)
    if (down) {
      GameRuntime.gameState.player1.direction.vertical = 0
    }

    const left = player1KeyboardMapping.direction.left.find(key => key === event.key)
    if (left) {
      GameRuntime.gameState.player1.direction.horizontal = 0
    }

    const right = player1KeyboardMapping.direction.right.find(key => key === event.key)
    if (right) {
      GameRuntime.gameState.player1.direction.horizontal = 0
    }

    const bottomAction = player1KeyboardMapping.buttons.primary.find(key => key === event.key)
    if (bottomAction) {
      GameRuntime.gameState.player1.bottom = false
    }

    const rightAction = player1KeyboardMapping.buttons.secondary.find(key => key === event.key)
    if (rightAction) {
      GameRuntime.gameState.player1.right = false
    }

    const leftAction = player1KeyboardMapping.buttons.button3.find(key => key === event.key)
    if (leftAction) {
      GameRuntime.gameState.player1.left = false
    }

    const topAction = player1KeyboardMapping.buttons.button4.find(key => key === event.key)
    if (topAction) {
      GameRuntime.gameState.player1.top = false
    }
  }

  constructor() {
    window.addEventListener('keydown', KeyboardManager.keyDown)
    window.addEventListener('keyup', KeyboardManager.keyUp)
  }
}

The code is similar to before but this time we are using the dynamic player keyboard mappings when deciding what player state changes to make for a given key event.

In both methods we are first getting a reference to the keyboard mappings. When a keyboard event is fired we compare the event code to any of the keys mapped to a particular aciton. If the keyDown method is called and we get a match for a given action we set the respective player state to true for actions.

Run the app and check that the player can be controlled by the w, a, s, d keys and f, g, h, j action keys.

Change the key input mappings

Modify KeyboardMappingNeutral and set the right direction to ['w','d']. Now start the app and press w, notice how the player moves diagonally upwards and to the right. This is because we have set the key mapping for w to be both the up and right key. If you wanted to make w make the player go down and s make the player go up, or extend our SupportedKeys this is now easy to do by changing the player's input mappings.

Summary

Having the ability to map custom keys is great but you may notice that the player still stalls when the direction keys are pressed in quick succession. Also doesn't the square look a little lonely and what if we wanted to plug in a gamepad?

We'll tackle all of these concerns throughout the rest of the course!