Как определить, нажимается ли несколько клавиш одновременно с использованием JavaScript?

Я пытаюсь разработать игровой движок JavaScript, и я столкнулся с этой проблемой:

  • Когда я нажимаю SPACE, персонаж прыгает.
  • Когда я нажимаю →, персонаж движется вправо.

Проблема в том, что когда я нажимаю вправо, а затем нажимаю пробел, персонаж прыгает, а затем перестает двигаться.

Я использую keydown функцию, чтобы получить клавишу. Как я могу проверить, нажаты ли несколько клавиш одновременно?

Ответ 1

Примечание. KeyCode теперь устарел.

Обнаружение множественных нажатий клавиш легко, если вы понимаете концепцию

То, как я это делаю, выглядит так:

var map = {}; // You could also use an array
onkeydown = onkeyup = function(e){
    e = e || event; // to deal with IE
    map[e.keyCode] = e.type == 'keydown';
    /* insert conditional here */
}

Этот код очень прост: поскольку компьютер одновременно выполняет только одно нажатие клавиши, создается массив для отслеживания нескольких клавиш. Затем этот массив можно использовать для проверки одного или нескольких ключей одновременно.

Просто для пояснения, скажем, вы нажимаете A и B, каждый из которых запускает событие keydown, которое устанавливает для map[e.keyCode] значение e.type == keydown, которое оценивается как истинное или ложное. Теперь map[65] и map[66] установлены на true. Когда вы отпускаете A, происходит событие keyup, в результате чего та же логика определяет противоположный результат для map[65] (A), который теперь неверен, но, поскольку map[66] (B) все еще "выключен" "(это не вызвало событие keyup), оно остается верным.

Массив map через оба события выглядит следующим образом:

// keydown A 
// keydown B
[
    65:true,
    66:true
]
// keyup A
// keydown B
[
    65:false,
    66:true
]

Теперь вы можете сделать две вещи:

A) Ключевой регистратор (пример) можно создать в качестве справочного материала для дальнейшего использования, когда вы хотите быстро выяснить один или несколько кодов клавиш. Предполагая, что вы определили элемент html и указали на него с помощью переменной element.

element.innerHTML = '';
var i, l = map.length;
for(i = 0; i < l; i ++){
    if(map[i]){
        element.innerHTML += '<hr>' + i;
    }
}

Примечание. Вы можете легко получить элемент по его атрибуту id.

<div id="element"></div>

Это создает HTML-элемент, на который можно легко ссылаться в javascript с помощью element

alert(element); // [Object HTMLDivElement]

Вам даже не нужно использовать document.getElementById() или $(), чтобы захватить его. Но для совместимости рекомендуется использовать jQuery $().

Просто убедитесь, что тег script идет после текста HTML. Совет по оптимизации. Большинство известных веб-сайтов ставят тег после тега тела для оптимизации. Это связано с тем, что тег script блокирует дальнейшую загрузку элементов до завершения загрузки скрипта. Размещая его перед контентом, можно загружать контент заранее.

B (в этом и состоит ваш интерес) Вы можете проверить один или несколько ключей за один раз, когда /*insert conditional here*/ был, возьмите этот пример:

if(map[17] && map[16] && map[65]){ // CTRL+SHIFT+A
    alert('Control Shift A');
}else if(map[17] && map[16] && map[66]){ // CTRL+SHIFT+B
    alert('Control Shift B');
}else if(map[17] && map[16] && map[67]){ // CTRL+SHIFT+C
    alert('Control Shift C');
}

Изменить: это не самый читаемый фрагмент. Читаемость важна, так что вы можете попробовать что-то вроде этого, чтобы облегчить глаза:

function test_key(selkey){
    var alias = {
        "ctrl":  17,
        "shift": 16,
        "A":     65,
        /* ... */
    };

    return key[selkey] || key[alias[selkey]];
}

function test_keys(){
    var keylist = arguments;

    for(var i = 0; i < keylist.length; i++)
        if(!test_key(keylist[i]))
            return false;

    return true;
}

