Как определить, нажимается ли несколько клавиш одновременно с использованием JavaScript? - PullRequest
150 голосов
/ 05 марта 2011

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

  • Когда я нажимаю ПРОБЕЛ символ прыгает.
  • Когда я нажимаю & rightarrow; , персонаж перемещается вправо.

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

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

Ответы [ 9 ]

281 голосов
/ 16 сентября 2012

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

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

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, которое оценивается как либо true , либо false . Теперь map[65] и map[66] установлены на true. Когда вы отпускаете A, происходит событие keyup, в результате чего та же логика определяет противоположный результат для map[65] (A), который теперь равен false , но с map[66] ( B) все еще "вниз" (это не вызвало событие keyup), оно остается true .

Массив 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. Совет по оптимизации : Большинство известных веб-сайтов ставят тег скрипта после тега body для оптимизации. Это связано с тем, что тег 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 Введите и Ctrl Введите ), поместите меньшие комбо после больших комбо, иначе меньшие комбо заменит более крупные комбо, если они достаточно похожи. Пример:

// 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's 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 ). В ваших интересах было бы как можно больше избегать кода полиглота.

класс помощника

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

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

Возвращает true, если key выключен, в противном случае - false.

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

Возвращает true, если все клавиши key1 .. keyN не работают, в противном случае - false.

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 InВ ответ на запрос опубликовать это на github, я создал gist .

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

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


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

29 голосов
/ 05 марта 2011

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

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

(Обновление: я воспроизводю код здесь, на случай, если 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');
});

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

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

11 голосов
/ 25 марта 2014
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");

    } 

}
6 голосов
/ 19 июля 2013

Я использовал этот способ (приходилось проверять, где нажата 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;
    }
});
2 голосов
/ 30 августа 2017

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

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);
2 голосов
/ 09 сентября 2015

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

document.keydown = function (key) {

    checkKey("x");
    checkKey("y");
};
1 голос
/ 03 января 2018

Если нажата одна из клавиш 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()
    }
  }
}
0 голосов
/ 21 января 2012
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;

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

0 голосов
/ 05 марта 2011

Я бы попробовал добавить обработчик 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, специфичной для браузера).

Отпишитесь, пожалуйста, помогает ли это или нет.

...