Jak zjistit, zda je stisknuto více kláves najednou pomocí JavaScriptu?

Snažím se vyvinout herní engine JavaScript a narazil jsem na tento problém:

  • Když stisknu SPACE postava skáče.
  • Když stisknu postava se pohybuje doprava.

Problém je v tom, že když stisknu vpravo a poté stisknu mezerník, postava vyskočí a pak se zastaví.

Používám keydown funkce pro stisknutí tlačítka. Jak mohu zkontrolovat, zda je stisknuto více kláves najednou?

Odpověď

Poznámka:KeyCode je nyní zastaralé.

Detekce více úhozů je snadná, pokud rozumíte konceptu

Způsob, jakým to dělám, je tento:

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

Tento kód je velmi jednoduchý:Vzhledem k tomu, že počítač předává pouze jeden stisk klávesy najednou, je vytvořeno pole pro sledování více klíčů. Pole pak lze použít ke kontrole jednoho nebo více klíčů najednou.

Jen pro vysvětlení, řekněme, že stisknete A a B , každý spustí keydown událost, která nastavuje map[e.keyCode] na hodnotu e.type == keydown , která se vyhodnotí jako pravda nebo false . Nyní oba map[65] a map[66] jsou nastaveny na true . Když pustíte A , keyup událost se spustí, což způsobí, že stejná logika určí opačný výsledek pro map[65] (A), což je nyní nepravda , ale od map[66] (B) je stále „dole“ (nespustilo událost keyup), zůstává pravda .

map pole prostřednictvím obou událostí vypadá takto:

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

Nyní můžete udělat dvě věci:

A) Keylogger (příklad) lze vytvořit jako referenci pro později, když chcete rychle zjistit jeden nebo více kódů klíčů. Za předpokladu, že jste definovali html prvek a ukázali na něj proměnnou element .

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

Poznámka:Prvek můžete snadno uchopit za jeho id atribut.

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

Tím se vytvoří html prvek, na který lze snadno odkazovat v javascriptu pomocí element

alert(element); // [Object HTMLDivElement]

Nemusíte ani používat document.getElementById() nebo $() chytit to. Ale kvůli kompatibilitě použijte $() jQuery se více doporučuje.

Jen zkontrolujte skript značka následuje za tělem HTML. Tip pro optimalizaci :Většina známých webů uvádí značku skriptu za body tag pro optimalizaci. Je to proto, že značka skriptu blokuje načítání dalších prvků, dokud není stahování skriptu dokončeno. Umístění před obsah umožňuje načíst obsah předem.

B (což vás zajímá) Můžete zkontrolovat jeden nebo více klíčů najednou, kde /*insert conditional here*/ byl, vezměte si tento příklad:

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

Upravit :To není nejčitelnější úryvek. Čitelnost je důležitá, takže můžete zkusit něco takového, aby to bylo pro oči jednodušší:

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

Použití:

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

Je to lepší?

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

(konec úprav)

Tento příklad kontroluje Ctrl Shift A , Ctrl Shift B a Ctrl Shift C

Je to stejně jednoduché 🙂

Poznámky

Sledování klíčových kódů

