О чем этот пример
Реализация перетаскивания объектов — одна из базовых механик в играх и интерактивных приложениях. Однако, когда объект находится внутри контейнера, с координатами может происходить путаница, и drag&drop работает некорректно. В этой статье мы разберем пример из официального репозитория Phaser, который демонстрирует проблему с обработкой координат при перетаскивании объектов внутри `Container`. Вы поймете, почему простой вызов `setPosition(x, y)` в обработчике события `GAMEOBJECT_DRAG_END` не дает ожидаемого результата, и как правильно работать с локальными и мировыми координатами в таких сценариях.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bg', 'assets/skies/gradient13.png');
this.load.atlas('rocket', 'assets/animations/rocket.png', 'assets/animations/rocket.json');
}
create ()
{
// const r0 = this.add.rectangle(this.scale.width * .75, this.scale.height * 0.75, 100, 100, 0xff0000).setInteractive({ draggable: true })
// .on(Phaser.Input.Events.GAMEOBJECT_DRAG, (pointer, x, y) =>
// {
// r0.setPosition(x, y);
// console.log(x, y);
// })
// .on(Phaser.Input.Events.GAMEOBJECT_DRAG_END, (pointer, x, y) =>
// {
// r0.setPosition(x, y); // this does not work as expected
// console.log(x, y);
// const localX = r0.x + x;
// const localY = r0.y + y;
// console.log(r0.x, r0.y, x, y);
// r0.setPosition(localX, localY); // this does not work either
// });
// const r1 = this.add.rectangle(this.scale.width * .75 + 150, this.scale.height * 0.75, 100, 100, 0xff0000).setInteractive({ draggable: true })
// .on(Phaser.Input.Events.GAMEOBJECT_DRAG, (pointer, x, y) =>
// {
// r1.setPosition(x, y);
// console.log(x, y);
// })
// .on(Phaser.Input.Events.GAMEOBJECT_DRAG_END, (pointer, x, y) =>
// {
// r1.setPosition(x, y); // this does not work as expected
// console.log(x, y);
// const localX = r1.x + x;
// const localY = r1.y + y;
// console.log(localX, localY);
// r1.setPosition(localX, localY); // this does not work either
// });
const c = this.add.container(250, 250);
const r2 = this.add.rectangle(0, 0, 100, 100, 0xff0000).setInteractive({ draggable: true })
.on(Phaser.Input.Events.GAMEOBJECT_DRAG, (pointer, x, y) =>
{
r2.setPosition(x, y);
console.log(x, y);
})
.on(Phaser.Input.Events.GAMEOBJECT_DRAG_END, (pointer, x, y) =>
{
// r2.setPosition(x, y); // this does not work as expected
// console.log(x, y);
const localX = r2.x + x;
const localY = r2.y + y;
console.log(localX, localY);
r2.setPosition(localX, localY); // this does not work either
});
const r3 = this.add.rectangle(150, 0, 100, 100, 0xff0000).setInteractive({ draggable: true })
.on(Phaser.Input.Events.GAMEOBJECT_DRAG, (pointer, x, y) =>
{
r3.setPosition(x, y);
console.log(x, y);
})
.on(Phaser.Input.Events.GAMEOBJECT_DRAG_END, (pointer, x, y) =>
{
// r3.setPosition(x, y); // this does not work as expected
// console.log(x, y);
const localX = r3.x + x;
const localY = r3.y + y;
console.log(localX, localY);
r3.setPosition(localX, localY); // this does not work either
});
c.add([ r2, r3 ]);
}
createText ()
{
this.text = this.add.text(16, 16, '', {
font: '16px Arial',
backgroundColor: '#000000',
fill: '#ffffff'
});
}
updateText (x, y)
{
const result = {
x: this.rocket.x + x,
y: this.rocket.y + y,
}
const message = [ `Rocket: ${this.rocket.x}, ${this.rocket.y}` ];
message.push(`Display Origin: ${this.rocket.displayOriginX}, ${this.rocket.displayOriginY}`);
message.push(`Mouse Position: ${this.rocket.input.localX}, ${this.rocket.input.localY}`);
message.push(`Expected RESULT: ${result.x}, ${result.y}`);
this.text.setText(message);
}
// update (time, delta)
// {
// const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
// const message = [ `Mouse Position: ${this.rocket.input.localX}, ${this.rocket.input.localY}` ];
// this.text.setText(message);
// // this.updateCircle(worldPoint);
// }
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Суть проблемы
В исходном примере разработчик столкнулся с неожиданным поведением при попытке зафиксировать позицию игрового объекта после окончания перетаскивания (событие GAMEOBJECT_DRAG_END).
Прямое присвоение координат из события (setPosition(x, y)) не работает, как и попытка сложить текущие координаты объекта с координатами события (r2.x + x, r2.y + y).
Основная причина в том, что координаты (x, y), передаваемые в событиях GAMEOBJECT_DRAG и GAMEOBJECT_DRAG_END, являются **мировыми координатами** (координатами указателя мыши в игровом мире). Однако, когда объект находится внутри контейнера (Container), его система координат становится локальной относительно этого контейнера. Метод setPosition() для дочернего объекта контейнера ожидает **локальные координаты**.
Простое присвоение мировых координат локальной системе приводит объект в неверную позицию относительно родителя.
Разбираем код примера
Давайте посмотрим на ключевую часть примера, где создается контейнер и два перетаскиваемых прямоугольника.
const c = this.add.container(250, 250);
const r2 = this.add.rectangle(0, 0, 100, 100, 0xff0000).setInteractive({ draggable: true })
.on(Phaser.Input.Events.GAMEOBJECT_DRAG, (pointer, x, y) =>
{
r2.setPosition(x, y);
console.log(x, y);
})
.on(Phaser.Input.Events.GAMEOBJECT_DRAG_END, (pointer, x, y) =>
{
const localX = r2.x + x;
const localY = r2.y + y;
console.log(localX, localY);
r2.setPosition(localX, localY);
});
Здесь r2 создается с локальными координатами (0, 0) и добавляется в контейнер `c. В обработчикеDRAGобъекту присваиваются мировые координаты указателя, что заставляет его "прыгать" в мировую позицию мыши, игнорируя контейнер. В обработчикеDRAG_END` разработчик пытается исправить это, складывая текущую локальную позицию объекта с мировой позицией мыши, что является некорректной операцией между двумя разными системами координат.
Решение: преобразование мировых координат в локальные
Для корректной работы необходимо преобразовать мировые координаты, полученные в событии, в локальные координаты относительно родительского контейнера. В Phaser для этого у класса Container есть метод pointToContainer().
Давайте перепишем обработчик события GAMEOBJECT_DRAG:
.on(Phaser.Input.Events.GAMEOBJECT_DRAG, (pointer, dragX, dragY) =>
{
// Преобразуем мировые координаты dragX/dragY в локальные относительно контейнера 'c'
const localPoint = c.pointToContainer(dragX, dragY);
// Устанавливаем прямоугольнику преобразованные локальные координаты
r2.setPosition(localPoint.x, localPoint.y);
})
Теперь, при перетаскивании, r2 будет плавно следовать за курсором, оставаясь корректно вложенным в систему координат своего контейнера.
Для события GAMEOBJECT_DRAG_END логика будет аналогичной. После окончания перетаскивания мы хотим зафиксировать объект в той позиции, где его отпустили, но в локальных координатах контейнера.
.on(Phaser.Input.Events.GAMEOBJECT_DRAG_END, (pointer, dragX, dragY) =>
{
const localPoint = c.pointToContainer(dragX, dragY);
r2.setPosition(localPoint.x, localPoint.y);
});
Этот подход гарантирует, что позиция объекта всегда будет согласована с иерархией сцены.
Альтернатива: использование pointer в событии DRAG
Обратите внимание, что в события перетаскивания также передается сам объект pointer. У него есть свойство worldX / worldY, но для нашей задачи они эквивалентны параметрам `xиy. Однако, если вам нужны дополнительные данные о состоянии ввода, доступ кpointer` может быть полезен.
Также стоит помнить о методе setInteractive(). В примере используется опция { draggable: true }, которая и включает механизм перетаскивания, генерирующий события GAMEOBJECT_DRAG.
.setInteractive({ draggable: true })
Без этой опции события перетаскивания не будут генерироваться, даже если на объекте слушаются соответствующие события.
Что попробовать дальше
Ключевой вывод: при работе с перетаскиванием объектов внутри контейнеров в Phaser всегда учитывайте разницу между мировыми и локальными системами координат. Используйте метод контейнера pointToContainer() для корректного преобразования позиции указателя.
Для экспериментов попробуйте:
1. Создать более сложную иерархию с вложенными контейнерами и реализовать перетаскивание объектов на разных уровнях.
2. Ограничить область перетаскивания (dragDistance, dropZone) и применить преобразование координат для этих ограничений.
3. Реализовать "привязку" (snap) объекта к сетке после перетаскивания, выполняя расчеты в локальных координатах контейнера.
