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

При разработке сложных игр на Phaser с большим количеством графики управление памятью WebGL становится критически важным. Неправильное удаление текстур может привести к постепенному накоплению утечек памяти, падению FPS и, в конечном счете, к крашу игры. В этой статье мы разберем пример, который наглядно демонстрирует, как отслеживать потребление памяти GPU и контролировать освобождение ресурсов при работе с динамическими текстурами.

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

Живой запуск

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

Исходный код


class TestScene extends Phaser.Scene {

    create ()
    {
        this.run = 0;


        this.left = this.add.text(0, 0).setLineSpacing(16);
        this.right = this.add.text(500, 0).setLineSpacing(16);
        this.current = this.add.text(0, 300, '', { fill: 'yellow' });
        this.resources = this.add.text(0, 420, '', { fill: 'lime' });
        this.runCounter = this.add.text(500, 300, 'Click to Run', { fill: 'orange' });

        this.initial = this.getMemoryInfo();

        this.input.on('pointerdown', () => this.destroyTest());

        // this.resizeTest();
    }


    resizeTest() {
        const initial = this.getMemoryInfo();

        this.add.text(500, 0, "Resize&Reuse Texture Test");

        this.add.text(500, 60, "Initial buffer: " + this.getMemoryInfo() + " MB");

        // Create the initial texture
        this.textures.addDynamicTexture("test1", 2048, 2048);

        this.add.text(500, 120, "After adding texture: " + this.getMemoryInfo() + " MB");

        // Resize the texture to 0x0 (internally destroys and creates a new texture)
        this.textures.get('test1').setSize(0, 0);

        this.add.text(500, 180, "After resizing texture to 0x0: " + this.getMemoryInfo() + " MB");

        // Resize the texture to 2048x2048 (internally destroys and creates a new texture)
        this.textures.get('test1').setSize(2048, 2048);

        this.add.text(500, 240, "After resizing texture to 2048x2048: " + this.getMemoryInfo() + " MB");

        const final = this.getMemoryInfo() - initial;

        this.add.text(500, 300, "Difference: " + final + " MB");
    }

    destroyTest ()
    {
        this.run++;
        this.runCounter.setText('Run: ' + this.run);

        this.textures.addDynamicTexture("test2", 2047, 2047);

        const before = this.getMemoryInfo();

        this.textures.remove("test2");

        const after = this.getMemoryInfo();

        const final = after - this.initial;

        this.left.setText([
            'Destroy & Create Texture Test',
            'Initial buffer: ' + this.initial + ' MB',
            'After adding texture: ' + before + ' MB',
            'After destroy texture: ' + after + ' MB',
            'Difference: ' + final + ' MB'
        ]);
    }

    getMemoryInfo() {
        const totalMemoryInfo = this.game.renderer.gl.getExtension('GMAN_webgl_memory').getMemoryInfo();
        return Math.round(totalMemoryInfo.memory.texture / 1024 / 1024);
    }

    update ()
    {
        const info = this.renderer.gl.getExtension('GMAN_webgl_memory').getMemoryInfo();

        this.current.setText([
            'Initial buffer: ' + this.initial + ' MB',
            'Current buffer: ' + Math.round(info.memory.buffer / 1024 / 1024) + ' MB',
            'Current texture: ' + Math.round(info.memory.texture / 1024 / 1024) + ' MB',
            'Current renderbuffer: ' + Math.round(info.memory.renderbuffer / 1024 / 1024) + ' MB',
            'Current drawingbuffer: ' + Math.round(info.memory.drawingbuffer / 1024 / 1024) + ' MB',
            'Current total: ' + Math.round(info.memory.total / 1024 / 1024) + ' MB',
            'Total minus Initial: ' + (Math.round(info.memory.total / 1024 / 1024) - this.initial) + ' MB'
        ]);

        this.resources.setText([
            'Resources buffer: ' + info.resources.buffer,
            'Resources renderbuffer: ' + info.resources.renderbuffer,
            'Resources program: ' + info.resources.program,
            'Resources shader: ' + info.resources.shader,
            'Resources texture: ' + info.resources.texture,
            'Resources vertexArray: ' + info.resources.vertexArray
        ]);
    }

}

const game = new Phaser.Game({
    type: Phaser.WEBGL,
    width: 800,
    height: 600,
    scene: TestScene,
    parent: 'phaser-example'
});

Подготовка к мониторингу памяти

Прежде чем управлять памятью, нужно научиться её измерять. В примере для этого используется специальное расширение WebGL GMAN_webgl_memory, доступное в некоторых браузерах (например, Chrome).

Основная функция для получения информации о памяти выделена в отдельный метод getMemoryInfo().

getMemoryInfo() {
    const totalMemoryInfo = this.game.renderer.gl.getExtension('GMAN_webgl_memory').getMemoryInfo();
    return Math.round(totalMemoryInfo.memory.texture / 1024 / 1024);
}

Этот метод обращается к рендереру игры (this.game.renderer), получает WebGL-контекст (.gl), находит нужное расширение и вызывает его метод getMemoryInfo(). Возвращаемое значение конвертируется из килобайт в мегабайты. В методе update() используется более подробная версия этого вызова, чтобы выводить разные категории памяти.

