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

При создании сложных UI-элементов, таких как слайдеры с перемещаемым ползунком, разработчики часто сталкиваются с конфликтом событий перетаскивания. Если и контейнер, и его дочерний объект являются перетаскиваемыми, возникает нежелательное поведение: перетаскивание ползунка может инициировать движение всего контейнера. Эта статья на практическом примере показывает, как правильно организовать вложенное перетаскивание, разделив логику событий для родительского контейнера и его элементов.

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

Живой запуск

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

Исходный код


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

    create () {

        const slider = this.add.container(400, 300);

        const bar = this.add.rectangle(0, 0, 400, 32, 0x9d9d9d);
        const control = this.add.circle(0, 0, 24, 0xff00ff)

        slider.add([ bar, control ]);

        control.setInteractive({ draggable: true });

        control.on('drag', function (pointer, dragX, dragY) {

            control.x = Phaser.Math.Clamp(dragX, -200, 200);
            // console.log(dragX);

        });

        slider.setSize(400, 32);
        slider.setInteractive({ draggable: true });

        slider.on('drag', function (pointer, dragX, dragY) {

            slider.x = dragX;
            slider.y = dragY;

        });

    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Проблема: конфликт событий drag

В представленном примере создаётся слайдер. Сам слайдер — это Container, содержащий фоновую полосу (bar) и управляющий ползунок (control). Оба объекта — и контейнер, и ползунок — сделаны перетаскиваемыми через setInteractive({ draggable: true }).

При попытке перетащить ползунок возникает проблема: событие drag может сработать и для контейнера, в котором находится ползунок. Это приводит к тому, что вместо плавного движения ползунка по полосе весь слайдер начинает перемещаться по сцене.

Корень проблемы в том, что события мыши (включая drag) всплывают вверх по иерархии объектов сцены. Если не управлять этим процессом, действие с дочерним элементом будет обработано и его родителем.

Создание структуры: Container и его дети

Первым шагом создаётся структура нашего UI-элемента. Контейнер позиционируется в центре экрана и служит локальной системой координат для своих дочерних объектов.

const slider = this.add.container(400, 300);

const bar = this.add.rectangle(0, 0, 400, 32, 0x9d9d9d);
const control = this.add.circle(0, 0, 24, 0xff00ff);

slider.add([ bar, control ]);

Здесь bar и control добавляются в контейнер slider. Их координаты (0, 0) теперь отсчитываются от центра контейнера, а не от глобальных координат сцены. Это ключевой момент для корректного позиционирования и расчёта границ перетаскивания ползунка.

Логика перетаскивания ползунка (control)

Ползунок control делается перетаскиваемым. В обработчике события drag реализуется основная логика слайдера: ограничение движения ползунка в пределах фоновой полосы.

control.setInteractive({ draggable: true });

control.on('drag', function (pointer, dragX, dragY) {
    control.x = Phaser.Math.Clamp(dragX, -200, 200);
});

Параметры dragX и dragY, передаваемые в функцию-обработчик, представляют собой **глобальные координаты указателя мыши**. Однако, поскольку control.x — это свойство, определяющее положение относительно родительского контейнера (slider), эти координаты автоматически конвертируются в локальные.

Важно: Phaser.Math.Clamp(dragX, -200, 200) ограничивает движение ползунка. Значения -200 и 200 — это половина ширины фоновой полосы (bar), что не позволяет ползунку выйти за её визуальные границы.

Логика перетаскивания контейнера (slider)

Чтобы можно было перемещать весь слайдер по сцене, делаем интерактивным и перетаскиваемым сам контейнер.

slider.setSize(400, 32);
slider.setInteractive({ draggable: true });

slider.on('drag', function (pointer, dragX, dragY) {
    slider.x = dragX;
    slider.y = dragY;
});

Метод setSize(400, 32) задаёт зону хитбокса контейнера. Без этого вызова событие перетаскивания на контейнер не сработает, так как по умолчанию контейнер не имеет физического размера для взаимодействия.

В обработчике drag для контейнера мы просто обновляем его глобальные координаты `xиy`, что приводит к перемещению всей группы объектов (фона и ползунка) по сцене.

Итог: как работают оба события вместе

Несмотря на то что оба объекта являются перетаскиваемыми, конфликта в итоговом примере **не происходит**. Почему?

1. Когда вы начинаете перетаскивать ползунок (control), событие drag возникает именно для него. Движение ползунка ограничивается в его локальных координатах. 2. Событие **не всплывает** до контейнера (slider), потому что система событий ввода Phaser по умолчанию останавливает распространение события, если оно было обработано на исходном целевом объекте (в данном случае — на control). 3. Чтобы перетащить весь слайдер, нужно начать drag на любом месте контейнера, кроме области, занятой ползунком. В этом случае событие сработает для slider.

Таким образом, логика разделяется корректно: клик по ползунку — двигаем ползунок, клик по фону слайдера — двигаем весь слайдер.

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

Грамотное использование контейнеров и разделение обработки событий позволяет создавать сложные, составные интерактивные элементы без конфликтов. Для экспериментов попробуйте

  1. Добавить текстовое поле, отображающее текущее значение слайдера (от -200 до 200)
  2. Связать положение ползунка с громкостью звука или прозрачностью спрайта
  3. Создать вертикальный слайдер, изменив логику ограничения в Clamp для оси Y