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

При разработке сложных игр на Phaser часто возникает необходимость динамически управлять сценами — создавать их, переключать и удалять. Однако неочевидные особенности жизненного цикла игровых объектов, особенно внутри контейнеров, могут привести к утечкам памяти и ошибкам. Эта статья на примере реального кода разбирает, как корректно удалять сцену, в которой находятся пользовательские контейнеры, и какие методы при этом вызываются. Понимание этого процесса сделает вашу игру стабильнее и поможет избежать скрытых багов.

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

Живой запуск

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

Исходный код


class ContainerTest extends Phaser.GameObjects.Container {
    constructor(scene, x, y) {
		super(scene, 166, 275);
        console.log("container:constructor");
	}

	addedToScene() {
		super.addedToScene()
		console.log("container:addedToScene");
	}
}

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://cdn.phaserfiles.com/v385');

}

function create ()
{
    console.log("create");

    container = this.add.container(400, 300);

    container2 = new ContainerTest(this, 100, 100);
    container.add(container2);

    this.input.keyboard.on("keyup-T", () => {
        console.log("Input detected")
        this.scene.remove();
    })
}

Суть проблемы: удаление сцены и её содержимого

В примере используется метод this.scene.remove() для удаления текущей сцены по нажатию клавиши `T`. Это штатный способ уничтожения сцены и освобождения связанных с ней ресурсов. Однако ключевой вопрос: что происходит с игровыми объектами, которые находятся на этой сцене, особенно если они являются частью иерархии (например, добавлены в контейнер)?

Игровой объект в Phaser имеет определённый жизненный цикл. Когда объект добавляется на сцену, вызывается его метод addedToScene(). Аналогично, при удалении со сцены (неважно, удаляется ли сам объект или вся сцена) должен вызываться метод removedFromScene(). Если этот метод не определён или работает некорректно, объект может не освободить свои ресурсы.

Пользовательский контейнер и его жизненный цикл

В примере создан пользовательский класс ContainerTest, наследующий от Phaser.GameObjects.Container. В нём переопределены два метода жизненного цикла.

Конструктор класса. Обратите внимание, что в нём явно задаются координаты (166, 275), но аргументы `xиy, переданные при создании (new ContainerTest(this, 100, 100)`), игнорируются. Это потенциальный источник путаницы.

constructor(scene, x, y) {
    super(scene, 166, 275);
    console.log("container:constructor");
}

Метод addedToScene(). Он вызывается автоматически, когда контейнер добавляется на сцену (в данном случае, после container.add(container2)).

addedToScene() {
    super.addedToScene()
    console.log("container:addedToScene");
}

Важно, что в этом примере не переопределён метод removedFromScene(). Это означает, что при удалении сцены контейнер не выполнит никакой своей специфической логики очистки.

Что происходит при вызове scene.remove()?

Внутри сцены создаётся корневой контейнер, в который добавляется пользовательский ContainerTest.

container = this.add.container(400, 300);
container2 = new ContainerTest(this, 100, 100);
container.add(container2);

При нажатии клавиши `T` сцена получает команду на самоуничтожение.

this.input.keyboard.on("keyup-T", () => {
    console.log("Input detected")
    this.scene.remove();
})

Когда вызывается this.scene.remove(), движок Phaser начинает рекурсивно удалять все игровые объекты, принадлежащие этой сцене. Для каждого объекта, включая стандартный контейнер (container) и пользовательский ContainerTest, движок вызовет их внутренние методы разрушения. Если бы в классе ContainerTest был определён метод removedFromScene(), он был бы вызван на этом этапе.

// Пример метода, который следовало бы добавить для чистоты
removedFromScene() {
    super.removedFromScene();
    console.log("container:removedFromScene");
    // Здесь можно освободить кастомные ресурсы, отписаться от событий и т.д.
}

Без этого метода объект всё равно будет удалён движком, но вы упустите шанс корректно завершить его работу.

Практические выводы и рекомендации

1. **Всегда переопределяйте removedFromScene() в сложных объектах.** Если ваш пользовательский контейнер, спрайт или любой другой GameObject создаёт собственные подписки на события (this.on(...)), таймеры, физические тела или ссылки на другие объекты, их необходимо явно уничтожать в removedFromScene() или destroy(). Это предотвратит утечки памяти.

2. **Используйте аргументы конструктора.** В приведённом примере координаты в конструкторе захардкожены. Правильнее передавать их через super():

constructor(scene, x, y) {
    super(scene, x, y); // Используем переданные x и y
    console.log("container:constructor");
}

3. **Помните об иерархии.** При удалении сцены через scene.remove() движок сам позаботится об удалении всей цепочки дочерних объектов. Вам не нужно вручную удалять (destroy()) каждый контейнер или спрайт внутри сцены.

4. **Для отладки используйте вывод в консоль.** Как в примере, логируйте ключевые этапы жизни объекта (constructor, addedToScene, removedFromScene). Это поможет понять порядок и факт вызова методов при тестировании сценариев удаления.

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

Удаление сцены в Phaser — мощный инструмент управления состоянием игры. Ключ к его безопасному использованию — понимание и правильная реализация жизненного цикла пользовательских игровых объектов. Всегда очищайте свои ресурсы в методе removedFromScene(). **Идеи для экспериментов:** 1. Добавьте в ContainerTest метод removedFromScene с выводом в консоль и убедитесь, что он вызывается. 2. Создайте в контейнере подписку на событие 'update' и попробуйте отписаться от него в removedFromScene. Что произойдёт, если этого не сделать? 3. Попробуйте удалять не всю сцену, а только дочерний контейнер через container2.destroy(). Будет ли при этом вызван removedFromScene?