Использование:

test_keys(13, 16, 65)
test_keys('ctrl', 'shift', 'A')
test_key(65)
test_key('A')

Это лучше?

if(test_keys('ctrl', 'shift')){
    if(test_key('A')){
        alert('Control Shift A');
    } else if(test_key('B')){
        alert('Control Shift B');
    } else if(test_key('C')){
        alert('Control Shift C');
    }
}

(конец редактирования)


В этом примере проверяются Ctrl Shift A, Ctrl Shift B и Ctrl Shift C

Это так же просто, как это :)

Примечания

Отслеживание кодов клавиш

Как правило, рекомендуется документировать код, особенно такие, как коды клавиш (например, // CTRL+ENTER), чтобы вы могли помнить, какими они были.

Вы также должны располагать коды клавиш в том же порядке, что и документация (CTRL+ENTER => map[17] && map[13], НЕ map[13] && map[17]). Таким образом, вы никогда не запутаетесь, когда вам нужно вернуться и отредактировать код.

Гоча с цепями if-else

Если вы проверяете комбинации разных сумм (например, Ctrl Shift Alt Enter и Ctrl Enter), поместите меньшие комбинации после больших комбинаций, иначе меньшие комбинации будут отменены большие комбо, если они достаточно похожи. Пример:

// Correct:
if(map[17] && map[16] && map[13]){ // CTRL+SHIFT+ENTER
    alert('Whoa, mr. power user');
}else if(map[17] && map[13]){ // CTRL+ENTER
    alert('You found me');
}else if(map[13]){ // ENTER
    alert('You pressed Enter. You win the prize!')
}

// Incorrect:
if(map[17] && map[13]){ // CTRL+ENTER
    alert('You found me');
}else if(map[17] && map[16] && map[13]){ // CTRL+SHIFT+ENTER
    alert('Whoa, mr. power user');
}else if(map[13]){ // ENTER
    alert('You pressed Enter. You win the prize!');
}
// What will go wrong: When trying to do CTRL+SHIFT+ENTER, it will
// detect CTRL+ENTER first, and override CTRL+SHIFT+ENTER.
// Removing the else is not a proper solution, either
// as it will cause it to alert BOTH "Mr. Power user" AND "You Found Me"

Поправил: "Эта комбинация клавиш продолжает активироваться, даже если я не нажимаю клавиши"

При работе с оповещениями или чем-либо, что фокусируется на главном окне, вы можете включить map = [] для сброса массива после выполнения условия. Это потому, что некоторые вещи, такие как alert(), убирают фокус из главного окна и приводят к тому, что событие 'keyup' не запускается. Например:

if(map[17] && map[13]){ // CTRL+ENTER
    alert('Oh noes, a bug!');
}
// When you Press any key after executing this, it will alert again, even though you 
// are clearly NOT pressing CTRL+ENTER
// The fix would look like this:

if(map[17] && map[13]){ // CTRL+ENTER
    alert('Take that, bug!');
    map = {};
}
// The bug no longer happens since the array is cleared

Принято: настройки браузера по умолчанию

Вот досадная вещь, которую я нашел, с включенным решением:

Проблема: поскольку браузер обычно выполняет действия по умолчанию для сочетаний клавиш (например, Ctrl D активирует окно закладок или Ctrl Shift C активирует skynote на maxthon), вы также можете добавить return false после map = [], поэтому пользователи вашего сайта не будут разочарованы, когда функция "Duplicate File", включенная в Ctrl D, вместо этого добавит страницу в закладки.

if(map[17] && map[68]){ // CTRL+D
    alert('The bookmark window didn\'t pop up!');
    map = {};
    return false;
}

Без return false, к ужасу пользователя, всплыло окно закладок.

Заявление о возврате (новое)

Итак, вы не всегда хотите выйти из функции в этот момент. Вот почему есть функция event.preventDefault(). Он устанавливает внутренний флаг, который указывает интерпретатору не разрешать браузеру выполнять действие по умолчанию. После этого выполнение функции продолжается (тогда как return немедленно выйдет из функции).

Поймите это различие, прежде чем решить, использовать ли return false или e.preventDefault()

event.keyCode устарела

Пользователь SeanVieira указал в комментариях, что event.keyCode устарела.

Там он дал отличную альтернативу: event.key, которая возвращает строковое представление нажатой клавиши, например, "a" для A или "Shift" для Shift.

Я пошел дальше и приготовил инструмент для проверки указанных строк.

element.onevent против element.addEventListener

Обработчики, зарегистрированные в addEventListener, могут быть сложены и вызываются в порядке регистрации, в то время как установка .onevent напрямую довольно агрессивна и переопределяет все, что у вас было раньше.

document.body.onkeydown = function(ev){
    // do some stuff
    ev.preventDefault(); // cancels default actions
    return false; // cancels this function as well as default actions
}

document.body.addEventListener("keydown", function(ev){
    // do some stuff
    ev.preventDefault() // cancels default actions
    return false; // cancels this function only
});

Кажется, что свойство .onevent перекрывает все, а поведение ev.preventDefault() и return false; может быть довольно непредсказуемым.

В любом случае, обработчики, зарегистрированные через addEventlistener, кажутся более простыми для написания и рассуждений.

Есть также attachEvent("onevent", callback) из нестандартной реализации Internet Explorer, но это не рекомендуется и даже не относится к JavaScript (это относится к эзотерическому языку, называемому JScript). В ваших интересах было бы как можно больше избегать использования кода полиглота.

Вспомогательный класс

Чтобы устранить путаницу/жалобы, я написал "класс", который делает эту абстракцию (вставка ссылки):

function Input(el){
    var parent = el,
        map = {},
        intervals = {};

    function ev_kdown(ev)
    {
        map[ev.key] = true;
        ev.preventDefault();
        return;
    }

    function ev_kup(ev)
    {
        map[ev.key] = false;
        ev.preventDefault();
        return;
    }

    function key_down(key)
    {
        return map[key];
    }

    function keys_down_array(array)
    {
        for(var i = 0; i < array.length; i++)
            if(!key_down(array[i]))
                return false;

        return true;
    }

    function keys_down_arguments()
    {
        return keys_down_array(Array.from(arguments));
    }

    function clear()
    {
        map = {};
    }

    function watch_loop(keylist, callback)
    {
        return function(){
            if(keys_down_array(keylist))
                callback();
        }
    }

    function watch(name, callback)
    {
        var keylist = Array.from(arguments).splice(2);

        intervals[name] = setInterval(watch_loop(keylist, callback), 1000/24);
    }

    function unwatch(name)
    {
        clearInterval(intervals[name]);
        delete intervals[name];
    }

    function detach()
    {
        parent.removeEventListener("keydown", ev_kdown);
        parent.removeEventListener("keyup", ev_kup);
    }

    function attach()
    {
        parent.addEventListener("keydown", ev_kdown);
        parent.addEventListener("keyup", ev_kup);
    }

    function Input()
    {
        attach();

        return {
            key_down: key_down,
            keys_down: keys_down_arguments,
            watch: watch,
            unwatch: unwatch,
            clear: clear,
            detach: detach
        };
    }

    return Input();
}

Этот класс не делает всего и не будет обрабатывать все возможные варианты использования. Я не библиотекарь. Но для общего интерактивного использования это должно быть хорошо.

Чтобы использовать этот класс, создайте экземпляр и укажите его на элемент, с которым вы хотите связать ввод с клавиатуры:

var input_txt = Input(document.getElementById("txt"));

input_txt.watch("print_5", function(){
    txt.value += "FIVE ";
}, "Control", "5");

Что это будет делать, так это присоединить новый слушатель ввода к элементу с помощью #txt (допустим, это текстовое поле) и установить точку наблюдения для комбинации клавиш Ctrl+5. Когда оба Ctrl и 5 не работают, будет вызвана функция обратного вызова, которую вы передали (в данном случае, функция, которая добавляет "FIVE " к текстовой области). Обратный вызов связан с именем print_5, поэтому, чтобы удалить его, вы просто используете:

input_txt.unwatch("print_5");

Чтобы отсоединить input_txt от элемента txt:

input_txt.detach();

Таким образом, сборщик мусора может забрать объект (input_txt), если он будет выброшен, и у вас не останется старый слушатель событий зомби.

Для краткости приведу краткую ссылку на API класса, представленный в стиле C/Java, чтобы вы знали, что они возвращают и какие аргументы они ожидают.

Boolean  key_down (String key);

Returns true if key is down, false otherwise.

Boolean  keys_down (String key1, String key2, ...);

Returns true if all keys key1 .. keyN are down, false otherwise.

void     watch (String name, Function callback, String key1, String key2, ...);

Создает "точку наблюдения", так что нажатие всех клавиш keyN вызовет обратный вызов

void     unwatch (String name);

Удаляет указанную точку наблюдения через ее имя

void     clear (void);

Стирает кеш "клавиш вниз". Эквивалентно map = {} выше

void     detach (void);

Отсоединяет прослушиватели ev_kdown и ev_kup от родительского элемента, что позволяет безопасно избавиться от экземпляра

Обновление 2017-12-02 В ответ на запрос опубликовать это на github я создал gist.

Обновление 2018-07-21 Некоторое время я играл с декларативным стилем программирования, и теперь этот способ мой любимый: fiddle, pastebin

Как правило, он будет работать с теми случаями, которые вы реально хотите (ctrl, alt, shift), но если вам нужно нажать, скажем, a+w в то же время, не составит труда "объединить" подходит к поиску нескольких ключей.


Надеюсь, этот подробный ответ мини-блог был полезен :)

