О чем этот пример
Работая с контейнерами в Phaser 3, вы могли столкнуться с неожиданным поведением: элементы, добавленные в `Container`, перестают реагировать на события ввода, хотя интерактивность для них была объявлена. Эта статья полезна, чтобы понять иерархию отображения и обработки событий в движке, и научиться правильно настраивать интерактивность для сложных составных объектов. Мы разберем пример, демонстрирующий эту проблему, и объясним, как её избежать.
Версия 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.image('buttonBG', 'assets/sprites/button-bg.png');
this.load.image('buttonText', 'assets/sprites/button-text.png');
}
function create ()
{
var bg = this.add.image(0, 0, 'buttonBG');
var bg2 = this.add.image(200, 400, 'buttonBG');
var text = this.add.image(0, 0, 'buttonText');
bg.setInteractive({ draggable: true });
bg2.setInteractive({ draggable: true });
var container = this.add.container(400, 100, [ bg, text ]);
bg2.on('pointerdown', function () {
console.log('Clicked button 2');
});
bg.on('pointerdown', function () {
console.log('Clicked button');
});
bg.on('pointerover', function () {
this.setTint(0xff44ff);
});
bg.on('pointerout', function () {
this.clearTint();
});
console.log('bg', bg.displayList);
console.log('bg2', bg2.displayList);
var graphics = this.add.graphics();
graphics.width = 1024 * 16;
graphics.height = 1024 * 16;
graphics.setInteractive({
draggable: true
});
}
Проблема: интерактивность внутри контейнера
В примере создаются два спрайта кнопки (bg и bg2) и один спрайт текста. Оба спрайта кнопок получают интерактивность через метод setInteractive({ draggable: true }). Однако bg помещается в контейнер вместе со спрайтом текста, а bg2 остаётся на сцене самостоятельно.
Код обработчиков событий для bg (pointerdown, pointerover, pointerout) выглядит корректно. Но при запуске примера и клике на bg (который внутри контейнера) в консоли не появляется сообщение "Clicked button". Более того, события наведения (pointerover) и ухода (pointerout) также не срабатывают. В то же время, клик по bg2 (вне контейнера) успешно выводит "Clicked button 2".
bg.setInteractive({ draggable: true });
bg2.setInteractive({ draggable: true });
var container = this.add.container(400, 100, [ bg, text ]);
bg.on('pointerdown', function () {
console.log('Clicked button'); // Не сработает!
});
Причина: Display List и контейнеры
Ключ к пониманию проблемы лежит в свойстве displayList. Phaser управляет отрисовкой и вводом через списки отображения. Когда вы выводите в консоль bg.displayList и bg2.displayList, становится ясно, в чём разница.
Спрайт bg2, созданный напрямую через this.add.image, попадает в основной список отображения сцены (this.sys.displayList). Его интерактивность обрабатывается системой ввода сцены (this.sys.input).
Спрайт bg после добавления в контейнер перемещается в список отображения этого контейнера (container.displayList). По умолчанию контейнеры в Phaser 3 **не являются интерактивными**. Система ввода сцены проверяет события только для объектов в своём собственном списке отображения. Поскольку bg теперь принадлежит списку контейнера, события мыши до него не доходят.
console.log('bg', bg.displayList); // Ссылается на container.displayList
console.log('bg2', bg2.displayList); // Ссылается на this.sys.displayList (основной список сцены)
Решение: делаем контейнер интерактивным
Чтобы события мыши доходили до дочерних элементов контейнера, сам контейнер также должен получить интерактивность. Это делается методом setInteractive на объекте контейнера. После этого события будут корректно передаваться вниз по иерархии к его дочерним элементам, таким как наш спрайт bg.
Важно отметить, что в примере также создаётся объект graphics с огромными размерами и интерактивностью. Его наличие может перехватывать все события ввода на своей области, что является отдельной потенциальной проблемой. В данном разборе мы фокусируемся именно на взаимосвязи контейнера и его содержимого.
var container = this.add.container(400, 100, [ bg, text ]);
// Делаем контейнер интерактивным
container.setInteractive();
После этого добавленные ранее обработчики событий на спрайте bg начнут корректно срабатывать при кликах и наведении.
Практический патч для примера
Внесём минимальные изменения в исходный код, чтобы исправить проблему. Всё, что нужно — это добавить одну строку после создания контейнера.
function create ()
{
var bg = this.add.image(0, 0, 'buttonBG');
var bg2 = this.add.image(200, 400, 'buttonBG');
var text = this.add.image(0, 0, 'buttonText');
bg.setInteractive({ draggable: true });
bg2.setInteractive({ draggable: true });
var container = this.add.container(400, 100, [ bg, text ]);
// ФИКС: Делаем контейнер интерактивным
container.setInteractive();
// ... остальной код обработчиков событий и логирования
}
Теперь при клике на bg в консоль будет выводиться "Clicked button", а также будет работать эффект tint при наведении. Принцип "контейнер-родитель должен быть интерактивным" является основополагающим для работы с составными интерактивными объектами в Phaser.
Что попробовать дальше
Интерактивность в Phaser привязана к спискам отображения. Контейнеры, будучи мощным инструментом для группировки объектов, по умолчанию не передают события ввода своим дочерним элементам. Решение простое и универсальное: всегда вызывайте container.setInteractive(), если планируете обрабатывать события на содержимом контейнера.
**Идеи для экспериментов:**
1. Попробуйте задать контейнеру хит-зону (input.hitArea) отличную от прямоугольной, используя Phaser.Geom.Polygon.
2. Создайте вложенную структуру из нескольких контейнеров и проверьте, как события всплывают через их иерархию.
3. Поэкспериментируйте с depth контейнера и интерактивного объекта graphics из примера, чтобы понять, какой объект перехватывает событие первым.
