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

При разработке игр с анимациями Spine и визуальными эффектами (FX) легко столкнуться с незаметными утечками памяти. Эти утечки могут постепенно снижать производительность, особенно на мобильных устройствах, вплоть до падения FPS или краша игры. Данный пример демонстрирует простой, но эффективный способ мониторинга одного из внутренних счетчиков WebGL-ресурсов в реальном времени, что позволяет вовремя обнаружить проблему и предотвратить её на ранних этапах разработки.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor()
    {
        super({
            pack: {
                files: [
                    {
                        type: 'scenePlugin',
                        key: 'SpinePlugin',
                        url: 'plugins/3.8.95/SpinePluginDebug.js',
                        sceneKey: 'spine'
                    }
                ]
            }
        });
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath('assets/spine/3.8/owl/');
        this.load.spine('owl', 'owl-pro.json', 'owl-pro.atlas', true);
    }

    create ()
    {
        this.add.spine(400, 500, 'owl', 'idle', true).setScale(0.7);
        this.counter = this.add.text(10, 10, "", { fontSize: 32 })
            .setOrigin(0)
            .setInteractive()
            .on('pointerdown', () =>
            {
                this.counter.preFX.addColorMatrix()
            });
    }

    update ()
    {
        this.counter.setText(
            "glAttribLocationWrappers:" + this.renderer.glAttribLocationWrappers.length
        );
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Суть проблемы: накопление WebGL-обёрток

Phaser для работы с WebGL использует внутренние структуры данных, такие как обёртки для атрибутов шейдеров (glAttribLocationWrappers). При частом создании и удалении графических объектов, особенно с эффектами (preFX, postFX), эти обёртки могут не освобождаться сборщиком мусора корректно, что приводит к утечке памяти.

Код в примере создаёт простой счётчик, который выводит текущее количество этих обёрток в интерфейс. Рост числа при неизменной сцене — явный признак проблемы.

Разбор сцены и загрузки Spine

В конструкторе сцены (Example) через параметр pack динамически подключается плагин Spine. Это альтернатива глобальной регистрации плагина в конфиге игры.

constructor()
{
    super({
        pack: {
            files: [
                {
                    type: 'scenePlugin',
                    key: 'SpinePlugin',
                    url: 'plugins/3.8.95/SpinePluginDebug.js',
                    sceneKey: 'spine'
                }
            ]
        }
    });
}

В методе preload задаётся базовый URL и путь для ассетов, после чего загружается модель Spine.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.setPath('assets/spine/3.8/owl/');
    this.load.spine('owl', 'owl-pro.json', 'owl-pro.atlas', true);
}

Создание объектов и интерактивный счётчик

В методе create добавляется анимированный спрайт Spine и текстовый объект-счётчик.

create ()
{
    this.add.spine(400, 500, 'owl', 'idle', true).setScale(0.7);
    this.counter = this.add.text(10, 10, "", { fontSize: 32 })
        .setOrigin(0)
        .setInteractive()
        .on('pointerdown', () =>
        {
            this.counter.preFX.addColorMatrix();
        });
}
Ключевые моменты:
1. `this.add.spine` создаёт и отображает модель Spine с анимацией `idle`.
2. Текстовый объект (`this.counter`) делает две вещи: отображает счётчик и является интерактивным.
3. При клике на текст к нему добавляется эффект Color Matrix через `this.counter.preFX.addColorMatrix()`. Многократные клики будут создавать новые экземпляры эффектов, что потенциально может увеличивать счётчик WebGL-обёрток.

Мониторинг в реальном времени

Метод update вызывается каждый кадр и обновляет текст, отображая текущую длину массива this.renderer.glAttribLocationWrappers.

update ()
{
    this.counter.setText(
        "glAttribLocationWrappers:" + this.renderer.glAttribLocationWrappers.length
    );
}

Это прямой доступ к внутреннему свойству рендерера. В стабильной сцене это число должно колебаться в небольших пределах. Его постоянный рост указывает на то, что объекты или эффекты создаются, но не уничтожаются должным образом.

Конфигурация игры и запуск

Стандартная конфигурация игры Phaser, в которую передаётся наша сцена Example.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Игра будет отображена в элементе с id="phaser-example".

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

Представленный код — это готовый инструмент для первичной диагностики утечек памяти, связанных с созданием графических объектов и эффектов. Для экспериментов попробуйте

  1. Добавить удаление эффектов через this.counter.preFX.remove() и понаблюдать за счётчиком
  2. Создавать и уничтожать множество Spine-спрайтов в цикле
  3. Проверить, как ведёт себя счётчик при переключении сцен с разными методами очистки (this.scene.restart, this.scene.start)