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

В мобильных браузерах и некоторых графических средах WebGL контекст может быть неожиданно утерян из-за переключения вкладок, нехватки памяти или системных прерываний. Это приводит к "черному экрану" и краху игры. Пример демонстрирует, как в Phaser можно программно симулировать эту потерю и корректно восстановить рендеринг, обеспечивая стабильность игрового процесса. Понимание этого механизма критически важно для разработчиков, стремящихся создать надежные игры, которые выдерживают нестандартные условия работы браузера.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('ship', 'assets/sprites/x2kship.png');
    }

    create ()
    {
        /** Period to lose WebGL, in ms. Enlarge this to do things while context is lost. */
        const delay = 1000;

        // Create objects which will be affected by context loss.
        // These objects should recover properly.
        // Note that objects like `DynamicTexture` will not recover properly.

        this.spinner = this.add.container(640, 360);
        const ship = this.add.image(0, -200, 'ship');
        this.spinner.add(ship);

        this.rect = this.add.rectangle(640, 360, 600, 100, 0xdd3333);

        this.text = this.add.text(640, 360, 'Click to lose WebGL context.\nRecovery in 1s.', { align: 'center' })
        .setOrigin(0.5);

        // Context loss.

        const glLoser = this.renderer.getExtension('WEBGL_lose_context');

        this.input.on('pointerdown', () => {

            glLoser.loseContext();

            this.time.delayedCall(delay, () => {

                glLoser.restoreContext();

            });

        });
    }

    update (time, delta)
    {
        // 1 rotation every 60 seconds
        this.spinner.rotation = time * Math.PI * 2 / 60000;
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 1280,
    height: 720,
    backgroundColor: '#2d2d2d',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и объектов

В методе preload загружается единственный спрайт — изображение корабля. В create создаются три визуальных объекта, которые будут проверены на устойчивость к потере контекста: контейнер с вращающимся кораблем, прямоугольник и текстовое поле.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('ship', 'assets/sprites/x2kship.png');
}
create ()
{
    this.spinner = this.add.container(640, 360);
    const ship = this.add.image(0, -200, 'ship');
    this.spinner.add(ship);

    this.rect = this.add.rectangle(640, 360, 600, 100, 0xdd3333);

    this.text = this.add.text(640, 360, 'Click to lose WebGL context.\nRecovery in 1s.', { align: 'center' })
    .setOrigin(0.5);
}

Эти объекты (Container, Image, Rectangle, Text) являются стандартными для Phaser и должны автоматически восстановиться после возобновления рендеринга.

Получение доступа к расширению WEBGL_lose_context

Ключевой инструмент для управления контекстом — это расширение WebGL WEBGL_lose_context. Оно не является частью основного API Phaser, но доступно через рендерер.

const glLoser = this.renderer.getExtension('WEBGL_lose_context');

Вызов this.renderer.getExtension('WEBGL_lose_context') возвращает объект, который предоставляет два метода: loseContext() и restoreContext(). Этот объект сохраняется в переменную glLoser для последующего использования. Без этого расширения симулировать потерю контекста программно было бы невозможно.

Обработка клика и симуляция сбоя

Механика примера активируется по клику мыши. При срабатывании события pointerdown вызывается метод loseContext(), что приводит к немедленной остановке рендеринга — экран "замирает".

this.input.on('pointerdown', () => {
    glLoser.loseContext();
    this.time.delayedCall(delay, () => {
        glLoser.restoreContext();
    });
});

Сразу после потери контекста планируется его восстановление через this.time.delayedCall. Через заданный delay (1000 мс) вызывается restoreContext(). Phaser автоматически перезапускает рендерер и восстанавливает отрисовку всех объектов, созданных стандартными фабриками (this.add).

Анимация и обновление состояния

В методе update реализована простая анимация вращения контейнера this.spinner. Она зависит от глобального времени игры time.

update (time, delta)
{
    // 1 rotation every 60 seconds
    this.spinner.rotation = time * Math.PI * 2 / 60000;
}

Эта анимация служит индикатором работы игрового цикла. Во время потери WebGL контекста (loseContext) кадры перестают обновляться, и вращение останавливается. После успешного восстановления (restoreContext) анимация возобновляется с того момента, где остановилась, так как значение time продолжает накапливаться.

Конфигурация игры и важные замечания

Конфигурация игры использует Phaser.AUTO, что позволяет движку выбрать WebGL рендерер, если он доступен. Именно этот рендерер подвержен потере контекста.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 1280,
    height: 720,
    backgroundColor: '#2d2d2d',
    scene: Example
};

Важно отметить, что автоматическому восстановлению подлежат объекты, созданные через встроенные фабрики (this.add.*). Динамические текстуры (DynamicTexture) или кастомные шейдеры могут требовать ручной переинициализации. В реальном сценарии потеря контекста инициируется системой, а не кодом, но механизм восстановления будет тем же.

Что попробовать дальше

Phaser предоставляет инфраструктуру для корректного восстановления после потери WebGL контекста, что критично для мобильных и веб-игр. Стандартные игровые объекты восстанавливаются автоматически. **Идеи для экспериментов:** 1. Увеличьте задержку delay и попробуйте взаимодействовать с игрой (передвигать объекты) во время "черного экрана". Проверьте, восстановится ли их состояние. 2. Создайте объект DynamicTexture и проверьте, перерисовывается ли он после restoreContext. 3. Обработайте события webglcontextlost и webglcontextrestored напрямую через this.game.canvas.addEventListener для кастомной логики.