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

При разработке игр с перетаскиваемыми элементами, например, карточной игры или пазла, часто возникает задача: объект, который вы "взяли", должен временно оказаться поверх всех остальных для визуальной ясности, но после отпускания — вернуться на свою исходную позицию в иерархии отображения. Прямой вызов `bringToTop` ломает порядок объектов навсегда. В этой статье разберем элегантное решение на Phaser 3, которое использует индекс внутри списка детей сцены для сохранения и восстановления позиции.

Версия 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.spritesheet('cards', 'assets/sprites/cards80x112.png', { frameWidth: 80, frameHeight: 112 });
    }

    create ()
    {
        const card1 = this.add.sprite(200, 300, 'cards', 1).setInteractive();
        const card2 = this.add.sprite(300, 300, 'cards', 2).setInteractive();
        const card3 = this.add.sprite(400, 300, 'cards', 3).setInteractive();
        const card4 = this.add.sprite(500, 300, 'cards', 4).setInteractive();
        const card5 = this.add.sprite(600, 300, 'cards', 5).setInteractive();

        this.input.setDraggable([ card1, card2, card3, card4, card5 ]);

        let index = 0;

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

            index = this.children.getIndex(gameObject);

            this.children.bringToTop(gameObject);

        });

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

            gameObject.x = dragX;
            gameObject.y = dragY;

        });

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

            this.children.moveTo(gameObject, index);

        });
    }
}

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

const game = new Phaser.Game(config);

Проблема "всплывающего" объекта

Когда игрок начинает перетаскивание, объект логично поднять на самый верхний слой, чтобы его было хорошо видно, и он не перекрывался другими элементами. Но если просто использовать this.children.bringToTop(gameObject), объект навсегда останется сверху, даже после окончания перетаскивания. Это может испортить запланированный порядок отрисовки, например, если у вас есть слои с фоном, игровыми картами и интерфейсом.

Нам нужен механизм, который запоминает, откуда мы подняли объект, и возвращает его обратно после завершения взаимодействия.

Запоминаем исходный индекс

Ключ к решению — работа со списком детей сцены (this.children). Каждый дочерний объект (спрайт, изображение, текст) имеет свой индекс в этом списке, который определяет порядок отрисовки: объекты с меньшим индексом рисуются первыми (оказываются "ниже").

В обработчике события dragstart мы получаем текущий индекс перетаскиваемого объекта и сохраняем его во внешнюю переменную. Только после этого поднимаем объект наверх.

let index = 0;

this.input.on('dragstart', (pointer, gameObject) => {
    // Получаем текущий индекс объекта в списке детей
    index = this.children.getIndex(gameObject);
    // Временно поднимаем его на самый верхний слой
    this.children.bringToTop(gameObject);
});

Переменная index объявлена во внешней области видимости, чтобы быть доступной и в других обработчиках событий.

Перетаскивание и завершение

Процесс самого перетаскивания (drag) стандартен: мы просто обновляем координаты `xиyобъекта, используя значения, которые автоматически предоставляет Phaser (dragX,dragY`).

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

Магия происходит в событии dragend. Когда игрок отпускает кнопку мыши, мы используем сохраненный ранее индекс, чтобы вернуть объект на его исходную позицию в списке отрисовки с помощью метода moveTo.

this.input.on('dragend', (pointer, gameObject) => {
    // Возвращаем объект на сохраненную позицию в списке детей
    this.children.moveTo(gameObject, index);
});

Метод this.children.moveTo(gameObject, index) физически перемещает объект внутри внутреннего массива детей на указанную позицию, эффективно восстанавливая первоначальный порядок отображения.

Полная картина: настройка сцены

Давайте соберем все вместе и посмотрим на код создания сцены. Важно не забыть сделать объекты перетаскиваемыми с помощью this.input.setDraggable() и загрузить необходимый спрайтшит.

create ()
{
    // Создаем несколько спрайтов-карт
    const card1 = this.add.sprite(200, 300, 'cards', 1).setInteractive();
    const card2 = this.add.sprite(300, 300, 'cards', 2).setInteractive();
    const card3 = this.add.sprite(400, 300, 'cards', 3).setInteractive();
    const card4 = this.add.sprite(500, 300, 'cards', 4).setInteractive();
    const card5 = this.add.sprite(600, 300, 'cards', 5).setInteractive();

    // Делаем все карты перетаскиваемыми
    this.input.setDraggable([ card1, card2, card3, card4, card5 ]);

    // ... обработчики событий dragstart, drag, dragend ...
}

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

Используя связку методов getIndex, bringToTop и moveTo из системы отображения Phaser, мы реализовали интуитивно понятное перетаскивание с сохранением глубины. Это фундаментальный паттерн для многих игровых механик. **Идеи для экспериментов:** 1. Вместо глобальной переменной index сохраняйте исходный индекс в пользовательское свойство самого gameObject (например, gameObject.originalIndex). 2. Реализуйте "стопку" карт: при отпускании карты над другой картой, вставляйте ее в список детей не на старую позицию, а на позицию, следующую за картой, на которую ее положили. 3. Добавьте визуальную обратную связь: слегка увеличивайте карту при dragstart и возвращайте к исходному размеру при dragend.