Como detectar se várias teclas são pressionadas ao mesmo tempo usando JavaScript?

173

Estou tentando desenvolver um mecanismo de jogo JavaScript e me deparei com este problema:

  • Quando eu pressiono SPACE o personagem pula.
  • Quando pressiono, o personagem se move para a direita.

O problema é que, quando estou pressionando a direita e pressionando espaço, o personagem pula e para de se mover.

Eu uso a keydownfunção para pressionar a tecla. Como posso verificar se há várias teclas pressionadas ao mesmo tempo?

XCS
fonte
3
Aqui está uma demonstração de uma página web que imprime automaticamente uma lista de todas as chaves que são pressionadas: stackoverflow.com/a/13651016/975097
Anderson Verde

Respostas:

327

Nota: keyCode agora está obsoleto.

A detecção de várias teclas é fácil se você entender o conceito

O jeito que eu faço é assim:

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 */
}

Esse código é muito simples: como o computador passa apenas um pressionamento de tecla de cada vez, é criada uma matriz para controlar várias chaves. A matriz pode ser usada para verificar uma ou mais chaves ao mesmo tempo.

Apenas para explicar, digamos que você pressione Ae B, cada um dispara um keydownevento que define map[e.keyCode]o valor de e.type == keydown, que é avaliado como verdadeiro ou falso . Agora ambos map[65]e map[66]estão definidos como true. Quando você solta A, o keyupevento é acionado, causando a mesma lógica para determinar o resultado oposto para map[65](A), que agora é falso , mas como map[66](B) ainda está "inativo" (não disparou um evento de keyup), permanece verdadeiro .

A mapmatriz, através dos dois eventos, fica assim:

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

Há duas coisas que você pode fazer agora:

A) Um registrador de chaves ( exemplo ) pode ser criado como referência para mais tarde, quando você quiser descobrir rapidamente um ou mais códigos de chave. Supondo que você tenha definido um elemento html e apontou para ele com a variável element.

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

Nota: Você pode facilmente pegar um elemento por seu idatributo.

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

Isso cria um elemento html que pode ser facilmente referenciado em javascript com element

alert(element); // [Object HTMLDivElement]

Você nem precisa usar document.getElementById()ou $()agarrar. Mas, por uma questão de compatibilidade, o uso de jQuery$() é mais amplamente recomendado.

Apenas verifique se a tag do script vem após o corpo do HTML. Dica de otimização : a maioria dos sites de grandes nomes coloca a tag de script após a tag body para otimização. Isso ocorre porque a tag de script impede que outros elementos sejam carregados até o término do download do script. Colocá-lo à frente do conteúdo permite que o conteúdo seja carregado com antecedência.

B (onde é o seu interesse) Você pode procurar uma ou mais chaves por vez, onde /*insert conditional here*/está, veja este exemplo:

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

Editar : esse não é o trecho mais legível. A legibilidade é importante, então você pode tentar algo assim para facilitar a visão:

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

Uso:

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

Isso é melhor?

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

(fim da edição)


Este exemplo verifica para CtrlShiftA, CtrlShiftBeCtrlShiftC

É tão simples assim :)

Notas

Acompanhamento dos códigos-chave

