О чем этот пример
Механика drag-and-drop — один из столпов геймдизайна для пазлов, инвентарей и редакторов уровней. Однако без точного позиционирования она быстро становится неудобной. В этой статье мы разберем пример из официальной коллекции Phaser, показывающий, как реализовать перетаскивание объектов с автоматической привязкой к сетке. Это позволит создавать аккуратные интерфейсы и точные игровые механики, например, расстановку предметов по ячейкам. Мы не только скопируем код, но и глубоко разберем его работу: как активировать перетаскивание, как использовать встроенную математику Phaser для привязки к сетке и как обрабатывать корректное 'бросание' объекта в целевую зону. Это практическое руководство поможет вам внедрить эту фичу в свои проекты.
Версия 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.image('bg', 'assets/skies/deepblue.png');
this.load.image('target', 'assets/sprites/brush1.png');
this.load.spritesheet('blocks', 'assets/sprites/heartstar.png', { frameWidth: 64, frameHeight: 64 });
}
create ()
{
this.add.image(400, 300, 'bg');
this.add.text(16, 16, 'Snap to Grid on Drag').setFontSize(24).setShadow(1, 1);
// Create some 'drop zones'
this.add.image(640, 192, 'target').setOrigin(0, 0);
this.add.image(640, 320, 'target').setOrigin(0, 0);
this.add.image(640, 448, 'target').setOrigin(0, 0);
// The blocks we can drag
const block1 = this.add.sprite(64, 192, 'blocks', 1).setOrigin(0, 0);
const block2 = this.add.sprite(64, 320, 'blocks', 1).setOrigin(0, 0);
const block3 = this.add.sprite(64, 448, 'blocks', 1).setOrigin(0, 0);
block1.setInteractive({ draggable: true });
block2.setInteractive({ draggable: true });
block3.setInteractive({ draggable: true });
let over1 = false;
let over2 = false;
let over3 = false;
this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
// This will snap our drag to a 64x64 grid
dragX = Phaser.Math.Snap.To(dragX, 64);
dragY = Phaser.Math.Snap.To(dragY, 64);
gameObject.setPosition(dragX, dragY);
});
// The following code just checks to see if the gameObject is over
// a zone when the drag ends and if so, we change frame and disable it
this.input.on('dragend', (pointer, gameObject) => {
const x = gameObject.x;
const y = gameObject.y;
if (x === 640 && y === 192 && !over1)
{
over1 = true;
gameObject.setFrame(0);
gameObject.disableInteractive();
}
else if (x === 640 && y === 320 && !over2)
{
over2 = true;
gameObject.setFrame(0);
gameObject.disableInteractive();
}
else if (x === 640 && y === 448 && !over3)
{
over3 = true;
gameObject.setFrame(0);
gameObject.disableInteractive();
}
});
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и создание объектов
В методе preload загружаются необходимые ресурсы: фон, спрайт для целевых зон (target) и спрайтшит с блоками. Обратите внимание, что спрайтшит 'blocks' загружается с указанием размера кадра 64x64 пикселя.
В методе create мы размещаем фон и заголовок. Затем создаются три целевые зоны (drop zones) — это спрайты изображения 'target', выровненные по вертикали. Они будут служить местами, куда можно перетащить блоки.
Далее создаются три блока из спрайтшита 'blocks', использующие кадр с индексом 1. Для каждого блока методом setInteractive({ draggable: true }) активируется возможность перетаскивания. Это ключевой шаг, без которого события drag не будут срабатывать.
// Создание блоков и активация drag
const block1 = this.add.sprite(64, 192, 'blocks', 1).setOrigin(0, 0);
block1.setInteractive({ draggable: true });
Событие drag и привязка к сетке
Сердце механики — обработка события 'drag'. Это событие срабатывает непрерывно при перемещении мыши или касания, пока объект удерживается.
Обработчик получает несколько параметров: pointer (указатель), gameObject (перетаскиваемый объект), dragX и dragY (текущие координаты указателя).
Здесь происходит магия привязки к сетке. Мы не просто назначаем объекту координаты указателя. Вместо этого, координаты dragX и dragY пропускаются через утилиту Phaser.Math.Snap.To. Эта функция округляет переданное значение до ближайшего кратного заданному шагу (в нашем случае — 64). Таким образом, объект будет 'прыгать' от одной ячейки сетки 64x64 к другой.
После вычисления новых координат объект перемещается методом setPosition.
this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
// Привязка координат к сетке 64x64
dragX = Phaser.Math.Snap.To(dragX, 64);
dragY = Phaser.Math.Snap.To(dragY, 64);
gameObject.setPosition(dragX, dragY);
});
Фиксация объекта в целевой зоне
Перетаскивание должно где-то заканчиваться. Для этого используется событие 'dragend', которое срабатывает, когда пользователь отпускает кнопку мыши или палец.
В этом обработчике проверяется, совпадают ли текущие координаты блока (gameObject.x, gameObject.y) с координатами одной из трех целевых зон. Важное условие — проверка флагов (over1, over2, over3). Это гарантирует, что каждая зона может принять только один блок.
Если проверка пройдена:
1. Устанавливается соответствующий флаг в true.
2. У блока меняется кадр спрайтшита на `0с помощьюsetFrame(0)`. Это визуально показывает успешное размещение (например, блок меняет цвет или форму).
3. Для блока вызывается disableInteractive(), что отключает дальнейшее перетаскивание. Блок теперь зафиксирован на месте.
this.input.on('dragend', (pointer, gameObject) => {
const x = gameObject.x;
const y = gameObject.y;
if (x === 640 && y === 192 && !over1) {
over1 = true;
gameObject.setFrame(0);
gameObject.disableInteractive();
}
// ... аналогичные проверки для других зон
});
Настройка игры и запуск
Конфигурация игры (config) стандартна: указывается тип рендерера, размеры холста, родительский HTML-элемент и класс сцены. После этого создается экземпляр игры new Phaser.Game(config), который запускает весь описанный цикл.
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Что попробовать дальше
Мы разобрали готовое решение для перетаскивания с привязкой к сетке в Phaser. Основные компоненты: активация draggable, обработка событий drag и dragend, использование Phaser.Math.Snap.To для точного позиционирования и управление состоянием объектов через disableInteractive.
Идеи для экспериментов:
1. **Динамическая сетка:** Сделайте шаг сетки переменным, например, зависящим от размера объекта или уровня игры.
2. **Визуальная сетка:** Нарисуйте линии сетки на фоне для лучшей обратной связи с игроком.
3. **Сложные зоны:** Вместо точечной проверки координат (x === 640) используйте прямоугольные области (Phaser.Geom.Rectangle.Contains), чтобы зоны были больше и удобнее.
4. **Обратная связь:** Добавьте звуковые эффекты или анимацию (например, плавное 'примагничивание') в момент dragend, если объект находится рядом с зоной.
