Improving our TypeScript game's keyboard input

In our previous keyboard input tutorial, we added a "keydown" event listener to listen to whenever the player pressed a key, which would then interact with our ship class to cause it to turn, accelerate and shoot. There is a bit of a problem with the way we did this, and that is when you press a key down, the first event comes in immediately, followed by a delay, followed by repeated "keydown" events. This is the sort of behavior you get if you hold a key down in a word processor, but it's not the best experience for a game. We're going to improve on our keyboard input by creating a class that will keep track of what keys have been pressed, and if they have been released. We will also set that class up to call specific functions set up as listeners to specific key events.

Creating a keydown dictionary

It would be nice to keep track of the keycodes that have been pressed while our game is being played, and whether or not they are currently being pressed. We are going to do this using what's called a dictionary. That will allow us to associate a number with a boolean true or false value. This is different from an array, because we will only be storing the numbers corresponding to the keycodes a player has actually pressed, instead of all numbers from 0-n. Here is what a dictionary looks like in TypeScript:

var keyDown: { [keycode: number]: boolean; } = {};
keyDown[39] = false;

The first line creates a keyDown dictionary that uses a number as the dictionary's key, and a boolean as the dictionary's value. The second line creats a key/value pair with a key of 39 and a value of false. This is how we are going to track which keys are pressed during our game.

A dictionary for our callback functions

We not only need to know what keys are currently pressed, but we also need to know what to do if those keys are pressed. The way we do this is with a callback. A callback is basically a variable used to represent a function with a specific signature. We are now going to need to create a dictionary of callback functions that ties a keycode to a function.

function callback(): void {
   console.log("the callback");
}
var keyCallback: { [keycode: number]: () => void; } = {};
keyCallback[39] = callback;

We're not actually going to use this function we called "callback", this is only for demonstration purposes. The dictionary "keyCallback" uses a keycode as it's key, and a function with a signature that takes no parameters and returns void as it's value.

Creating our Keyboard Input class

Now let's create the start of our keyboard input class:

class cKeyboardInput {
   public keyCallback: { [keycode: number]: () => void; } = {};
   public keyDown: { [keycode: number]: boolean; } = {};

   constructor() {
      document.addEventListener('keydown', this.keyboardDown);
      document.addEventListener('keyup', this.keyboardUp);
   }
   public keyboardDown = (event: KeyboardEvent): void => {
      event.preventDefault();
      this.keyDown[event.keyCode] = true;
   }
   public keyboardUp = (event: KeyboardEvent): void => {
      this.keyDown[event.keyCode] = false;
   }
}

What we are starting out with, is our dictionaries that we discussed earlier. We're also starting out with a constructor that adds the event listeners that will call the keyboardDown and keyboardUp methods in our class. Inside the keyboardUp and keyboardDown methods we are going to set the value for that keycode in our keyDown dictionary. We also have added a way to prevent the default scrolling behavior when the user presses the arrow keys in our window using "event.preventDefault()". We're not done yet with our keyboard input class. Let's add a method to add a callback for a given keycode.

public addKeycodeCallback = (keycode: number, f: () => void): void => {
   this.keyCallback[keycode] = f;
   this.keyDown[keycode] = false;
}

I also need to add some code that is going to get called from within the main game loop. This is going to manage the keyboard input every frame:

public inputLoop = (): void => {
   for (var key in this.keyDown) {
      var is_down: boolean = this.keyDown[key];
      if (is_down) {
         var callback: () => void = this.keyCallback[key];
         if (callback != null) {
            callback();
         }
      }
   }
}

Then we need to add a call to this method from within the game loop:

function gameLoop() {
   deltaTime = (new Date().getTime() - lastTime) / 1000;
   lastTime = Date.now();
   if (bulletWait > 0) {
      bulletWait -= deltaTime;
   }
   requestAnimationFrame(gameLoop);
   ctx.fillStyle = "black";
   ctx.fillRect(0, 0, 1280, 720);
   var bullet: cBullet;
   var asteroid: cAsteroid;
   keyInput.inputLoop(); // KEYBOARD INPUT LOOP
   space_ship.draw();
   for (var i: number = 0; i < bullet_array.length; i++) {
      bullet = bullet_array[i];
      bullet.draw();
   }
   for (i = 0; i < asteroid_array.length; i++) {
      asteroid = asteroid_array[i];
      if (asteroid.active == false) {
         continue;
      }
      asteroid.draw();
   }
}

We also need to create our keyboard input object when the class loads and add all of our callbacks to that object:

var keyInput: cKeyboardInput;
window.onload = () => {
   canvas = <HTMLCanvasElement>document.getElementById('cnvs');
   ctx = canvas.getContext("2d");
   space_ship = new cSpaceShip(200, 450, 8);
   asteroid_array.push(new cAsteroid(850, 600, 20));
   asteroid_array.push(new cAsteroid(150, 100, 20));
   asteroid_array.push(new cAsteroid(650, 200, 20));
   asteroid_array.push(new cAsteroid(1200, 500, 20));
   asteroid_array.push(new cAsteroid(200, 600, 20));

   keyInput = new cKeyboardInput();

   // PRESS LEFT ARROW OR 'A' KEY
   keyInput.addKeycodeCallback(37, space_ship.turnLeft);
   keyInput.addKeycodeCallback(65, space_ship.turnLeft);

   // PRESS UP ARROW OR 'W' KEY
   keyInput.addKeycodeCallback(38, space_ship.accelerate);
   keyInput.addKeycodeCallback(87, space_ship.accelerate);

   // PRESS RIGHT ARROW OR 'D' KEY
   keyInput.addKeycodeCallback(39, space_ship.turnRight);
   keyInput.addKeycodeCallback(68, space_ship.turnRight);

   // PRESS DOWN ARROW OR 'S' KEY
   keyInput.addKeycodeCallback(40, space_ship.decelerate);
   keyInput.addKeycodeCallback(83, space_ship.decelerate);

   // PRESS SPACE BAR
   keyInput.addKeycodeCallback(32, space_ship.shoot);


   gameLoop();
}

Here is a demonstration of what we have for our new keyboard input. I've also added some additional game play features that I'm going to need to cover in a later tutorial.

You can take a look at the full source code.

Part 3 - Projectiles and simple object pooling

Part 2 - Moving A Space Ship with Keyboard Input

Part 1 - Basic Keyboard Input

TypeScript Key Codes