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

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

Версия 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.image('char', 'assets/pics/nayuki.png');
    }

    create ()
    {
        const sprite = this.add.sprite(400, 300, 'char');

        sprite.setInteractive();

        this.input.on(Phaser.Input.Events.POINTER_DOWN, () => {
            console.log('SCENE POINTER_DOWN');
        });

        this.input.on(Phaser.Input.Events.POINTER_UP, () => {
            console.log('SCENE POINTER_UP');
        });

        sprite.on(Phaser.Input.Events.POINTER_DOWN, () => {
            console.log('>>>> SPRITE POINTER_DOWN');
        });

        sprite.on(Phaser.Input.Events.POINTER_UP, () => {
            console.log('>>>> SPRITE POINTER_UP');
            sprite.input.enabled = false;
            console.log(sprite.input.enabled);
            this.time.delayedCall(1000, () => {
                sprite.input.enabled = true;
                console.log("------------------------")
            });
        });
    }
}

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

const game = new Phaser.Game(config);

Разбираем исходный код примера

В предоставленном примере создается сцена с одним интерактивным спрайтом. На сцену и на сам спрайт навешиваются обработчики событий POINTER_DOWN и POINTER_UP. В обработчике спрайта есть важная деталь: после события POINTER_UP интерактивность спрайта временно отключается на 1 секунду.

Давайте посмотрим на структуру сцены:

class Example extends Phaser.Scene {
    constructor() {
        super();
    }
preload () {
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('char', 'assets/pics/nayuki.png');
}
create () {
    const sprite = this.add.sprite(400, 300, 'char');
    sprite.setInteractive();
}

Иерархия событий ввода: сцена vs спрайт

Ключевой момент — понимание порядка срабатывания событий. В Phaser события ввода всплывают от целевого объекта (например, спрайта) к сцене. Это похоже на модель событий DOM в браузере.

В коде мы видим четыре обработчика:

// Обработчики на уровне сцены
this.input.on(Phaser.Input.Events.POINTER_DOWN, () => {
    console.log('SCENE POINTER_DOWN');
});

this.input.on(Phaser.Input.Events.POINTER_UP, () => {
    console.log('SCENE POINTER_UP');
});

// Обработчики на уровне спрайта
sprite.on(Phaser.Input.Events.POINTER_DOWN, () => {
    console.log('>>>> SPRITE POINTER_DOWN');
});

sprite.on(Phaser.Input.Events.POINTER_UP, () => {
    console.log('>>>> SPRITE POINTER_UP');
});

Если кликнуть по спрайту, в консоли сначала появится >>>> SPRITE POINTER_DOWN, а затем SCENE POINTER_DOWN. Аналогично для POINTER_UP. Это происходит потому, что событие сначала обрабатывается спрайтом (целевым объектом), а затем всплывает до сцены.

Управление интерактивностью: `sprite.input.enabled`

В примере используется важное свойство sprite.input.enabled. Оно позволяет динамически включать и отключать обработку ввода для конкретного игрового объекта.

Обратите внимание на этот фрагмент в обработчике POINTER_UP спрайта:

sprite.on(Phaser.Input.Events.POINTER_UP, () => {
    console.log('>>>> SPRITE POINTER_UP');
    // Отключаем интерактивность спрайта
    sprite.input.enabled = false;
    console.log(sprite.input.enabled); // Выведет: false

    // Включаем обратно через 1 секунду
    this.time.delayedCall(1000, () => {
        sprite.input.enabled = true;
        console.log("------------------------");
    });
});

После клика по спрайту его свойство input.enabled устанавливается в false. Это означает, что в течение следующей секунды спрайт не будет реагировать на события мыши или касания. События будут проходить «сквозь» него и срабатывать только на уровне сцены. Через секунду таймер, созданный через this.time.delayedCall, снова включает интерактивность.

Это полезный паттерн для создания кнопок с «периодом восстановления» или предотвращения множественных быстрых кликов.

Почему это важно: предотвращение ошибок

Понимание этой механики помогает избежать распространенных ошибок.

1. **Дублирование логики:** Если вы навесили одинаковую логику и на спрайт, и на сцену, она выполнится дважды. Убедитесь, что обработка происходит на нужном уровне. 2. **Остановка всплытия:** В отличие от DOM, в Phaser нет стандартного метода вроде stopPropagation(). Если нужно, чтобы событие не доходило до сцены, не вешайте обработчик на сцену или обрабатывайте событие только на целевом объекте. 3. **Динамическое управление:** Свойство input.enabled — ваш инструмент для сложной логики. Например, можно отключать интерактивность у всех объектов в группе или включать её только при выполнении определённого условия.

Проверка состояния интерактивности:

if (sprite.input.enabled) {
    // Спрайт готов принимать ввод
}

Практические рекомендации

1. **Чёткое разделение:** Используйте обработку на уровне спрайта для логики, связанной непосредственно с ним (например, анимация нажатия кнопки). Логику уровня игры (переход между состояниями, нанесение урона) лучше размещать на уровне сцены или в менеджерах. 2. **Используйте setInteractive с параметрами:** Метод setInteractive может принимать хитбокс. Это повышает точность.

// Делаем спрайт интерактивным, используя его физическое тело как хитбокс
sprite.setInteractive({ hitArea: sprite.getBounds(), useHandCursor: true });

3. **Альтернатива delayedCall:** Для более сложных сценариев отложенного включения можно использовать состояния сцены или асинхронные функции.

async enableSpriteWithDelay(sprite, delay) {
    await this.time.delayedCall(delay);
    sprite.input.enabled = true;
}

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

Главное правило — помнить об иерархии событий в Phaser: они всплывают от спрайта к сцене. Динамическое управление свойством input.enabled — мощный инструмент для контроля интерактивности. Для экспериментов попробуйте создать интерфейс, где кнопка отключается после нажатия и включается только после завершения какой-либо анимации или сетевого запроса. Или реализуйте систему, где клик по одному объекту временно отключает все остальные в его группе.