Управляем кучей таймеров в JavaScript

В прошлом посте было о том, как я писал игру для конкурса js13kGames, цель которого — уместить свою поделку на стеке открытых web-технологий в 13 килобайт.


Помимо ухищрений с минификацией, игра вдохновила меня на создание инструмента для управления большим количеством таймеров путём оборачивания их в удобный интерфейс и объединения в группы. Код и кейсы, в которых это может пригодиться — под катом.


9dfd1d90dbeb42aaa492085e51cff6f1.png

Демо, где можно позапускать ракеты и заценить пару примеров кода


Где это можно использовать?


Зачастую, логика в играх разбивается на состояния (states) — небольшие независимые части, например: меню, уровень, экран победы. Если предположить, что игрок может в процессе игры перейти в меню, поставив происходящее в уровне на паузу, нам нужно озаботиться сохранением контекста таймеров.


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


Ещё один пример. Если игровое пространство состоит из множества небольших локаций (вспомним, к примеру, The Binding of Isaac), мы можем захотеть хранить состояния объектов и юнитов в некоторых из них. Опять же, нужно прописать возможность поставить таймеры на паузу для каждого юнита.


timestore


Основная идея — создать «пространство таймеров», которое можно целиком ставить на паузу, возобновлять, очищать и переиспользовать. Первая, примитивная версия появилась уже в процессе разработки игры, а за прошедшие выходные я написал более адекватный вариант — timestore.


В качестве таймеров используются два класса: Timeout и Interval. Внутри оба используют setTimeout(). У обоих классов есть методы с говорящими названиями: .clear(), .pause(), .resume() и ещё некоторые. Таймеры можно использовать напрямую, но основная фишка — в классе Timestore.


Когда мы создаём таймер через timestore, он сохраняется в коллекцию, и после к нему можно обратиться не только напрямую, но и по ID:


var gameTimers = new timestore.Timestore(),
    simpleTimeout = gameTimers.setTimeout(callback, 5000),
    timeoutWithCustomId = gameTimers.setTimeout('customId', callback, 5000);

    someButton.on('click', function () {
        simpleTimeout.clear();
        gameTimers.clearTimeout('customId');
    });

Важно: нельзя использовать числа (и строки наподобие '10') в качестве кастомных ID, поскольку числа используются как дефолтные идентификаторы внутри timestore.


Управление целыми коллекциями:


gameTimers.pauseAll();
menuTimers.resumeAll();

Если таймеру передать флаг fireBeforeClear, то в момент очистки он сработает:


var lightBulb = new Interval(toggleLight, 100, true);

function switchOff() {
    lightBulb.clear(); // В этот момент свет переключится в последний раз.
}

Ещё один полезный метод — .getTimeLeft(). Он возвращает количество милисекунд до следующего срабатывания таймера.


Что дальше?


В планах на ближайшие два-три дня:


  • добавить проверку пользовательских ID, чтобы нельзя было передать число;
  • написать методы типа .clearIntervals(['id1', 'id3', 'id5']), чтобы было удобнее управлять частями коллекции;
  • добавить Interval.fireCounter — свойство, показывающее, сколько раз сработал таймер;
  • и родственный метод Interval.clearIn(times), позволяющий очистить таймер через несколько срабатываний.

Также в ближайшее время выложу пару примеров на JSFiddle и CodePen.
В данный момент есть страница с примерами.


PS. Кстати, делая timestore, я впервые использовал тесты и, для тех кто ещё сомневается, скажу: тесты — это круто!


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


PPPS. Про состояния, которые states, можно подробнее почитать здесь.

Комментарии (1)

  • 28 сентября 2016 в 10:17

    0

    В играх часто удобнее обходиться одним таймером с самым высоким разрешением для выбранного FPS (например, 60 тиков в секунду), который дёргает все модели _активной сцены_, которые внутри себя сами принимают решение о том, на какой тик и как реагировать. Так игровая логика получается более детерминированной (что особенно важно для игр «с физикой»). Т.е., лучше абстрагироваться не до «пространств таймеров», а до «сцен».

© Habrahabr.ru