О чем этот пример
При разработке сложных игр на 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. Представленный пример даёт готовый инструмент для отслеживания утечек, связанных с текстурами. Для экспериментов попробуйте
- Создавать текстуры разного размера и формата (RGB, RGBA) и сравните их «вес» в памяти
- Проверить, как ведут себя не
DynamicTexture, а загруженные из файла изображения при удалении черезthis.textures.remove(key) - Написать тест, который имитирует быструю смену сцен с созданием и (не)корректным уничтожением ресурсов
