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

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


let info;
let timer;
let alive = 0;

class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/skies/sky4.png');
        this.load.image('crate', 'assets/sprites/crate.png');
    }

    create ()
    {
        //  How many crates can you click on in 10 seconds?
        this.add.image(400, 300, 'bg');

        //  Create a bunch of images
        for (var i = 0; i < 64; i++)
        {
            var x = Phaser.Math.Between(0, 800);
            var y = Phaser.Math.Between(0, 600);

            var box = this.add.image(x, y, 'crate');

            //  Make them all input enabled
            box.setInteractive();

            //  The images will dispatch a 'clicked' event when they are clicked on
            box.on('clicked', this.clickHandler, this);

            alive++;
        }

        //  If a Game Object is clicked on, this event is fired.
        //  We can use it to emit the 'clicked' event on the game object itself.
        this.input.on('gameobjectup', function (pointer, gameObject)
        {
            gameObject.emit('clicked', gameObject);
        }, this);

        //  Display the game stats
        info = this.add.text(10, 10, '', { font: '48px Arial', fill: '#000000' });

        timer = this.time.addEvent({ delay: 10000, callback: this.gameOver, callbackScope: this });
    }

    update ()
    {
        info.setText('Alive: ' + alive + '\nTime: ' + Math.floor(10000 - timer.getElapsed()));
    }

    clickHandler (box)
    {
        alive--;

        box.off('clicked', this.clickHandler);
        box.input.enabled = false;
        box.setVisible(false);
    }

    gameOver ()
    {
        this.input.off('gameobjectup');
    }
}

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

const game = new Phaser.Game(config);

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

В методах preload и create происходит базовая настройка сцены. Мы загружаем фон и спрайт ящика, а затем создаем 64 экземпляра этого спрайта в случайных позициях.

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

box.setInteractive();

Затем мы подписываем каждый ящик на свое собственное событие 'clicked'. Это кастомное событие, которое мы будем генерировать вручную. Метод on принимает имя события, функцию-обработчик и контекст, в котором будет вызван этот обработчик.

box.on('clicked', this.clickHandler, this);

Глобальная переменная alive отслеживает количество еще не нажатых ящиков.

Мост от системного события к объектному

Phaser генерирует системное событие 'gameobjectup' когда пользователь отпускает кнопку мыши (или касание) над интерактивным игровым объектом. Мы можем перехватить это событие на уровне ввода (this.input).

Обработчик 'gameobjectup' получает указатель (pointer) и сам игровой объект (gameObject), над которым произошло действие. Внутри этого обработчика мы эмитируем (генерируем) наше кастомное событие 'clicked' непосредственно на этом объекте, используя метод emit. В качестве параметра передаем сам объект.

this.input.on('gameobjectup', function (pointer, gameObject) {
    gameObject.emit('clicked', gameObject);
}, this);

Таким образом, системное событие ввода служит триггером, который «пробрасывает» вызов к персонализированному событию конкретного объекта. Это разделяет ответственность: система ввода только сообщает о факте взаимодействия, а логика реакции определяется подпиской на событие самого объекта.

Обработка кастомного события и логика игры

Функция clickHandler вызывается каждый раз, когда срабатывает событие 'clicked' на ящике. Она выполняет три действия: 1. Уменьшает счетчик активных ящиков (alive--). 2. Отписывает этот конкретный ящик от события 'clicked' с помощью метода off. Это хорошая практика для очистки, чтобы избежать утечек памяти и повторных срабатываний. 3. Деактивирует ввод и скрывает ящик, визуально и логически удаляя его из игры.

clickHandler (box) {
    alive--;
    box.off('clicked', this.clickHandler);
    box.input.enabled = false;
    box.setVisible(false);
}

Игровой таймер создается с помощью this.time.addEvent. Он настроен на 10 секунд и по истечении времени вызывает метод gameOver, который отписывает сцену от глобального события 'gameobjectup', останавливая всю обработку кликов.

timer = this.time.addEvent({ delay: 10000, callback: this.gameOver, callbackScope: this });

Метод update обновляет текстовое поле, показывая оставшееся время и количество ящиков.

Почему это эффективный паттерн?

Использование кастомных событий на объектах, а не одна большая функция-обработчик в create, дает несколько преимуществ:

* **Инкапсуляция:** Логика реакции на клик (clickHandler) находится рядом с определением подписки (box.on). Легче понять, что происходит с объектом. * **Гибкость:** Разные типы объектов могут иметь одно и то же событие 'clicked', но обрабатывать его по-разному, просто подписав на него разные функции. * **Управление памятью:** Метод off позволяет явно удалять обработчики, когда объект больше не нужен. Это критически важно в играх, где объекты постоянно создаются и уничтожаются. * **Масштабируемость:** В более сложных сценариях объект может генерировать множество разных событий ('over', 'out', 'collected'), и другие системы могут подписываться на них, не вмешиваясь во внутреннюю логику объекта.

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

Событийная модель Phaser — это мощный инструмент для создания отзывчивых и хорошо структурированных игр. Разделение системных событий ввода и объектовых событий делает код чище и модульнее. **Идеи для экспериментов:** 1. Добавьте разные типы ящиков (например, с разными текстурами), которые при клике будут вычитать разное количество очков из alive. 2. Замените событие 'gameobjectup' на 'gameobjectdown' или 'gameobjectover' и посмотрите, как изменится игровой процесс. 3. Сделайте так, чтобы ящики не скрывались, а воспроизводили анимацию разрушения или «улетали» за экран, прежде чем стать неактивными. 4. Реализуйте систему подсчета очков, где каждый кликнутый ящик добавляет очки, и выводите этот счет рядом с таймером.