О чем этот пример
При разработке игр на Phaser важно правильно управлять памятью. Уничтожение контейнера (`Container`) кажется простой операцией, но есть нюанс: по умолчанию он не удаляет свои дочерние объекты. Это может привести к утечкам памяти или неожиданному поведению, если вы не знаете об этой особенности. В этой статье мы разберем пример из баг-трекера, чтобы понять, как работает `Container.destroy()` и как избежать проблем.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#010101',
parent: 'phaser-example',
scene: {
preload: preload,
create: create
}
};
var game = new Phaser.Game(config);
function preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('walker', 'assets/animations/walker.png', 'assets/animations/walker.json');
}
function create ()
{
const animConfig = {
key: 'walk',
frames: 'walker',
frameRate: 60,
repeat: -1
};
this.anims.create(animConfig);
// const sprite = this.add.sprite(0, 0, 'walker', 'frame_0000');
const sprite = new Phaser.GameObjects.Sprite(this, 0, 0, 'walker', 'frame_0000');
sprite.play('walk');
// sprite.destroy();
var container = this.add.container(400, 300);
container.add(sprite);
container.destroy();
console.log(this);
}
Суть проблемы: destroy() против removeAll()
Метод destroy() у контейнера выполняет его деактивацию и удаление из сцены, но, в отличие от некоторых других объектов, по умолчанию он **не уничтожает автоматически дочерние элементы**, добавленные через container.add(). Они остаются в памяти и в иерархии сцены, если только они не были добавлены в контейнер как единственный родитель.
В исходном коде видно, что спрайт создается и добавляется в контейнер. После вызова container.destroy() контейнер уничтожается, а вот судьба спрайта становится неочевидной.
Анализ исходного кода
Давайте посмотрим на ключевые строки примера. Создается анимация и спрайт. Обратите внимание, что спрайт создается через new, а не через фабрику сцены (this.add.sprite). Это важная деталь.
const sprite = new Phaser.GameObjects.Sprite(this, 0, 0, 'walker', 'frame_0000');
sprite.play('walk');
Затем создается контейнер, и спрайт добавляется в него как дочерний объект. После этого контейнер сразу уничтожается.
var container = this.add.container(400, 300);
container.add(sprite);
container.destroy();
В этот момент спрайт, созданный вручную, не имеет другого родителя, кроме уничтоженного контейнера. Он может "зависнуть" в памяти.
Практическое пояснение работы destroy()
Поведение метода destroy() контролируется его параметрами. Посмотрим на сигнатуру метода из документации Phaser 3:
container.destroy(removeFromScene, destroyChildren);
* removeFromScene (логический): Нужно ли удалять контейнер из сцены. По умолчанию true.
* destroyChildren (логический): Нужно ли также уничтожить всех дочерних объектов. По умолчанию false.
**Почему по умолчанию false?** Это дает гибкость. Возможно, вы хотите уничтожить только контейнер, но сохранить его содержимое, чтобы позже добавить в другой контейнер или напрямую на сцену.
В нашем примере вызов container.destroy() равен вызову container.destroy(true, false). Контейнер удален, а спрайт — нет.
Как правильно уничтожать контейнер с детьми
Чтобы гарантированно очистить все объекты, нужно явно указать флаг destroyChildren. Вот исправленная и безопасная версия кода:
// Уничтожаем контейнер и ВСЕХ его дочерних объектов
container.destroy(true, true);
Альтернативный подход — вручную очистить контейнер перед уничтожением, используя removeAll():
// Удаляем всех детей из контейнера (они останутся на сцене, если были в ней)
container.removeAll();
// Затем уничтожаем пустой контейнер
container.destroy();
Однако второй подход не уничтожает спрайты, а только отсоединяет их от контейнера. Для полной очистки памяти лучше использовать первый вариант с destroyChildren: true.
Почему важно это знать?
Игнорирование этого поведения — частая причина утечек памяти в долгоживущих играх или при частой перезагрузке уровней. Накопление неиспользуемых, но не уничтоженных объектов (Sprite, Image, Text) может привести к падению производительности и, в конечном счете, к падению FPS.
Всегда задавайте себе вопрос: "А что происходит с детьми, когда я уничтожаю родительский контейнер или объект-группу?" Проверяйте документацию на методы destroy() для таких объектов, как Container, Group или Layer.
Что попробовать дальше
Ключевой вывод: Container.destroy() без параметров не трогает дочерние объекты. Для полной очистки используйте container.destroy(true, true). Для экспериментов попробуйте
- Создать сцену, которая постоянно генерирует и уничтожает контейнеры с разными флагами, и следить за потреблением памяти
- Проверить, как себя ведут дочерние объекты, созданные через фабрику сцены (
this.add.sprite) и черезnew, после уничтожения контейнера - Сравнить поведение
ContainerсPhaser.GameObjects.Groupи его методомdestroy()
