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

В Phaser есть несколько способов обрабатывать ввод с клавиатуры: от привязки к конкретному объекту клавиши до глобальных обработчиков на сцене. Понимание их различий и того, как события всплывают между сценами, критически важно для создания сложных игр с перекрывающимися интерфейсами или параллельными игровыми состояниями. Эта статья на практическом примере с тремя одновременно активными сценами покажет, как работают `Key.on`, `keydown_SPACE`, `keydown` и методы управления их всплытием `stopPropagation` и `stopImmediatePropagation`.

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

Живой запуск

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

Исходный код


function createSpeechBubble (scene, x, y, width, height, quote)
{
    var bubbleWidth = width;
    var bubbleHeight = height;
    var bubblePadding = 10;
    var arrowHeight = bubbleHeight / 3;

    var bubble = scene.add.graphics({ x: x, y: y });

    //  Bubble shadow
    bubble.fillStyle(0x222222, 0.5);
    bubble.fillRoundedRect(6, 6, bubbleWidth, bubbleHeight, 16);

    //  Bubble color
    bubble.fillStyle(0xffffff, 1);

    //  Bubble outline line style
    bubble.lineStyle(4, 0x565656, 1);

    //  Bubble shape and outline
    bubble.strokeRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16);
    bubble.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16);

    //  Calculate arrow coordinates
    var point1X = Math.floor(bubbleWidth / 4);
    var point1Y = bubbleHeight;
    var point2X = Math.floor((bubbleWidth / 4) * 1.4);
    var point2Y = bubbleHeight;
    var point3X = Math.floor(bubbleWidth / 4);
    var point3Y = Math.floor(bubbleHeight + arrowHeight);

    //  Bubble arrow shadow
    bubble.lineStyle(4, 0x222222, 0.5);
    bubble.lineBetween(point2X - 1, point2Y + 6, point3X + 2, point3Y);

    //  Bubble arrow fill
    bubble.fillTriangle(point1X, point1Y, point2X, point2Y, point3X, point3Y);
    bubble.lineStyle(2, 0x565656, 1);
    bubble.lineBetween(point2X, point2Y, point3X, point3Y);
    bubble.lineBetween(point1X, point1Y, point3X, point3Y);

    var content = scene.add.text(0, 0, quote, { fontFamily: 'Arial', fontSize: 20, color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } });

    var b = content.getBounds();

    content.setPosition(bubble.x + (bubbleWidth / 2) - (b.width / 2), bubble.y + (bubbleHeight / 2) - (b.height / 2));

    var container = scene.add.container();

    container.add([ bubble, content ]);

    return container;
}

class SceneA extends Phaser.Scene {

    constructor ()
    {
        super('sceneA');
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('jellies', 'assets/atlas/jellies.png', 'assets/atlas/jellies.json');
    }

    create ()
    {
        let jelly = this.add.image(150, 500, 'jellies', 'WithShadow/Jelly1').setScale(0.5);
        let bubble1 = createSpeechBubble(this, 20, 30, 220, 80, "Scene A\nKey.on").setVisible(false);
        let bubble2 = createSpeechBubble(this, 20, 160, 220, 80, "Scene A\nkeydown_SPACE").setVisible(false);
        let bubble3 = createSpeechBubble(this, 20, 290, 220, 80, "Scene A\nkeydown").setVisible(false);

        let spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

        //  Phase 1: Key event.
        //  Emits only when the SPACE BAR is pressed down, and dispatches from the local Key object.
        //  Call stopImmediatePropagation to stop it reaching the two global handlers in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        spaceKey.on('down', function (key, event) {

            // event.stopPropagation();
            // event.stopImmediatePropagation();

            bubble1.setVisible(true);

        });

        //  Phase 2: Global keydown + keycode handler.
        //  Emits only on the SPACE BAR keycode event, but dispatches globally.
        //  Call stopImmediatePropagation to stop it reaching the global handler in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        this.input.keyboard.on('keydown_SPACE', function (event) {

            // event.stopPropagation();
            // event.stopImmediatePropagation();

            bubble2.setVisible(true);

        });

        //  Phase 3: Global keydown handler.
        //  Fires on ANY key press, so we need to check the keyCode internally.
        //  Calling stopImmediatePropagation has no effect here, as it's the least specific handler in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        this.input.keyboard.on('keydown', function (event) {

            if (event.keyCode === Phaser.Input.Keyboard.KeyCodes.SPACE)
            {
                // event.stopPropagation();

                bubble3.setVisible(true);
            }

        });

        //  Launch the other 2 Scenes, so they are running in parallel to SceneA.
        this.scene.launch('sceneB');
        this.scene.launch('sceneC');
    }

}

