О чем этот пример
Вы когда-нибудь анимировали группу объектов через `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 массив, а использует его как есть. Для стабильной работы всегда "замораживайте" набор целей для твина, создавая его копию или используя группы. Для экспериментов попробуйте
- Добавить несколько объектов после твина и посмотреть, какие из них сдвинутся
- Использовать
this.children.sendToBack(t)вместоmoveToи увидеть другой эффект - Реализовать систему, где каждый новый объект добавляется в группу и сразу анимируется отдельным, но синхронным твином
