О чем этот пример
Работая с контейнерами в 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(); или изучить, как влияет на производительность наличие сотни «дублированных» спрайтов.
