О чем этот пример

При разработке игр на 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. Применить простой шейдер-эффект к спрайту и убедиться, что он также корректно обрабатывает ресайз.