class SceneB extends Phaser.Scene {

    constructor ()
    {
        super('sceneB');
    }

    create ()
    {
        let jelly = this.add.image(400, 500, 'jellies', 'WithShadow/Jelly2').setScale(0.5);
        let bubble1 = createSpeechBubble(this, 290, 30, 220, 80, "Scene B\nKey.on").setVisible(false);
        let bubble2 = createSpeechBubble(this, 290, 160, 220, 80, "Scene B\nkeydown_SPACE").setVisible(false);
        let bubble3 = createSpeechBubble(this, 290, 290, 220, 80, "Scene B\nkeydown").setVisible(false);

        let spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

        //  Phase 1: Key event.
        //  Emits only when the SPACE BAR is pressed down, and dispatches from the local Key object.
        //  Call stopImmediatePropagation to stop it reaching the two global handlers in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        spaceKey.on('down', function (key, event) {

            // event.stopPropagation();
            // event.stopImmediatePropagation();

            bubble1.setVisible(true);

        });

        //  Phase 2: Global keydown + keycode handler.
        //  Emits only on the SPACE BAR keycode event, but dispatches globally.
        //  Call stopImmediatePropagation to stop it reaching the global handler in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        this.input.keyboard.on('keydown_SPACE', function (event) {

            // event.stopPropagation();
            // event.stopImmediatePropagation();

            bubble2.setVisible(true);

        });

        //  Phase 3: Global keydown handler.
        //  Fires on ANY key press, so we need to check the keyCode internally.
        //  Calling stopImmediatePropagation has no effect here, as it's the least specific handler in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        this.input.keyboard.on('keydown', function (event) {

            if (event.keyCode === Phaser.Input.Keyboard.KeyCodes.SPACE)
            {
                // event.stopPropagation();

                bubble3.setVisible(true);
            }

        });
    }

}

class SceneC extends Phaser.Scene {

    constructor ()
    {
        super('sceneC');
    }

    create ()
    {
        let jelly = this.add.image(650, 500, 'jellies', 'WithShadow/Jelly3').setScale(0.5);
        let bubble1 = createSpeechBubble(this, 560, 30, 220, 80, "Scene C\nKey.on").setVisible(false);
        let bubble2 = createSpeechBubble(this, 560, 160, 220, 80, "Scene C\nkeydown_SPACE").setVisible(false);
        let bubble3 = createSpeechBubble(this, 560, 290, 220, 80, "Scene C\nkeydown").setVisible(false);

        let spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

        //  Phase 1: Key event.
        //  Emits only when the SPACE BAR is pressed down, and dispatches from the local Key object.
        //  Call stopImmediatePropagation to stop it reaching the two global handlers in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        spaceKey.on('down', function (key, event) {

            // event.stopPropagation();
            // event.stopImmediatePropagation();

            bubble1.setVisible(true);

        });

        //  Phase 2: Global keydown + keycode handler.
        //  Emits only on the SPACE BAR keycode event, but dispatches globally.
        //  Call stopImmediatePropagation to stop it reaching the global handler in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        this.input.keyboard.on('keydown_SPACE', function (event) {

            // event.stopPropagation();
            // event.stopImmediatePropagation();

            bubble2.setVisible(true);

        });

        //  Phase 3: Global keydown handler.
        //  Fires on ANY key press, so we need to check the keyCode internally.
        //  Calling stopImmediatePropagation has no effect here, as it's the least specific handler in this Scene.
        //  Call stopPropagation to stop it reaching any other Scene.

        this.input.keyboard.on('keydown', function (event) {

            if (event.keyCode === Phaser.Input.Keyboard.KeyCodes.SPACE)
            {
                // event.stopPropagation();

                bubble3.setVisible(true);
            }

        });
    }

}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    backgroundColor: '#0072bc',
    scene: [ SceneA, SceneB, SceneC ]
};

let game = new Phaser.Game(config);

Архитектура примера: три параллельные сцены

В примере создаётся игра с тремя сценами (SceneA, SceneB, SceneC). SceneA запускается первой и в своём методе create() запускает SceneB и SceneC через this.scene.launch(). Все три сцены отображаются и обновляются одновременно. В каждой сцене есть три графических "пузыря" с текстом, изначально невидимых. Нажатие клавиши ПРОБЕЛ делает видимыми соответствующие пузыри в одной или нескольких сценах, наглядно демонстрируя путь события.

Функция createSpeechBubble — это вспомогательный метод для отрисовки стилизованного контейнера с текстом. Она использует Graphics API Phaser для рисования закруглённого прямоугольника и стрелки, а затем размещает в нём текстовый объект.

