О чем этот пример
В веб-разработке часто возникает необходимость динамически скрывать или показывать игровое поле, например, для переключения между вкладками интерфейса или паузы. Однако простое применение CSS-свойства `display: none` к родительскому контейнеру может привести к неожиданным последствиям для внутренних процессов Phaser, таким как остановка физики или анимаций. В этой статье разберем практический пример, демонстрирующий, как корректно реализовать переключение видимости канваса, сохраняя работоспособность всех игровых систем после повторного отображения. Этот подход полезен для создания гибких пользовательских интерфейсов, где игра — лишь один из компонентов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.55.2.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
var config = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.RESIZE,
width: 800,
height: 600
},
parent: 'phaser-example',
physics: {
default: "arcade",
arcade: {
gravity: { y: 200 }
}
},
scene: {
preload: preload,
create: create
}
};
var game = new Phaser.Game(config);
function preload() {
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image("sky", "assets/skies/space3.png");
this.load.image("logo", "assets/sprites/phaser3-logo.png");
this.load.image("red", "assets/particles/red.png");
}
function create() {
this.add.image(400, 300, "sky");
var particles = this.add.particles("red");
var emitter = particles.createEmitter({
speed: 100,
scale: { start: 1, end: 0 },
blendMode: "ADD"
});
var logo = this.physics.add.image(400, 100, "logo");
logo.setVelocity(100, 200);
logo.setBounce(1, 1);
logo.setCollideWorldBounds(true);
emitter.startFollow(logo);
}
let button = document.createElement('button');
button.textContent = 'Toggle';
document.body.appendChild(button);
let isHidden = false;
const div = document.getElementById('phaser-example');
button.addEventListener("click", () => {
isHidden = !isHidden;
div.style.display = isHidden ? "block" : "none";
});
Базовый пример: игра с физикой и частицами
Исходный код представляет собой типичную сцену на Phaser 3. В ней загружаются фоновое изображение, логотип и текстура для частиц. Создается физический спрайт (логотип), который двигается, отскакивает от границ мира, а за ним следует эмиттер частиц.
function create() {
this.add.image(400, 300, "sky");
var particles = this.add.particles("red");
var emitter = particles.createEmitter({
speed: 100,
scale: { start: 1, end: 0 },
blendMode: "ADD"
});
var logo = this.physics.add.image(400, 100, "logo");
logo.setVelocity(100, 200);
logo.setBounce(1, 1);
logo.setCollideWorldBounds(true);
emitter.startFollow(logo);
}
Ключевые моменты:
- this.physics.add.image создает спрайт с физическим телом Arcade.
- setVelocity задает начальную скорость.
- setBounce определяет упругость при столкновении.
- setCollideWorldBounds включает отскок от границ игрового мира.
- particles.createEmitter настраивает систему частиц, которая следует за спрайтом через startFollow.
Добавление кнопки для переключения видимости
За пределами контекста Phaser, в обычном JavaScript, создается кнопка и добавляется в DOM. Ее назначение — управлять CSS-свойством display родительского контейнера игры (элемента с id='phaser-example').
let button = document.createElement('button');
button.textContent = 'Toggle';
document.body.appendChild(button);
let isHidden = false;
const div = document.getElementById('phaser-example');
button.addEventListener("click", () => {
isHidden = !isHidden;
div.style.display = isHidden ? "block" : "none";
});
Механизм прост: при каждом клике флаг isHidden инвертируется, а контейнеру присваивается display: block (показать) или display: none (скрыть). Важно понимать, что display: none полностью удаляет элемент из потока документа, останавливая рендеринг и, что критично, внутренние циклы Phaser.
Проблема: скрытие останавливает игровые процессы
Хотя визуально игра исчезает и появляется по клику, простое использование display: none имеет серьезный побочный эффект. Когда контейнер скрыт, Phaser перестает обновлять игровой цикл (step). Это означает, что расчеты физики (arcade), анимации, системы частиц и таймеры приостанавливаются.
**Что происходит в примере:**
1. Логотип двигается и отскакивает.
2. При клике на Toggle и скрытии (display: none) все замирает.
3. При повторном показе (display: block) игра возобновляется, но **с позициями и состояниями на момент скрытия**. Физика не рассчитывала перемещение, пока игра была невидима.
Это поведение может быть желательным для полной паузы, но часто требуется, чтобы после показа игра продолжилась плавно, как будто ничего не прерывалось.
Решение: приостановка (pause) и возобновление (resume) сцены
Более контролируемый подход — использовать встроенные методы жизненного цикла сцены Phaser: scene.pause() и scene.resume(). Они позволяют программно остановить и запустить обновление конкретной сцены, не затрагивая видимость DOM-элемента напрямую.
// Внутри функции create() или в области видимости сцены
let button = document.createElement('button');
button.textContent = 'Toggle Pause';
document.body.appendChild(button);
button.addEventListener("click", () => {
if (this.scene.isPaused()) {
this.scene.resume();
// div.style.display = 'block'; // Опционально
} else {
this.scene.pause();
// div.style.display = 'none'; // Опционально
}
});
**Преимущества этого подхода:**
- Физика, частицы и таймеры корректно приостанавливаются и возобновляются.
- Вы можете независимо управлять видимостью контейнера (display) и логикой игры.
- Состояние игры сохраняется во время паузы.
Гибридный подход: управление и display, и игровым циклом
Для полного контроля, когда скрытие должно также приостанавливать логику игры, можно комбинировать оба метода. Основная идея — слушать события видимости страницы (VisibilityChange) или изменения стилей, чтобы синхронизировать состояние Phaser с состоянием DOM.
function create() {
// ... инициализация игры ...
const gameContainer = document.getElementById('phaser-example');
const observer = new MutationObserver(() => {
if (gameContainer.style.display === 'none') {
this.scene.pause();
} else {
this.scene.resume();
}
});
observer.observe(gameContainer, { attributes: true, attributeFilter: ['style'] });
}
Этот код использует MutationObserver для отслеживания изменений атрибута style у контейнера игры. При обнаружении изменения на display: none сцена ставится на паузу, а при возврате значения — возобновляется. Это создает надежную связь между интерфейсом и игровой логикой.
Что попробовать дальше
Динамическое скрытие игрового канваса через display: none — мощный инструмент, но требующий понимания его влияния на игровой цикл Phaser. Для простой визуальной паузы достаточно CSS, но для корректной остановки физики и анимаций следует использовать методы сцены pause() и resume(). Для экспериментов попробуйте:
1. Добавить звуки и проверить, как они реагируют на display: none.
2. Реализовать плавное затемнение интерфейса через opacity вместо display.
3. Создать систему вкладок, где в каждой активна своя игровая сцена, а неактивные — полностью остановлены.