Ответ 2

Вы должны использовать событие keydown, чтобы отслеживать нажатые клавиши, и вы должны использовать событие keyup, чтобы отслеживать, когда клавиши отпускаются.

Смотрите этот пример: http://jsfiddle.net/vor0nwe/mkHsU/

(Обновление: Im воспроизводит код здесь, в случае, если jsfiddle.net поручает:) HTML:

<ul id="log">
    <li>List of keys:</li>
</ul>

... и Javascript (используя jQuery):

var log = $('#log')[0],
    pressedKeys = [];

$(document.body).keydown(function (evt) {
    var li = pressedKeys[evt.keyCode];
    if (!li) {
        li = log.appendChild(document.createElement('li'));
        pressedKeys[evt.keyCode] = li;
    }
    $(li).text('Down: ' + evt.keyCode);
    $(li).removeClass('key-up');
});

$(document.body).keyup(function (evt) {
    var li = pressedKeys[evt.keyCode];
    if (!li) {
       li = log.appendChild(document.createElement('li'));
    }
    $(li).text('Up: ' + evt.keyCode);
    $(li).addClass('key-up');
});

В этом примере Im использует массив, чтобы отслеживать, какие клавиши нажаты. В реальном приложении вам может понадобиться delete каждый элемент после того, как их связанный ключ будет выпущен.

