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

Механика перетаскивания и бросания объектов — основа для множества игровых жанров: от пазлов и карточных игр до инвентарей и редакторов уровней. В этом примере Phaser мы разберем, как создать визуальную drop-зону, реализовать плавное перетаскивание карт и добавить понятный визуальный фидбек при взаимодействии с зоной. Вы научитесь обрабатывать события `dragstart`, `drag`, `dragenter`, `dragleave`, `drop` и `dragend`, что позволит создавать сложные и отзывчивые интерфейсы.

Версия 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
        const zone = this.add.zone(500, 300, 300, 300).setRectangleDropZone(300, 300);

        //  Just a visual display of the drop zone
        const graphics = this.add.graphics();
        graphics.lineStyle(2, 0xffff00);
        graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);

        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.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);

        });

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

            graphics.clear();
            graphics.lineStyle(2, 0xffff00);
            graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);

        });

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

            graphics.clear();
            graphics.lineStyle(2, 0xffff00);
            graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);

        });

    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и создание перетаскиваемых объектов

Первым делом в методе preload загружается атлас спрайтов с картами. В create мы получаем список всех кадров (фреймов) из этого атласа, чтобы случайным образом выбирать из них.

Создается стопка из 64 карт. Каждая карта — это объект Image, которому сразу назначается интерактивность через setInteractive(). Метод this.input.setDraggable(image) сообщает системе ввода, что этот объект теперь можно перетаскивать. Карты располагаются вертикально со смещением, создавая эффект стопки.

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

Создание и визуализация Drop Zone

Drop Zone (зона сброса) — это невидимая область, которая может принимать перетаскиваемые объекты. Она создается с помощью this.add.zone(), которой задаются координаты и размеры. Ключевой метод setRectangleDropZone() определяет прямоугольную область, которая будет реагировать на события перетаскивания.

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

const zone = this.add.zone(500, 300, 300, 300).setRectangleDropZone(300, 300);

const graphics = this.add.graphics();
graphics.lineStyle(2, 0xffff00);
graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);

Важно: zone.input.hitArea содержит размеры, заданные в setRectangleDropZone. Отрисовка прямоугольника центрируется относительно координат зоны (zone.x, zone.y).

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

Phaser генерирует события на каждом этапе перетаскивания. Обработчики назначаются через this.input.on().

Событие dragstart срабатывает в момент начала перетаскивания. В примере оно используется, чтобы поднять перетаскиваемый объект (gameObject) на верхний слой отображения, чтобы он не скрывался под другими картами.

this.input.on('dragstart', function (pointer, gameObject) {
    this.children.bringToTop(gameObject);
}, this);

Событие drag происходит постоянно, пока объект перемещается. Здесь мы просто обновляем координаты объекта на те, которые предоставляет система ввода (dragX, dragY). Это обеспечивает плавное движение объекта за курсором.

this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
    gameObject.x = dragX;
    gameObject.y = dragY;
});

Визуальный фидбек при взаимодействии с Drop Zone

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

this.input.on('dragenter', (pointer, gameObject, dropZone) => {
    graphics.clear();
    graphics.lineStyle(2, 0x00ffff);
    graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);
});

this.input.on('dragleave', (pointer, gameObject, dropZone) => {
    graphics.clear();
    graphics.lineStyle(2, 0xffff00);
    graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);
});

Обратите внимание: перед отрисовкой нового контура вызывается graphics.clear(), чтобы стереть предыдущий.

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

Логика завершения перетаскивания разделена между двумя событиями: drop и dragend.

Событие drop происходит, если объект был отпущен именно над drop-зоной (параметр dropped в dragend будет true). В этом примере объект "прилипает" к центру зоны, а его интерактивность отключается (gameObject.input.enabled = false), чтобы его нельзя было перетащить снова.

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

Событие dragend срабатывает всегда, когда игрок отпускает объект, независимо от того, был ли он сброшен в зону. Если сброса не произошло (!dropped), объект возвращается на свою исходную позицию, которая хранится в gameObject.input.dragStartX/Y. Также здесь восстанавливается стандартный желтый контур зоны.

this.input.on('dragend', (pointer, gameObject, dropped) => {
    if (!dropped) {
        gameObject.x = gameObject.input.dragStartX;
        gameObject.y = gameObject.input.dragStartY;
    }
    graphics.clear();
    graphics.lineStyle(2, 0xffff00);
    graphics.strokeRect(zone.x - zone.input.hitArea.width / 2, zone.y - zone.input.hitArea.height / 2, zone.input.hitArea.width, zone.input.hitArea.height);
});

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

Вы реализовали полноценную систему перетаскивания с визуальной drop-зоной и обратной связью. Этот паттерн — основа для более сложных механик. Для экспериментов попробуйте

  1. Сделать зону круглой с помощью setCircleDropZone
  2. Добавить физическое тело объекту и бросать его в зону с учетом физики мира
  3. Реализовать сортировку объектов — разные зоны для разных типов карт
  4. Добавить звуковые эффекты для событий dragenter и drop