О чем этот пример
В этом примере из официальных демонстраций Phaser показана реализация классической аркадной игры в рамках одной сцены. Код демонстрирует ключевые концепции для создания динамичных игр: управление группами объектов, обработка столкновений, управление камерами и таймеры. Разбор этого примера поможет вам понять, как структурировать игровую логику и эффективно использовать физический движок Phaser для прототипирования жанра shoot 'em up.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Invaders extends Phaser.Scene {
constructor (handle, parent)
{
super(handle);
this.parent = parent;
this.left;
this.right;
this.ship;
this.invaders;
this.mothership;
this.bullet;
this.topLeft;
this.bottomRight;
this.bulletTimer;
this.mothershipTimer;
this.isGameOver = false;
this.invadersBounds = { x: 12, y: 62, right: 152 };
}
create (config)
{
this.left = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT);
this.right = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT);
this.physics.world.setBounds(4, 22, 400, 300);
this.cameras.main.setViewport(this.parent.x, this.parent.y, Invaders.WIDTH, Invaders.HEIGHT);
this.cameras.main.setBackgroundColor('#000');
this.createInvaders();
this.bullet = this.physics.add.image(200, 290, 'invaders.bullet2');
this.mothership = this.physics.add.image(500, 40, 'invaders.mothership');
this.ship = this.physics.add.image(200, 312, 'invaders.ship');
var bg = this.add.image(0, 0, 'invadersWindow').setOrigin(0);
this.ship.setCollideWorldBounds(true);
this.physics.add.overlap(this.bullet, this.invaders, this.bulletHit, null, this);
this.physics.add.overlap(this.bullet, this.mothership, this.bulletHitMothership, null, this);
this.launchBullet();
this.mothershipTimer = this.time.addEvent({ delay: 10000, callback: this.launchMothership, callbackScope: this, repeat: -1 });
this.invaders.setVelocityX(50);
}
launchMothership ()
{
this.mothership.setVelocityX(-100);
}
bulletHit (bullet, invader)
{
this.launchBullet();
invader.body.enable = false;
this.invaders.killAndHide(invader);
this.refreshOutliers();
}
bulletHitMothership (bullet, mothership)
{
this.launchBullet();
this.mothership.body.reset(500, 40);
}
refreshOutliers ()
{
const list = this.invaders.getChildren();
let first = this.invaders.getFirst(true);
let last = this.invaders.getLast(true);
for (let i = 0; i < list.length; i++)
{
const vader = list[i];
if (vader.active)
{
if (vader.x < first.x)
{
first = vader;
}
else if (vader.x > last.x)
{
last = vader;
}
}
}
if (this.topLeft === null && this.bottomRight === null)
{
this.gameOver();
}
this.topLeft = first;
this.bottomRight = last;
}
launchBullet ()
{
this.bullet.body.reset(this.ship.x, this.ship.y);
this.bullet.body.velocity.y = -400;
}
createInvaders ()
{
this.invaders = this.physics.add.group();
let x = this.invadersBounds.x;
let y = this.invadersBounds.y;
for (let i = 0; i < 10; i++)
{
this.invaders.create(x, y, 'invaders.invader1').setTint(0xff0000).play('invader1');
x += 26;
}
x = this.invadersBounds.x;
y += 28
for (let i = 0; i < 16; i++)
{
this.invaders.create(x, y, 'invaders.invader2').setTint(0x00ff00).play('invader2');
x += 33;
if (i === 7)
{
x = this.invadersBounds.x;
y += 28;
}
}
x = this.invadersBounds.x;
y += 28
for (let i = 0; i < 14; i++)
{
this.invaders.create(x, y, 'invaders.invader3').setTint(0x00ffff).play('invader3');
x += 38;
if (i === 6)
{
x = this.invadersBounds.x;
y += 28;
}
}
// We can use these markers to work out where the whole Group is and how wide it is
this.topLeft = this.invaders.getFirst(true);
this.bottomRight = this.invaders.getLast(true);
}
refresh ()
{
this.cameras.main.setPosition(this.parent.x, this.parent.y);
this.scene.bringToTop();
}
gameOver ()
{
this.invaders.setVelocityX(0);
this.ship.setVisible(false);
this.bullet.setVisible(false);
this.isGameOver = true;
}
update ()
{
if (this.isGameOver || (this.bottomRight === null && this.topLeft === null))
{
return;
}
if (this.left.isDown)
{
this.ship.body.velocity.x = -400;
}
else if (this.right.isDown)
{
this.ship.body.velocity.x = 400;
}
else
{
this.ship.body.velocity.x = 0;
}
// Bullet bounds
if (this.bullet.y < -32)
{
this.launchBullet();
}
// Invaders bounds
let moveDown = false;
if (this.bottomRight.body.velocity.x > 0 && this.bottomRight.x >= 390)
{
this.invaders.setVelocityX(-50);
moveDown = true;
}
else if (this.topLeft.body.velocity.x < 0 && this.topLeft.x <= 12)
{
this.invaders.setVelocityX(50);
moveDown = true;
}
if (moveDown)
{
const list = this.invaders.getChildren();
let lowest = 0;
for (let i = 0; i < list.length; i++)
{
const vader = list[i];
vader.body.y += 4;
if (vader.active && vader.body.y > lowest)
{
lowest = vader.body.y;
}
}
if (lowest > 240)
{
this.gameOver();
}
}
}
}
Invaders.WIDTH = 408;
Invaders.HEIGHT = 326;
Инициализация сцены и настройка ввода
Класс Invaders наследуется от Phaser.Scene. В конструкторе инициализируются свойства для хранения ссылок на игровые объекты и состояния.
В методе create происходит первичная настройка сцены. Сначала создаются объекты для управления с клавиатуры, используя Phaser.Input.Keyboard.KeyCodes. Затем задаются границы физического мира и настраивается вид камеры с заданным цветом фона. Важно отметить, что камера позиционируется относительно переданных извне координат (this.parent.x, this.parent.y), что позволяет встраивать эту сцену как часть более крупного проекта.
this.left = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT);
this.right = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT);
this.physics.world.setBounds(4, 22, 400, 300);
this.cameras.main.setViewport(this.parent.x, this.parent.y, Invaders.WIDTH, Invaders.HEIGHT);
this.cameras.main.setBackgroundColor('#000');
Создание и управление группами объектов (Groups)
Одна из центральных механик — управление флотом пришельцев как единым целым. Для этого используется Physics Group. Метод createInvaders создает группу this.invaders и заполняет ее спрайтами в три ряда, используя циклы и заранее заданные координаты из this.invadersBounds. Каждому пришельцу задается текстура, оттенок (setTint) и запускается анимация (play).
Группа позволяет управлять всеми объектами одновременно, например, задавать общую скорость. Также сохраняются ссылки на крайние объекты (this.topLeft, this.bottomRight) для последующей проверки столкновений с границами экрана.
this.invaders = this.physics.add.group();
// ... создание спрайтов в цикле
this.invaders.create(x, y, 'invaders.invader1').setTint(0xff0000).play('invader1');
// ...
this.topLeft = this.invaders.getFirst(true);
this.bottomRight = this.invaders.getLast(true);
Обработка столкновений и игровая логика
Физика столкновений настраивается с помощью метода this.physics.add.overlap. Он регистрирует обработчики для двух пар объектов: пули и пришельцев, а также пули и материнского корабля. При срабатывании коллбэка bulletHit пуля перезапускается, а пришелец деактивируется через body.enable = false и скрывается из группы методом killAndHide. После этого вызывается refreshOutliers, который пересчитывает крайние активные объекты в группе.
Для автоматического запуска пуль и появления материнского корабля используются таймеры Phaser.
this.physics.add.overlap(this.bullet, this.invaders, this.bulletHit, null, this);
this.physics.add.overlap(this.bullet, this.mothership, this.bulletHitMothership, null, this);
this.mothershipTimer = this.time.addEvent({ delay: 10000, callback: this.launchMothership, callbackScope: this, repeat: -1 });
bulletHit (bullet, invader) {
this.launchBullet();
invader.body.enable = false;
this.invaders.killAndHide(invader);
this.refreshOutliers();
}
Игровой цикл и движение флота
Основная игровая логика обновления находится в методе update. Здесь обрабатывается ввод для движения корабля игрока, проверяются границы для пули и реализуется знаменитое "зигзагообразное" движение флота пришельцев.
Движение флота управляется через общую скорость группы this.invaders.setVelocityX. Когда крайний правый или левый пришелец (this.bottomRight или this.topLeft) достигает границы игровой зоны, скорость инвертируется, и весь флот опускается вниз на 4 пикселя. Если флот опускается слишком низко (lowest > 240), игра завершается.
if (this.bottomRight.body.velocity.x > 0 && this.bottomRight.x >= 390) {
this.invaders.setVelocityX(-50);
moveDown = true;
}
// ...
if (moveDown) {
const list = this.invaders.getChildren();
for (let i = 0; i < list.length; i++) {
const vader = list[i];
vader.body.y += 4;
}
}
Что попробовать дальше
Этот пример — отличная основа для создания собственных аркадных игр в Phaser. Он наглядно показывает работу с группами, физикой и базовым игровым циклом. Для экспериментов попробуйте: изменить скорость движения флота и частоту выстрелов, добавить разные типы оружия, реализовать систему очков при уничтожении пришельцев или создать уровни с увеличивающейся сложностью. Также можно расширить логику gameOver, добавив экран с рестартом.
