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

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

Версия 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');

    sprite.play('walk');

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

    container.add(sprite);

    this.add.existing(sprite);

    console.log(this);
}

Что не так с этим кодом?

На первый взгляд, пример создаёт анимированного персонажа и помещает его в контейнер. Однако в нём содержится критическая ошибка, нарушающая внутреннюю структуру отображения. Проблема кроется в двух вызовах, которые пытаются управлять одним и тем же объектом sprite. Давайте посмотрим на ключевой фрагмент в функции create:

const sprite = this.add.sprite(0, 0, 'walker', 'frame_0000');
sprite.play('walk');

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

this.add.existing(sprite); // Проблемная строка

Метод this.add.sprite() уже автоматически добавляет созданный спрайт в дисплейный список текущей сцены. Последующий вызов this.add.existing(sprite) пытается добавить этот же объект снова, что приводит к дублированию.

Как работают дисплейные списки и контейнеры

Phaser, как и многие другие фреймворки, использует древовидную структуру для отображения объектов. Когда вы вызываете this.add.sprite(), спрайт становится дочерним элементом корневой системы отображения сцены.

Контейнер (this.add.container) — это специальный объект, который может содержать в себе другие дисплейные объекты, образуя поддерево. Метод container.add(sprite) не создаёт копию, а перемещает ссылку на существующий спрайт. Таким образом, спрайт становится дочерним элементом контейнера, а не сцены напрямую.

Вызов this.add.existing() предназначен для ручного добавления в дисплейный список объектов, созданных не через фабрику this.add. Например, если бы спрайт был создан через new Phaser.GameObjects.Sprite(...). В нашем примере он избыточен и вреден.

Последствия двойного добавления

Дублирование спрайта в дисплейном списке ломает его внутреннюю логику. Вот что может произойти:

1. **Сломанные трансформации:** Позиция, масштаб и вращение могут применяться неправильно, так как объект находится в двух местах иерархии. 2. **Проблемы с анимацией:** Обновление кадров анимации (sprite.play('walk')) может работать некорректно или не работать вовсе. 3. **Утечки и ошибки при уничтожении:** При попытке удалить объект (sprite.destroy()) может очиститься не весь связанный с ним ресурс или возникнуть ошибка, так как система будет искать его в нескольких местах.

Проще говоря, спрайт оказывается в состоянии, которое движок не ожидает и не может корректно обработать.

Исправленный и правильный подход

Решение простое: не добавляйте объект дважды. Если вы помещаете спрайт в контейнер, этого достаточно. Фабричный метод this.add.sprite уже выполнил первоначальное добавление, а container.add его переопределил.

Вот исправленная версия функции create:

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');
    sprite.play('walk');

    // Создаём контейнер и добавляем в него спрайт.
    // Это действие также *удаляет* спрайт из предыдущего родителя (сцены)
    // и делает контейнер его новым родителем.
    var container = this.add.container(400, 300);
    container.add(sprite);

    // Строка this.add.existing(sprite); была удалена как лишняя.
}

Теперь спрайт корректно принадлежит только контейнеру, и все преобразования (позиция контейнера в 400x300) применяются к нему предсказуемо.

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

Главный вывод: в Phaser объект должен находиться в дисплейном списке ровно один раз. Используйте this.add.existing() только для объектов, созданных вручную через new. Для экспериментов попробуйте: создать спрайт через new, а затем добавить его в контейнер с помощью container.add() и this.add.existing(); или изучить, как влияет на производительность наличие сотни «дублированных» спрайтов.