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

Частая проблема при разработке гибридных интерфейсов в Phaser — это непредсказуемое перекрытие событий между игровыми объектами и HTML-элементами, добавленными через `add.dom`. Эта статья наглядно покажет, почему клики могут "пролетать" сквозь ваш UI, и даст практические решения для управления их обработкой. Понимание этих механизмов критично для создания отзывчивых игр с веб-формами или сложным интерфейсом.

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

Живой запуск

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

Исходный код


new Phaser.Game({
    type: Phaser.AUTO,
    width: 300,
    height: 300,
    backgroundColor: 0x2d2d88,
    scene: {
      create () {
        let count = 0
        const text = this.add.text(0, 0, 'Click blue')
        const phaserRedRectangle = this.add.rectangle(50, 50, 150, 150, 0xFF4444).setOrigin(0).setAlpha(0.5)
        phaserRedRectangle.setInteractive()
        phaserRedRectangle.on('pointerdown', () => {
          count++;
          text.setText(`Clicked ${count} times`)
          console.log('clicked', count)
        })

        const element = this.add.dom(100, 100, 'div', 'width: 150px; height: 150px; background: #0099FF; opacity: 0.5;');
      }
    },
    dom: {
        createContainer: true
    },
    parent: 'phaser-example',
    // input: { windowEvents: false, touch: { capture: true } }
  })

Проблема: невидимый барьер для кликов

В приведённом примере создаётся синий полупрозрачный DOM-элемент (div) и красный полупрозрачный игровой прямоугольник (rectangle). Оба объекта имеют одинаковые размеры и частично перекрываются.

Красный прямоугольник является интерактивным — на него назначен слушатель события pointerdown. Однако, если кликнуть в область пересечения этих двух фигур, событие не сработает, и счётчик не увеличится. В консоли также не появится сообщение. Это происходит потому, что DOM-элемент, добавленный поверх канваса, перехватывает все события мыши, не позволяя им "дойти" до игровых объектов Phaser.

const phaserRedRectangle = this.add.rectangle(50, 50, 150, 150, 0xFF4444).setOrigin(0).setAlpha(0.5)
phaserRedRectangle.setInteractive()
phaserRedRectangle.on('pointerdown', () => {
  count++;
  text.setText(`Clicked ${count} times`)
  console.log('clicked', count)
})

const element = this.add.dom(100, 100, 'div', 'width: 150px; height: 150px; background: #0099FF; opacity: 0.5;');

Причина: порядок рендеринга и система событий

Phaser использует отдельный HTML-контейнер для DOM-элементов, который по умолчанию располагается поверх основного canvas, где отрисовываются все игровые объекты (спрайты, прямоугольники, текст). Это архитектурное решение.

1. **Порядок слоёв:** Сначала рендерится canvas с игрой, затем поверх него — контейнер для DOM. Поэтому DOM-элементы всегда визуально поверх игровых объектов. 2. **Всплытие событий:** События мыши (pointer events) в браузере всегда возникают на самом верхнем элементе в иерархии. Клик попадает сначала в DOM-div, и стандартное поведение браузера не передаёт это событие "ниже", на canvas.

Ключевая конфигурация, которая создаёт контейнер для DOM-элементов, находится в объекте конфигурации игры:

dom: {
    createContainer: true
}

Решение 1: Отключение событий у DOM-элемента

Самый простой способ — сделать DOM-элемент "невидимым" для событий мыши с помощью CSS-свойства pointer-events. Это можно задать прямо при создании элемента через add.dom.

const element = this.add.dom(100, 100, 'div', 'width: 150px; height: 150px; background: #0099FF; opacity: 0.5; pointer-events: none;');

После этого клики будут беспрепятственно проходить сквозь синий div и активировать интерактивный красный прямоугольник под ним. Это идеально подходит для статичных или анимированных декоративных HTML-элементов, которые не должны реагировать на ввод.

Решение 2: Обработка событий на самом DOM-элементе

Если ваш DOM-элемент является частью интерфейса (например, кнопка или форма), и ему тоже нужна реакция на клики, обрабатывайте события непосредственно на нём. У объекта, возвращаемого add.dom, есть свойство node, содержащее ссылку на нативный HTML-элемент.

const element = this.add.dom(100, 100, 'div', 'width: 150px; height: 150px; background: #0099FF; opacity: 0.5;');
element.node.addEventListener('click', () => {
    count++;
    text.setText(`Clicked from DOM: ${count} times`);
    console.log('DOM clicked', count);
});

В этом случае оба объекта (красный прямоугольник Phaser и синий div) будут иметь свои независимые обработчики. Однако клик в области пересечения вызовет только событие у DOM-элемента, так как он находится сверху.

Решение 3: Тонкая настройка ввода (Input Configuration)

В закомментированной части исходного кода показана продвинутая настройка. Раскомментирование параметра touch.capture может изменить поведение в мобильных браузерах.

input: { windowEvents: false, touch: { capture: true } }

* windowEvents: false — отключает автоматическую привязку событий Phaser к глобальному объекту window. Это полезно, если игра встроена в сложную страницу. * touch: { capture: true } — включает режим capture для touch-событий. Это может повлиять на порядок обработки событий между Phaser и другими элементами страницы в мобильных браузерах.

Эти настройки не решат базовую проблему перекрытия DOM и canvas, но важны для управления вводом в специфичных окружениях.

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

Ключ к управлению вводом в гибридной среде Phaser — понимание слоёв: DOM-элементы всегда поверх canvas. Для декоративных элементов используйте pointer-events: none. Для интерактивных — обрабатывайте события напрямую на DOM-узле. Поэкспериментируйте: создайте интерфейс, где кнопка в DOM меняет цвет спрайта в canvas, или реализуйте сложную форму входа, которая взаимодействует с игровым миром, корректно управляя фокусом событий.