О чем этот пример
Геймпады — это естественный и удобный способ управления для многих игр. Phaser предоставляет простой и эффективный API для работы с ними, позволяя назначать действия на кнопки и обрабатывать состояние стиков. В этой статье мы разберем пример, где геймпад используется не только для перемещения спрайта, но и для переключения между игровыми сценами. Вы научитесь корректно обрабатывать подключение контроллера, слушать события его кнопок и обеспечивать непрерывность управления при смене сцены — ключевой навык для создания сложных игр с разными состояниями или уровнями.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class MainScene extends Phaser.Scene {
constructor ()
{
super('MainScene');
this.pad = null;
this.sprite = null;
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('sky', 'assets/skies/lightblue.png');
this.load.image('elephant', 'assets/sprites/elephant.png');
this.load.image('truck1', 'assets/sprites/alienbusters.png');
this.load.image('truck2', 'assets/sprites/astorm-truck.png');
}
create ()
{
this.add.image(0, 0, 'sky').setOrigin(0);
var text = this.add.text(10, 10, 'Press any button on a connected Gamepad', { font: '16px Courier', fill: '#ffffff' });
if (this.input.gamepad.total === 0)
{
this.input.gamepad.once('connected', pad => {
this.pad = pad;
this.sprite = this.add.image(400, 300, 'elephant');
text.setText('Main Scene. Press Button 0 to change Scene');
pad.on('down', (index, value, button) => {
if (index === 0)
{
console.log('M to A');
this.scene.start('SceneA');
}
});
});
}
else
{
this.pad = this.input.gamepad.pad1;
this.sprite = this.add.image(400, 300, 'elephant');
text.setText('Main Scene. Press Button 0 to change Scene');
this.pad.on('down', (index, value, button) => {
if (index === 0)
{
console.log('M to A');
this.scene.start('SceneA');
}
});
}
}
update ()
{
const pad = this.pad;
if (!pad)
{
return;
}
const sprite = this.sprite;
if (pad.left)
{
sprite.x -= 4;
sprite.flipX = false;
}
else if (pad.right)
{
sprite.x += 4;
sprite.flipX = true;
}
if (pad.up)
{
sprite.y -= 4;
}
else if (pad.down)
{
sprite.y += 4;
}
}
}
class SceneA extends Phaser.Scene {
constructor ()
{
super('SceneA');
this.pad = null;
this.sprite = null;
}
create ()
{
this.add.image(0, 0, 'sky').setOrigin(0);
this.add.text(10, 10, 'Scene A. Press Button 0 to change Scene.', { font: '16px Courier', fill: '#ffffff' });
this.pad = this.input.gamepad.pad1;
this.sprite = this.add.image(400, 300, 'truck1');
this.pad.on('down', (index, value, button) => {
if (index === 0)
{
console.log('A to B');
this.scene.start('SceneB');
}
});
}
update ()
{
const pad = this.pad;
if (!pad)
{
return;
}
const sprite = this.sprite;
if (pad.left)
{
sprite.x -= 4;
sprite.flipX = true;
}
else if (pad.right)
{
sprite.x += 4;
sprite.flipX = false;
}
if (pad.up)
{
sprite.y -= 4;
}
else if (pad.down)
{
sprite.y += 4;
}
}
}
class SceneB extends Phaser.Scene {
constructor ()
{
super('SceneB');
this.pad = null;
this.sprite = null;
}
create ()
{
this.add.image(0, 0, 'sky').setOrigin(0);
this.add.text(10, 10, 'Scene B. Press Button 0 to change Scene.', { font: '16px Courier', fill: '#ffffff' });
this.pad = this.input.gamepad.pad1;
this.sprite = this.add.image(400, 300, 'truck2');
this.pad.on('down', (index, value, button) => {
if (index === 0)
{
console.log('B to M');
this.scene.start('MainScene');
}
});
}
update ()
{
const pad = this.pad;
if (!pad)
{
return;
}
const sprite = this.sprite;
if (pad.left)
{
sprite.x -= 4;
sprite.flipX = true;
}
else if (pad.right)
{
sprite.x += 4;
sprite.flipX = false;
}
if (pad.up)
{
sprite.y -= 4;
}
else if (pad.down)
{
sprite.y += 4;
}
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
input: {
gamepad: true
},
scene: [ MainScene, SceneA, SceneB ]
};
const game = new Phaser.Game(config);
Настройка проекта и конфигурация геймпада
Вся работа с геймпадом в Phaser начинается с включения соответствующего плагина в конфигурации игры. Это обязательный шаг.
const config = {
type: Phaser.AUTO,
// ... другие настройки ...
input: {
gamepad: true // Включаем поддержку геймпадов
},
scene: [ MainScene, SceneA, SceneB ]
};
После этого в любой сцене можно получить доступ к менеджеру геймпадов через this.input.gamepad. В нашем примере используются три сцены: MainScene, SceneA и SceneB. Они перечислены в массиве scene при создании игры, что позволяет Phaser заранее знать об их существовании и корректно переключаться между ними методом this.scene.start().
Обработка подключения геймпада
Геймпад может быть подключен как до старта игры, так и во время ее работы. Код должен уметь обрабатывать оба сценария. В классе MainScene (первой активной сцене) это реализовано в методе create().
if (this.input.gamepad.total === 0)
{
this.input.gamepad.once('connected', pad => {
this.pad = pad;
// ... настройка спрайта и текста ...
});
}
else
{
this.pad = this.input.gamepad.pad1;
// ... настройка спрайта и текста ...
}
Свойство this.input.gamepad.total показывает количество уже подключенных контроллеров. Если оно равно нулю, мы подписываемся на одноразовое событие 'connected'. Как только игрок подключит геймпад, сработает колбэк, и мы сохраним ссылку на объект pad в свойстве сцены this.pad. Если же геймпад уже был подключен, мы сразу берем первый (pad1) и присваиваем его this.pad. Это гарантирует, что свойство this.pad будет доступно в методах create и update для дальнейшей работы.
Управление спрайтом с помощью крестовины (D-Pad)
Перемещение объекта реализовано в методе update() каждой сцены. Phaser автоматически обновляет состояние кнопок геймпада, и мы можем проверять булевы свойства left, right, up, down у объекта pad.
if (pad.left)
{
sprite.x -= 4;
sprite.flipX = false;
}
else if (pad.right)
{
sprite.x += 4;
sprite.flipX = true;
}
Код проверяет, нажата ли в данный кадр соответствующая方向ная кнопка. Если да, координаты спрайта изменяются на фиксированное значение (4 пикселя). Свойство sprite.flipX используется для отражения спрайта по горизонтали, создавая иллюзию поворота в сторону движения. Важно: метод update вызывается на каждом кадре, поэтому движение получается плавным и непрерывным, пока игрок удерживает кнопку.
Переключение сцен по нажатию кнопки
Самая интересная часть примера — использование геймпада для навигации по сценам. В каждой сцене мы навешиваем обработчик на событие 'down' объекта геймпада. Это событие генерируется в момент нажатия любой кнопки.
pad.on('down', (index, value, button) => {
if (index === 0)
{
console.log('M to A');
this.scene.start('SceneA');
}
});
Колбэк получает индекс нажатой кнопки (index). В данном случае мы проверяем, равна ли она 0 (обычно это кнопка «A» на Xbox-совместимых или «Cross» на PlayStation-совместимых контроллерах). Если условие выполняется, запускается новая сцена с помощью this.scene.start('SceneA'). Текущая сцена будет остановлена и уничтожена, а управление передастся SceneA. При этом геймпад остается подключенным к системе, и в новой сцене мы снова получаем к нему доступ через this.input.gamepad.pad1.
Сохранение состояния геймпада между сценами
Критически важно понимать, что при запуске новой сцены через start(), объекты старой сцены уничтожаются. Однако сам физический геймпад и его состояние в системе — нет. Поэтому в методах create() сцен SceneA и SceneB мы заново инициализируем связь с контроллером.
create ()
{
// ... создание фона и текста ...
this.pad = this.input.gamepad.pad1; // Получаем геймпад заново
this.sprite = this.add.image(400, 300, 'truck1');
// ... навешивание нового обработчика для переключения ...
}
Мы снова присваиваем this.pad = this.input.gamepad.pad1. Это необходимо, потому что контекст (this) и все свойства сцены — новые. Также мы заново создаем обработчик события 'down' для кнопки 0, который теперь будет запускать следующую сцену. Таким образом, геймпад действует как глобальный контроллер навигации, не зависящий от жизни конкретной сцены.
Что попробовать дальше
Пример наглядно демонстрирует два основных сценария использования геймпада в Phaser: для непрерывного управления (перемещение спрайта в update) и для дискретных действий (переключение сцен по событию 'down'). Ключевой вывод — объект геймпада нужно заново получать и настраивать в каждой новой сцене, но само устройство при этом остается «живым». Для экспериментов попробуйте
- Назначить переключение сцен на другие кнопки или комбинации
- Добавить вибрацию (
pad.vibration) при смене сцены - Реализовать систему, где состояние спрайта (например, его позиция) передается из одной сцены в другую при переходе
