О чем этот пример

Механика 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, если объект находится рядом с зоной.