О чем этот пример
В классических аркадных играх после установки рекорда часто требуется ввести своё имя для таблицы лидеров. Этот пример демонстрирует, как создать интерактивный интерфейс для ввода короткого имени, используя как клавиатурную навигацию (стрелки и Enter/Space), так и управление мышью. Вы научитесь обрабатывать ввод с двух устройств одновременно, создавать визуальный курсор и динамически обновлять текст на экране — навык, полезный для любых игровых меню и форм.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('block', 'assets/input/block.png');
this.load.image('rub', 'assets/input/rub.png');
this.load.image('end', 'assets/input/end.png');
this.load.bitmapFont('arcade', 'assets/fonts/bitmap/arcade.png', 'assets/fonts/bitmap/arcade.xml');
}
create ()
{
const chars = [
[ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' ],
[ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T' ],
[ 'U', 'V', 'W', 'X', 'Y', 'Z', '.', '-', '<', '>' ]
];
const cursor = { x: 0, y: 0 };
let name = '';
const input = this.add.bitmapText(130, 50, 'arcade', 'ABCDEFGHIJ\n\nKLMNOPQRST\n\nUVWXYZ.-').setLetterSpacing(20);
input.setInteractive();
const rub = this.add.image(input.x + 430, input.y + 148, 'rub');
const end = this.add.image(input.x + 482, input.y + 148, 'end');
const block = this.add.image(input.x - 10, input.y - 2, 'block').setOrigin(0);
const legend = this.add.bitmapText(80, 260, 'arcade', 'RANK SCORE NAME').setTint(0xff00ff);
this.add.bitmapText(80, 310, 'arcade', '1ST 50000 ').setTint(0xff0000);
this.add.bitmapText(80, 360, 'arcade', '2ND 40000 ICE').setTint(0xff8200);
this.add.bitmapText(80, 410, 'arcade', '3RD 30000 GOS').setTint(0xffff00);
this.add.bitmapText(80, 460, 'arcade', '4TH 20000 HRE').setTint(0x00ff00);
this.add.bitmapText(80, 510, 'arcade', '5TH 10000 ETE').setTint(0x00bfff);
const playerText = this.add.bitmapText(560, 310, 'arcade', name).setTint(0xff0000);
this.input.keyboard.on('keyup', event =>
{
if (event.keyCode === 37)
{
// left
if (cursor.x > 0)
{
cursor.x--;
block.x -= 52;
}
}
else if (event.keyCode === 39)
{
// right
if (cursor.x < 9)
{
cursor.x++;
block.x += 52;
}
}
else if (event.keyCode === 38)
{
// up
if (cursor.y > 0)
{
cursor.y--;
block.y -= 64;
}
}
else if (event.keyCode === 40)
{
// down
if (cursor.y < 2)
{
cursor.y++;
block.y += 64;
}
}
else if (event.keyCode === 13 || event.keyCode === 32)
{
// Enter or Space
if (cursor.x === 9 && cursor.y === 2 && name.length > 0)
{
// Submit
}
else if (cursor.x === 8 && cursor.y === 2 && name.length > 0)
{
// Rub
name = name.substr(0, name.length - 1);
playerText.text = name;
}
else if (name.length < 3)
{
// Add
name = name.concat(chars[cursor.y][cursor.x]);
playerText.text = name;
}
}
});
input.on('pointermove', (pointer, x, y) =>
{
const cx = Phaser.Math.Snap.Floor(x, 52, 0, true);
const cy = Phaser.Math.Snap.Floor(y, 64, 0, true);
const char = chars[cy][cx];
cursor.x = cx;
cursor.y = cy;
block.x = input.x - 10 + (cx * 52);
block.y = input.y - 2 + (cy * 64);
}, this);
input.on('pointerup', (pointer, x, y) =>
{
const cx = Phaser.Math.Snap.Floor(x, 52, 0, true);
const cy = Phaser.Math.Snap.Floor(y, 64, 0, true);
const char = chars[cy][cx];
cursor.x = cx;
cursor.y = cy;
block.x = input.x - 10 + (cx * 52);
block.y = input.y - 2 + (cy * 64);
if (char === '<' && name.length > 0)
{
// Rub
name = name.substr(0, name.length - 1);
playerText.text = name;
}
else if (char === '>' && name.length > 0)
{
// Submit
}
else if (name.length < 3)
{
// Add
name = name.concat(char);
playerText.text = name;
}
}, this);
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и создание виртуальной клавиатуры
В методе create() мы создаём основу для нашего интерфейса ввода. Сначала определяется двумерный массив chars, представляющий собой сетку символов, аналогичную старым аркадным автоматам.
Затем создаётся объект bitmapText, который отображает все эти символы на экране в три строки. Важно, что этот текстовый объект также получает интерактивность с помощью метода setInteractive(), что позволит нам обрабатывать события мыши.
Рядом с текстом добавляются две иконки спрайтов: rub (для удаления символа) и end (для подтверждения ввода). Спрайт block служит визуальным курсором, выделяющим текущий выбранный символ. Он позиционируется относительно текстового поля.
const chars = [
[ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' ],
[ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T' ],
[ 'U', 'V', 'W', 'X', 'Y', 'Z', '.', '-', '<', '>' ]
];
const cursor = { x: 0, y: 0 };
let name = '';
const input = this.add.bitmapText(130, 50, 'arcade', 'ABCDEFGHIJ\n\nKLMNOPQRST\n\nUVWXYZ.-').setLetterSpacing(20);
input.setInteractive();
const rub = this.add.image(input.x + 430, input.y + 148, 'rub');
const end = this.add.image(input.x + 482, input.y + 148, 'end');
const block = this.add.image(input.x - 10, input.y - 2, 'block').setOrigin(0);
Обработка ввода с клавиатуры
Логика управления с клавиатуры подписывается на глобальное событие keyup с помощью this.input.keyboard.on(). Обработчик анализирует event.keyCode для определения нажатой клавиши.
Стрелки (keyCode 37-40) перемещают внутренний объект cursor и визуальный спрайт block по сетке символов, проверяя границы (от 0 до 9 по X и от 0 до 2 по Y). Клавиши Enter (13) и Space (32) выполняют действие в зависимости от текущей позиции курсора:
- Если курсор на символе '>' (позиция 9,2) и имя не пустое — можно отправить имя (логика отправки не реализована в примере).
- Если курсор на символе '<' (позиция 8,2) и имя не пустое — удаляется последний символ из строки name.
- В любой другой позиции и если длина имени меньше 3 символов — выбранный символ добавляется к имени.
После любого изменения строки name обновляется текст playerText.
this.input.keyboard.on('keyup', event => {
if (event.keyCode === 37) { // left
if (cursor.x > 0) {
cursor.x--;
block.x -= 52;
}
}
else if (event.keyCode === 39) { // right
if (cursor.x < 9) {
cursor.x++;
block.x += 52;
}
}
// ... обработка up/down ...
else if (event.keyCode === 13 || event.keyCode === 32) { // Enter or Space
if (cursor.x === 9 && cursor.y === 2 && name.length > 0) {
// Submit
}
else if (cursor.x === 8 && cursor.y === 2 && name.length > 0) {
// Rub
name = name.substr(0, name.length - 1);
playerText.text = name;
}
else if (name.length < 3) {
// Add
name = name.concat(chars[cursor.y][cursor.x]);
playerText.text = name;
}
}
});
Обработка ввода с помощью мыши
Поскольку текстовый объект input интерактивен, к нему можно привязать события мыши. Событие pointermove обновляет позицию курсора и блока при движении мыши над сеткой символов. Ключевую роль здесь играет утилита Phaser.Math.Snap.Floor. Она "привязывает" координаты мыши (`x,y`) к ячейкам сетки с заданным шагом (52 пикселя по горизонтали, 64 по вертикали), что позволяет точно определить, над каким символом находится указатель.
input.on('pointermove', (pointer, x, y) => {
const cx = Phaser.Math.Snap.Floor(x, 52, 0, true);
const cy = Phaser.Math.Snap.Floor(y, 64, 0, true);
cursor.x = cx;
cursor.y = cy;
block.x = input.x - 10 + (cx * 52);
block.y = input.y - 2 + (cy * 64);
}, this);
Событие pointerup обрабатывает клик. Логика аналогична обработке Enter/Space в клавиатурном управлении, но решение о действии принимается на основе символа (char), полученного из массива chars по вычисленным индексам.
input.on('pointerup', (pointer, x, y) => {
const cx = Phaser.Math.Snap.Floor(x, 52, 0, true);
const cy = Phaser.Math.Snap.Floor(y, 64, 0, true);
const char = chars[cy][cx];
// ... обновление cursor и block ...
if (char === '<' && name.length > 0) {
// Rub
name = name.substr(0, name.length - 1);
playerText.text = name;
}
else if (char === '>' && name.length > 0) {
// Submit
}
else if (name.length < 3) {
// Add
name = name.concat(char);
playerText.text = name;
}
}, this);
Визуализация и отладка
Пример также создаёт статичную таблицу лидеров с помощью bitmapText, чтобы имитировать игровой контекст. Текст для игрока (playerText) инициализируется пустой строкой name и обновляется при любом изменении. Использование setTint позволяет легко раскрашивать текст в нужные цвета, соответствующие ретро-стилю.
Обратите внимание на синхронизацию состояния. Объект cursor и спрайт block являются единым источником истины для позиции выбора. Они обновляются как событиями клавиатуры, так и событиями мыши, что обеспечивает согласованное поведение интерфейса независимо от способа ввода.
const playerText = this.add.bitmapText(560, 310, 'arcade', name).setTint(0xff0000);
// ...
playerText.text = name; // Обновление при изменении
Что попробовать дальше
Вы создали гибридный интерфейс ввода, который реагирует и на клавиатуру, и на мышь — важный шаг к доступности и удобству игровых меню. Для экспериментов попробуйте: добавить звуковые эффекты при нажатии и удалении символов, реализовать отправку имени и переход к другой сцене, изменить раскладку символов или визуальный стиль курсора block, а также ограничить ввод не тремя, а, например, десятью символами.
