Управляем кучей таймеров в JavaScript
В прошлом посте было о том, как я писал игру для конкурса js13kGames, цель которого — уместить свою поделку на стеке открытых web-технологий в 13 килобайт.
Помимо ухищрений с минификацией, игра вдохновила меня на создание инструмента для управления большим количеством таймеров путём оборачивания их в удобный интерфейс и объединения в группы. Код и кейсы, в которых это может пригодиться — под катом.
Демо, где можно позапускать ракеты и заценить пару примеров кода
Где это можно использовать?
Зачастую, логика в играх разбивается на состояния (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 тиков в секунду), который дёргает все модели _активной сцены_, которые внутри себя сами принимают решение о том, на какой тик и как реагировать. Так игровая логика получается более детерминированной (что особенно важно для игр «с физикой»). Т.е., лучше абстрагироваться не до «пространств таймеров», а до «сцен».