Собирает все подсистемы и владеет игровым циклом:
const engine = new Engine({ virtualWidth: 480, virtualHeight: 270, scale: 3, background: 0x0b0b12, fixedFps: 60, maxFps: 30, parent });
await engine.init(); // WebGL, ввод, оверлей fade-переходов, FPS-счётчик
await engine.start(); // запуск цикла
Доступные подсистемы: engine.renderer, engine.camera, engine.scenes, engine.input, engine.tweens, engine.events.
Опции: maxFps (по умолчанию 0 — без лимита) — лок частоты отрисовки; autoResize (по умолчанию true) — пересчитывать целочисленный масштаб при ресайзе окна.
Логика — фиксированный шаг 60 Гц; рендер после каждого шага. dt в Scene.update(dt) всегда 1/60. После лага («вкладка спала») выполняется максимум 5 шагов за кадр — игра замедляется, а не «телепортируется».
Лок кадров (maxFps, например 30): срезанные rAF-кадры не делают ни шагов, ни рендера, но время не теряется — копится в аккумулятор, и шаги выполняются пачкой на отрисованном кадре. Рендер — не чаще лимита, логика — те же 60 Гц.
Дебаг-плашка в левом верхнем углу (engine.fpsMeter.view): среднее FPS за окно ~полсекунды. Скрыт по умолчанию, переключается по F3 — движок слушает клавишу сам (независимо от привязок ввода приложения; F3 в игре может быть занята своим дебаг-оверлеем — они переключаются вместе, как слои отладки).
Вручную цикл обычно не трогают; Engine.tick делает по порядку: input.update() (опрос геймпада) → tweens.update(dt) → scenes.update(dt) → input.endTick().
Типизированные по темам события — связь систем без прямых зависимостей:
engine.events.on<{ gold: number }>('loot:picked', ({ gold }) => console.log(gold));
const off = engine.events.on('quest:done', () => { ... });
engine.events.emit('loot:picked', { gold: 5 });
off(); // отписка
Обновляется циклом автоматически. Все методы возвращают handle с cancel().
engine.tweens.to(sprite, { x: 100, alpha: 1 }, {
duration: 0.5,
ease: cubicOut, // linear, quadIn/Out, cubicIn/Out, sineInOut
delay: 0.2, // стартовые значения снимаются ПОСЛЕ delay
onDone: () => console.log('готово')
});
engine.tweens.delay(1.5, () => spawnEnemy()); // таймер, сек
engine.tweens.cancelFor(sprite); // отменить всё для объекта
Гарантии: значения не выходят за цель (зажим на последнем кадре), onDone вызывается один раз, тик, пересекающий конец delay, не «съедает» часть движения.
engine.fxОбъекты, тикаемые движком сами между твин-ами и логикой сцены — эффекты, аниматоры, вспышки (см. anim.md):
engine.fx.add(emitter); // Updatable: { update(dt), destroyed? }
engine.fx.clear(); // при выходе из сцены, если она не разрушает дерево
Записи с destroyed === true (Pixi destroy() выставляет флаг сам) не тикаются и вычищаются из реестра автоматически — разрушенный Pixi-объект касаться нельзя (transform обнулён).
Для one-shot визуальных эффектов предпочтителен FxLayer (render/fx/, см. render.md): слой добавляется в engine.fx один раз, сам тикает и вычищает всё, что спавнит (fx.ring/floatText/fade/pop/flash/burst/trail). Прямое engine.fx.add(emitter) оставлено для движковых эффектов вне сцены. SpriteFlash теперь имеет destroyed (следует за спрайтом) — его можно писать в реестр напрямую; повторный start() продолжает работать.
Состояние прохождения: флаги и именованные переменные. Жанронезависимо — какие флаги есть, решает игра. Сериализуется в сейв (GameStateData).
const state = new GameState();
state.setFlag('met_elder');
state.setVar('gold', 30);
state.getNumber('gold'); // 30 (getString/getBool — аналогично)
state.hasFlag('met_elder'); // true
// Сейв/загрузка + миграции при изменении формата:
state.dataVersion = 2;
state.addMigration((d) => ({ ...d, version: 2, vars: { ...d.vars, gold: 0 } }));
const data = state.serialize(); // положить в сейв
state.load(data); // миграции применятся автоматически
Контракт миграций: миграция сама повышает version в возвращаемых данных; регистрируются от старых версий к новым, применяются по порядку при load. Цикл загрузки останавливается, как только данные догнали dataVersion, — миграция, забывшая повысить version, не даст следующей миграции примениться к уже повышенным данным дважды.
Конечный автомат для состояний сущностей (idle/walk/attack), AI и т.п.:
const sm = new StateMachine((state, event) => console.warn(`${state} не знает событие ${event}`));
sm.add('idle', { enter, exit, update: (dt) => {...} });
sm.add('chase', { enter, exit, update });
sm.transition('idle', 'seePlayer', 'chase');
sm.transition('chase', 'losePlayer', 'idle');
sm.change('idle'); // exit старого -> enter нового
sm.handleEvent('seePlayer'); // переход по таблице
sm.update(dt); // тикает текущее состояние; sm.time — время в состоянии
Переход «из любого состояния» — для событий вроде hurt/die, чтобы не регистрировать их из каждого состояния (обычные переходы имеют приоритет):
sm.transitionAny('die', 'dead');
Кулдаун на фиксированном шаге (секунды, без performance.now()):
const cd = new Cooldown(0.45); cd.trigger(); // запустить (false, если ещё не готов) cd.triggerForce(); // перезапустить принудительно cd.update(dt); // тикать каждый шаг cd.ready; // готов? cd.timeRemaining; // секунд до готовности cd.progress; // 1..0 — удобно для полоски перезарядки
Настройки игрока (громкости, язык, прочее) в отдельном слоте хранилища — не попадают в игровые сейвы:
const settings = new Settings(localStorage, 'settings'); // или любой StorageLike
settings.load();
settings.update({ master: 0.8, music: 0.6 });
settings.data.master; // 0.8
settings.update({ extra: { hintSeen: true } });
settings.getExtra('hintSeen');
const off = settings.onChange((s) => audio.setBusVolume('master', s.master));
Хранилище может быть недоступно (приватный режим) — Settings работает в памяти, не падает.