Обратите внимание, что, хотя я использовал jQuery для упрощения для себя в этом примере, концепция работает так же хорошо, когда вы работаете в "сыром" Javascript.

Ответ 3

document.onkeydown = keydown; 

function keydown (evt) { 

    if (!evt) evt = event; 

    if (evt.ctrlKey && evt.altKey && evt.keyCode === 115) {

        alert("CTRL+ALT+F4"); 

    } else if (evt.shiftKey && evt.keyCode === 9) { 

        alert("Shift+TAB");

    } 

}

Ответ 4

Я использовал этот путь (должен был проверить, где находится Shift + Ctrl):

// create some object to save all pressed keys
var keys = {
    shift: false,
    ctrl: false
};

$(document.body).keydown(function(event) {
// save status of the button 'pressed' == 'true'
    if (event.keyCode == 16) {
        keys["shift"] = true;
    } else if (event.keyCode == 17) {
        keys["ctrl"] = true;
    }
    if (keys["shift"] && keys["ctrl"]) {
        $("#convert").trigger("click"); // or do anything else
    }
});

$(document.body).keyup(function(event) {
    // reset status of the button 'released' == 'false'
    if (event.keyCode == 16) {
        keys["shift"] = false;
    } else if (event.keyCode == 17) {
        keys["ctrl"] = false;
    }
});