Como regra geral, é uma boa prática documentar código, especialmente coisas como códigos-chave (como // CTRL+ENTER ), para que você possa se lembrar do que eram.

Você também deve colocar os códigos de chave na mesma ordem que a documentação ( CTRL+ENTER => map[17] && map[13], NÃOmap[13] && map[17] ). Dessa forma, você nunca ficará confuso quando precisar voltar e editar o código.

Uma pegadinha com cadeias if-else

Se verificar combos de quantidades diferentes (como CtrlShiftAltEntere CtrlEnter), coloque combos menores depois de combos maiores, caso contrário os combos menores substituirão os combos maiores se forem semelhantes o suficiente. Exemplo:

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

Gotcha: "Essa combinação de teclas continua sendo ativada mesmo que eu não esteja pressionando as teclas"

Ao lidar com alertas ou qualquer coisa que tenha foco na janela principal, convém incluir map = []a redefinição da matriz após a conclusão da condição. Isso ocorre porque algumas coisas alert()afastam o foco da janela principal e fazem com que o evento 'keyup' não seja acionado. Por exemplo:

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

Gotcha: Padrões do navegador

Aqui está uma coisa chata que eu achei, com a solução incluída:

Problema: como o navegador geralmente possui ações padrão em combinações de teclas (como CtrlDativa a janela de favoritos ou CtrlShiftCskynote no maxthon), convém adicionar return falsedepois map = [], para que os usuários do seu site não fiquem frustrados quando o "Arquivo duplicado" função, sendo colocadoCtrlD , marca a página.

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

Sem return false , a janela do marcador seria exibida, para desgosto do usuário.

A declaração de retorno (nova)

Ok, então você nem sempre deseja sair da função nesse ponto. É por isso que a event.preventDefault()função está lá. O que ele faz é definir um sinalizador interno que instrua o intérprete a não permitir que o navegador execute sua ação padrão. Depois disso, a execução da função continua (enquantoreturn sairá imediatamente da função).

Entenda essa distinção antes de decidir se deve usar return falseou nãoe.preventDefault()

event.keyCode está obsoleto

O usuário SeanVieira apontou nos comentários que foram event.keyCodepreteridos.

Lá, ele ofereceu uma excelente alternativa event.key:, que retorna uma representação em cadeia da tecla que está sendo pressionada, como "a"para Aou "Shift"paraShift .

Fui em frente e cozinhei uma ferramenta para examinar as ditas cordas.

element.onevent vs element.addEventListener

Os manipuladores registrados com addEventListenerpodem ser empilhados e são chamados na ordem do registro, enquanto a configuração .oneventdiretamente é bastante agressiva e substitui tudo o que você tinha anteriormente.

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

A .oneventpropriedade parece substituir tudo e o comportamento de ev.preventDefault()ereturn false; pode ser bastante imprevisível.

Nos dois casos, os manipuladores registrados via addEventlistener parecem ser mais fáceis de escrever e raciocinar.

Também existe attachEvent("onevent", callback)na implementação fora do padrão do Internet Explorer, mas isso está além de obsoleto e nem pertence ao JavaScript (refere-se a uma linguagem esotérica chamada JScript ). Seria do seu interesse evitar o máximo possível o código poliglota.

Uma classe auxiliar

Para resolver confusão / reclamações, escrevi uma "classe" que faz essa abstração ( link pastebin ):

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

Essa classe não faz tudo e não lida com todos os casos de uso concebíveis. Eu não sou um cara da biblioteca. Mas, para uso interativo geral, tudo bem.

Para usar essa classe, crie uma instância e aponte para o elemento ao qual você deseja associar a entrada do teclado:

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

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

O que isso fará é anexar um novo ouvinte de entrada ao elemento #txt(vamos supor que seja uma área de texto) e definir um ponto de controle para a combinação de teclas Ctrl+5. Quando ambos Ctrle 5estão inativos, a função de retorno de chamada que você passou (nesse caso, uma função que adiciona "FIVE "à área de texto) será chamada. O retorno de chamada está associado ao nome print_5; portanto, para removê-lo, basta usar:

input_txt.unwatch("print_5");

Para desanexar input_txtdo txtelemento:

input_txt.detach();

Dessa forma, a coleta de lixo pode pegar o objeto ( input_txt), caso ele seja jogado fora, e você não terá um ouvinte de evento zumbi antigo sobrando.

Para mais detalhes, aqui está uma referência rápida à API da classe, apresentada no estilo C / Java para que você saiba o que eles retornam e quais argumentos eles esperam.

Boolean  key_down (String key);

Retorna truese keyestiver inativo, caso contrário, false.

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

Retorna truese todas as chaves key1 .. keyNestiverem inativas , caso contrário, false.

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

Cria um "ponto de controle", de modo que pressionar todos keyNacionará o retorno de chamada

void     unwatch (String name);

Remove o referido watchpoint pelo nome

void     clear (void);

Limpa o cache "keys down". Equivalente a map = {}acima

void     detach (void);

Desanexa os ouvintes ev_kdowne ev_kupdo elemento pai, tornando possível se livrar da instância com segurança

Atualização 2017-12-02 Em resposta a uma solicitação para publicá-la no github, criei uma essência .

Atualização 2018-07-21 Eu jogo com programação de estilo declarativo há um tempo e agora é o meu favorito: fiddle , pastebin

Geralmente, ele funciona com os casos que você deseja realisticamente (ctrl, alt, shift), mas se precisar pressionar, digamos, a+wao mesmo tempo, não seria muito difícil "combinar" as abordagens em um pesquisa com várias teclas.


Espero que esta resposta minimamente explicada seja útil :)

