Remove key press delay in Javascript
Asked Answered
G

9

30

I have the following problem: I'm trying to write a Javascript game, and the character is being controlled by the arrow keys.
The problem is, when one keeps the key pressed, there is a short delay between firing the first keypress and the repeated keypress.
Also, when one presses the "right arrow key" and keeps it pressed, and then presses the "up arrow key" the character doesn't move to the top right corner, but stops the moving in the right direction and starts moving up.
This is the code I'm using:

<body onLoad="Load()" onKeyDown="Pressed(event)">
function Pressed(e) { 
        cxc = e.keyCode;
        if(cxc == 37)
            Move(-1,0);
        if(cxc == 38)
            Move(0,-1);
        if(cxc == 39)
            Move(1,0);
        if(cxc == 40)
            Move(0,1);
    }

Does anybody have an idea?

Genocide answered 11/9, 2010 at 15:1 Comment(10)
your code wont work, thats for sure. am not sure if you can test that two keys are pressed at the same time in JS. will wait for the answers.Equerry
well, it does in deed work, but not as expected. My game is a timer-based game, and waisting 1sec by waiting for the repeated keystrokes is not good. It's frustrating.Genocide
why dont you try using a flash based approach. a transparent flash swf which captures events and then accordingly calls JS. java2s.com/Code/Flash-Flex-ActionScript/Development/…Equerry
No flash, thats too much work and it doesn't work for everybody. My game is going to be a "non-flash-game". I prefer HTML5Genocide
HTML5? That doesn't work for everybody either! Infact Flash has 98% adoption rate.Equerry
Yes, but then I can say that my game does not use any flash.Genocide
Yes, but HTML5 WILL work for everybody....Orman
I wish our IE6 friends good luck! Here's something you might want to have a look at- quirksmode.org/js/keys.htmlEquerry
@Genocide - Read the following answer. It may help you.Sevilla
@AlecSmart - Yes it is possible to test whether two keys are pressed at the same time in JavaScript. However you need to create custom events to do so. Read the answer I linked to in my above comment.Sevilla
G
43

If you want key repeat in a controllable fashion, you will have to implement it yourself, as keypress events are fired dependent on the OS's idea of how keys should repeat. That means there may be variable initial and following delays, and holding down two keys at once will cause only one of them to repeat.

You will have to keep a record of whether each key is currently pressed, and ignore keydown events when the key is already down. This is because many browsers will fire a keydown as well as a keypress event when an autorepeat occurs, and if you're reproducing key repeat yourself you'll need to suppress that.

For example:

// Keyboard input with customisable repeat (set to 0 for no key repeat)
//
function KeyboardController(keys, repeat) {
    // Lookup of key codes to timer ID, or null for no repeat
    //
    var timers= {};

    // When key is pressed and we don't already think it's pressed, call the
    // key action callback and set a timer to generate another one after a delay
    //
    document.onkeydown= function(event) {
        var key= (event || window.event).keyCode;
        if (!(key in keys))
            return true;
        if (!(key in timers)) {
            timers[key]= null;
            keys[key]();
            if (repeat!==0)
                timers[key]= setInterval(keys[key], repeat);
        }
        return false;
    };

    // Cancel timeout and mark key as released on keyup
    //
    document.onkeyup= function(event) {
        var key= (event || window.event).keyCode;
        if (key in timers) {
            if (timers[key]!==null)
                clearInterval(timers[key]);
            delete timers[key];
        }
    };

    // When window is unfocused we may not get key events. To prevent this
    // causing a key to 'get stuck down', cancel all held keys
    //
    window.onblur= function() {
        for (key in timers)
            if (timers[key]!==null)
                clearInterval(timers[key]);
        timers= {};
    };
};

then:

// Arrow key movement. Repeat key five times a second
//
KeyboardController({
    37: function() { Move(-1, 0); },
    38: function() { Move(0, -1); },
    39: function() { Move(1, 0); },
    40: function() { Move(0, 1); }
}, 200);

Although, most action-based games have a fixed-time main frame loop, which you can tie the key up/down handling into.

Gingergingerbread answered 11/9, 2010 at 16:4 Comment(0)
G
6

I've solved it like this:

var pressedl = 0;
var pressedu = 0;
var pressedr = 0;
var pressedd = 0;

