О чем этот пример
В Phaser сцены могут не только запускаться и останавливаться, но и временно "засыпать", сохраняя своё состояние. Это мощный механизм для создания сложных переходов между игровыми экранами, паузами или меню без перезагрузки ресурсов. В этой статье мы разберем официальный пример, который показывает, как использовать события `WAKE` и методы `switch` для плавного переключения между двумя активными сценами, каждая из которых продолжает свою анимацию после возвращения.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class SceneA extends Phaser.Scene
{
constructor ()
{
super('sceneA');
this.pic;
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('arrow', 'assets/sprites/longarrow.png');
}
create ()
{
this.pic = this.add.image(400, 300, 'arrow').setOrigin(0, 0.5);
this.input.once(Phaser.Input.Events.POINTER_DOWN, function ()
{
this.scene.switch('sceneB');
}, this);
this.events.on(Phaser.Scenes.Events.WAKE, function ()
{
this.wake(this.input, this.scene);
}, this);
}
wake (input, scene)
{
input.once(Phaser.Input.Events.POINTER_DOWN, () =>
{
scene.switch('sceneB');
}, this);
}
update (time, delta)
{
this.pic.rotation += 0.01;
}
}
class SceneB extends Phaser.Scene
{
constructor ()
{
super('sceneB');
this.graphics;
this.timerEvent;
this.clockSize = 240;
}
preload ()
{
// this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('face', 'assets/pics/bw-face.png');
}
create ()
{
this.add.image(400, 300, 'face').setAlpha(0.5);
this.timerEvent = this.time.addEvent({ delay: 8000, loop: true });
this.graphics = this.add.graphics({ x: 0, y: 0 });
this.input.once(Phaser.Input.Events.POINTER_DOWN, function (event)
{
this.scene.switch('sceneA');
}, this);
this.events.on(Phaser.Scenes.Events.WAKE, function ()
{
this.wake(this.input, this.scene);
}, this);
}
wake (input, scene)
{
input.once(Phaser.Input.Events.POINTER_DOWN, event =>
{
scene.switch('sceneA');
}, this);
}
update ()
{
this.graphics.clear();
this.drawClock(400, 300, this.timerEvent);
}
drawClock (x, y, timer)
{
// Progress is between 0 and 1, where 0 = the hand pointing up and then rotating clockwise a full 360
const graphics = this.graphics;
const clockSize = this.clockSize;
// The frame
graphics.lineStyle(6, 0xffffff, 1);
graphics.strokeCircle(x, y, clockSize);
let angle;
let dest;
let p1;
let p2;
let size;
// The overall progress hand (only if repeat > 0)
if (timer.repeat > 0)
{
size = clockSize * 0.9;
angle = (360 * timer.getOverallProgress()) - 90;
dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), size);
graphics.lineStyle(2, 0xff0000, 1);
graphics.beginPath();
graphics.moveTo(x, y);
p1 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle - 5), size * 0.7);
graphics.lineTo(p1.x, p1.y);
graphics.lineTo(dest.x, dest.y);
graphics.moveTo(x, y);
p2 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle + 5), size * 0.7);
graphics.lineTo(p2.x, p2.y);
graphics.lineTo(dest.x, dest.y);
graphics.strokePath();
graphics.closePath();
}
// The current iteration hand
size = clockSize * 0.95;
angle = (360 * timer.getProgress()) - 90;
dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), size);
graphics.lineStyle(2, 0xffff00, 1);
graphics.beginPath();
graphics.moveTo(x, y);
p1 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle - 5), size * 0.7);
graphics.lineTo(p1.x, p1.y);
graphics.lineTo(dest.x, dest.y);
graphics.moveTo(x, y);
p2 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle + 5), size * 0.7);
graphics.lineTo(p2.x, p2.y);
graphics.lineTo(dest.x, dest.y);
graphics.strokePath();
graphics.closePath();
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [ SceneA, SceneB ]
};
const game = new Phaser.Game(config);
Зачем нужны состояния Sleep и Wake?
По умолчанию, при переходе на новую сцену, старая сцена останавливается (this.scene.stop()). Это означает, что её системы обновления (update) отключаются, а все созданные объекты уничтожаются. Однако иногда нужно временно свернуть активность сцены, но сохранить её визуальное состояние и данные для быстрого возврата. Именно для этого Phaser предоставляет механизм сна и пробуждения.
Когда вы переключаетесь между сценами с помощью this.scene.switch('ключСцены'), текущая сцена не останавливается, а **засыпает** (SLEEP). Новая сцена либо запускается, либо **пробуждается** (WAKE), если она уже была запущена ранее и затем усыплена. Это позволяет избежать дорогостоящих операций по пересозданию объектов и перезагрузке данных.
Анализ примера: две интерактивные сцены
Пример состоит из двух сцен: SceneA и SceneB. Они переключаются по клику мыши.
**SceneA** отображает вращающуюся стрелку. Ключевой момент: её анимация (обновление rotation в методе update) должна продолжиться с того же места, когда мы вернемся из SceneB. Если бы мы использовали stop и start, стрелка сбрасывалась бы.
**SceneB** отображает статичное полупрозрачное изображение и анимированные стрелки часов, которые визуализируют ход таймера. Таймер создается один раз в create и продолжает тикать, даже когда сцена спит.
Конфигурация игры включает обе сцены в массив, что позволяет системе сцен управлять ими.
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [ SceneA, SceneB ] // Обе сцены зарегистрированы здесь
};
const game = new Phaser.Game(config);
Переключение сцен и событие WAKE
Основная логика переключения привязана к событию клика мыши Phaser.Input.Events.POINTER_DOWN. Обратите внимание, используется input.once, чтобы обработчик сработал только один раз. После переключения он удаляется.
Внутри метода create каждой сцены происходит две важные вещи:
1. Установка одноразового обработчика для первоначального переключения.
2. Подписка на событие Phaser.Scenes.Events.WAKE.
Событие WAKE генерируется каждый раз, когда сцена пробуждается из состояния сна. В его обработчике мы снова устанавливаем одноразовый клик для обратного переключения. Без этого после пробуждения сцена не реагировала бы на клик, так как первоначальный обработчик (установленный в create) уже был выполнен и удален.
// В методе create SceneA
this.input.once(Phaser.Input.Events.POINTER_DOWN, function ()
{
this.scene.switch('sceneB'); // Усыпляет SceneA, будит или запускает SceneB
}, this);
this.events.on(Phaser.Scenes.Events.WAKE, function ()
{
// Этот код выполнится при каждом пробуждении SceneA
this.wake(this.input, this.scene);
}, this);
Метод wake выделен для чистоты кода и просто повторно устанавливает обработчик клика.
wake (input, scene)
{
input.once(Phaser.Input.Events.POINTER_DOWN, () =>
{
scene.switch('sceneB');
}, this);
}
Жизненный цикл сцены и сохранение состояния
Давайте проследим, что происходит при первом и последующих переключениях:
1. **Старт игры**: Запускается `SceneA` (так как она первая в массиве). Выполняются её `preload`, `create` и начинает работать `update`.
2. **Первый клик**: Срабатывает `input.once` в `SceneA`. Вызывается `this.scene.switch('sceneB')`.
* `SceneA` получает событие `SLEEP`, её `update` перестает вызываться.
* `SceneB` получает событие `START` (так как она запускается впервые), выполняются её `preload` и `create`.
3. **Клик в SceneB**: Срабатывает `input.once` в `SceneB`. Вызывается `this.scene.switch('sceneA')`.
* `SceneB` засыпает (`SLEEP`), её `update` останавливается, но таймер `timerEvent` продолжает работу в фоне.
* `SceneA` пробуждается (`WAKE`), её `update` немедленно возобновляется, и стрелка продолжает вращение с того же угла. Срабатывает обработчик `WAKE`, который переустанавливает клик.
4. **Последующие переключения**: Процесс повторяется. Сцены циклически усыпляются и пробуждаются, сохраняя свое внутреннее состояние (угол поворота стрелки, прогресс таймера).
Ключевое отличие от stop/start в том, что методы create и preload вызываются только один раз за время жизни игры, если только вы явно не перезапустите сцену.
Визуализация таймера в SceneB
SceneB демонстрирует, как можно визуализировать работу систем Phaser, которые активны даже для "спящей" сцены. Таймер timerEvent создается с опцией loop: true, поэтому он работает непрерывно.
Метод drawClock использует Phaser.Math.RotateAroundDistance для расчета позиций концов стрелок часов на основе прогресса таймера. Красная стрелка показывает общий прогресс за все повторения (getOverallProgress), а желтая — прогресс текущей итерации (getProgress).
Поскольку в update сцены this.graphics.clear() вызывается каждый кадр, а затем рисуются новые стрелки, анимация выглядит плавной. Когда сцена спит, update не вызывается, и часы "замирают", но сам таймер продолжает отсчет. При пробуждении update снова запускается и отрисовывает часы, которые сразу показывают актуальное, "набежавшее" за время сна время.
// Фрагмент отрисовки текущей итерации таймера
angle = (360 * timer.getProgress()) - 90;
dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), size);
// ... дальше отрисовка линии до точки dest
Что попробовать дальше
Использование switch() вместе с событиями SLEEP и WAKE — это эффективный паттерн для создания сложных навигаций внутри игры. Он идеально подходит для переключения между игровыми уровнями и картой мира, основным геймплеем и инвентарем, или любыми другими экранами, состояние которых должно сохраняться. Для экспериментов попробуйте: добавить третью сцену и организовать циклическое переключение между всеми; использовать событие SLEEP для паузы игровой логики; или проверить, как ведут себя физические тела при усыплении и пробуждении сцены.