Braden Best
fonte
Acabei de fazer uma grande atualização para esta resposta! O exemplo do keylogger é mais coerente, atualizei a formatação para facilitar a leitura da seção "notes" e adicionei uma nova nota sobre return falsevspreventDefault()
Braden Best
E quando você pressiona / mantém pressionada uma tecla com o documento em foco, depois clica na caixa URL e solta a tecla. O keyup nunca é acionado, mas a chave está ativada, fazendo com que a lista esteja incorreta. Também vice-versa: mantenha pressionada a tecla na caixa URL, a tecla pressionada nunca é acionada e, em seguida, coloque o foco no documento e o status da tecla pressionada não está na lista. Basicamente, sempre que o documento recupera o foco, você nunca pode ter certeza do status principal.
User3015682
3
NB: keyCodeestá obsoleto - se você alternar para key, obtém a representação real de caracteres da chave, o que pode ser bom.
Sean Vieira
1
@SeanVieira Então, novamente, você pode fazer algumas coisas estranhas em C também. Por exemplo, você sabia que myString[5]é o mesmo que 5[myString]e nem sequer fornece um aviso de compilação (mesmo com -Wall -pedantic)? É porque a pointer[offset]notação pega o ponteiro, adiciona o deslocamento e desreferencia o resultado, fazendo myString[5]o mesmo que *(myString + 5).
Braden Best
1
@inorganik você está se referindo à classe auxiliar? Gists podem ser usadas como repositórios? Seria tedioso fazer um repositório inteiro para um pequeno trecho de código. Claro, eu vou fazer uma essência. Eu vou atirar esta noite. Midnight mountain Time -ish
Braden Melhor
30

Você deve usar o evento keydown para acompanhar as teclas pressionadas, e deve usar o evento keyup para acompanhar quando as teclas são liberadas.

Veja este exemplo: http://jsfiddle.net/vor0nwe/mkHsU/

(Atualização: estou reproduzindo o código aqui, caso o jsfiddle.net seja bloqueado :) O HTML:

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

... e o Javascript (usando 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');
});

Nesse exemplo, estou usando uma matriz para acompanhar quais teclas estão sendo pressionadas. Em um aplicativo real, você pode quererdelete cada elemento depois que a chave associada for liberada.

Observe que, embora eu tenha usado o jQuery para facilitar as coisas para mim neste exemplo, o conceito funciona tão bem quando se trabalha com Javascript 'bruto'.

Martijn
fonte
Mas como eu pensei que há um bug. Se você mantiver pressionado um botão, mude para outra guia (ou foco fraco) enquanto ainda pressiona o botão ao refocalizar a tela, mostrará que o botão está pressionado, mesmo que não esteja. : D
XCS
3
@ Cris: então você também pode adicionar um onblurmanipulador de eventos, que remove todas as teclas pressionadas da matriz. Depois de perder o foco, faria sentido pressionar todas as teclas novamente. Infelizmente, não há JS equivalente a GetKeyboardState.
Martijn
1
Tendo um problema com Colar em um Mac (Chrome). Ele obtém com êxito o keydown 91 (comando), keydown 86 (v), mas depois apenas insere o 91, deixando 86 pressionado. Lista de teclas: Acima: 91, Abaixo: 86. Isso parece acontecer apenas quando a tecla de comando é solta em segundo lugar - se eu soltar a tecla primeiro, ela registra corretamente a chave em ambas.
James Alday
2
Parece que, quando você pressiona três ou mais teclas ao mesmo tempo, para de detectar mais algumas teclas até você levantar uma. (Testado com Firefox 22)
Qvcool 28/07
1
@JamesAlday Mesmo problema. Aparentemente, afeta apenas a tecla Meta (OS) nos Macs. Veja a edição # 3 aqui: bitspushedaround.com/…
Don McCurdy,
20
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");

    } 

}
Eduardo La Hoz Miranda
fonte
1
Isso era tudo que eu queria, melhor resposta
Randall Coding
7

