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

Вы когда-нибудь анимировали группу объектов через `tweens.add`, передав массив в `targets`, а потом добавляли новые элементы? Анимация может внезапно перестать работать на новых объектах, и причина — в тонкостях ссылок в JavaScript. Эта статья разбирает реальный пример бага и объясняет, как Phaser внутренне работает со списком детей сцены и почему прямое использование `this.children.list` для анимации может привести к неожиданным результатам. Понимание этой механики убережёт вас от часов отладки.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
    }

    create ()
    {
        this.add.circle(0, 0, 100, 0xff0000);
        this.add.rectangle(this.scale.width, 0, 100, 100, 0x00ff00);
        this.add.ellipse(0, this.scale.height, 100, 100, 0x00ff00);
        this.add.star(this.scale.width, this.scale.height, 5, 25, 100, 0x00ff00);

        this.tweens.add({ targets: this.children.list, duration: 1000, props: { x: this.scale.width / 2, y: this.scale.height / 2 } })
            .on(Phaser.Tweens.Events.TWEEN_START, () => console.log('added on start listener'));

        // add another item afterwards
        const t = this.add.text(0, 0, 'Test Text', { align: 'center' }).setOrigin(0.5);
        console.log('add item');

        // move it so that it's within the original lists count
        this.children.moveTo(t, 1);
    }
}

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

const game = new Phaser.Game(config);

Суть проблемы: живая ссылка и её изменчивость

Ключевой объект в примере — this.children.list. Это массив, содержащий всех прямых потомков (children) текущей сцены (Scene). В Phaser это **живая, изменяемая ссылка** на внутренний массив.

Когда вы создаёте твин и передаёте this.children.list в свойство targets, твин-система Phaser запоминает **текущее состояние этого массива**. Однако сам массив продолжает жить и меняться по мере добавления, удаления или перемещения объектов в сцене.

this.tweens.add({
    targets: this.children.list, // Запоминается ссылка на массив
    duration: 1000,
    props: { x: this.scale.width / 2, y: this.scale.height / 2 }
})

Проблема возникает, если после создания твина структура массива this.children.list изменится. Твин будет работать с устаревшим набором ссылок на объекты.

Разбор примера: что происходит на самом деле

Давайте пройдёмся по коду примера по порядку.

1.  **Создание фигур:** В сцену добавляются четыре графических объекта (круг, прямоугольник, эллипс, звезда). На этом этапе `this.children.list` содержит ровно 4 элемента.
2.  **Создание твина:** Запускается твин, который должен анимировать все объекты в массиве `this.children.list` к центру экрана. Твин "видит" массив из 4 элементов и начинает с ними работу.
3.  **Добавление текста:** После создания твина в сцену добавляется пятый объект — текст.
const t = this.add.text(0, 0, 'Test Text', { align: 'center' }).setOrigin(0.5);

Теперь this.children.list содержит 5 элементов. Но твин был создан раньше и управляет теми 4 объектами, которые были в массиве на момент его создания. Новый текстовый объект в анимации не участвует. 4. **Критическое перемещение:** Следующая строка — источник путаницы и потенциальный баг.

this.children.moveTo(t, 1);

Метод moveTo перемещает объект `t(текст) на позицию с индексом 1 внутри списка детей. Теперь порядок объектов вthis.children.list` изменился. **Однако твин по-прежнему анимирует старые объекты по их старым индексам в своём внутреннем представлении.** Это может привести к тому, что анимация будет применена не к тем объектам, которые вы ожидаете, или вообще перестанет корректно работать для части из них.

Практическое решение: работа с копией или группой

Чтобы избежать этой проблемы, никогда не передавайте напрямую изменяемый массив this.children.list в targets, если планируете потом менять состав детей. Вместо этого используйте один из двух безопасных подходов.

**Способ 1: Создание копии массива.** Самый простой способ — создать новый массив, содержащий те же объекты. Твин будет работать с этой "снимком", и изменения в оригинальном this.children.list его не затронут.

// Создаём копию массива детей
const childrenCopy = [...this.children.list];

this.tweens.add({
    targets: childrenCopy, // Работаем с копией
    duration: 1000,
    props: { x: this.scale.width / 2, y: this.scale.height / 2 }
});

**Способ 2: Использование Groups.** Более структурный и мощный подход — использовать Phaser.GameObjects.Group. Группы в Phaser идеально подходят для управления коллекциями объектов с общим поведением, включая анимацию.

create () {
    // Создаём группу
    const shapeGroup = this.add.group();

    // Добавляем объекты сразу в группу
    shapeGroup.create(0, 0, null).setCircle(100, 0xff0000); // Нужна текстура для create
    // ... или добавляем существующие объекты
    const rect = this.add.rectangle(...);
    shapeGroup.add(rect);

    // Анимируем всю группу
    this.tweens.add({
        targets: shapeGroup.getChildren(), // Получаем массив детей группы
        duration: 1000,
        props: { x: 400, y: 300 }
    });
}

Группа даёт больше контроля: вы можете добавлять/удалять объекты из группы, не боясь сломать уже запущенные твины, если они были созданы для конкретного набора объектов группы.

Итог: правило для безопасной анимации

Запомните простое правило: **targets твина должен быть статичным набором объектов на момент создания анимации.**

*   **Опасно:** `targets: this.children.list`, `targets: someArray` (если массив `someArray` может измениться).
*   **Безопасно:** `targets: myGroup.getChildren()`, `targets: [...this.children.list]`, `targets: [obj1, obj2, obj3]` (явный перечисление).

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

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

Баг в примере наглядно показывает разницу между ссылкой на массив и его содержимым. Phaser не клонирует переданный в targets массив, а использует его как есть. Для стабильной работы всегда "замораживайте" набор целей для твина, создавая его копию или используя группы. Для экспериментов попробуйте

  1. Добавить несколько объектов после твина и посмотреть, какие из них сдвинутся
  2. Использовать this.children.sendToBack(t) вместо moveTo и увидеть другой эффект
  3. Реализовать систему, где каждый новый объект добавляется в группу и сразу анимируется отдельным, но синхронным твином