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

При разработке игр на Phaser 3 вы могли столкнуться с неочевидной проблемой: интерактивный спрайт с `useHandCursor: true` перестаёт менять курсор мыши после перевода сцены в режим сна с помощью `this.scene.sleep()`. Эта статья на конкретном примере из баг-трекера Phaser объясняет, почему это происходит и как правильно управлять состоянием сцен, чтобы избежать подобных артефактов. Понимание этого механизма критически важно для создания сложных многопользовательских интерфейсов и пауз в игре.

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

Живой запуск

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

Исходный код


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

    create ()
    {
        this.scene.launch('test');
    }
}

class Test extends Phaser.Scene
{
    constructor() { super('test') }

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

    create ()
    {
        this.input.setPollOnMove();
        
        console.log('Test Scene');
        const ball = this.add.image(400, 300, 'ball');
        ball.setInteractive({ useHandCursor: true });
        ball.on('pointerdown', () =>
        {
            // this.scale.canvas.style.cursor = 'default'; // I had to manually change the cursor.
            this.scene.sleep();
            // this.scene.stop();
        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#1d1d1d',
    parent: 'phaser-example',
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    scene: [Example, Test]
};

const game = new Phaser.Game(config);

Суть проблемы: курсор "залипает"

В предоставленном примере мы имеем две сцены: Example и Test. Основная сцена Example сразу запускает (launch) сцену Test. В сцене Test создаётся интерактивный спрайт мяча.

ball.setInteractive({ useHandCursor: true });
ball.on('pointerdown', () => {
    this.scene.sleep();
});

При наведении на мяч курсор должен смениться на "руку" (hand cursor). При клике сцена Test переводится в спящий режим методом this.scene.sleep(). Проблема в том, что после этого действия курсор мыши остаётся в виде "руки", даже если мышь больше не находится над интерактивной областью. Это происходит потому, что спящая сцена (Test) больше не обрабатывает события ввода, включая событие pointerout, которое должно вернуть курсор к значению по умолчанию. Система ввода Phaser просто "забывает" обновить состояние курсора.

Sleep vs Stop: в чём разница для ввода?

Phaser предлагает два основных метода для деактивации сцены: sleep() и stop(). Их влияние на систему ввода различно, и это ключ к решению проблемы.

*   `this.scene.sleep()`: Сцена приостанавливается (её `update` перестаёт вызываться), но **не уничтожается**. Все созданные в ней объекты, включая интерактивные, остаются в памяти. Однако сама сцена перестаёт быть активной в менеджере ввода, поэтому события мыши (вроде ухода курсора с элемента) для её объектов не обрабатываются.
*   `this.scene.stop()`: Сцена полностью останавливается и уничтожается. Все её игровые объекты, системы и настройки ввода удаляются. Это приводит к полному сбросу состояния, и курсор возвращается к стандартному виду.

В коде примера закомментирована "костыльная" строка, которая вручную сбрасывает курсор. Это плохая практика, так как она напрямую манипулирует DOM, минуя API Phaser.

Практическое решение: сброс курсора перед сном

Правильный способ решить проблему — явно сообщить системе ввода Phaser о необходимости сбросить курсор перед переводом сцены в сон. Для этого используется метод this.input.setDefaultCursor().

Исправленный обработчик клика по мячу должен выглядеть так:

ball.on('pointerdown', () => {
    // Сбрасываем курсор к значению по умолчанию
    this.input.setDefaultCursor();
    // Теперь можно безопасно усыплять сцену
    this.scene.sleep();
});

Вызов this.input.setDefaultCursor() без аргументов возвращает системный курсор мыши к его стандартному виду (обычно стрелке). Это гарантирует, что в момент деактивации сцены не останется визуальных артефактов. Это решение использует штатный API Phaser и является рекомендуемым подходом.

Когда использовать sleep(), а когда stop()?

Выбор метода зависит от вашей задачи:

// Используйте SLEEP, если:
// - Вам нужна быстрая пауза в игре (например, открытие модального окна-меню).
// - Вы планируете позже быстро возобновить сцену с тем же состоянием.
// - В сцене тяжёлые assets, которые не хочется перезагружать.
this.scene.sleep('GameScene');

// Используйте STOP, если:
// - Вы завершили уровень и больше не вернётесь к этой сцене.
// - Вам нужно полностью очистить память и контекст.
// - Вы хотите гарантированно сбросить все состояния, включая ввод.
this.scene.stop('GameScene');

В контексте нашего примера, если сцена Test — это игровое меню или панель, которое будет часто открываться и закрываться, sleep() предпочтительнее из-за скорости. Но не забывайте сбрасывать курсор. Если же это разовый экран, который больше не понадобится, безопаснее использовать stop().

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

Баг с "залипшим" курсором — наглядный пример того, как состояние сцены в Phaser связано с системой ввода. Ключевой вывод: перед вызовом scene.sleep() для сцены, содержащей интерактивные объекты с кастомным курсором, всегда сбрасывайте курсор через this.input.setDefaultCursor(). Для экспериментов попробуйте создать интерфейс с несколькими перекрывающимися интерактивными сценами, управляя их сном и пробуждением (this.scene.wake()), и отслеживайте, как ведёт себя курсор. Это поможет глубже понять жизненный цикл сцен и работу с вводом в сложных композициях.