function Down(e) { 
        cxc = e.keyCode;
        if(cxc == 37)
            pressedl = 1;
        if(cxc == 38)
            pressedu = 1;
        if(cxc == 39)
            pressedr = 1;
        if(cxc == 40)
            pressedd = 1;
        //alert(cxc);
    }
    function Up(e) {
        cxc = e.keyCode;
        if(cxc == 37)
            pressedl = 0;
        if(cxc == 38)
            pressedu = 0;
        if(cxc == 39)
            pressedr = 0;
        if(cxc == 40)
            pressedd = 0;
        //alert(cxc);
    }

<body onLoad="Load()" onKeyDown="Down(event)" onKeyUp="Up(event)">

Genocide answered 11/9, 2010 at 16:23 Comment(0)
L
3

You could start the movement onkeydown and only end it onkeyup?

Libbielibbna answered 11/9, 2010 at 16:8 Comment(0)
F
2

This is nearly the same as the excellent answer from @bobince

I've amended it slightly to allow individual values for the interval

// Keyboard input with customisable repeat (set to 0 for no key repeat)
// usage
/**
KeyboardController({
    32: {interval:0, callback: startGame },
    37: {interval:10, callback: function() { padSpeed -= 5; } },
    39: {interval:10, callback: function() { padSpeed += 5; } }
});
*/

function KeyboardController(keyset) {
    // Lookup of key codes to timer ID, or null for no repeat
    //
    var timers= {};

    // When key is pressed and we don't already think it's pressed, call the
    // key action callback and set a timer to generate another one after a delay
    //
    document.onkeydown= function(event) {
        var key= (event || window.event).keyCode;
        if (!(key in keyset))
            return true;
        if (!(key in timers)) {
            timers[key]= null;
            keyset[key].callback();
            if (keyset[key].interval !== 0)
                timers[key]= setInterval(keyset[key].callback, keyset[key].interval);
        }
        return false;
    };

    // Cancel timeout and mark key as released on keyup
    //
    document.onkeyup= function(event) {
        var key= (event || window.event).keyCode;
        if (key in timers) {
            if (timers[key]!==null)
                clearInterval(timers[key]);
            delete timers[key];
        }
    };

    // When window is unfocused we may not get key events. To prevent this
    // causing a key to 'get stuck down', cancel all held keys
    //
    window.onblur= function() {
        for (key in timers)
            if (timers[key]!==null)
                clearInterval(timers[key]);
        timers= {};
    };
};

I've also got a plan to use setTimeout instead of setInterval for the reasons given in this question: setTimeout or setInterval?

I'll update this answer once I've amended and tested.

Forborne answered 11/9, 2010 at 15:1 Comment(0)
R
2

The typical solution to triggering actions in a game is to add a layer of indirection: don't let the user's action update entity state until the game loop runs on the next frame. (Yes, this applies to mouse events and just about anything else that affects game state as well in most cases)

It might make intuitive sense to trigger a game event as soon as a key is pressed; after all, that's how you'd normally respond to an event: right away in the listener callback.

However, in games and animations, the update/rendering loop is the only place where entity updates such as movement should occur. Messing with positions outside of the rendering loop bypasses the normal flow illustrated below:

[initialize state]
        |
        v
.-----------------.
|  synchronously  |
|  update/render  |
|  a single frame |
`-----------------`
   ^           |
   |           v
(time passes asynchronously, 
 events fire between frames)

When events fire, they should modify intermediate state that the update code can then take into consideration when it comes time to update entity positions and states.

Specifically, you could use flags that represent which keys were pressed, flip them on whenever a keydown event fires, and flip them off whenever a keyup event fires. Then, the key will be processed regardless of any operating system delay buffering in the update loop.

Rather than a boolean for every key, simply add the keys to a set when pressed and remove them when they're released.

Here's a minimal example:

const keysPressed = new Set();

document.addEventListener("keydown", e => {
  keysPressed.add(e.code);
});
document.addEventListener("keyup", e => {
  keysPressed.delete(e.code);
});

(function update() {
  requestAnimationFrame(update);  
  document.body.innerHTML = `
    <p>These keys are pressed:</p>
    <ul>
      ${[...keysPressed]
        .map(e => `<li>${e}</li>`)
        .join("")
      }
    </ul>
  `;
})();

The above code works as a drop-in to implement rudimentary game movement, with some default prevention as needed:

const keysPressed = new Set();
const preventedKeys = new Set([
  "ArrowUp",
  "ArrowDown",
  "ArrowLeft",
  "ArrowRight",
]);

document.addEventListener("keydown", e => {
  if (preventedKeys.has(e.code)) {
    e.preventDefault();
  }
  
  keysPressed.add(e.code);
});
document.addEventListener("keyup", e => {
  keysPressed.delete(e.code);
});

const player = {
  x: 0,
  y: 0,
  speed: 2,
  el: document.querySelector("#player"),
  render() {
    this.el.style.left = player.x + "px";
    this.el.style.top = player.y + "px";  
  },
  actions: {
    ArrowLeft() { this.x -= this.speed; },
    ArrowDown() { this.y += this.speed; },
    ArrowUp() { this.y -= this.speed; },
    ArrowRight() { this.x += this.speed; },
  },
  update(keysPressed) {
    Object.entries(this.actions)
      .forEach(([key, action]) =>
        keysPressed.has(key) && action.call(this)
      )
    ;
  },
};

(function update() {
  requestAnimationFrame(update);
  player.update(keysPressed);
  player.render();
})();
.wrapper {
  position: relative;
}
#player {
  width: 40px;
  height: 40px;
  background: purple;
  position: absolute;
}
<p>Use arrow keys to move</p>
<div class="wrapper">
  <div id="player"></div>
</div>

If you want some sort of cooldown/retrigger period (for example, the player is holding down a "fire" key but a gun should not fire a new bullet on every frame), I suggest handling that in each entity rather than in the logic for the above key-handling code. The key logic is responsible for identifying which keys are being pressed and nothing else.

Note that keyboards have limits to how many keys they register as pressed at once.

setTimeout and setInterval are imprecise. Rely on requestAnimationFrame as much as possible for games and animations. You can use a tick counter to determine elapsed time such that all entities in the game are synchronized to the same clock. Of course, much is application-dependent.


See also:

Rother answered 19/6, 2021 at 1:44 Comment(0)
O
1

As this event is to move whatever from one position to one position, why don't you use onkeypress event, so in that way if the user key pressed the up key, the whatever will keep moving up, as the Pressed(e) will be called many times until the user releases the key.

<body onLoad="Load()" onkeypress="Pressed(event)">
Orman answered 11/9, 2010 at 15:13 Comment(1)
same problem. the delay of 1 sec is still there, and when two keys are pressed, it only recognizez 1 of them.Genocide
I
1

This here is Lucas' solution in a more abstract version:

http://jsfiddle.net/V2JeN/4/

Seems you can only press 3 keys at a time to my surprise.

Inappetence answered 12/1, 2011 at 18:55 Comment(0)
E
1

I'm a total novice at this, but why not combine KeyDown with KeyUp? I am working on a similar project right now and, after checking out quirksmode I am going to set forth at figuring out how to combine the two events such that the whole time between a Down and Up realizes the desired affect.

Ecclesiology answered 19/6, 2011 at 21:24 Comment(1)
Thats way too much work, if the solution is so simple. Just use the code from the accepted answerGenocide
E
1

Modernized @bobince's great answer for my project as follows...

// Keyboard.js
'use strict'

class KeyboardController {
  constructor(keys, repeat) {
    this.keys = keys;
    this.repeat = repeat;
    this.timers = {};

    document.onkeydown = event => this.keydown(event);
    document.onkeyup = event => this.keyup(event);
    window.onblur = () => this.blur;
  }

  keydown(event) {
    event.stopPropagation();
    const code = event.code;
    if (!(code in this.keys)) return true;
    if (!(code in this.timers)) {
      this.timers[code] = null;
      this.keys[code]();
      if (this.repeat) this.timers[code] = setInterval(this.keys[code], this.repeat);
    }
    return false;
  }

  keyup(event) {
    const code = event.code;
    if (code in this.timers) {
      if (this.timers[code]) clearInterval(this.timers[code]);
      delete this.timers[code];
    }
  }

  blur() {
    for (let key in this.timers)
      if (this.timers[key]) clearInterval(this.timers[key]);
    this.timers = {};
  }
}

Same caller pattern...

const keyboard = new KeyboardController({
  ArrowLeft:  () => {/* your logic here */},
  ArrowRight: () => {},
  ArrowUp:    () => {},
  ArrowDown:  () => {}
}, 65);
Eigenvalue answered 16/1, 2022 at 18:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.