О чем этот пример
Работа с контейнерами (`Container`) в 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 container1 = this.add.container(400, 300);
var container2 = this.add.container(400, 300);
container1.add(sprite);
container2.add(sprite);
sprite.destroy();
container1.destroy();
container2.destroy();
console.log(this);
}
В чём проблема исходного кода?
В примере создаётся один спрайт с анимацией, а затем предпринимается попытка добавить его в два разных контейнера. Это нарушает внутреннюю логику Phaser.
var container1 = this.add.container(400, 300);
var container2 = this.add.container(400, 300);
container1.add(sprite);
container2.add(sprite); // Ошибка! Sprite уже имеет родителя.
В Phaser 3 игровой объект (например, Sprite, Image) может иметь только одного прямого родителя в дереве отображения. Родителем может быть сама сцена (this.add) или контейнер. Как только объект добавлен в один контейнер вызовом container.add(), он не может быть добавлен в другой, пока не будет удалён из первого. Попытка сделать это приводит к ошибке, а последующие вызовы destroy() могут завершиться некорректно.
Как правильно работать с контейнерами
Если вам нужны два визуально идентичных объекта в разных контейнерах, необходимо создать два отдельных экземпляра спрайта. Phaser эффективно управляет текстурами в памяти, поэтому использование одного и того же ключа атласа ('walker') для нескольких спрайтов не создаёт избыточной нагрузки.
// Создаём два отдельных спрайта
const sprite1 = this.add.sprite(0, 0, 'walker', 'frame_0000');
const sprite2 = this.add.sprite(0, 0, 'walker', 'frame_0000');
// Проигрываем анимацию на каждом
sprite1.play('walk');
sprite2.play('walk');
// Добавляем каждый спрайт в свой контейнер
container1.add(sprite1);
container2.add(sprite2);
Таким образом, каждый контейнер управляет своим собственным объектом, и не возникает конфликтов владения.
Альтернатива: клонирование контейнера
Иногда логика требует, чтобы в разных частях сцены находилась идентичная группа объектов. Вместо ручного создания каждого элемента группы дважды, можно создать один контейнер-прототип, а затем создать его независимые копии.
Один из подходов — создать фабричную функцию, которая возвращает новый контейнер с набором объектов.
function createWalkerContainer(scene, x, y) {
const container = scene.add.container(x, y);
const sprite = scene.add.sprite(0, 0, 'walker', 'frame_0000');
sprite.play('walk');
container.add(sprite);
// Можно добавить другие объекты: текст, частицы
return container;
}
// Использование
const container1 = createWalkerContainer(this, 200, 300);
const container2 = createWalkerContainer(this, 600, 300);
Этот подход обеспечивает чистоту кода и упрощает модификацию сложных групп в будущем.
Важность корректного уничтожения
В исходном примере присутствуют вызовы destroy(). Важно понимать их порядок. При уничтожении контейнера с помощью container.destroy(), опционально можно уничтожить и все его дочерние элементы.
Однако если объект был добавлен в несколько контейнеров (что привело к ошибке), его состояние становится неопределённым, и вызов destroy() может не сработать правильно.
При правильной архитектуре с отдельными объектами, уничтожение становится предсказуемым:
// Уничтожаем контейнеры и ВСЕ их дети
container1.destroy(true);
container2.destroy(true);
// Или уничтожаем детей вручную, если нужен больший контроль
// container1.removeAll(true);
// container1.destroy();
Передача true в метод destroy() гарантирует, что все дочерние игровые объекты также будут уничтожены и очищены из памяти.
Что попробовать дальше
Ключевое правило: один игровой объект — один родитель в дереве отображения. Для размещения одинакового контента в нескольких местах создавайте отдельные экземпляры объектов или используйте фабричные методы. Это обеспечивает стабильность, предотвращает трудноуловимые ошибки и соответствует архитектурным принципам Phaser 3. **Идеи для экспериментов:** 1. Создайте контейнер, который включает спрайт и текстовую метку, и сделайте несколько его копий с разными позициями и текстом. 2. Исследуйте, как трансформации (масштаб, поворот) родительского контейнера применяются ко всем его дочерним элементам. 3. Попробуйте добавить один контейнер в качестве дочернего элемента другого контейнера, создавая иерархические структуры.