Obecným pravidlem je, že je dobré zdokumentovat kód, zejména věci jako kódy klíčů (jako // CTRL+ENTER ), abyste si mohli pamatovat, co to bylo.

Měli byste také umístit kódy klíčů ve stejném pořadí jako dokumentace (CTRL+ENTER => map[17] && map[13] , NE map[13] && map[17] ). Tímto způsobem nebudete nikdy zmateni, když se budete muset vrátit a upravit kód.

Dobře s řetězci if-else

Pokud kontrolujete kombinace různých částek (jako Ctrl Shift Alt Zadejte a Ctrl Zadejte ), vložte menší komba za větší komba, jinak menší komba přepíší větší komba, pokud jsou dostatečně podobná. Příklad:

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

Dostal jsem:„Tato kombinace kláves se stále aktivuje, i když klávesy nestisknu.“

Při práci s výstrahami nebo čímkoli, co se zaměřuje z hlavního okna, můžete zahrnout map = [] k resetování pole po provedení podmínky. Je to proto, že některé věci, například alert() , přemístit fokus z hlavního okna a způsobit, že událost ‚keyup‘ se nespustí. Například:

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:Výchozí nastavení prohlížeče

Zde je nepříjemná věc, kterou jsem našel, včetně řešení:

Problém:Protože prohlížeč má obvykle výchozí akce pro kombinace kláves (jako Ctrl D aktivuje okno záložky nebo Ctrl Shift C aktivuje skynote na maxthonu), můžete také přidat return false po map = [] , takže uživatelé vašeho webu nebudou frustrovaní, když je funkce „Duplikovat soubor“ umístěna na Ctrl D , přidá stránku do záložek.

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

Bez return false , okno Záložka by vyskočí ke zděšení uživatele.

Příkaz return (nový)

Dobře, takže v tomto okamžiku nechcete vždy funkci opustit. Proto event.preventDefault() funkce tam je. Nastaví interní příznak, který tlumočníkovi řekne, aby ne umožnit prohlížeči spustit jeho výchozí akci. Poté vykonávání funkce pokračuje (zatímco return okamžitě opustí funkci).

Než se rozhodnete, zda použít return false, pochopte tento rozdíl nebo e.preventDefault()

event.keyCode je zastaralé

Uživatel SeanVieira v komentářích poukázal na to, že event.keyCode je zastaralá.

Tam dal vynikající alternativu:event.key , která vrací řetězcovou reprezentaci stisknuté klávesy, například "a" pro A nebo "Shift" pro Shift .

Pokračoval jsem a připravil jsem si nástroj pro zkoumání uvedených řetězců.

element.onevent vs element.addEventListener

Obslužné nástroje registrované u addEventListener lze skládat na sebe a jsou volány v pořadí registrace při nastavení .onevent přímo je poměrně agresivní a potlačuje vše, co jste dříve měli.

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 Zdá se, že vlastnost přepíše vše a chování ev.preventDefault() a return false; může být poněkud nepředvídatelné.

V obou případech se handleři zaregistrovali prostřednictvím addEventlistener Zdá se, že je jednodušší psát a uvažovat.

Existuje také attachEvent("onevent", callback) z nestandardní implementace Internet Exploreru, ale toto je již více než zastaralé a netýká se ani JavaScriptu (týká se esoterického jazyka zvaného JScript ). Bylo by ve vašem nejlepším zájmu se co nejvíce vyhýbat polyglotnímu kódu.

Pomocná třída

Abych se vypořádal se zmatky/stížnostmi, napsal jsem „třídu“, která provádí tuto abstrakci (odkaz na 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();
}

Tato třída nedělá všechno a nezvládne všechny myslitelné případy použití. Nejsem knihovník. Ale pro obecné interaktivní použití by to mělo být v pořádku.

Chcete-li použít tuto třídu, vytvořte instanci a nasměrujte ji na prvek, ke kterému chcete přiřadit vstup z klávesnice:

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

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

Co to udělá, je připojit nový vstupní posluchač k prvku s #txt (předpokládejme, že se jedná o textovou oblast) a nastavte sledovací bod pro kombinaci kláves Ctrl+5 . Když obě Ctrl a 5 are down, funkce zpětného volání, kterou jste předali (v tomto případě funkce, která přidá "FIVE " do textové oblasti) bude voláno. Zpětné volání je spojeno s názvem print_5 , takže jej odstraníte jednoduše pomocí:

input_txt.unwatch("print_5");

Chcete-li odpojit input_txt z txt prvek:

input_txt.detach();

Tímto způsobem může garbage collection sebrat objekt (input_txt ), pokud by byl vyhozen, a nezůstane vám starý posluchač zombie událostí.

Pro důkladnost je zde rychlý odkaz na API třídy, prezentované ve stylu C/Java, abyste věděli, co vracejí a jaké argumenty očekávají.

Boolean  key_down (String key);

Vrátí true pokud key je dole, jinak je nepravda.

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

Vrátí true pokud jsou všechny klíče key1 .. keyN jsou dole, jinak nepravda.

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

Vytvoří „sledovací bod“ tak, že stisknete všechny keyN spustí zpětné volání

void     unwatch (String name);

Odebere uvedený bod sledování prostřednictvím jeho názvu

void     clear (void);

Vymaže mezipaměť „klíčů dolů“. Ekvivalent map = {} výše

void     detach (void);

Odpojí ev_kdown a ev_kup posluchače z rodičovského prvku, což umožňuje bezpečně se zbavit instance

Aktualizace 2017-12-02 V reakci na žádost o zveřejnění tohoto na github jsem vytvořil gist.

Aktualizace 21. 7. 2018 Chvíli jsem si hrál s programováním ve stylu deklarativního stylu a tento způsob je nyní můj osobní oblíbený:housle, pastebin

Obecně to bude fungovat s případy, které byste realisticky chtěli (ctrl, alt, shift), ale pokud potřebujete stisknout, řekněme, a+w zároveň by nebylo příliš obtížné „zkombinovat“ přístupy do víceklíčového vyhledávání.

Doufám, že tato důkladně vysvětlená odpověď miniblog byl užitečný 🙂