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

При разработке игр на 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). Для экспериментов попробуйте

  1. Создать сцену, которая постоянно генерирует и уничтожает контейнеры с разными флагами, и следить за потреблением памяти
  2. Проверить, как себя ведут дочерние объекты, созданные через фабрику сцены (this.add.sprite) и через new, после уничтожения контейнера
  3. Сравнить поведение Container с Phaser.GameObjects.Group и его методом destroy()