Some subtleties of keyboard inputs in JS games

September 2016 - August 2021




The WASD issue

WASD keys can be used as an alternative for arrow keys in a vast majority of keyboards worldwide.
All QWERTY and QWERTZ keyboards (and some hybrid keyboards like QWERTY/russian for example) support this pattern just fine.
But there's another layout widely used too (especially in France, Belgium and Canada): AZERTY.
On this kind of keyboard, WASD becomes ZQSD.
So the idea is to teach game developpers to natively support not only WASD, but also ZQSD AND arrow keys!
Why support only one scheme when we can please everyone with a super light overhead?

To sum up, up arrow should be aliased with W and Z, left arrow with A and Q, right arrow with D, and down arrow by S.



The JS keyboard events issue

As you can see on this keyboard keyCodes demo, when you press a key, two events are fired by the browser: keydown and keypress.
When you release it, another event is fired: keyup.
And if you keep a key pressed for a moment, after a short pause, both keydown and keypress are fired repeatedly.
There are many problems with this default behavior:

  • Firstly, the pause that happens after pressing a key is not good for a video game.
    If you play a platform game, you don't want Mario to stay idle for a few frames before he starts moving.
  • Secondly, the keypress event is totally messed up.
    (The keyCode returned by keyPress is wrong in most cases, and wrong differently depending on the browser you're using!)

So my advice is to avoid keypress events, and also to avoid relying on keydown and keyup events only to update your game's state.
Instead, the keydown and keyup events should update a global input manager, which can be queried at any moment.



The solution

Both issues above can be solved with a simple input manager:

Recommended version (115b):

// Update four global variables (u,l,d,r) based on Arrow keys, WASD or ZQSD inputs. (73b)
// These variables are truthy (== 1) if the corresponding key is pressed, falsy (== 0) when it's released.
u=r=d=l=0;
onkeydown=onkeyup=e=>this['lurd************************l**r************l*d***u**u'[e.which-37]]=e.type[5]
DEMO

Tiny version (74b, collisions may happen with other keys):

u=l=d=r=0;
onkeydown=onkeyup=e=>this['lld*rlurdu'[e.which%32%17]]=e.type[5]
DEMO

Micro version (58b, arrow keys only):

X=Y=0;onkeydown=e=>this["XYXY"[g=e.which-37]]-="2200"[g]-1
DEMO

Ungolfed source code (useful if you want to add support for other keys):

// Keys states (false: key is released / true: key is pressed)
up = right = down = left = false;

// Keydown listener
onkeydown = (e) => {

  // Up (up / W / Z)
  if(e.keyCode == 38 || e.keyCode == 90 || e.keyCode == 87){
    up = true;
  }
  
  // Right (right / D)
  if(e.keyCode == 39 || e.keyCode == 68){
    right = true;
  }
  
  // Down (down / S)
  if(e.keyCode == 40 || e.keyCode == 83){
    down = true;
  }
  
  // Left (left / A / Q)
  if(e.keyCode == 37 || e.keyCode == 65 ||e.keyCode == 81){
    left = true;
  }
}

// Keyup listener
onkeyup = (e) => {
    
  // Up
  if(e.keyCode == 38 || e.keyCode == 90 || e.keyCode == 87){
    up = false;
  }
  
  // Right
  if(e.keyCode == 39 || e.keyCode == 68){
    right = false;
  }
  
  // Down
  if(e.keyCode == 40 || e.keyCode == 83){
    down = false;
  }
  
  // Left
  if(e.keyCode == 37 || e.keyCode == 65 || e.keyCode == 81){
    left = false;
  }
}

Cheers,

@MaximeEuziere (thanks to @p01, @subzey, @BalintCsala and @ETHProd!)