О чем этот пример
При разработке игр с перетаскиваемыми элементами, например, карточной игры или пазла, часто возникает задача: объект, который вы "взяли", должен временно оказаться поверх всех остальных для визуальной ясности, но после отпускания — вернуться на свою исходную позицию в иерархии отображения. Прямой вызов `bringToTop` ломает порядок объектов навсегда. В этой статье разберем элегантное решение на Phaser 3, которое использует индекс внутри списка детей сцены для сохранения и восстановления позиции.
Версия 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.spritesheet('cards', 'assets/sprites/cards80x112.png', { frameWidth: 80, frameHeight: 112 });
}
create ()
{
const card1 = this.add.sprite(200, 300, 'cards', 1).setInteractive();
const card2 = this.add.sprite(300, 300, 'cards', 2).setInteractive();
const card3 = this.add.sprite(400, 300, 'cards', 3).setInteractive();
const card4 = this.add.sprite(500, 300, 'cards', 4).setInteractive();
const card5 = this.add.sprite(600, 300, 'cards', 5).setInteractive();
this.input.setDraggable([ card1, card2, card3, card4, card5 ]);
let index = 0;
this.input.on('dragstart', (pointer, gameObject) => {
index = this.children.getIndex(gameObject);
this.children.bringToTop(gameObject);
});
this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
gameObject.x = dragX;
gameObject.y = dragY;
});
this.input.on('dragend', (pointer, gameObject) => {
this.children.moveTo(gameObject, index);
});
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Проблема "всплывающего" объекта
Когда игрок начинает перетаскивание, объект логично поднять на самый верхний слой, чтобы его было хорошо видно, и он не перекрывался другими элементами. Но если просто использовать this.children.bringToTop(gameObject), объект навсегда останется сверху, даже после окончания перетаскивания. Это может испортить запланированный порядок отрисовки, например, если у вас есть слои с фоном, игровыми картами и интерфейсом.
Нам нужен механизм, который запоминает, откуда мы подняли объект, и возвращает его обратно после завершения взаимодействия.
Запоминаем исходный индекс
Ключ к решению — работа со списком детей сцены (this.children). Каждый дочерний объект (спрайт, изображение, текст) имеет свой индекс в этом списке, который определяет порядок отрисовки: объекты с меньшим индексом рисуются первыми (оказываются "ниже").
В обработчике события dragstart мы получаем текущий индекс перетаскиваемого объекта и сохраняем его во внешнюю переменную. Только после этого поднимаем объект наверх.
let index = 0;
this.input.on('dragstart', (pointer, gameObject) => {
// Получаем текущий индекс объекта в списке детей
index = this.children.getIndex(gameObject);
// Временно поднимаем его на самый верхний слой
this.children.bringToTop(gameObject);
});
Переменная index объявлена во внешней области видимости, чтобы быть доступной и в других обработчиках событий.
Перетаскивание и завершение
Процесс самого перетаскивания (drag) стандартен: мы просто обновляем координаты `xиyобъекта, используя значения, которые автоматически предоставляет Phaser (dragX,dragY`).
this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
gameObject.x = dragX;
gameObject.y = dragY;
});
Магия происходит в событии dragend. Когда игрок отпускает кнопку мыши, мы используем сохраненный ранее индекс, чтобы вернуть объект на его исходную позицию в списке отрисовки с помощью метода moveTo.
this.input.on('dragend', (pointer, gameObject) => {
// Возвращаем объект на сохраненную позицию в списке детей
this.children.moveTo(gameObject, index);
});
Метод this.children.moveTo(gameObject, index) физически перемещает объект внутри внутреннего массива детей на указанную позицию, эффективно восстанавливая первоначальный порядок отображения.
Полная картина: настройка сцены
Давайте соберем все вместе и посмотрим на код создания сцены. Важно не забыть сделать объекты перетаскиваемыми с помощью this.input.setDraggable() и загрузить необходимый спрайтшит.
create ()
{
// Создаем несколько спрайтов-карт
const card1 = this.add.sprite(200, 300, 'cards', 1).setInteractive();
const card2 = this.add.sprite(300, 300, 'cards', 2).setInteractive();
const card3 = this.add.sprite(400, 300, 'cards', 3).setInteractive();
const card4 = this.add.sprite(500, 300, 'cards', 4).setInteractive();
const card5 = this.add.sprite(600, 300, 'cards', 5).setInteractive();
// Делаем все карты перетаскиваемыми
this.input.setDraggable([ card1, card2, card3, card4, card5 ]);
// ... обработчики событий dragstart, drag, dragend ...
}
Что попробовать дальше
Используя связку методов getIndex, bringToTop и moveTo из системы отображения Phaser, мы реализовали интуитивно понятное перетаскивание с сохранением глубины. Это фундаментальный паттерн для многих игровых механик.
**Идеи для экспериментов:**
1. Вместо глобальной переменной index сохраняйте исходный индекс в пользовательское свойство самого gameObject (например, gameObject.originalIndex).
2. Реализуйте "стопку" карт: при отпускании карты над другой картой, вставляйте ее в список детей не на старую позицию, а на позицию, следующую за картой, на которую ее положили.
3. Добавьте визуальную обратную связь: слегка увеличивайте карту при dragstart и возвращайте к исходному размеру при dragend.
