简体   繁体   中英

Remove key press delay in Javascript

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?

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.

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)">

您可以在 onkeydown 开始移动并仅在 onkeyup 上结束它吗?

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.

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:

由于此事件是将whatever从一个位置移动到一个位置,为什么不使用onkeypress事件,所以如果用户键按下up键,则whatever都会继续向上移动,因为Pressed(e)将被多次调用,直到用户释放密钥。

<body onLoad="Load()" onkeypress="Pressed(event)">

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.

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.

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);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM