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

При разработке игр на Phaser часто возникает необходимость управлять группами объектов. Слои (Layers) — мощный инструмент для организации отрисовки и обновления. Однако их добавление в сцену может привести к неочевидным багам с обработкой ввода. В этой статье мы разберем конкретный пример, где интерактивные спрайты перестают реагировать на клики после помещения в слой, и объясним, как этого избежать. Понимание этого механизма сэкономит вам часы отладки.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene {
    constructor() {
      super();
    }

    preload() {
      
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image(
        "elephant", "assets/sprites/elephant.png"
      );
    }

    create() {
      const elephant1 = this.add.sprite(100, 100, "elephant");
      const elephant2 = this.add.sprite(130, 120, "elephant");
      elephant1.depth = 2;
      elephant1.setInteractive({
        pixelPerfect: false,
        useHandCursor: true
      });
      elephant2.depth = 3;
      elephant2.setInteractive({
        pixelPerfect: false,
        useHandCursor: true
      });
      elephant1.on('pointerdown', function (){
        alert("elephant 1");
      });
      elephant2.on('pointerdown', function (){
        alert("elephant 2");
      });

      const layer = this.add.layer();

      // remove the line below and the alerts work as expected
      layer.add([elephant1, elephant2]);
    }
  }

  const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: "#2d2d2d",
    parent: "phaser-example",
    scene: Example
  };

  const game = new Phaser.Game(config);

Проблема: клики перестают работать

В представленном примере создаются два спрайта слона, каждому назначается интерактивность и обработчик события pointerdown. По отдельности они работают корректно. Однако при добавлении этих спрайтов в слой с помощью layer.add(), клики по ним больше не вызывают ожидаемых alert-сообщений.

Код настройки интерактивности для спрайтов выглядит так:

elephant1.setInteractive({
  pixelPerfect: false,
  useHandCursor: true
});
elephant1.on('pointerdown', function (){
  alert("elephant 1");
});

Парадокс в том, что спрайты по-прежнему отображаются, их свойства depth установлены, но события мыши «проходят сквозь них».

Причина: приоритет обработки ввода

В Phaser объекты Layer по умолчанию также являются интерактивными игровыми объектами (Game Objects). Когда слой добавляется на сцену (даже без явного вызова setInteractive), он создает свою собственную хит-зону (hit area), которая по умолчанию может покрывать всю свою область.

Ключевой момент: система ввода Phaser обрабатывает события от объектов, находящихся **выше** в порядке отрисовки (с учетом depth). В данном случае, оба спрайта добавлены в слой. Сам слой, как родительский контейнер, получает свой собственный depth и может оказаться в иерархии обработки событий таким образом, что он «перехватывает» клик раньше, чем его дочерние спрайты. Если у слоя нет своих обработчиков событий, клик просто игнорируется.

Добавление объектов в слой:

const layer = this.add.layer();
layer.add([elephant1, elephant2]);

Решение: отключение интерактивности слоя

Самое простое и прямое решение — явно отключить интерактивность для самого слоя. Это можно сделать с помощью метода setInteractive с аргументом null или false, либо установив свойство input после создания.

Вот как это исправить в коде примера:

const layer = this.add.layer();
layer.setInteractive(false); // Или layer.input = null;
layer.add([elephant1, elephant2]);

После этого события мыши будут корректно доставляться до интерактивных дочерних спрайтов elephant1 и elephant2, и alert-сообщения снова появятся.

Важно: если вам нужна интерактивность на самом слое (например, для фонового клика), вам следует назначить ему обработчик события pointerdown и, возможно, управлять всплытием событий вручную.

Альтернатива: работа без слоя

Если функциональность слоя в вашем случае ограничивается только группировкой объектов для управления depth, и интерактивность критически важна, рассмотрите альтернативы. Вы можете управлять глубиной объектов индивидуально или использовать другие методы группировки, которые не затрагивают обработку ввода.

Например, порядок отрисовки в данном примере уже управляется через свойство depth спрайтов:

elephant1.depth = 2;
elephant2.depth = 3;

Если убрать строку с добавлением в слой, интерактивность работает, а порядок отрисовки (где elephant2 с глубиной 3 будет поверх elephant1) сохраняется. Слои нужны для более сложных операций над группой, таких как массовое перемещение, видимость или отрисовка.

Что попробовать дальше

Добавление интерактивных объектов в слой Phaser может неожиданно отключить их реакцию на ввод из-за того, что сам слой становится интерактивным контейнером. Решение простое: явно отключите интерактивность слоя через layer.setInteractive(false). Для экспериментов попробуйте

  1. Назначить обработчик события самому слою и посмотреть, как события распределяются между родителем и детьми
  2. Использовать pixelPerfect: true для спрайтов в сочетании со слоем
  3. Создать несколько слоев с разной интерактивностью и проверить их взаимодействие