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

Управление жизненным циклом объектов — ключевой навык в разработке игр. Неправильное удаление спрайтов и изображений может привести к утечкам памяти и нестабильной работе игры. В этой статье разберем, как корректно уничтожать объекты в Phaser, используя встроенный метод `destroy()`, и почему важно занулять ссылки на них.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    image;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('einstein', 'assets/pics/ra-einstein.png');
    }

    create ()
    {
        this.image = this.add.image(400, 300, 'einstein');

        this.input.once('pointerdown', () =>
        {

            console.log('nuked');

            this.image.destroy();

            this.image = null;

        });
    }

    update ()
    {
        if (this.image)
        {
            this.image.rotation += 0.01;
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Зачем нужно уничтожать объекты?

В процессе игры постоянно создаются новые объекты: выстрелы, враги, эффекты. Если их не удалять после завершения использования, они будут продолжать занимать оперативную память и, возможно, обрабатываться в цикле обновления, что снизит производительность.

Метод destroy() объекта Phaser.GameObjects.Image (и других игровых объектов) выполняет несколько важных действий: - Удаляет объект из текущей сцены. - Останавливает любые активные анимации или таймеры, связанные с объектом. - Освобождает текстуру из памяти (если она больше не используется). - Делает объект непригодным для дальнейшего использования.

После вызова destroy() обращаться к свойствам и методам объекта нельзя.

Корректное уничтожение изображения

Рассмотрим пример кода, где изображение вращается, а по клику мыши уничтожается. Обратите внимание на последовательность действий.

Сначала в методе create() создается и сохраняется изображение:

this.image = this.add.image(400, 300, 'einstein');

Затем мы настраиваем обработчик события клика, который вызовет уничтожение:

this.input.once('pointerdown', () => {
    console.log('nuked');
    this.image.destroy();
    this.image = null;
});
Ключевые моменты:
1. Используем `this.input.once`, чтобы событие сработало один раз.
2. Вызываем `this.image.destroy()` для очистки.
3. **Важно:** Присваиваем переменной `this.image` значение `null`. Это предотвращает попытки обращения к уничтоженному объекту в других частях кода, например, в `update()`.

Защита от ошибок в update-цикле

Если объект уничтожен, но ссылка на него не обнулена, попытка изменить его свойства вызовет ошибку. В нашем примере метод update() каждый кадр пытается увеличивать вращение изображения, но только если оно существует.

update ()
{
    if (this.image)
    {
        this.image.rotation += 0.01;
    }
}

Проверка if (this.image) гарантирует, что код внутри выполнится, только когда переменная содержит действительный объект (не null и не undefined). После присваивания this.image = null условие станет ложным, и вращения не произойдет.

Такой паттерн — проверка перед использованием — стандартная практика для безопасной работы с объектами, которые могут быть уничтожены в ходе выполнения программы.

Что происходит под капотом?

При вызове `this.image.destroy()` Phaser выполняет следующие шаги:
1. Помечает объект как `active=false` и `visible=false`.
2. Удаляет его из списка обновления сцены (`this.sys.updateList`).
3. Удаляет его из списка отображения сцены (`this.sys.displayList`).
4. Если к объекту были привязаны слушатели событий или таймеры, они также очищаются.
5. Освобождается внутренняя ссылка на текстуру (если на эту же текстуру не ссылаются другие объекты).

После этого объект становится «мертвым» с точки зрения движка. Попытка вызвать его методы или изменить свойства приведет к предупреждению в консоли или ошибке.

Именно поэтому после destroy() необходимо явно обнулить свою собственную ссылку, как сделано в примере: this.image = null;. Это сообщает сборщику мусора JavaScript, что память можно освободить.

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

Корректное уничтожение объектов с помощью destroy() и последующее обнуление ссылок — обязательная практика для создания стабильных игр на Phaser. Это предотвращает утечки памяти и ошибки выполнения. Для экспериментов попробуйте: 1. Убрать строку this.image = null; и понаблюдать за ошибками в консоли. 2. Создать несколько изображений и уничтожать их по очереди, отслеживая потребление памяти в инструментах разработчика. 3. Проверить, как ведут себя другие типы объектов (спрайты, текст, группы) при вызове destroy().