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

При разработке карточных или настольных игр часто возникает задача управлять объектами, которые перекрывают друг друга. Как обработать клик именно по верхней карте в стопке и красиво её переместить? Пример из официальной документации Phaser демонстрирует элегантное решение с использованием событий ввода и твинов. Этот подход полезен не только для карточных игр, но и для любых интерфейсов, где игровые объекты (спрайты, кнопки, плитки) могут накладываться друг на друга. Вы научитесь правильно настраивать интерактивность и создавать плавные, составные анимации по клику.

Версия 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();

        let x = 100;
        let y = 100;

        for (let i = 0; i < 64; i++)
        {
            this.add.image(x, y, 'cards', Phaser.Math.RND.pick(frames)).setInteractive();

            x += 4;
            y += 4;
        }

        this.input.on('gameobjectdown', function (pointer, gameObject)
        {

            //  Will contain the top-most Game Object (in the display list)
            this.tweens.add({
                targets: gameObject,
                x: { value: 1100, duration: 1500, ease: 'Power2' },
                y: { value: 500, duration: 500, ease: 'Bounce.easeOut', delay: 150 }
            });

        }, this);
    }
}

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

const game = new Phaser.Game(config);

Подготовка атласа и создание стопки

В методе preload загружается атлас cards, который содержит все изображения карт в одном файле PNG и данные о кадрах в JSON. Это эффективнее, чем загружать каждую карту отдельно.

В create сначала получаем список всех имен кадров (фреймов) из текстуры cards. Эти имена понадобятся для случайного выбора карты.

const frames = this.textures.get('cards').getFrameNames();

Затем в цикле создается стопка из 64 карт. Каждая следующая карта смещается на 4 пикселя по осям X и Y относительно предыдущей, создавая эффект разложенной веером колоды. Ключевой момент — каждой карте сразу назначается интерактивность с помощью метода .setInteractive(). Без этого карты не будут генерировать события ввода (клики).

this.add.image(x, y, 'cards', Phaser.Math.RND.pick(frames)).setInteractive();
x += 4;
y += 4;

Обработка клика по верхнему объекту

Самая важная часть логики — обработчик события gameobjectdown. Это событие генерируется, когда указатель (мышь или касание) нажимает на интерактивный игровой объект (gameObject).

Система ввода Phaser автоматически определяет, какой объект находится *визуально сверху* (выше в порядке отрисовки) в точке клика, и передает именно его в функцию-обработчик. Это избавляет нас от необходимости вручную вычислять порядок и проверять пересечения.

this.input.on('gameobjectdown', function (pointer, gameObject) {
    // gameObject здесь — это верхняя карта в стопке, на которую кликнули
}, this);

Обратите внимание на третий аргумент this. Он задает контекст, в котором будет выполняться функция-обработчик. В данном случае это гарантирует, что внутри обработчика this будет ссылаться на текущую сцену (Example), что необходимо для доступа к менеджеру твинов this.tweens.

Создание составной анимации с помощью твинов

Когда карта выбрана, она анимируется с помощью системы твинов. В примере используется один твин с двумя анимируемыми свойствами (`xиy), но для каждого свойства задана своя длительность и функция плавности (ease`). Это создает эффект составного движения.

this.tweens.add({
    targets: gameObject,
    x: { value: 1100, duration: 1500, ease: 'Power2' },
    y: { value: 500, duration: 500, ease: 'Bounce.easeOut', delay: 150 }
});

* targets: объект для анимации (наша карта gameObject). * `x: карта плавно перемещается в точку X=1100 за 1500 мс с ускорениемPower2`. * `y: движение по оси Y начинается с задержкой (delay) в 150 мс, длится всего 500 мс и завершается эффектом отскока (Bounce.easeOut`).

Такое разделение параметров делает движение более живым и "игровым", чем просто линейное перемещение из одной точки в другую.

Конфигурация игры и запуск сцены

Код завершается стандартной конфигурацией игры (config) и её созданием. В конфиге указывается тип рендерера, элемент-контейнер, размеры холста и стартовая сцена.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 1024,
    height: 600,
    scene: Example
};
const game = new Phaser.Game(config);

Это базовая структура любого проекта на Phaser. Сцена Example, описанная выше, автоматически запускается при старте игры, вызывая по очереди свои методы preload, create, а затем update. В данном примера логика содержится только в preload и create.

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

Пример наглядно показывает два мощных принципа Phaser: автоматическое определение верхнего объекта при клике и простоту создания сложных анимаций через твины. Для экспериментов попробуйте изменить логику в обработчике: например, не перемещайте карту, а переворачивайте её (setFlipX), отправляйте "в сброс" или меняйте её глубину (depth), чтобы тапнуть следующую карту в стопке. Также можно поиграть с другими событиями, такими как gameobjectup или gameobjectover, чтобы добавить эффекты наведения.