О чем этот пример
При работе с динамическими текстурами в Phaser.js можно столкнуться с незаметной утечкой памяти, которая постепенно замедляет игру. В этой статье разберем, почему просто вызов метода `destroy()` не всегда освобождает память, и как правильно управлять жизненным циклом Render Textures, чтобы избежать проблем с производительностью.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Scene extends Phaser.Scene {
create() {
this.dynamicTextures = [];
const add_button = this.add.text(100, 100, 'add').setInteractive();
const clear_button = this.add.text(200, 100, 'destroy').setInteractive()
add_button.on('pointerdown', this.addRenderTextures, this);
clear_button.on('pointerdown', this.removeRenderTextures, this)
}
addRenderTextures() {
console.log("Adding render textures");
for (let i = 0; i < 20; i++) {
const dt = this.textures.addDynamicTexture(`key${i}`, 1000, 1000);
this.dynamicTextures.push(dt);
}
}
removeRenderTextures() {
console.log("Removing render textures", this.dynamicTextures.length);
while (this.dynamicTextures.length > 0) {
const dt = this.dynamicTextures.pop();
dt.destroy();
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
backgroundColor: '#000000',
scene: Scene
};
const game = new Phaser.Game(config);
Проблема: уничтожение не равно очистке
В примере мы создаем 20 динамических текстур при каждом нажатии кнопки 'add'. Каждая текстура создается через this.textures.addDynamicTexture() и сохраняется в массив для последующего удаления.
Когда пользователь нажимает 'destroy', мы в цикле вызываем dt.destroy() для каждой текстуры и удаляем ее из массива.
while (this.dynamicTextures.length > 0) {
const dt = this.dynamicTextures.pop();
dt.destroy(); // Вызываем destroy для каждой текстуры
}
Однако, если открыть инструменты разработчика и отслеживать использование памяти, можно заметить, что память не освобождается полностью. Это происходит потому, что метод destroy() удаляет ссылку на текстуру внутри Phaser, но не гарантирует немедленного освобождения памяти браузером. Особенно это заметно при многократном создании и удалении большого количества текстур.
Как работают динамические текстуры в Phaser
Динамическая текстура (Dynamic Texture) — это холст в памяти, который можно использовать для рендеринга графики во время выполнения игры. При создании через addDynamicTexture() Phaser выделяет память под текстуру и регистрирует ее в своем внутреннем менеджере текстур.
const dt = this.textures.addDynamicTexture(`key${i}`, 1000, 1000);
Здесь:
- key${i} — уникальный ключ текстуры
- 1000, 1000 — ширина и высота текстуры в пикселях
Важно понимать, что текстура размером 1000×1000 пикселей занимает в памяти примерно 4 МБ (1000 × 1000 × 4 байта на RGBA-пиксель). Двадцать таких текстур — это уже 80 МБ оперативной памяти.
Правильный подход к управлению памятью
Чтобы избежать утечек памяти, нужно не только уничтожать текстуры, но и правильно управлять их жизненным циклом:
1. Используйте пул текстур вместо постоянного создания новых
2. Очищайте все ссылки на текстуру после вызова destroy()
3. Давайте сборщику мусора время на работу
// Правильная реализация удаления с очисткой ссылок
removeRenderTextures() {
console.log("Removing render textures", this.dynamicTextures.length);
while (this.dynamicTextures.length > 0) {
const dt = this.dynamicTextures.pop();
dt.destroy(); // Уничтожаем текстуру в Phaser
// Явно очищаем ссылку (опционально, но рекомендуется)
dt = null;
}
// Принудительный вызов сборки мусора (только для отладки!)
if (window.gc) {
window.gc();
}
}
Важно: window.gc() работает только с включенным флагом Chrome --js-flags="--expose-gc" и не должен использоваться в продакшене.
Альтернатива: повторное использование текстур
Вместо создания и удаления текстур рассмотрите возможность их повторного использования. Это особенно эффективно для текстур одинакового размера.
class Scene extends Phaser.Scene {
create() {
this.texturePool = []; // Пул свободных текстур
this.usedTextures = []; // Текстуры в использовании
// Создаем начальный пул
for (let i = 0; i < 10; i++) {
this.createTextureToPool();
}
}
createTextureToPool() {
const dt = this.textures.addDynamicTexture(
`pool_${Date.now()}_${Math.random()}`,
1000,
1000
);
this.texturePool.push(dt);
}
getTexture() {
if (this.texturePool.length === 0) {
this.createTextureToPool();
}
const dt = this.texturePool.pop();
this.usedTextures.push(dt);
return dt;
}
}
Такой подход минимизирует аллокации памяти и снижает нагрузку на сборщик мусора.
Что попробовать дальше
Управление памятью при работе с динамическими текстурами в Phaser требует внимания. Метод destroy() не всегда приводит к немедленному освобождению памяти, поэтому важно:
- Мониторить использование памяти в DevTools
- Использовать пулы для повторного использования текстур
- Избегать частого создания/удаления больших текстур
Для экспериментов: попробуйте добавить мониторинг памяти через performance.memory (в Chrome) или создать систему приоритетного кэширования текстур, которая автоматически выгружает наименее используемые ресурсы.
