Realmscape

Key Bindings and Handlers

19 Aug 2015  -  Jon Hall

Time to make something worth showing off!
Last post I introduced Event Handlers, and focused on the KeyboardEvent.keyCode[1] property - which gives us a number identifying the key represented in the event. We can use that identifying number to bind a given key to an action or function elsewhere in your engine, and in doing so we can allow the user to change the key bound to each available action.

I'm going to cover the basics of a reconfigurable key binding object, and integrating that with the GlobalEventHandlers interface.

Three distinct parts

There's 3 base components to creating an event handler that can modify key/action binds: The KeyBind object which links a keyCode with an action, the Bindings object which maps the KeyBinds and fires their handlers, and the event handlers that update the Bindings state.

Each KeyBind represents a set of actions able to be bound to any given keyCode, such as an entity jumping, moving, or interacting.

The Bindings object is a bit like the KeyBind 'manager', in that it maps the KeyBinds to keyCode relationship and triggers individual KeyBind events when the events are raised. It maintains a list of currently held keys, so we can regulate the frequency at which the keyPress event fires (or rather, our simulation of the keyPress event).

The event handlers are used to update the Bindings state, and flag a key as held or released.

The KeyBind object

function KeyBind(description,keyDown,keyUp,keyHold) {
 this.keyCode = 0;
 this.desc = description || "No description";
 this.keyDown = keyDown || false; //This will be the function to call on KeyDown
 this.keyUp = keyUp || false; //This will be the function to call on KeyUp
 this.keyHold = keyHold || false; //This will be the function called by our game loop if they key is held down
}
The KeyBind object, describing an individual action able to be bound to any keyCode

Reasonably self-explanatory? Each KeyBind instance has it's keyCode identified (this is managed and maintained by the Bindings object) and an optional string description. You can also define one or more of the functions for various key handlers, but these are not actually integrated with the event handlers just yet.

The Bindings object

function Bindings() {
 this.keyBinding = new Array(); //Array of KeyBinds
 this.holdKeyCodes = new Array(); //Array of key codes with a keyHold function defined
 this.currentlyPressedKeys = {}; //An object with numerical indices, storing the state of keyCodes
}
The Bindings object, looking nice and simple before we add additional functionality

Looks pretty simple, huh?
The Bindings object has an indexed array containing all the KeyBind objects, with the index as the keyCode (so we can easily find the KeyBind bound to keycode '65' with Bindings.keyBinding[65]).

There's also an array of holdKeyCodes, used to keep track of all the active KeyBinds with keyHold functions defined. The purpose of this is mostly a performance optimisation, as now we can perform the keyHold function from each of those KeyBinds that have been added to this array, rather than looking through all the active KeyBinds for those with keyHold function defined.

Lastly, there's the currentlyPressedKeys object containing the state of all the keyCodes as a boolean value for held, or not held.

Triggering KeyBind handlers through the Bindings object

Bindings.prototype.triggerKeyDown = function(event){
 if(this.keyBinding[event.keyCode]  != undefined && this.keyBinding[event.keyCode].keyDown != false){
  this.keyBinding[event.keyCode].keyDown(event); //Call KeyBind.keyDown handler
 }
}
Bindings.prototype.triggerKeyHold = function(keyCode){
 if(this.keyBinding[keyCode]  != undefined && this.keyBinding[keyCode].keyHold != false){
  this.keyBinding[keyCode].keyHold(keyCode); //Call KeyBind.keyHold handler
 }
}
Bindings.prototype.triggerKeyUp = function(event){
 if(this.keyBinding[event.keyCode]  != undefined && this.keyBinding[event.keyCode].keyUp != false){
  this.keyBinding[event.keyCode].keyUp(event); //Call KeyBind.keyUp handler
 }
}
The Bindings triggers, for 'activating' KeyBind functions

For both the triggerKeyDown(event) and triggerKeyUp(event) functions we are checking for the existence of the associated KeyBind for the keyCode referred to in the event, and that the appropriate handler function is not false. If so, the KeyBind event handler is executed. Both of these functions are executed at the time of the event being fired.