Eu usei desta maneira (tive que verificar onde Shift + Ctrl é pressionado):

// 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;
    }
});
Matriz
fonte
5

para quem precisa de código de exemplo completo. Direita + esquerda adicionada

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);
Reza Ramezanpour
fonte
3

Faça com que o keydown chame várias funções, com cada função verificando uma chave específica e respondendo adequadamente.

document.keydown = function (key) {

    checkKey("x");
    checkKey("y");
};
AnonymousGuest
fonte
2

Eu tentaria adicionar um keypress Eventmanipulador keydown. Por exemplo:

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

Isso serve apenas para ilustrar um padrão; Não entrarei em detalhes aqui (especialmente não no nível2 + específico do navegador Event).

Envie de volta, por favor, se isso ajuda ou não.

FK82
fonte
1
Isso não iria funcionar: keypress não gatilho em um monte de chaves que KeyDown e KeyUp fazer gatilho. Além disso, nem todos os navegadores acionam repetidamente eventos de pressionamento de tecla.
Martijn
Quirksmode diz seu erro: quirksmode.org/dom/events/keys.html . Mas não vou argumentar isso porque não testei minha proposta.
FK82
Citado nessa página: "Quando o usuário pressiona teclas especiais, como as teclas de seta, o navegador NÃO deve acionar eventos de pressionamento de tecla" . Quanto às repetições, lista o Opera e o Konqueror como não fazendo isso corretamente.
Martijn
2

Se uma das teclas pressionadas for Alt / Crtl / Shift, você poderá usar este método:

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()
    }
  }
}
Michael Lester
fonte
2
    $(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;
            }
        });

    });
Prosun Chakraborty
fonte
Obrigado pela sua contribuição. tente não apenas postar código, adicione algumas explicações.
Tim Rutter
2

Este não é um método universal, mas é útil em alguns casos. É útil para combinações como CTRL+ somethingou Shift+ somethingou CTRL+ Shift+ something, etc.

Exemplo: quando você deseja imprimir uma página usando CTRL+ P, a primeira tecla pressionada é sempre CTRLseguida por P. Mesmo com CTRL+ S, CTRL+ Ue outras combinações.

document.addEventListener('keydown',function(e){
      
    //SHIFT + something
    if(e.shiftKey){
        switch(e.code){

            case 'KeyS':
                console.log('Shift + S');
                break;

        }
    }

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

            case 'KeyS':
                console.log('CTRL + Shift + S');
                break;

        }
    }

});

Jakub Muda
fonte
1
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;

Não é o melhor caminho, eu sei.

Anônimo
fonte
-1
Easiest, and most Effective Method

//check key press
    function loop(){
        //>>key<< can be any string representing a letter eg: "a", "b", "ctrl",
        if(map[*key*]==true){
         //do something
        }
        //multiple keys
        if(map["x"]==true&&map["ctrl"]==true){
         console.log("x, and ctrl are being held down together")
        }
    }

//>>>variable which will hold all key information<<
    var map={}

//Key Event Listeners
    window.addEventListener("keydown", btnd, true);
    window.addEventListener("keyup", btnu, true);

    //Handle button down
      function btnd(e) {
      map[e.key] = true;
      }

    //Handle Button up
      function btnu(e) {
      map[e.key] = false;
      }

//>>>If you want to see the state of every Key on the Keybaord<<<
    setInterval(() => {
                for (var x in map) {
                    log += "|" + x + "=" + map[x];
                }
                console.log(log);
                log = "";
            }, 300);
voador
fonte