Ответ 5

для тех, кому нужен полный примерный код. Right + Left добавлено

var keyPressed = {};
document.addEventListener('keydown', function(e) {

   keyPressed[e.key + e.location] = true;

    if(keyPressed.Shift1 == true && keyPressed.Control1 == true){
        // Left shift+CONTROL pressed!
        keyPressed = {}; // reset key map
    }
    if(keyPressed.Shift2 == true && keyPressed.Control2 == true){
        // Right shift+CONTROL pressed!
        keyPressed = {};
    }

}, false);

document.addEventListener('keyup', function(e) {
   keyPressed[e.key + e.location] = false;

   keyPressed = {};
}, false);

Ответ 6

Сделайте keydown даже вызовом нескольких функций, каждая из которых проверяет конкретный ключ и отвечает соответствующим образом.

document.keydown = function (key) {

    checkKey("x");
    checkKey("y");
};

Ответ 7

Если одна из нажатых клавиш Alt/Crtl/Shift, вы можете использовать этот метод:

document.body.addEventListener('keydown', keysDown(actions) );

function actions() {
   // do stuff here
}

// simultaneous pressing Alt + R
function keysDown (cb) {
  return function (zEvent) {
    if (zEvent.altKey &&  zEvent.code === "KeyR" ) {
      return cb()
    }
  }
}

Ответ 8

    $(document).ready(function () {
        // using ascii 17 for ctrl, 18 for alt and 83 for "S"
        // ctr+alt+S
        var map = { 17: false, 18: false, 83: false };
        $(document).keyup(function (e) {
            if (e.keyCode in map) {
                map[e.keyCode] = true;
                if (map[17] && map[18] && map[83]) {
                    // Write your own code here, what  you want to do
                    map[17] = false;
                    map[18] = false;
                    map[83] = false;
                }
            }
            else {
                // if u press any other key apart from that "map" will reset.
                map[17] = false;
                map[18] = false;
                map[83] = false;
            }
        });

    });

Ответ 9

Я попробую добавить обработчик keypress Event на keydown. Например:

window.onkeydown = function() {
    // evaluate key and call respective handler
    window.onkeypress = function() {
       // evaluate key and call respective handler
    }
}

window.onkeyup = function() {
    window.onkeypress = void(0) ;
}

Это просто предназначено для иллюстрации шаблона; Я не буду вдаваться в подробности здесь (особенно, если не зарегистрирован в браузере уровень2 + Event).

Отправляйте назад, пожалуйста, помогаете ли это или нет.

Ответ 10

case 65: //A
jp = 1;
setTimeout("jp = 0;", 100);

if(pj > 0) {
ABFunction();
pj = 0;
}
break;

case 66: //B
pj = 1;
setTimeout("pj = 0;", 100);

if(jp > 0) {
ABFunction();
jp = 0;
}
break;

Не лучший способ, я знаю.

Ответ 11

Это не универсальный метод, но он полезен в некоторых случаях. Он полезен для таких комбинаций, как CTRL + something или Shift + something или CTRL + Shift + something и т.д.

Пример: если вы хотите распечатать страницу, используя CTRL + P, первой нажатой клавишей всегда будет CTRL, а затем P. То же самое с CTRL + S, CTRL + U и другими комбинациями.

document.addEventListener('keydown',function(e){

  //SHIFT + something
  if(e.shiftKey){
    switch(e.code){

      case 'KeyS':
        alert('Shift + S');
        break;

    }
  }

  //CTRL + SHIFT + something
  if(e.ctrlKey && e.shiftKey){
    switch(e.code){

      case 'KeyS':
        alert('CTRL + Shift + S');
        break;

    }
  }

});