Getting input from the browser requires us to listen for dom events. As our game runs in a loop we might have the need to use a typical “Is the A button currently down” method, rather than getting an event telling us it was pressed down.
Listen for key events
The simplest way would be to bind to any keyboard events, and once they are pushed put them in a map for later consumption. This would require us to have a method to set up binding of events when you want you keyboard to start listening.
ts/game.ts
export class Keyboard {
private static keyMap: KeyMap = [];
public static init(): void {
window.addEventListener("keydown", (event) => {
if (!Keyboard.keyMap[event.keyCode] === true) {
Keyboard.keyMap[event.keyCode] = true;
console.log("Key Down:" + String.fromCharCode(event.keyCode), event.keyCode);
}
}, false);
}
}
You might notice that the type of the keymap is
KeyMap
. This is just an alias for{ [id: number]: boolean }
. Addtype KeyMap = { [id: number]: boolean };
to create the alias in your file.
Now we have an overview of any keys that has been pressed, but the is one issue. It’s a one-time keyboard, once a key is pressed it stays pressed forever. Fixing this requires us to add a keyup event listener as well. Our
ts/game.ts
window.addEventListener("keyup", (event) => {
Keyboard.keyMap[event.keyCode] = false;
console.log("Key up:" + String.fromCharCode(event.keyCode), event.keyCode);
}, false);
We now have a simple utility holding a map of which keys are currently pressed.
Creating simple is down and up allows us to test against this map
ts/game.ts
public static IsKeyDown(keycode: number): boolean {
return !!Keyboard.keyMap[keycode];
}
You might notice that we pass a number as the keycode, rather than a character. To be able to use the name of the key rather than the code we can add fields to our keyboard.
ts/game.ts
export class Keyboard {
public static W = 87;
public static A = 65;
public static S = 83;
public static D = 68;
.....
These codes assume a QWERTY layout
Having these fields allows us to use the keyboard utility like
if(Keyboard.IsKeyDown(Keyboard.A)) {
//Do something
}
Pressing and releasing key within a frame
+> GameLoop
| +
| v
| Start Loop
| +
| v
| Game logic code
| +
| v
| Key Down
| +
| v
| Key Up
| +
| v
| Game Logic checking for key down
| +
| v
+--+ Game Loop End
There is a corner-case in that the key could be pressed and released within one update of our game, this would yield a false value even though we could consider the key being pressed.
To fix this, instead of clearing it in the up event, we will add the key from our keyup
event to “KeysToRemove” map and process this at the end of our event frame
Add a field to our Keyboard
class, and alter the keyup
event slightly
private static keysReleased: Array<number> = [];
...
window.addEventListener("keyup", (event) => {
Keyboard.keysReleased.push(event.keyCode);
console.log("Key Up:" + String.fromCharCode(event.keyCode), event.keyCode);
}, false);
Now we need to actively remove the released keys from our current keymap.
To keep the keyboard agnostic of any game framework, we will explicitly have a onFrame
method.
public static onFrame(): void {
for (let i = 0, l = Keyboard.keysReleased.length; i < l; i++) {
console.log('Key removed:' + String.fromCharCode(Keyboard.keysReleased[i]));
Keyboard.keyMap[Keyboard.keysReleased[i]] = false;
}
Keyboard.keysReleased = [];
}
Implementing IsPressed and IsReleased keys
Currently our keyboard class can tell us if a key is down or not, but another useful feature would be if it could tell us if the key was pressed or released this in this update. This would enable us to trigger an action once, rather than continuously while the key is down and the game is running.
To fix this we will keep a snapshot of the keymap for the previous update, and diff these.
Add a previousKeyMap
field to our keyboard class.
private static previousKeyMap: KeyMap = [];
In our onFrame
method we will name take a snapshot of our previous keymap.
public static onFrame(): void {
...
this.previousKeyMap = JSON.parse(JSON.stringify(this.keyMap));
....
}
Now we can have methods like
public static IsKeyPressed(keycode: number): boolean {
return !!Keyboard.keyMap[keycode] && !Keyboard.previousKeyMap[keycode];
}
Final file
If you have any problems or want to just get to the code, here is the entire file.
type KeyMap = { [id: number]: boolean };
/**
* Keeps and exposes the state of the keyboard from keydown events
*
* @export
* @class Keyboard
*/
export class Keyboard {
public static W = 87;
public static A = 65;
public static S = 83;
public static D = 68;
public static X = 88;
public static Q = 81;
public static E = 69;
public static R = 82;
public static F = 70;
public static Space = 32;
public static Left = 37;
public static Up = 38;
public static Right = 39;
public static Down = 40;
public static Shift = 16;
public static Ctrl = 17;
/**
* Our map of keys pressed down for the current frame
*
* @private
* @static
* @type {KeyMap}
* @memberOf Keyboard
*/
private static keyMap: KeyMap = [];
/**
* The key map for the previous frame
*
* @private
* @static
* @type {KeyMap}
* @memberOf Keyboard
*/
private static previousKeyMap: KeyMap = [];
/**
* The keys that was currently released this update
*
* @private
* @static
* @type {Array<number>}
* @memberof Keyboard
*/
private static keysReleased: Array<number> = [];
/**
* Initializes the keyboard events needed to track state
* Needs to be called before use
*
* @static
*
* @memberOf Keyboard
*/
public static init(): void {
window.addEventListener("keydown", (event) => {
if (!Keyboard.keyMap[event.keyCode] === true) {
Keyboard.keyMap[event.keyCode] = true;
console.log("Key Down:" + String.fromCharCode(event.keyCode), event.keyCode);
}
}, false);
window.addEventListener("keyup", (event) => {
Keyboard.keysReleased.push(event.keyCode);
console.log("Key Up:" + String.fromCharCode(event.keyCode), event.keyCode);
}, false);
}
/**
* Notify the keyboard of a new update and clear any pressed keys
*
* @static
*
* @memberof Keyboard
*/
public static onFrame(): void {
this.previousKeyMap = JSON.parse(JSON.stringify(this.keyMap));;
for (let i = 0, l = Keyboard.keysReleased.length; i < l; i++) {
console.log('Key removed:' + String.fromCharCode(Keyboard.keysReleased[i]));
Keyboard.keyMap[Keyboard.keysReleased[i]] = false;
console.log('Current keymap:', Keyboard.keyMap);
}
Keyboard.keysReleased = [];
}
/**
* Query if the given keycode is currently pressed
*
* @static
* @param {number} keycode
* @returns {boolean}
*
* @memberOf Keyboard
*/
public static IsKeyDown(keycode: number): boolean {
return !!Keyboard.keyMap[keycode];
}
/**
* Query if the given keycode was pressed down this frame
*
* @static
* @param {number} keycode
* @returns {boolean}
*
* @memberOf Keyboard
*/
public static IsKeyPressed(keycode: number): boolean {
return !!Keyboard.keyMap[keycode] && !Keyboard.previousKeyMap[keycode];
}
/**
* Query if the given keycode was released this frame
*
* @static
* @param {number} keycode
* @returns {boolean}
*
* @memberOf Keyboard
*/
public static IsKeyReleased(keycode: number): boolean {
return !Keyboard.keyMap[keycode] && !!Keyboard.previousKeyMap[keycode];
}
}