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

При работе с контейнерами (`Phaser.GameObjects.Container`) в Phaser можно столкнуться с неочевидной ошибкой: добавленный в контейнер спрайт с активной анимацией может перестать её проигрывать. Эта статья разбирает пример бага #6052, объясняет его причину и показывает, как правильно инициализировать и добавлять анимированные объекты в контейнеры, чтобы анимация работала корректно. Понимание этого нюанса сэкономит время при создании сложных составных игровых объектов.

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

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

    // container.add(sprite);

    console.log(this.sys.updateList)

    this.time.delayedCall(2000, () => { container.add(sprite); console.log(sprite); console.log(this.sys.updateList) });
}

В чём проблема?

В исходном примере создаётся анимированный спрайт и через две секунды добавляется в контейнер. Однако после этого его анимация останавливается.

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

Давайте взглянем на проблемный участок кода:

const sprite = this.add.sprite(0, 0, 'walker', 'frame_0000');
sprite.play('walk');
// ... спустя 2 секунды
this.time.delayedCall(2000, () => { container.add(sprite); });

Анализ систем обновления

Phaser использует несколько систем для управления жизненным циклом игровых объектов. Одна из ключевых — this.sys.updateList. В неё автоматически попадают объекты, созданные через фабричные методы, такие как this.add.sprite().

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

Вот как выглядит логирование списков обновления в примере:

console.log(this.sys.updateList); // Спрайт в списке до добавления
this.time.delayedCall(2000, () => {
    container.add(sprite);
    console.log(this.sys.updateList); // Статус спрайта может измениться
});

Правильное решение: создание внутри контейнера

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

Перепишем код создания спрайта. Вместо того чтобы создавать его на сцене и потом перемещать, создадим его и сразу добавим в контейнер. Обратите внимание, что координаты спрайта (0,0) теперь будут относительными к позиции контейнера (400, 300).

function create() {
    const animConfig = {
        key: 'walk',
        frames: 'walker',
        frameRate: 60,
        repeat: -1
    };
    this.anims.create(animConfig);

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

    // Создаём спрайт и СРАЗУ добавляем в контейнер
    const sprite = new Phaser.GameObjects.Sprite(this, 0, 0, 'walker', 'frame_0000');
    container.add(sprite);
    sprite.play('walk');
}

Важно: здесь мы используем конструктор new Phaser.GameObjects.Sprite(), а не фабричный метод this.add.sprite(). Это потому, что фабричный метод автоматически добавляет объект на сцену и в её список обновления, что нам не нужно. Мы хотим, чтобы спрайт управлялся контейнером с самого начала.

Альтернатива: корректное перемещение объекта

Если по архитектуре игры требуется сначала создать объект на сцене, а позже «упаковать» его в контейнер, нужно использовать правильный API для передачи управления. Phaser предоставляет методы для удаления объекта из списка обновления сцены и его последующего добавления.

Однако в данном конкретном примере из баг-трекера простой вызов container.add(sprite) после создания приводит к ошибке. Более чистым решением, если объект уже создан через this.add, будет сначала явно удалить его из списка обновления сцены.

function create() {
    // ... создание анимации и контейнера
    const sprite = this.add.sprite(0, 0, 'walker', 'frame_0000');
    sprite.play('walk');

    // Удаляем спрайт из списка обновления сцены
    this.sys.updateList.remove(sprite);

    // Теперь можно безопасно добавить в контейнер
    container.add(sprite);
}

Этот подход менее элегантен, чем создание внутри контейнера, но может быть полезен в динамических сценариях, например, когда игрок подбирает предмет, который становится частью его инвентаря (контейнера).

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

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