Then there's the triggerKeyHold(keyCode) function, notably different from the others because we will (theoretically) be indirectly calling this from our game loop, rather than when the keyHold event is fired.
It takes the keyCode as an argument, so lacks the additional information the other handlers are given through the Event object, but is repeatedly called in sync with our game loop (as you'll see when we integrate the handlers), rather than as the device or browser see appropriate.

Binding a KeyBind

The workhorse of the Bindings object, this function maps KeyBinds to keyCodes and completes the appropriate Bindings information

Bindings.prototype.requestKeyBind = function(keyCode,binding){
 if(this.keyBinding[keyCode] == undefined){
  //If there's not KeyBind object bound to 'keyCode' yet
  this.keyBinding[keyCode] = binding; //Add the KeyBind to the keyBinding array
  binding.keyCode = keyCode; //Set the KeyBind's keyCode identifier
  //If the KeyBind has a keyHold handler, add the identifying keyCode to the holdKeyCodes array
  if(binding.keyHold != false){this.holdKeyCodes.push(keyCode);}
  return true;
 } else {
  //Resolution of multiple binds to a keyCode... (You can implement this as you like)
  console.warn("BINDINGS","KeyCode already bound! - "+keyCode); //I'll just warn you that there's a collision
  return false;
 }
}
Requesting a new bind between a KeyBind and keyCode

If there's no existing KeyBind for the given keyCode, the Bindings.requestKeyBind(keyCode,binding) function adds the KeyBind to the Bindings.keyBinding array, sets the KeyBind.keyCode identifier (so we can tell from a given KeyBind, which keyCode it is mapped to without the Bindings object), and if there's a KeyBind.keyHold handler then add the keyCode to the Bindings.holdKeyCodes array (for performance optimisation, as mentioned earlier).

Lastly, you may want to implement some sort of Bindings output, to get a map or string of all the existing bindings with something like Bindings.protoype.toString(). Between the use of keyCodes and KeyBind.desc you can then display the current list of controls for your game.
A textual representation of the keyCodes could be achieved with some sort of mapped array similar to this:

var KEYCODE_MAPPING = new Array(
 "","","","","","","","","backspace","tab","","","","enter","","","shift","ctrl","alt","pause/break",
 "caps lock","","","","","","","escape","","","","","space","page up","page down","end","home","left arrow","up arrow","right arrow",
 "down arrow","","","","","insert","delete","","0","1","2","3","4","5","6","7","8","9","","",
 "","","","","","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o",
 "p","q","r","s","t","u","v","w","x","y","z","left window key","right window key","select key","","","numpad 0","numpad 1","numpad 2","numpad 3",
 "numpad 4","numpad 5","numpad 6","numpad 7","numpad 8","numpad 9","multiply","add","","subtract","decimal point","divide","f1","f2","f3","f4","f5","f6","f7","f8",
 "f9","f10","f11","f12","","","","","","","","","","","","","","","","",
 "","","","","num lock","scroll lock","","","","","","","","","","","","","","",
 "","","","","","","","","","","","","","","","","","","","",
 "","","","","","","semi-colon","equal sign","comma","dash","period","forward slash","grave accent","","","","","","","",
 "","","","","","","","","","","","","","","","","","","","open bracket",
 "back slash","close bracket","single quote"
);
KEYCODE_MAPPING with a string representation of each keyCode, which may differ between devices

Integrating event handlers

var handleKeyDown = function(event){
 // Handles Key Presses once on Key Down
 //(Triggered multiple times at short intervals if key is held down, browser dependant)
 if(bindings.currentlyPressedKeys[event.keyCode] != true){
  bindings.currentlyPressedKeys[event.keyCode] = true;
  bindings.triggerKeyDown(event);
 }
}
var handleKeyUp = function(event){
 if(bindings.currentlyPressedKeys[event.keyCode] == true){
  bindings.currentlyPressedKeys[event.keyCode] = false;
  bindings.triggerKeyUp(event);
 }
}
var handleKeys = function(){
 // Handles simultaneous Key Presses, and applies a consistent modifier every tick
 for(var i in bindings.holdKeyCodes){
  var kc = bindings.holdKeyCodes[i];
  if(bindings.currentlyPressedKeys[kc]){
   bindings.triggerKeyHold(kc);
  }
 }
}
Event handlers that make calls to a Bindings instance, bindings

Starting with handleKeyDown(event), if the given key is not currently pressed then add it to the Bindings.currentlyPressedKeys object and trigger the Bindings.triggerKeyDown(event) function passing the Event as an argument. The check for if the key is already pressed prevents repeated triggering of the Bindings 'trigger' functions, as most browsers will repeatedly fire the event if a key is held.

The handleKeyUp(event) handler is pretty straight-forward. If the key in the Event is currently pressed, then flag it as released and trigger the Bindings.triggerKeyUp(event) function.

The handleKeys() function is a bit different to the other two. When called (each engine tick), it loops through each value in Bindings.holdKeyCodes, checks if the related keyCode is currently flagged as pressed in Bindings.currentlyPressedKeys, then triggers the keyHold handler for the given keyCode.
The use of the Bindings.holdKeyCodes array becomes apparent here, because every time handleKeys() is called, every KeyBind with a keyHold handler has the potential to be triggered if that key is currently held. This is why handleKeys() is only called once per game loop, as it triggers the handler for all pressed keys with keyHold handlers.

var bindings = new Bindings();
document.onkeyup = handleKeyUp;
document.onkeydown = handleKeyDown;
Define the document event handlers, and create a Bindings instance

Lastly, to tidy up we define the event handlers for the document (or you may choose to only apply them to one or multiple elements within the document) and create a Bindings instance named however you've referred to it in the event handlers (for the example, I've made a global variable named 'bindings'. You'd probably not use a global variable, and name it something more appropriate).

As always, here's the functioning code from this post for you to play around with yourself (You'll need to make use of the debugging console to interact with this one, but you can integrate our text interface from previously for output!). Try pressing the 'A' key with the debugging console open, and take a look at line 91.
It's also available on GitHub.

