10 августа 2010

Игры в браузере: Нажимаем на кнопки

Привет. Продолжая тему игр в браузере, хочу затронуть вопрос обработки событий с клавиатуры. Сразу хочу сказать, что с данной проблемой я сталкивался только в браузерах на WebKit. О чем же я?

Я о событие нажатие кнопки. Что с ним не так?

Действительно, что же. Так как, увидеть один раз лучше (а так же интереснее), чем читать (особенно то что написал я), продемонстрирую проблему так.

var count = 0;
window.onkeydown = function (event) {
    count++;
}
window.onkeyup = function (event) {
    count--;
    console.log(count);
    count = 0;
}

Для тех кому лень проверять как поведет себя данный код, говорю, значение переменной будет отлично от нуля, если вы подержите какую-нибудь кнопку несколько секунд (не совсем какую-нибудь, модификаторы вроде shift, meta, ctrl, не в счет). Почему? Потому что событие нажатие кнопки будет повторяться.

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

// Таймер, он нужен что бы повторять событие, (если это необходимо).
var timer = null;

// Список действий.
var onActions = [];

// Добавляет действие.
function addAction(func, keyCode, repeat) {
    onActions.push({
        repeat : repeat, // Повторять ли событие.
        ready : true,    // Готово ли событие снова выполнится.
        func : func,     // Функция которая должна выполнится.
        key : keyCode    // Код клавиши.
    });
}

// Список выполняемых действий.
var doActionsList = [];

// Обработчик наступивших действий.
function doActions() {
    // Обнуляем таймер, нужно, что бы действие не повторялось чаще чем нужно.
    clearTimeout(timer);
    // Выполняем все действия.
    for(var i = 0; i < doActionsList.length; i++)
        doActionsList[i]();
    // Если есть наступившие действия, то через 200 мс повторяем обработку.
    if (doActionsList.length > 0)
        timer = setTimeout(function () { doActions(); }, 200);
}

window.onkeyup = function (event) {
    // Проверяем какое действие нужно отменить.
    for (var i = 0; i < onActions.length; i++) {
        if (onActions[i].key == event.keyCode) {
            // Если оно не готово. А вдруг готово?
            if (!onActions[i].ready) {
                // Делаем готовым.
                onActions[i].ready = true;
                // Удаляем действие из списка наступивших,
                // если оно туда могло попасть.
                if (onActions[i].repeat)
                    doActionsList.splice(
                        doActionsList.indexOf(onActions[i].func),
                        1);
            }
        }
    }
}
window.onkeydown = function (event) {
    // Проверяем какое действие произошло.
    for (var i = 0; i < onActions.length; i++) {
        if (onActions[i].key == event.keyCode) {
            // Если готово,
            if (onActions[i].ready) {
                // то теперь не готово.
                onActions[i].ready = false;
                // Проверяем, может ли действие повторятся.
                if (onActions[i].repeat) {
                    // Заносим функцию действия в список наступивших.
                    doActionsList.push(onActions[i].func);
                    // Вызываем обработчик выполняемых действий.
                    doActions();
                }
                else {
                    // Если действие не повторяется, то функцию
                    // вызываем только один раз.
                    onActions[i].func();
                }
            }
        }
    }
}

// Добавляем повторяющееся действие на space.
addAction(function () { console.log("space") }, 32, true);
// Добавляем не повторяющееся действие на enter.
addAction(function () { console.log("enter") }, 13, false);

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

Да, но это работает, а так же дает ряд плюшек, вроде фиксированного времени повторения действия. Вот собственно и все. Ушел пить чай.