О чем этот пример
При разработке игр на Phaser 3 вы можете столкнуться с ошибкой рендеринга графики при использовании режима масштабирования `Phaser.Scale.RESIZE`. В частности, это касается работы с Frame Buffer Objects (FBO), например, при применении шейдеров или постобработки. В этой статье мы разберем типичный случай из баг-трекера (bugs/5563) и покажем, как корректно инициализировать сцену и её объекты, чтобы избежать артефактов и "плавающего" отображения спрайтов при изменении размеров окна браузера. Понимание этой проблемы поможет создавать более стабильные и адаптивные игры.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class MyGame extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('logo', 'assets/sprites/phaser2.png');
}
create ()
{
this.logo = this.add.image(200, 200, 'logo');
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: MyGame,
scale: {
mode: Phaser.Scale.RESIZE
}
};
const game = new Phaser.Game(config);
В чём суть проблемы?
Ошибка, описанная в bugs/5563, возникает при комбинации двух факторов: использование режима масштабирования Phaser.Scale.RESIZE и добавление игровых объектов (например, спрайтов) в методе create() сцены.
При RESIZE размеры игрового холста (canvas) динамически подстраиваются под размеры окна или контейнера. Однако, если объекты создаются в момент, когда размеры холста ещё не стабилизировались (например, до полной загрузки страницы или во время её инициализации), их позиционирование и привязка к системам рендеринга (включая FBO) могут сбиться. Это приводит к тому, что спрайты могут отображаться не в том месте, "уплывать" за границы или вообще не рендериться.
Анализ исходного кода примера
Давайте посмотрим на предоставленный пример. В нём используется стандартная структура сцены Phaser.
class MyGame extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('logo', 'assets/sprites/phaser2.png');
}
create ()
{
this.logo = this.add.image(200, 200, 'logo');
}
}
Конфигурация игры задаёт режим Phaser.Scale.RESIZE.
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: MyGame,
scale: {
mode: Phaser.Scale.RESIZE
}
};
const game = new Phaser.Game(config);
Проблема в том, что метод create() сцены MyGame выполняется один раз при её создании. Если в этот момент физические размеры DOM-контейнера (с id='phaser-example') отличаются от указанных в конфиге width и height (800x600), система scale может не успеть корректно применить новые параметры к внутренним буферам, включая FBO. Спрайт logo будет создан с координатами (200, 200) в предположении одних размеров, а рендериться — в других.
Практическое решение: слушатель события 'resize'
Наиболее надёжным решением является создание или обновление игровых объектов в ответ на событие изменения размера. Phaser генерирует событие 'resize' на экземпляре игры (game) и на каждой сцене (this.scale), когда размер холста изменился.
Вам нужно добавить в сцену слушатель этого события. Лучше всего это сделать в методе create(), после чего сразу вызвать обработчик для первоначальной настройки.
create ()
{
// Создаём спрайт, но пока не добавляем его на сцену
this.logo = null;
// Функция, которая будет размещать или обновлять спрайт
const placeLogo = () => {
const centerX = this.scale.width / 2;
const centerY = this.scale.height / 2;
if (this.logo) {
// Если спрайт уже существует, обновляем его позицию
this.logo.setPosition(centerX, centerY);
} else {
// Если спрайта нет, создаём его по текущим центральным координатам
this.logo = this.add.image(centerX, centerY, 'logo');
}
};
// Подписываемся на событие изменения размера
this.scale.on('resize', placeLogo);
// Вызываем вручную для первоначальной расстановки
placeLogo();
}
Ключевые моменты:
1. Мы подписываемся на событие `'resize'` объекта `this.scale`.
2. Функция `placeLogo` рассчитывает позицию (например, центр) исходя из актуальных `this.scale.width` и `this.scale.height`.
3. Первоначальный вызов `placeLogo()` гарантирует, что спрайт появится сразу с правильными координатами.
4. При последующих изменениях размера окна браузера позиция спрайта будет автоматически корректироваться.
Важные замечания по API Phaser
Работая с масштабированием, помните о следующих свойствах и методах:
* `this.scale.width` и `this.scale.height` — это актуальные внутренние размеры игрового поля (game size) после применения режима масштабирования. Используйте их для расчёта позиций игровых объектов.
* `this.sys.game.config.width` и `this.sys.game.config.height` — это исходные размеры из конфигурации (в нашем примере 800x600). Не используйте их для расчётов при `RESIZE`, так как они не меняются.
* Событие `'resize'` может срабатывать несколько раз во время начальной загрузки и при ручном изменении размера окна. Ваш обработчик должен быть идемпотентным (не создавать дублирующихся объектов). В примере выше мы проверяем существование `this.logo`.
* Для более сложных случаев (например, пересчёт макетов UI) можно использовать встроенный менеджер `ScaleManager` и его методы, но для базового позиционирования спрайтов подхода со событием `'resize'` достаточно.
Что попробовать дальше
Использование Phaser.Scale.RESIZE — мощный инструмент для создания адаптивных игр, но оно требует внимательного отношения к моменту создания и обновления игровых объектов. Всегда инициализируйте или обновляйте позиции ваших спрайтов, текста и графики внутри обработчика события 'resize'. Это гарантирует корректную работу не только базового рендеринга, но и таких продвинутых техник, как шейдеры и постобработка, которые активно используют FBO.
Для экспериментов попробуйте:
1. Создать интерфейс, элементы которого привязываются к краям экрана (this.scale.width - 10, 10).
2. Реализовать камеру, которая следит за игроком, но не выходит за границы мира, рассчитанные динамически.
3. Применить простой шейдер-эффект к спрайту и убедиться, что он также корректно обрабатывает ресайз.