let bubble1 = createSpeechBubble(this, 20, 30, 220, 80, "Scene A\nKey.on").setVisible(false);

Уровень 1: Событие на объекте Key (Key.on)

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

- **Создание объекта клавиши:** this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE) возвращает объект Phaser.Input.Keyboard.Key. - **Обработчик:** Событие 'down' срабатывает только при нажатии именно этой клавиши. - **Область видимости:** Событие диспетчеризуется локально, от этого объекта Key. - **Управление всплытием:** Используйте event.stopPropagation() для остановки всплытия события к другим сценам. Используйте event.stopImmediatePropagation() для остановки всплытия к более глобальным обработчикам (keydown_SPACE и keydown) **внутри этой же сцены**.

let spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
spaceKey.on('down', function (key, event) {
    // event.stopPropagation(); // Остановит всплытие в другие сцены
    // event.stopImmediatePropagation(); // Остановит всплытие в другие обработчики ЭТОЙ сцены
    bubble1.setVisible(true);
});

Уровень 2: Глобальный обработчик конкретной клавиши (keydown_SPACE)

Более глобальный уровень. Мы слушаем событие на объекте клавиатуры всей сцены, но оно фильтруется по коду клавиши.

- **Обработчик:** Событие 'keydown_SPACE' срабатывает при нажатии ПРОБЕЛА. - **Область видимости:** Событие диспетчеризуется глобально от this.input.keyboard. - **Управление всплытием:** stopPropagation() предотвратит всплытие в другие сцены. stopImmediatePropagation() остановит всплытие к следующему по специфичности обработчику — глобальному 'keydown' **в этой же сцене**.

this.input.keyboard.on('keydown_SPACE', function (event) {
    // event.stopPropagation();
    // event.stopImmediatePropagation();
    bubble2.setVisible(true);
});

Уровень 3: Глобальный обработчик всех клавиш (keydown)

Самый общий обработчик. Он ловит нажатие любой клавиши, поэтому внутри нужно проверять event.keyCode.

- **Обработчик:** Событие 'keydown' срабатывает при нажатии любой клавиши. - **Область видимости:** Глобальная, от this.input.keyboard. - **Управление всплытием:** stopImmediatePropagation() здесь не имеет эффекта, так как это наименее специфичный обработчик в цепочке сцены (ему некуда всплывать внутри неё). stopPropagation() по-прежнему может остановить всплытие в другие сцены.

this.input.keyboard.on('keydown', function (event) {
    if (event.keyCode === Phaser.Input.Keyboard.KeyCodes.SPACE) {
        // event.stopPropagation();
        bubble3.setVisible(true);
    }
});

Порядок всплытия и практические следствия

Событие клавиатуры в Phaser всплывает по определённому пути, начиная от самого специфичного обработчика к самому общему.

1. **Внутри одной сцены:** Порядок вызова: Key.on('down') -> keydown_SPACE -> keydown. Если в Key.on вызвать stopImmediatePropagation(), обработчики keydown_SPACE и keydown в этой сцене не сработают. 2. **Между сценами:** Если событие не остановлено (stopPropagation()), оно будет диспетчеризовано во все активные сцены в том же порядке. Например, нажатие ПРОБЕЛА вызовет Key.on в SceneA, затем Key.on в SceneB, затем Key.on в SceneC, потом keydown_SPACE в SceneA и так далее.

Это поведение — мощный инструмент. Например, вы можете: - Сделать паузу в игровой сцене (stopPropagation() в UI-сцене), чтобы игровые объекты не реагировали. - Создать глобальные "хоткеи" в главной сцене, работающие поверх всех остальных. - Разделить управление: UI-сцена обрабатывает меню, а игровая — управление персонажем.

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

Phaser предлагает гибкую систему обработки ввода с чёткой иерархией и контролем над всплытием событий. Используйте Key.on для прямого и изолированного управления, keydown_KEY для глобальных, но специфичных действий, и keydown для перехвата всех клавиш. Ключевые методы stopPropagation и stopImmediatePropagation позволяют тонко настраивать, какая сцена или обработчик получит событие. **Идеи для экспериментов:** 1. Раскомментируйте разные комбинации stopPropagation в SceneA и понаблюдайте, какие пузыри в SceneB и SceneC перестают появляться. 2. Добавьте чётвёртую сцену-обработчик, которая ловит все события и выводит в консоль их путь, чтобы увидеть порядок всплытия. 3. Создайте сцену-меню, которая при открытии останавливает всплытие событий в игровую сцену, блокируя управление геймплеем.