О чем этот пример
Вы добавляете спрайт в контейнер, навешиваете на него обработчик события `pointerdown`, но клик никогда не срабатывает. Всё работает, если убрать другой, казалось бы, несвязанный спрайт с флагом `dropZone`. Эта статья разберет, почему возникает этот неочевидный баг в Phaser 3, как устроена система событий ввода и как её правильно обойти. Понимание этой механики спасёт вас от часов дебагга при работе с интерактивными контейнерами и перетаскиванием.
Версия 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('lemming', 'assets/sprites/lemming.png');
}
function create () {
// if you comment this line, pointerdown will work as expected
const dropArea = this.add.sprite(400, 320, 'lemming').setInteractive({dropZone: true});
const container = this.add.container(400, 300);
const sprite0 = this.add.sprite(0, 0, 'lemming');
container.add(sprite0).setSize(64, 64)
.setInteractive()
.on('pointerdown', () => console.log('I will never happen :-('));;
}
Суть проблемы: приоритет обработки событий
В Phaser 3 система ввода (InputPlugin) обрабатывает события (клик, перемещение) в определённом порядке. Ключевое правило: **интерактивный объект с флагом dropZone: true получает приоритет при обработке событий указателя (pointer events), даже если он находится "под" другими объектами.**
В исходном примере создаются два объекта:
1. Спрайт dropArea с dropZone: true.
2. Контейнер container, содержащий спрайт sprite0. Контейнеру также задана интерактивность (setInteractive()).
При клике по sprite0 (который внутри контейнера) система ввода сначала проверяет, не попал ли клик в зону dropZone. Поскольку dropArea существует и его геометрия перекрывает клик, событие «захватывается» им, и дальнейшая проверка по цепочке (до контейнера и его содержимого) не происходит. Вот почему обработчик pointerdown на контейнере никогда не выполняется.
Разбор кода: создание конфликтующей сцены
Давайте посмотрим, как именно создаются объекты в примере. Сцена состоит из двух ключевых функций: preload и create.
Функция preload загружает один спрайт:
function preload () {
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('lemming', 'assets/sprites/lemming.png');
}
В функции create возникает конфликт. Сначала создаётся спрайт-зона сброса (dropZone):
const dropArea = this.add.sprite(400, 320, 'lemming').setInteractive({dropZone: true});
Затем создаётся контейнер с интерактивным спрайтом внутри:
const container = this.add.container(400, 300);
const sprite0 = this.add.sprite(0, 0, 'lemming');
container.add(sprite0).setSize(64, 64)
.setInteractive()
.on('pointerdown', () => console.log('I will never happen :-('));
Обратите внимание: метод setInteractive() вызывается на контейнере после добавления в него спрайта. Это делает интерактивной всю область контейнера, а не только дочерний спрайт. Однако, как мы выяснили, событие до этой области не доходит.
Решение 1: Убрать или переместить dropZone
Самый прямой способ — избавиться от конфликта приоритетов. Если функционал перетаскивания (dropZone) в данный момент не нужен, просто уберите этот флаг или удалите объект.
Закомментировав создание dropArea, вы разрешите системе ввода проверять объекты под курсором в обычном порядке (сверху вниз), и событие успешно дойдёт до контейнера.
// function create () {
// const dropArea = this.add.sprite(400, 320, 'lemming').setInteractive({dropZone: true}); // Теперь этой помехи нет
// ... остальной код с контейнером
// }
Если dropZone необходим, но должен быть невидим или находиться в другом месте, убедитесь, что его геометрия (хитбокс) не пересекается с интерактивными объектами, которые должны получать события pointerdown.
Решение 2: Использовать события на самом dropZone
Если объект с dropZone: true должен быть всегда на сцене и перекрывать другие элементы, но вам всё равно нужно обрабатывать клики, можно навесить обработчик непосредственно на него. События будут срабатывать на dropArea, а не на контейнере.
Измените код создания dropArea:
const dropArea = this.add.sprite(400, 320, 'lemming')
.setInteractive({dropZone: true})
.on('pointerdown', () => console.log('Клик обработан на dropZone!'));
Это не заставит сработать обработчик на контейнере, но позволит обрабатывать ввод через объект с высшим приоритетом. Это полезно, когда dropZone является фоном или основной активной областью.
Решение 3: Временное отключение dropZone
В более сложных сценариях состояние dropZone может быть динамическим. Например, зона сброса должна активироваться только во время перетаскивания. В этом случае вы можете управлять свойством dropZone вручную.
Установите dropZone: false по умолчанию и включайте его только когда это необходимо (например, в начале перетаскивания другого объекта):
let dropArea;
function create() {
// Создаём зону, но НЕ как dropZone изначально
dropArea = this.add.sprite(400, 320, 'lemming').setInteractive();
dropArea.dropZone = false; // Свойство можно менять
// ... создание контейнера и других объектов
// Где-то в коде при начале перетаскивания:
// dropArea.dropZone = true;
// При завершении перетаскивания или отмене:
// dropArea.dropZone = false;
}
Такой подход даёт полный контроль и позволяет избежать конфликта приоритетов, когда dropZone не нужен.
Что попробовать дальше
Баг наглядно демонстрирует важность понимания внутреннего порядка обработки событий в Phaser. Ключевой вывод: объект с dropZone: true всегда «перехватывает» события мыши в своей области, блокируя их всплытие. Для экспериментов попробуйте
- Создать несколько контейнеров с интерактивными объектами и одну
dropZone— увидите, как зона «глушит» все клики под собой - Реализовать систему перетаскивания, где
dropZoneдинамически включается/выключается, чтобы не мешать обычному взаимодействию с интерфейсом
