1. ホーム
  2. javascript

[解決済み】JavaScriptで複数のキーが同時に押されたかどうかを検出するには?

2022-04-13 23:20:13

質問

JavaScriptのゲームエンジンを開発しようとしているのですが、このような問題に遭遇しました。

  • を押すと スペース キャラクターがジャンプする。
  • を押すと キャラクターが右に移動する。

問題は、右を押しているときに、スペースを押すと、キャラクターがジャンプして、動かなくなることです。

を使っています。 keydown 関数を使用して、押されたキーを取得します。一度に複数のキーが押されているかどうかを確認するにはどうしたらよいでしょうか。

どのように解決するのですか?

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

このコードは非常にシンプルです。コンピュータは一度に一つのキーストロークしか渡さないので、複数のキーを追跡するために配列を作成します。そして、この配列を使って、1つまたは複数のキーを一度にチェックすることができる。

簡単に説明すると、例えば、あなたが A B を起動し、それぞれ keydown を設定するイベントです。 map[e.keyCode] の値を e.type == keydown と評価されます。 または 虚偽 . これで、両方の map[65]map[66] が設定されます。 true . を離すと A は、その keyup イベントが発生すると、同じロジックで map[65] (A)となり、現在は が、しかし map[66] (B) はまだ "down"(キーアップイベントをトリガーしていない)のままです。 .

map の配列は、両方のイベントを通して、次のようになります。

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

今できることは2つあります。

A) A キーロガー ( ) は、後で1つまたは複数のキーコードを素早く把握したい場合の参考資料として作成することができます。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>

これは、javascript で簡単に参照できる html 要素を作成するものです。 element

alert(element); // [Object HTMLDivElement]

を使う必要もありません。 document.getElementById() または $() を使用して取得します。しかし、互換性のために、jQueryの使用は $() がより広く推奨されます。

を確認するだけです。 スクリプト タグはHTMLのボディの後に来ます。 最適化のヒント : 有名なウェブサイトの多くは、scriptタグを の後に bodyタグの最適化。これは、scriptタグが、そのスクリプトのダウンロードが終了するまで、他の要素の読み込みをブロックするためです。コンテンツの前に置くことで、コンテンツがあらかじめ読み込まれるようになります。

B(これはあなたの興味があるところです) 一度に1つまたは複数のキーをチェックすることができます。 /*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 シフト A , Ctrl シフト B Ctrl シフト C

ただそれだけのことです :)

注意事項

キーコードの管理

一般的なルールとして、コード、特にキーコード(例えば、"key "コード)のようなものを文書化することは良い習慣です。 // CTRL+ENTER を覚えておくことができます。

また、キーコードはドキュメントと同じ順番に並べる必要があります( CTRL+ENTER => map[17] && map[13] NOT map[13] && map[17] ). こうすることで、コードを編集する際に混乱することがなくなります。

if-else チェーンの問題

異なる量のコンボをチェックする場合(例えば Ctrl シフト 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"

Gotcha: "キーを押していないのに、このキーコンボが起動し続ける".This key combo keeps activating even though I'm not pressing the keys"

アラートなど、メインウィンドウからフォーカスを奪うものを扱う場合は、「アラート」の文字列の中に 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 シフト C はmaxthonでskynoteを有効にします)、また、以下のように return false の後に map = [] そのため、あなたのサイトのユーザーは、quot;Duplicate File"関数が Ctrl D は、代わりにそのページをブックマークします。

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

なし return false ブックマーク・ウィンドウ がポップアップして、ユーザーをがっかりさせた。

returnステートメント(新規)

さて、では必ずしもその時点で関数を終了したいかというと、そうではありません。そのため event.preventDefault() という関数があります。この関数は内部フラグを設定し、インタープリタに次のように伝えます。 ない ブラウザがデフォルトのアクションを実行することを許可します。その後、関数の実行は継続されます(一方 return は直ちに関数を終了します)。

この区別を理解した上で return false または e.preventDefault()

event.keyCode は非推奨

ユーザー ショーン・ビエイラ がコメントで指摘されました。 event.keyCode は非推奨です。

そこで、優れた代替案を提示した。 event.key のように、押されたキーの文字列表現を返します。 "a" に対して A または "Shift" に対して シフト .

を料理してきました。 ツール という文字列を調べます。

element.oneventelement.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リンク ):

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 (ここでは textarea とします)、そしてキーコンボ Ctrl+5 . 両方の Ctrl5 がダウンしている場合、渡されたコールバック関数(この場合は、その関数に "FIVE " が呼び出されます。このコールバックには print_5 で、これを削除するには、単純に

input_txt.unwatch("print_5");

デタッチする場合 input_txt から txt 要素を使用します。

input_txt.detach();

こうすることで、ガベージコレクションはオブジェクトを拾うことができます ( input_txt ) が捨てられた場合、古いゾンビイベントリスナーが残ることはありません。

このクラスが何を返し、どんな引数を期待するのかがわかるように、C/Javaスタイルで表示されています。

<ブロッククオート
Boolean  key_down (String key);

戻り値 true もし key はダウン、それ以外は偽。

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

戻り値 true もしすべてのキーが key1 .. keyN はダウン、それ以外は偽。

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

をすべて押すと、quot;watchpoint" を作成します。 keyN はコールバックを起動します。

void     unwatch (String name);

ウォッチポイントをその名前から削除します。

void     clear (void);

キーダウンキャッシュをワイプします。と同じです。 map = {} 上記

void     detach (void);

をデタッチします。 ev_kdownev_kup リスナーを親要素から安全に取り除くことができるようになります。

2017-12-02更新 githubに公開してほしいというご要望にお応えして 要旨 .

2018-07-21更新 しばらく宣言型スタイルのプログラミングで遊んでいましたが、今はこのやり方が個人的に一番好きです。 フィドル , ペーストビン

一般的には、現実的に必要なケース(ctrl、alt、shift)で動作しますが、例えば、打つ必要がある場合は、以下のようになります。 a+w を同時に使用することで、複数のキーを使用した検索を行うことができます。


こうあってほしい 徹底的な説明による回答 ミニブログが役に立ったようです :)