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

Создание отзывчивых игровых интерфейсов требует точной обработки взаимодействий с множеством элементов. В этой статье разберем, как эффективно управлять событиями нажатия и отпускания для десятков или сотен объектов на сцене. Вы узнаете, как использовать события `gameobjectdown` и `gameobjectup` для создания интерактивных сеток карт, а также как комбинировать их с общими событиями ввода для отладки и визуальной обратной связи.

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

        this.input.on('pointerdown', function (pointer)
        {

            console.log(this.game.getFrame(), 'pd', pointer.x, pointer.y);

            this.add.rectangle(pointer.x - 4, pointer.y - 4, 8, 8, 0xff00ff);

        }, this);

        this.input.on('pointerup', function (pointer)
        {

            this.add.rectangle(pointer.x - 4, pointer.y - 4, 8, 8, 0x00ff00);

        }, this);

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

            if (gameObject.alpha === 1)
            {
                this.tweens.add({
                    targets: gameObject,
                    alpha: 0,
                    scaleX: 0,
                    scaleY: 0
                });

                gameObject.disableInteractive();
            }

        }, this);

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

            if (gameObject.alpha === 1)
            {
                this.tweens.add({
                    targets: gameObject,
                    alpha: 0,
                    scaleX: 0,
                    scaleY: 0
                });

                gameObject.disableInteractive();
            }

        }, this);
    }

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

        for (let i = 0; i < 64; i++)
        {
            const x = Phaser.Math.Between(0, 800);
            const y = Phaser.Math.Between(0, 600);
            const s = Phaser.Math.FloatBetween(0.5, 1);

            this.add.image(x, y, 'cards', Phaser.Math.RND.pick(frames)).setScale(s).setInteractive();
        }
    }

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

        for (let y = 0; y < 8; y++)
        {
            for (let x = 0; x < 15; x++)
            {
                this.add.image(x * 56, y * 75, 'cards', Phaser.Math.RND.pick(frames)).setScale(0.4).setOrigin(0).setInteractive();
            }
        }
    }
}

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

const game = new Phaser.Game(config);

Подготовка ассетов и создание сетки объектов

В методе preload загружается атлас текстур с картами. Ключевой метод — createCards2, который создает упорядоченную сетку из 120 карт (15x8). Каждая карта — это интерактивный объект Image.

createCards2 ()
{
    const frames = this.textures.get('cards').getFrameNames();
    for (let y = 0; y < 8; y++)
    {
        for (let x = 0; x < 15; x++)
        {
            this.add.image(x * 56, y * 75, 'cards', Phaser.Math.RND.pick(frames)).setScale(0.4).setOrigin(0).setInteractive();
        }
    }
}

Циклы for расставляют карты в сетку с фиксированными интервалами (56px по X, 75px по Y). Phaser.Math.RND.pick(frames) случайно выбирает кадр из атласа для каждой карты. Вызов setInteractive() делает каждый объект Image чувствительным к вводу. setOrigin(0) устанавливает точку отсчета (origin) в левый верхний угол, что упрощает позиционирование сетки.

События ввода: общие и объектно-специфичные

Phaser разделяет события ввода на два типа: общие (на сцену) и объектно-специфичные. В примере настраиваются оба.

this.input.on('pointerdown', function (pointer) {
    console.log(this.game.getFrame(), 'pd', pointer.x, pointer.y);
    this.add.rectangle(pointer.x - 4, pointer.y - 4, 8, 8, 0xff00ff);
}, this);

Событие 'pointerdown' срабатывает при любом клике на канвасе. В обработчике выводится номер кадра и координаты в консоль, а также рисуется маленький розовый квадрат для визуальной отладки позиции клика. Контекст this передается третьим аргументом, чтобы внутри функции оставалась ссылка на сцену.

this.input.on('gameobjectdown', function (pointer, gameObject) {
    if (gameObject.alpha === 1) {
        this.tweens.add({
            targets: gameObject,
            alpha: 0,
            scaleX: 0,
            scaleY: 0
        });
        gameObject.disableInteractive();
    }
}, this);

Событие 'gameobjectdown' срабатывает только при клике на объект, у которого вызван setInteractive(). В обработчик передаются указатель pointer и сам объект gameObject. Если объект еще видим (alpha = 1), запускается твин, который одновременно скрывает и уменьшает его, а затем disableInteractive() отключает дальнейшие взаимодействия с этим объектом. Аналогичная логика реализована для события 'gameobjectup'.

Практическое применение и логика взаимодействия

Комбинация событий позволяет создавать сложное поведение. Например, 'pointerdown' и 'pointerup' можно использовать для системных действий или визуальных маркеров (зеленый квадрат при отпускании), в то время как 'gameobjectdown' и 'gameobjectup' отвечают за логику конкретных игровых элементов.

if (gameObject.alpha === 1) {
    this.tweens.add({
        targets: gameObject,
        alpha: 0,
        scaleX: 0,
        scaleY: 0
    });
    gameObject.disableInteractive();
}

Проверка alpha === 1 предотвращает повторную обработку клика на уже исчезающем объекте. Твин использует несколько свойств одновременно, создавая плавный эффект "схлопывания". После начала анимации объект сразу становится неинтерактивным, что предотвращает возможные конфликты ввода. Важно: события 'gameobjectdown' и 'gameobjectup' срабатывают независимо, поэтому в данном примере объект будет исчезать как при нажатии, так и при отпускании кнопки мыши на нем.

Отладка и визуализация событий ввода

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

this.add.rectangle(pointer.x - 4, pointer.y - 4, 8, 8, 0xff00ff);

Этот код создает розовый прямоугольник в точке нажатия. Смещение на -4 пикселя центрирует квадрат относительно курсора. Аналогичный зеленый квадрат создается при событии 'pointerup'. Эти маркеры помогают увидеть, где именно произошло событие и как оно соотносится с позициями интерактивных объектов.

console.log(this.game.getFrame(), 'pd', pointer.x, pointer.y);

Логирование номера кадра (this.game.getFrame()) позволяет анализировать временные аспекты ввода, например, задержки между событиями. В реальном проекте такую отладочную визуализацию можно включать флагом в конфигурации.

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

Обработка множественных кликов через события gameobjectdown и gameobjectup — мощный механизм Phaser для создания сложных интерактивных сцен. Для экспериментов попробуйте: изменить логику, чтобы объекты исчезали только при 'gameobjectup'; добавить разные реакции на левый и правый клик (проверяя pointer.button в обработчике); реализовать перетаскивание объектов, комбинируя события с pointermove; или создать систему выделения, где клик меняет tint объекта вместо его удаления. Эти техники станут основой для UI, инвентарей или игровых полей.