Some subtleties of keyboard inputs in JS games

September 2016, January 2017, February 2018




TL;DR - state of the art:

// Update four variables (u,l,d,r) based on Arrow keys, WASD or ZQSD inputs. (73b)
u=l=d=r=0;onkeydown=onkeyup=e=>top['lld*rlurdu'[e.which%32%17]]=e.type[5]



After JS13kGames 2016, we realized on Slack that many games were only playable with W, A, S, D keys, and many players were bothered by that.

In this article I'll cover this point plus a few other subtleties of keyboard input in JS, and provide a tiny library that you can drop directly in your future games to get these problems out of the way.



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. Other minor layouts exist but we can ignore them for now.

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 must 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 page displaying the keyCodes of all your keyboard events, 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 (depending on your OS settings), after a short pause during which nothing happens, both keydown and keypress are fired repeatedly. There's also an "input" event, but it happens only in form elements, so it's not really relevant here.

There are many problems with this default behavior: firstly, this 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 running or jumping. And secondly, the keypress event is totally messed up (look at the keyCode returned by this event when you press letter keys or arrow keys, and you'll see absurd values almost everytime, and different absurd values depending on the browser you're using.

So my advice is to avoid relying on keypress events altogther, and also to avoid relying on keydown at each frame to see if a key is down or not, because the pause will bother your players.



The solution

So here's a super short solution to all these issues.

It introduces four global boolean variables to keep in memory the state of each direction.

Of course, feel free to fork it and replace them with non-global vars if you want, but for this example, I'll just keep things simple, and who cares about global vars in code-golfing anyway? Okay, maybe everyone cares, but not me! :p

So here's the code:

// 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;
  }
}

Then, during you game loop, you just need to rely on the state of these four variables to know if the player is currently pressing a direction (or an alias in WASD or ZQSD) or not!

Want to see a demo? Of course! Just launch the first level of my js13kgames entry. It uses this exact technique :)



P.S: if you want a minified ang golfed version, here it is: only 160b! (the four vars u,r,d,l, are not booleans but just truthy and falsy)

u=r=d=l=0;onkeydown=(e)=>t(e,1);onkeyup=(e)=>t(e);t=(e,v,l,i)=>{for(i in l={u:[38,90,87],r:[39,68],d:[40,83],l:[37,65,81]})if(l[i].includes(e.keyCode))top[i]=v}

P.P.S: Here's a 122b version by @p01, using a radically different idea: a look-up table of keyCodes represented as a string, and ".which" instead of ".keyCode" (because it's the same thing):

u=r=d=l=0;onkeyup=t=(e,v)=>top['lurd************************l**r************l*d***u**u'[e.which-37]]=v;onkeydown=e=>t(e,1)



January 2017 golfing

I made a new version that's only 87b, but it works if the only keys you need to support are arrows, WASD and ZQSD (most of the other keys may collide with one of the four directions):

u=d=l=r=0;onkeyup=t=(e,v)=>top['lurdl*d*l*ur*u'[(e.which-37)%20]]=v;onkeydown=e=>t(e,1)

It fell down to 85b with the help of nderscore:

u=d=l=r=0;onkeydown=e=>(onkeyup=(e,v)=>top['lurdl*d*l*ur*u'[(e.which-37)%20]]=v)(e,1)

then 83, by merging the two event listeners:

u=d=l=r=0;onkeydown=onkeyup=e=>top['lurdl*d*l*ur*u'[(e.which-37)%20]]=e.type[3]<'u'

and finally 82 with Subzey's touch:

u=d=l=r=0;onkeydown=onkeyup=e=>top['lurdl*d*l*ur*u'[(e.which+3)%20]]=e.type[3]<'u'

(DEMO)

Note: there's no collision with the keys "E", "R", "T" "space", "shift" and "Enter", so you can also handle them just fine with just a 100 bytes:

E=R=T=_=s=e=u=d=l=r=0;onkeydown=onkeyup=z=>top['lurdlRdTl*urEu*_e**s'[(z.which+3)%20]]=z.type[3]<'u'

(DEMO)



February 2018 update, thanks to @BalintCsala and @ETHProd !

Here's how to save the state of four variables (u,l,d,r) based on Arrow keys, WASD or ZQSD, in 78b:

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

And here's a different approach, where keydown events directly update two variables called X and Y using the arrow keys, in 56b only!

X=Y=0;onkeydown=e=>top["YXYX"[g=e.which%4]]-="0220"[g]-1

Unfortunately, if you keep one key pressed, the browser will make a short pause just after firing the first event, before firing around 60 times per second.

NB: if you make a JS1k or itch.io game using these techniques, you'll need to replace 'top' with 'this' to avoid escaping your iframe.



Cheers,

@MaximeEuziere