Создание и уничтожение динамических текстур

Динамические текстуры — мощный инструмент для генерации графики на лету. Однако каждая такая текстура занимает память GPU. В примере проверяется, корректно ли эта память освобождается при удалении.

Метод destroyTest() создаёт текстуру, замеряет память до и после её удаления.

destroyTest ()
{
    this.run++;
    this.runCounter.setText('Run: ' + this.run);

    // Создаем динамическую текстуру размером 2047x2047 пикселей
    this.textures.addDynamicTexture("test2", 2047, 2047);

    const before = this.getMemoryInfo(); // Замер памяти ДО удаления

    // Удаляем текстуру по её ключу
    this.textures.remove("test2");

    const after = this.getMemoryInfo(); // Замер памяти ПОСЛЕ удаления
    const final = after - this.initial; // Разница с начальным состоянием
    ... // Вывод результатов на экран
}
Ключевые API здесь:
- `this.textures.addDynamicTexture(key, width, height)` — создаёт новую пустую текстуру.
- `this.textures.remove(key)` — полностью удаляет текстуру из менеджера текстур Phaser и, что важно, освобождает связанную с ней память WebGL.

Повторяя клики (запуская destroyTest), можно убедиться, что разница в памяти (final) остаётся близкой к нулю — это признак отсутствия утечек.

Особый случай: изменение размера текстуры (закомментировано)

В исходном коде есть закомментированный метод resizeTest(). Он исследует менее очевидный сценарий.

resizeTest() {
    // ... инициализация
    // Создаем текстуру
    this.textures.addDynamicTexture("test1", 2048, 2048);
    // Меняем её размер на 0x0
    this.textures.get('test1').setSize(0, 0);
    // Меняем её размер обратно на 2048x2048
    this.textures.get('test1').setSize(2048, 2048);
    // ... замеры памяти
}

Метод setSize(width, height) объекта DynamicTexture внутренне уничтожает старый WebGL-объект текстуры и создаёт новый указанного размера. Этот тест позволяет проверить, накапливается ли память при таких операциях «пересоздания». В реальном проекте стоит быть внимательным при частом вызове setSize для больших текстур.

Визуализация данных в реальном времени

Для наглядности все замеры выводятся на экран с помощью объектов Phaser.GameObjects.Text. Это позволяет в режиме реального времени наблюдать за потреблением памяти.

Метод update() вызывается на каждом кадре и обновляет два блока информации.

update ()
{
    // Получаем полную информацию о памяти через расширение
    const info = this.renderer.gl.getExtension('GMAN_webgl_memory').getMemoryInfo();

    // Выводим детализированную информацию по памяти (в МБ)
    this.current.setText([
        'Initial buffer: ' + this.initial + ' MB',
        'Current buffer: ' + Math.round(info.memory.buffer / 1024 / 1024) + ' MB',
        'Current texture: ' + Math.round(info.memory.texture / 1024 / 1024) + ' MB',
        // ... другие категории
    ]);

    // Выводим количество активных WebGL-ресурсов каждого типа
    this.resources.setText([
        'Resources buffer: ' + info.resources.buffer,
        'Resources texture: ' + info.resources.texture,
        // ... другие типы ресурсов
    ]);
}

Важно отслеживать не только общий объём памяти (memory.total), но и количество ресурсов (resources.texture). Рост последнего при удалении текстур через this.textures.remove() явно укажет на утечку.

Интеграция в рабочий проект

Как использовать этот подход в реальной игре? 1. **Отладка:** Создайте служебную сцену или панель, аналогичную примеру. Запускайте её в development-сборке для проверки проблемных мест. 2. **Критические операции:** Оборачивайте в замеры памяти операции массового создания/удаления ресурсов (загрузка/выгрузка уровней). 3. **Автоматизация:** Напишите простой тест, который создаёт и удаляет объекты в цикле, и следите за стабильностью показателя info.resources.texture.

Базовый каркас для интеграции:

class MemoryDebugScene extends Phaser.Scene {
    create() {
        // Проверяем доступность расширения
        if (!this.renderer.gl.getExtension('GMAN_webgl_memory')) {
            console.warn('WebGL memory extension not available.');
            return;
        }
        this.debugText = this.add.text(10, 10, '', { fontSize: '12px' });
    }

    update() {
        const info = this.renderer.gl.getExtension('GMAN_webgl_memory').getMemoryInfo();
        // Обновляем текст на экране с ключевыми метриками
    }
}

Помните, что расширение GMAN_webgl_memory — инструмент отладки. Не используйте его вызовы в production-коде, так как это может повлиять на производительность.

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

Контроль за памятью WebGL — обязательный навык для разработчика сложных игр на Phaser. Представленный пример даёт готовый инструмент для отслеживания утечек, связанных с текстурами. Для экспериментов попробуйте

  1. Создавать текстуры разного размера и формата (RGB, RGBA) и сравните их «вес» в памяти
  2. Проверить, как ведут себя не DynamicTexture, а загруженные из файла изображения при удалении через this.textures.remove(key)
  3. Написать тест, который имитирует быструю смену сцен с созданием и (не)корректным уничтожением ресурсов