О чем этот пример
При создании сложных 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.
Таким образом, логика разделяется корректно: клик по ползунку — двигаем ползунок, клик по фону слайдера — двигаем весь слайдер.
Что попробовать дальше
Грамотное использование контейнеров и разделение обработки событий позволяет создавать сложные, составные интерактивные элементы без конфликтов. Для экспериментов попробуйте
- Добавить текстовое поле, отображающее текущее значение слайдера (от -200 до 200)
- Связать положение ползунка с громкостью звука или прозрачностью спрайта
- Создать вертикальный слайдер, изменив логику ограничения в
Clampдля оси Y