<html>
 <head>
  <script type="text/javascript">
var KEYCODE_MAPPING = new Array(
 "","","","","","","","","backspace","tab","","","","enter","","","shift","ctrl","alt","pause/break",
 "caps lock","","","","","","","escape","","","","","space","page up","page down","end","home","left arrow","up arrow","right arrow",
 "down arrow","","","","","insert","delete","","0","1","2","3","4","5","6","7","8","9","","",
 "","","","","","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o",
 "p","q","r","s","t","u","v","w","x","y","z","left window key","right window key","select key","","","numpad 0","numpad 1","numpad 2","numpad 3",
 "numpad 4","numpad 5","numpad 6","numpad 7","numpad 8","numpad 9","multiply","add","","subtract","decimal point","divide","f1","f2","f3","f4","f5","f6","f7","f8",
 "f9","f10","f11","f12","","","","","","","","","","","","","","","","",
 "","","","","num lock","scroll lock","","","","","","","","","","","","","","",
 "","","","","","","","","","","","","","","","","","","","",
 "","","","","","","semi-colon","equal sign","comma","dash","period","forward slash","grave accent","","","","","","","",
 "","","","","","","","","","","","","","","","","","","","open bracket",
 "back slash","close bracket","single quote"
);

function KeyBind(description,keyDown,keyUp,keyHold) {
 this.keyCode = 0;
 this.desc = description || "No description";
 this.keyDown = keyDown || false;
 this.keyUp = keyUp || false;
 this.keyHold = keyHold || false;
}

function Bindings() {
 this.keyBinding = new Array();
 this.holdKeyCodes = new Array();
 this.currentlyPressedKeys = {};
}
Bindings.prototype.requestKeyBind = function(keyCode,binding){
 if(this.keyBinding[keyCode] == undefined){
  this.keyBinding[keyCode] = binding;
  binding.keyCode = keyCode;
  if(binding.keyHold != false){this.holdKeyCodes.push(keyCode);}
  return true;
 } else {
  //Resolve binding collisions here...
  console.warn("BINDINGS","KeyCode already bound! - "+keyCode);
  return false;
 }
}
Bindings.prototype.triggerKeyDown = function(event){
 if(this.keyBinding[event.keyCode]  != undefined && this.keyBinding[event.keyCode].keyDown != false){
  this.keyBinding[event.keyCode].keyDown(event);
 }
}
Bindings.prototype.triggerKeyHold = function(keyCode){
 if(this.keyBinding[keyCode]  != undefined && this.keyBinding[keyCode].keyHold != false){
  this.keyBinding[keyCode].keyHold(keyCode);
 }
}
Bindings.prototype.triggerKeyUp = function(event){
 if(this.keyBinding[event.keyCode]  != undefined && this.keyBinding[event.keyCode].keyUp != false){
  this.keyBinding[event.keyCode].keyUp(event);
 }
}
Bindings.prototype.toString = function(){
 //For you to optionally complete...
}

var handleKeyDown = function(event){
 // Handles Key Presses once on Key Down (Triggered multiple times at short intervals if key is held down, depending on browser)
 if(bindings.currentlyPressedKeys[event.keyCode] != true){
  bindings.currentlyPressedKeys[event.keyCode] = true;
  bindings.triggerKeyDown(event);
 }
}
var handleKeyUp = function(event){
 if(bindings.currentlyPressedKeys[event.keyCode] == true){
  bindings.currentlyPressedKeys[event.keyCode] = false;
  bindings.triggerKeyUp(event);
 }
}
var handleKeys = function(){
 // Handles simultaneous Key Presses, and applies a consistent modifier every tick
 for(var i in bindings.holdKeyCodes){
  var kc = bindings.holdKeyCodes[i];
  if(bindings.currentlyPressedKeys[kc]){
   bindings.triggerKeyHold(kc);
  }
 }
}

var bindings = new Bindings();
document.onkeyup = handleKeyUp;
document.onkeydown = handleKeyDown;

//Added a demonstration KeyBind, to output to console when 'a' key is released (onKeyUp)
bindings.requestKeyBind(65,new KeyBind("console output",false,function(e){console.info("Key was released.")},false));

  </script>
  <style type="text/css">
/*There's no need for any styles in this example*/
  </style>
 </head>
 <body>
<!-- If you want to integrate the previous text interface, you'll need to add some content to the BODY -->
 </body>
</html>
The full code from this example, for your convenience
Comments, Feedback, and Discussion can be found on our subreddit

 

[1] Same as last post, I'd just like to make note that KeyboardEvent.keyCode is currently marked as becoming deprecated and KeyboardEvent.key should be used instead where available. (Though, you'll have to adapt this entire post for a String value, rather than an Integer)
[2] MDN MouseEvent documentation