О чем этот пример
При разработке игр на 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()), и отслеживайте, как ведёт себя курсор. Это поможет глубже понять жизненный цикл сцен и работу с вводом в сложных композициях.
