О чем этот пример

Вы добавляете спрайт в контейнер, навешиваете на него обработчик события `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 всегда «перехватывает» события мыши в своей области, блокируя их всплытие. Для экспериментов попробуйте

  1. Создать несколько контейнеров с интерактивными объектами и одну dropZone — увидите, как зона «глушит» все клики под собой
  2. Реализовать систему перетаскивания, где dropZone динамически включается/выключается, чтобы не мешать обычному взаимодействию с интерфейсом