О чем этот пример
В этой статье мы разберем пример космического шутера на движке Phaser. Вы научитесь создавать игровые объекты с физикой, управлять кораблем с помощью клавиатуры, реализовывать пул снарядов для оптимизации стрельбы и добавлять визуальные эффекты, такие как частицы выхлопа и параллакс-скроллинг фона. Этот пример — отличная основа для вашей собственной аркадной игры.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Bullet extends Phaser.Physics.Arcade.Image
{
constructor (scene)
{
super(scene, 0, 0, 'space', 'blaster');
this.setBlendMode(1);
this.setDepth(1);
this.speed = 1000;
this.lifespan = 1000;
this._temp = new Phaser.Math.Vector2();
}
fire (ship)
{
this.lifespan = 1000;
this.setActive(true);
this.setVisible(true);
this.setAngle(ship.body.rotation);
this.setPosition(ship.x, ship.y);
this.body.reset(ship.x, ship.y);
const angle = Phaser.Math.DegToRad(ship.body.rotation);
this.scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);
this.body.velocity.x *= 2;
this.body.velocity.y *= 2;
}
update (time, delta)
{
this.lifespan -= delta;
if (this.lifespan <= 0)
{
this.setActive(false);
this.setVisible(false);
this.body.stop();
}
}
}
class Example extends Phaser.Scene
{
lastFired = 0;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('background', 'assets/tests/space/nebula.jpg');
this.load.image('stars', 'assets/tests/space/stars.png');
this.load.atlas('space', 'assets/tests/space/space.png', 'assets/tests/space/space.json');
}
create ()
{
// Prepare some spritesheets and animations
this.textures.addSpriteSheetFromAtlas('mine-sheet', { atlas: 'space', frame: 'mine', frameWidth: 64 });
this.textures.addSpriteSheetFromAtlas('asteroid1-sheet', { atlas: 'space', frame: 'asteroid1', frameWidth: 96 });
this.textures.addSpriteSheetFromAtlas('asteroid2-sheet', { atlas: 'space', frame: 'asteroid2', frameWidth: 96 });
this.textures.addSpriteSheetFromAtlas('asteroid3-sheet', { atlas: 'space', frame: 'asteroid3', frameWidth: 96 });
this.textures.addSpriteSheetFromAtlas('asteroid4-sheet', { atlas: 'space', frame: 'asteroid4', frameWidth: 64 });
this.anims.create({ key: 'mine-anim', frames: this.anims.generateFrameNumbers('mine-sheet', { start: 0, end: 15 }), frameRate: 20, repeat: -1 });
this.anims.create({ key: 'asteroid1-anim', frames: this.anims.generateFrameNumbers('asteroid1-sheet', { start: 0, end: 24 }), frameRate: 20, repeat: -1 });
this.anims.create({ key: 'asteroid2-anim', frames: this.anims.generateFrameNumbers('asteroid2-sheet', { start: 0, end: 24 }), frameRate: 20, repeat: -1 });
this.anims.create({ key: 'asteroid3-anim', frames: this.anims.generateFrameNumbers('asteroid3-sheet', { start: 0, end: 24 }), frameRate: 20, repeat: -1 });
this.anims.create({ key: 'asteroid4-anim', frames: this.anims.generateFrameNumbers('asteroid4-sheet', { start: 0, end: 23 }), frameRate: 20, repeat: -1 });
// World size is 8000 x 6000
this.bg = this.add.tileSprite(400, 300, 800, 600, 'background').setScrollFactor(0);
// Add our planets, etc
this.add.image(512, 680, 'space', 'blue-planet').setOrigin(0).setScrollFactor(0.6);
this.add.image(2833, 1246, 'space', 'brown-planet').setOrigin(0).setScrollFactor(0.6);
this.add.image(3875, 531, 'space', 'sun').setOrigin(0).setScrollFactor(0.6);
const galaxy = this.add.image(5345 + 1024, 327 + 1024, 'space', 'galaxy').setBlendMode(1).setScrollFactor(0.6);
this.add.image(908, 3922, 'space', 'gas-giant').setOrigin(0).setScrollFactor(0.6);
this.add.image(3140, 2974, 'space', 'brown-planet').setOrigin(0).setScrollFactor(0.6).setScale(0.8).setTint(0x882d2d);
this.add.image(6052, 4280, 'space', 'purple-planet').setOrigin(0).setScrollFactor(0.6);
for (let i = 0; i < 8; i++)
{
this.add.image(Phaser.Math.Between(0, 8000), Phaser.Math.Between(0, 6000), 'space', 'eyes').setBlendMode(1).setScrollFactor(0.8);
}
this.stars = this.add.tileSprite(400, 300, 800, 600, 'stars').setScrollFactor(0);
const emitter = this.add.particles(0, 0, 'space', {
frame: 'blue',
speed: 100,
lifespan: {
onEmit: (particle, key, t, value) =>
{
return Phaser.Math.Percent(this.ship.body.speed, 0, 300) * 2000;
}
},
alpha: {
onEmit: (particle, key, t, value) =>
{
return Phaser.Math.Percent(this.ship.body.speed, 0, 300);
}
},
angle: {
onEmit: (particle, key, t, value) =>
{
return (this.ship.angle - 180) + Phaser.Math.Between(-10, 10);
}
},
scale: { start: 0.6, end: 0 },
blendMode: 'ADD'
});
this.bullets = this.physics.add.group({
classType: Bullet,
maxSize: 30,
runChildUpdate: true
});
this.ship = this.physics.add.image(4000, 3000, 'space', 'ship').setDepth(2);
this.ship.setDrag(300);
this.ship.setAngularDrag(400);
this.ship.setMaxVelocity(600);
emitter.startFollow(this.ship);
this.cameras.main.startFollow(this.ship);
this.cursors = this.input.keyboard.createCursorKeys();
this.fire = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
this.add.sprite(4300, 3000).play('asteroid1-anim');
this.tweens.add({
targets: galaxy,
angle: 360,
duration: 100000,
ease: 'Linear',
loop: -1
});
}
update (time, delta)
{
const { left, right, up } = this.cursors;
if (left.isDown)
{
this.ship.setAngularVelocity(-150);
}
else if (right.isDown)
{
this.ship.setAngularVelocity(150);
}
else
{
this.ship.setAngularVelocity(0);
}
if (up.isDown)
{
this.physics.velocityFromRotation(this.ship.rotation, 600, this.ship.body.acceleration);
}
else
{
this.ship.setAcceleration(0);
}
if (this.fire.isDown && time > this.lastFired)
{
const bullet = this.bullets.get();
if (bullet)
{
bullet.fire(this.ship);
this.lastFired = time + 100;
}
}
this.bg.tilePositionX += this.ship.body.deltaX() * 0.5;
this.bg.tilePositionY += this.ship.body.deltaY() * 0.5;
this.stars.tilePositionX += this.ship.body.deltaX() * 2;
this.stars.tilePositionY += this.ship.body.deltaY() * 2;
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scene: Example
};
const game = new Phaser.Game(config);
Настройка сцены и загрузка ресурсов
Класс Example расширяет Phaser.Scene и является основной сценой игры. В методе preload загружаются изображения и атлас текстур для фонов, планет, корабля и астероидов. Обратите внимание на использование setBaseURL для указания базового пути к ресурсам.
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('background', 'assets/tests/space/nebula.jpg');
this.load.image('stars', 'assets/tests/space/stars.png');
this.load.atlas('space', 'assets/tests/space/space.png', 'assets/tests/space/space.json');
В create происходит основная инициализация: создаются спрайтшиты и анимации из атласа, добавляются фоновые элементы (планеты, звезды) с различными коэффициентами параллакса (setScrollFactor), создается система частиц (add.particles) для выхлопа корабля, настраивается физическая группа для пула снарядов и создается сам корабль с физическими свойствами.
Класс снаряда: переиспользование объектов
Снаряд реализован как отдельный класс Bullet, расширяющий Phaser.Physics.Arcade.Image. Это позволяет легко управлять его жизненным циклом и физикой.
class Bullet extends Phaser.Physics.Arcade.Image
{
constructor (scene)
{
super(scene, 0, 0, 'space', 'blaster');
this.speed = 1000;
this.lifespan = 1000;
}
}
Конструктор задает начальную текстуру, скорость и время жизни снаряда. Метод fire активирует снаряд, устанавливает его позицию и угол поворота, совпадающие с кораблем, и задает начальную скорость с помощью physics.velocityFromRotation. Скорость дополнительно удваивается для более резкого визуального эффекта.
fire (ship)
{
this.setActive(true);
this.setVisible(true);
this.setAngle(ship.body.rotation);
this.scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);
this.body.velocity.x *= 2;
this.body.velocity.y *= 2;
}
Метод update уменьшает lifespan на величину delta (время кадра). Когда время жизни истекает, снаряд деактивируется и становится невидимым, возвращаясь в пул для повторного использования. Это ключевой паттерн для оптимизации в играх с множеством объектов.
Управление кораблем и физическое движение
Корабль создается как физический спрайт с помощью this.physics.add.image. Ему задаются свойства сопротивления и максимальной скорости для создания ощущения инерции в космосе.
this.ship = this.physics.add.image(4000, 3000, 'space', 'ship').setDepth(2);
this.ship.setDrag(300);
this.ship.setAngularDrag(400);
this.ship.setMaxVelocity(600);
В методе update сцены обрабатывается ввод с клавиатуры. Стрелки влево/вправо управляют угловой скоростью (setAngularVelocity), а стрелка вверх — линейным ускорением. Ускорение рассчитывается методом physics.velocityFromRotation на основе текущего угла поворота корабля.
if (up.isDown)
{
this.physics.velocityFromRotation(this.ship.rotation, 600, this.ship.body.acceleration);
}
else
{
this.ship.setAcceleration(0);
}
Камера следует за кораблем благодаря this.cameras.main.startFollow(this.ship).
Система стрельбы и пул объектов
Стрельба реализована с помощью физической группы (PhysicsGroup). Группа создается с указанием класса Bullet, максимального размера пула и флагом runChildUpdate, который автоматически вызывает метод update у каждого активного снаряда.
this.bullets = this.physics.add.group({
classType: Bullet,
maxSize: 30,
runChildUpdate: true
});
Выстрел происходит при нажатии пробела. Метод this.bullets.get() извлекает первый неактивный снаряд из пула. Если такой найден, у него вызывается метод fire, и устанавливается задержка до следующего выстрела (this.lastFired).
if (this.fire.isDown && time > this.lastFired)
{
const bullet = this.bullets.get();
if (bullet)
{
bullet.fire(this.ship);
this.lastFired = time + 100;
}
}
Такой подход предотвращает создание и удаление объектов в реальном времени, что положительно сказывается на производительности.
Визуальные эффекты: частицы и параллакс
Система частиц (ParticleEmitter) создает эффект выхлопа корабля. Эмиттер следует за кораблем (startFollow), а его параметры (длительность жизни lifespan, прозрачность alpha, угол angle) динамически меняются в зависимости от скорости корабля с помощью колбэков onEmit.
lifespan: {
onEmit: (particle, key, t, value) => {
return Phaser.Math.Percent(this.ship.body.speed, 0, 300) * 2000;
}
}
Эффект параллакса достигается за счет смещения текстур фона (tilePositionX/Y) на величину, пропорциональную перемещению корабля (ship.body.deltaX/Y). Разные множители (0.5 для туманности, 2 для звезд) создают ощущение глубины.
this.bg.tilePositionX += this.ship.body.deltaX() * 0.5;
this.stars.tilePositionX += this.ship.body.deltaX() * 2;
Что попробовать дальше
Мы разобрали ключевые компоненты динамичного космического шутера на Phaser: от создания физических объектов и управления ими до оптимизации через пулы и добавления сочных визуальных эффектов. Для экспериментов попробуйте: изменить параметры физики корабля (сопротивление, скорость), добавить разные типы снарядов, реализовать столкновения снарядов с астероидами (overlap) или создать более сложное поведение для врагов. Исходный код дает мощную и гибкую основу для ваших идей.
