О чем этот пример
Система перетаскивания объектов — ключевой элемент для множества игровых механик: от инвентаря и пазлов до карточных игр. В Phaser это реализуется с помощью встроенного менеджера ввода, который избавляет разработчика от ручной обработки событий мыши или касаний. Эта статья покажет, как создать интерактивную стопку карт, которую можно перетаскивать, и визуальную зону, где карты можно "сбрасывать", меняя их состояние. Вы научитесь навешивать обработчики на все этапы drag-and-drop, создавать визуальный фидбек и управлять порядком отрисовки объектов.
Версия 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('ayu', 'assets/pics/ayu2.png');
this.load.atlas('cards', 'assets/atlas/cards.png', 'assets/atlas/cards.json');
}
create ()
{
// Create a stack of random cards
const frames = this.textures.get('cards').getFrameNames();
const x = 100;
let y = 100;
for (let i = 0; i < 64; i++)
{
const image = this.add.image(x, y, 'cards', Phaser.Math.RND.pick(frames)).setInteractive();
this.input.setDraggable(image);
y += 6;
}
// A drop zone
const zone = this.add.image(500, 300, 'ayu').setInteractive();
zone.input.dropZone = true;
this.input.on('dragstart', function (pointer, gameObject)
{
this.children.bringToTop(gameObject);
}, this);
this.input.on('drag', (pointer, gameObject, dragX, dragY) =>
{
gameObject.x = dragX;
gameObject.y = dragY;
});
this.input.on('dragenter', (pointer, gameObject, dropZone) =>
{
zone.setTint(0x00ff00);
});
this.input.on('dragleave', (pointer, gameObject, dropZone) =>
{
zone.clearTint();
});
this.input.on('drop', (pointer, gameObject, dropZone) =>
{
gameObject.x = dropZone.x;
gameObject.y = dropZone.y;
gameObject.setScale(0.2);
gameObject.input.enabled = false;
zone.clearTint();
});
this.input.on('dragend', (pointer, gameObject, dropped) =>
{
if (!dropped)
{
gameObject.x = gameObject.input.dragStartX;
gameObject.y = gameObject.input.dragStartY;
}
});
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1024,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и создание интерактивной стопки
В методе preload загружаются необходимые ресурсы: одно изображение для фона зоны и атлас с кадрами карт.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('ayu', 'assets/pics/ayu2.png');
this.load.atlas('cards', 'assets/atlas/cards.png', 'assets/atlas/cards.json');
}
В create сначала создается стопка из 64 карт. Для этого мы получаем все имена кадров из атласа cards и в цикле создаем изображения, смещая каждое следующее по оси Y, чтобы создать эффект стопки. Каждая карта сразу делается интерактивной с помощью setInteractive(), а this.input.setDraggable(image) помечает ее как перетаскиваемый объект для системы ввода Phaser.
const frames = this.textures.get('cards').getFrameNames();
const x = 100;
let y = 100;
for (let i = 0; i < 64; i++)
{
const image = this.add.image(x, y, 'cards', Phaser.Math.RND.pick(frames)).setInteractive();
this.input.setDraggable(image);
y += 6;
}
Создание и настройка дроп-зоны
Дроп-зона — это область, куда можно перетащить объект. В коде она представлена обычным изображением (ayu), которому также назначена интерактивность. Ключевой момент — установка специального флага dropZone в true для свойства input этого объекта. Именно по этому флагу система ввода Phaser понимает, что данный спрайт является целевой зоной для событий dragenter, dragleave и drop.
const zone = this.add.image(500, 300, 'ayu').setInteractive();
zone.input.dropZone = true;
Обработка начала и процесса перетаскивания
Событие dragstart срабатывает в момент начала перетаскивания объекта. Здесь используется this.children.bringToTop(gameObject), чтобы перетаскиваемая карта отобразилась поверх всех других дочерних объектов сцены. Это важно для визуального восприятия: карта, которую тащат, должна быть поверх стопки.
this.input.on('dragstart', function (pointer, gameObject)
{
this.children.bringToTop(gameObject);
}, this);
Событие drag возникает постоянно, пока объект перемещается. В его обработчике координаты перетаскиваемого объекта (gameObject) обновляются на переданные системой ввода координаты курсора (dragX, dragY). Это создает иллюзию, что карта "прилипла" к указателю мыши.
this.input.on('drag', (pointer, gameObject, dragX, dragY) =>
{
gameObject.x = dragX;
gameObject.y = dragY;
});
Визуальный фидбек при взаимодействии с зоной
Чтобы дать игроку понять, что он может бросить карту в зону, используется визуальная подсветка. Событие dragenter срабатывает, когда перетаскиваемый объект входит в границы дроп-зоны. В обработчике зона окрашивается в зеленый цвет с помощью setTint(0x00ff00).
this.input.on('dragenter', (pointer, gameObject, dropZone) =>
{
zone.setTint(0x00ff00);
});
Если объект покидает зону, срабатывает dragleave, и подсветка убирается методом clearTint().
this.input.on('dragleave', (pointer, gameObject, dropZone) =>
{
zone.clearTint();
});
Логика завершения перетаскивания: бросок и отмена
Событие drop — это момент, когда игрок отпускает кнопку мыши, и перетаскиваемый объект находится над дроп-зоной. В этом случае карта "притягивается" к центру зоны (dropZone.x, dropZone.y), уменьшается в размере (setScale(0.2)) и отключается для дальнейшего взаимодействия (gameObject.input.enabled = false). Подсветка зоны также сбрасывается.
this.input.on('drop', (pointer, gameObject, dropZone) =>
{
gameObject.x = dropZone.x;
gameObject.y = dropZone.y;
gameObject.setScale(0.2);
gameObject.input.enabled = false;
zone.clearTint();
});
Событие dragend срабатывает всегда, когда перетаскивание завершено, независимо от того, была ли это зона. Параметр dropped будет true, если сработал drop. Если карту бросили мимо зоны (!dropped), она возвращается на свою исходную позицию, которая была сохранена системой ввода в gameObject.input.dragStartX/Y.
this.input.on('dragend', (pointer, gameObject, dropped) =>
{
if (!dropped)
{
gameObject.x = gameObject.input.dragStartX;
gameObject.y = gameObject.input.dragStartY;
}
});
Что попробовать дальше
Вы реализовали полный цикл взаимодействия drag-and-drop с визуальной обратной связью и разной логикой для успешного и неудачного "броска". Этот паттерн — основа для создания инвентарей, интерфейсов или логических головоломок. Для экспериментов попробуйте
- Сделать зону невидимой (
setAlpha(0)), но оставить интерактивной - Реализовать несколько разных дроп-зон с уникальными эффектами (например, зона "уничтожения", удаляющая карту)
- Добавить звуковые эффекты для событий
dragstart,dropиdragend
