Why are onkeyup events not firing in Javascript game
Asked Answered
L

2

7

I have the beginnings of a 2d Javascript game - at the moment the player can drive a triangle around the screen using the arrow keys.

The problem is that sometimes the triangle will get stuck rotating in one direction or moving forward until the corresponding control key is pressed again and the onkeyup event is fired again. This usually happens when more than one control key is pressed at the same time.

I can't work out why it's getting stuck in the first place unless the onkeyup events aren't getting fired for some reason. Any help would be much appreciated, thank you.

Here's some of the code, you can find a fully working example on JSFiddle:

...

function init(){
    var canvas = document.getElementById('canvas');
    if(canvas.getContext){
        setInterval(play, 50);
    }
}

function play(){
    printTriState();
    updateTriAcceleration();
    applyTriVelocity();
    updateTriRotation();
    draw();
}

document.onkeydown = function(event){

if(!event.keyCode){
    keycode = window.event.keyCode;
} else {
    keycode = event.keyCode;
}
console.log(event.keyCode);
switch(keycode){
    //left
    case 37:
    tri.rotation = -1;
    break;

    //up
    case 38:
    tri.throttle = true;
    break;

    //right
    case 39:
    tri.rotation = 1;
    break;

    //down
    case 40:
    tri.currentSpeed = 0;
    break;

    default:
    break;
}
};

document.onkeyup = function(event){

if(!event.keyCode){
    keycode = window.event.keyCode;
} else {
    keycode = event.keyCode;
}
console.log(event.keyCode);
switch(keycode){
    //left
    case 37:
    tri.rotation = 0;
    break;

    //up
    case 38:
    tri.throttle = false;
    break;

    //right
    case 39:
    tri.rotation = 0;
    break;

    //down
    case 40:

    break;

    default:
    break;
}
};

function updateTriRotation(){
    if(tri.rotation == 1){
        tri.orientation += tri.rotationSpeed;
    } else if (tri.rotation == -1){
        tri.orientation -= tri.rotationSpeed;
    } else {
        tri.orientation += 0;
    }
}

...
Liver answered 27/6, 2012 at 12:0 Comment(2)
can you give a link to the working example? can you put that on jsfiddle?Placative
jsfiddle.net/6dMpF It starts in the top left hand corner so you have to turn it around and drive down into the canvas. Thanks.Liver
P
15

Problem description

The keyup events are fired, but sometimes a final keydown event fires directly after the keyup. Weird? Yes.

The problem is the way how the repetition of the keydown event for a held key is implemented:
the keydown event for the last held key is fired repeatedly. Due to the asynchronous, single-threaded nature of optimized JavaScript code execution, it can happen that sometimes such a repeated keydown event is fired after the corresponding keyup event was invoked.

See what happens when i hold and release the keys [UP] and [RIGHT] simultaneously in my JSFiddle example.

Observed with Firefox 13.0.1 on Windows 7:

rotation: 1 key down: 39
rotation: 1 key down: 38
rotation: 1 key down: 38
rotation: 1 key down: 38
rotation: 1 key up: 38
rotation: 0 key up: 39
rotation: 1 key down: 38

You can see three things in this example console output:

  1. only the keydown event for [RIGHT] (38) is fired repeatedly.
  2. The keyup event for [RIGHT] (38) is fired when i release the key.
  3. A last scheduled keydown event is fired, after the keyup was executed.

Thats the reason why your rotation state is "stuck" with value 1 even if no key is pressed.


Solution

To avoid this, you can keep track of the microseconds when a key is released. When a keydown event occurs just a few microseconds ahead, then we silently ignore it.

I introduced a helper object Key into your code. See it working in this JSFiddle example.

Now the console output looks like:

rotation: 1 key down: 40 time: 1340805132040
rotation: 1 key down: 40 time: 1340805132071
rotation: 1 key down: 40 time: 1340805132109
rotation: 1 key up: 40 time: 1340805132138
rotation: 0 key up: 39 time: 1340805132153
key down: 40 release time: 1340804109335
keydown fired after keyup .. nothing to do.
rotation: 0 key down: 39 time: 1340805132191

My solution is inspired by this blog article, which goes a bit further and explains how to avoid the sloppyness and "one-direction-only" issues you have when using JavaScript key events for game development. It's definitely worth reading.

The workaround to ignore the delayed keydown event basically looks like this:

var Key = {
    _pressed: {},
    _release_time: {},
    
    MAX_KEY_DELAY: 100,

    onKeydown: function(event) {

        // avoid firing of the keydown event when a keyup occured
        // just +/- 100ms before or after a the keydown event.
        // due to the asynchronous nature of JS it may happen that the
        // timestamp of keydown is before the timestamp of keyup, but 
        // the actual execution of the keydown event happens *after* 
        // the keyup.   Weird? Yes. Confusing!

        var time = new Date().getTime();
        if ( this._release_time[event.keyCode] &&
             time < this._release_time[event.keyCode] + this.MAX_KEY_DELAY ) {
            console.log('keydown fired after keyup .. nothing to do.');
            return false;
        }

        this._pressed[event.keyCode] = true;

        // LOGIC FOR ACTUAL KEY CODE HANDLING GOES HERE
    },
  
    onKeyup: function(event) {
        delete this._pressed[event.keyCode];
        this._release_time[event.keyCode] = new Date().getTime();

        // LOGIC FOR ACTUAL KEY CODE HANDLING GOES HERE
    }
};

document.onkeydown = function(event) { return Key.onKeydown(event); };
document.onkeyup = function(event) { return Key.onKeyup(event); };

Update #1 Find an updated (fixed) version of your code on JSFiddle.

Placative answered 27/6, 2012 at 13:50 Comment(4)
Thanks for the detailed answer... I tried implementing it but it hasn't worked. However I have found that when I quickly tap LEFT, UP, RIGHT it shows the keycodes to be: keydown:37 33 35 keyup:33 35. I'm using ubuntu 12.04 with firefox. Maybe pressing the arrow keys in this order is a shortcut to home and end keys.Liver
@Yoshima Find an updated (fixed) version of your code on JSFiddle.Placative
@Yoshima regarding the [HOME] & [END] issue ... you can either map those keycodes to the right function as well, or fix your browser/desktop environment settings ;) Nevertheless, there is also that timing issue i have observed. (which can be worked around, as illustrated in my answer)Placative
@Placative Thanks... you've been a real help.Liver
L
7

This question is nearly 8 years old, but I hit similar issues and stumbled across this page, so in case it helps anyone else:

I was having jamming issues in my code, which was using a Set to add keyboard events on a keydown, and delete them on a keyup. I felt it was good practice to use the event.key field to distinguish the keypress, rather than, say, the keyCode, to maintain the semantic mapping between what was written on the user's keyboard and what my code interprets. (I believe, for instance, that the W of a QWERTY keyboard would report the same keyCode as the Z of an AZERTY keyboard, but don't have a French keyboard to hand to verify.)

This was working fine (I didn't see the problem with out-of-sequence events that others reported), until modifiers came in to the picture. I found two issues with these:

  • ...where Shift was held. If the sequence of user's keystrokes goes (hold-shift, hold-R, release-shift, release-R), then the sequence of JS events wrt. the event.key would go (keydown-shift, keydown-R, keyup-shift, keyup-r). In other words, an R is pressed, but an r is released, so my Set has the R stuck down. My solution to this was to turn the Set into a Map, where the keys were the event.keyCode's, and the values were the event.key's that were reported at the time of the keydown event. This way, when I get a keyup event I just delete the map key corresponding to the keyCode and keys don't get stuck. I expect option/alt would need the same handling.
  • ...where the Meta key was held (Command on my Mac; I'm guessing Control on Windows?). In this case, a sequence that goes (hold-meta, hold-K, release-meta, release-K) would generate the events (keydown-meta, keydown-k, keyup-meta). So — the keyup event for the K is genuinely missing, even if I'm sure to do event.preventDefault's to prevent the system stealing my events. I wish I had a better solution for this, but what I'm doing right now is nuking the whole map when I see Meta key events. I should say for the record that my code isn't actually using Meta as a modifier; I just want to be sure that it's safe, and when users type quickly, it seemed easy for my key handler to get jammed.

Oh, also, on blur — I'm nuking the map. All bets are off then...

Lux answered 18/4, 2020 at 18:48 Comment(3)
Something else notable about MacOS's odd key events is Shift in conjunction with the Command key... The key events fired are keydown-Shift, keydown-Meta, keydown-Key, keyup-Shift... It literally skips over Meta and the key... This is quite annoying...Mystique
@kelsny I noticed that too. One Meta-up, I clear the Set of keys that are down as a workaround. But this means, with Meta, you cannot keep a letter held down while alternative Meta no-Meta. Good enough though.Stephainestephan
@kelsny sometimes the problem goes away too. Huh.Stephainestephan

© 2022 - 2024 — McMap. All rights reserved.