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

Механика drag-and-drop — частый элемент игрового интерфейса: от инвентаря до пазлов. Стандартные прямоугольные зоны сброса не всегда подходят, особенно для игр, где важна точность или визуальная стилизация. В этой статье мы разберем, как создать и визуализировать круглую зону сброса (Circular Drop Zone) в Phaser 3, используя встроенные методы. Вы научитесь обрабатывать события перетаскивания, менять визуальное отображение зоны при наведении и корректно размещать объекты в её центре.

Версия 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.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 positioned at 600x300 with a circular drop zone 128px in radius
        const zone = this.add.zone(600, 300).setCircleDropZone(128);

        //  Just a visual display of the drop zone
        const graphics = this.add.graphics();

        graphics.lineStyle(2, 0xffff00);

        graphics.strokeCircle(zone.x, zone.y, zone.input.hitArea.radius);

        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) =>
        {

            graphics.clear();
            graphics.lineStyle(2, 0x00ffff);
            graphics.strokeCircle(zone.x, zone.y, zone.input.hitArea.radius);

        });

        this.input.on('dragleave', (pointer, gameObject, dropZone) =>
        {

            graphics.clear();
            graphics.lineStyle(2, 0xffff00);
            graphics.strokeCircle(zone.x, zone.y, zone.input.hitArea.radius);

        });

        this.input.on('drop', (pointer, gameObject, dropZone) =>
        {

            gameObject.x = dropZone.x;
            gameObject.y = dropZone.y;

            gameObject.input.enabled = false;

        });

        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 загружается атлас с изображениями карт, который будет использоваться для создания объектов.

В методе create сначала создается набор перетаскиваемых карт. Для этого из текстуры 'cards' получаются имена всех кадров (фреймов). Затем в цикле создаются 64 изображения, которые располагаются вертикально с небольшим смещением.

Каждое изображение делается интерактивным с помощью .setInteractive(), а затем регистрируется как перетаскиваемое в системе ввода.

const image = this.add.image(x, y, 'cards', Phaser.Math.RND.pick(frames)).setInteractive();
this.input.setDraggable(image);

Создание и визуализация круглой зоны сброса

Зона сброса — это невидимый игровой объект, который может принимать события ввода. Создается он с помощью this.add.zone(x, y). Чтобы задать ему круглую область для взаимодействия, используется метод .setCircleDropZone(radius). В этом примере зона создается в точке (600, 300) с радиусом 128 пикселей.

Чтобы игрок видел область взаимодействия, поверх зоны рисуется круг с помощью графики (Graphics). Рисуется контур желтого цвета, используя координаты и радиус хит-зоны самой зоны.

const zone = this.add.zone(600, 300).setCircleDropZone(128);
const graphics = this.add.graphics();
graphics.lineStyle(2, 0xffff00);
graphics.strokeCircle(zone.x, zone.y, zone.input.hitArea.radius);

Обработка событий перетаскивания: начало и движение

Phaser предоставляет систему событий для управления drag-and-drop. Событие dragstart срабатывает в момент начала перетаскивания объекта. В обработчике объект перемещается на верхний слой отображения, чтобы он был поверх других карт при движении.

Событие drag вызывается непрерывно при перемещении мыши или касании. В обработчике координаты перетаскиваемого объекта (gameObject) обновляются на переданные координаты (dragX, dragY).

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;
});

Визуальная обратная связь при взаимодействии с зоной

Ключевые события для обратной связи — dragenter и dragleave. Они срабатывают, когда перетаскиваемый объект пересекает границу хит-зоны и покидает её.

В обработчике dragenter графический круг перерисовывается, но уже цветом 0x00ffff (голубой), сигнализируя, что объект теперь над зоной и может быть сброшен.

В обработчике dragleave круг снова отрисовывается исходным желтым цветом, показывая, что объект покинул зону.

this.input.on('dragenter', (pointer, gameObject, dropZone) => {
    graphics.clear();
    graphics.lineStyle(2, 0x00ffff);
    graphics.strokeCircle(zone.x, zone.y, zone.input.hitArea.radius);
});

this.input.on('dragleave', (pointer, gameObject, dropZone) => {
    graphics.clear();
    graphics.lineStyle(2, 0xffff00);
    graphics.strokeCircle(zone.x, zone.y, zone.input.hitArea.radius);
});

Завершение перетаскивания: сброс и отмена

Событие drop происходит, когда пользователь отпускает кнопку мыши, и объект находится над зоной сброса. В этом случае объект перемещается в центр зоны (координаты dropZone), а его интерактивность отключается, фиксируя его на новом месте.

Событие dragend срабатывает всегда по окончании перетаскивания. Параметр dropped указывает, было ли событие drop. Если объект был отпущен вне зоны (!dropped), он возвращается на свои исходные координаты, которые были сохранены системой ввода в dragStartX и dragStartY.

this.input.on('drop', (pointer, gameObject, dropZone) => {
    gameObject.x = dropZone.x;
    gameObject.y = dropZone.y;
    gameObject.input.enabled = false;
});

this.input.on('dragend', (pointer, gameObject, dropped) => {
    if (!dropped) {
        gameObject.x = gameObject.input.dragStartX;
        gameObject.y = gameObject.input.dragStartY;
    }
});

Что попробовать дальше

Используя Phaser.GameObjects.Zone с методом setCircleDropZone, можно легко создавать нестандартные области для сброса объектов. Этот подход идеально подходит для игр с механикой точного попадания, кастомизации или головоломок. Для экспериментов попробуйте: создать несколько зон разной формы (используя setRectangleDropZone), реализовать систему очков за сброс в нужную зону или добавить анимацию (например, пульсацию) для графического отображения зоны.