О чем этот пример
В Phaser работа с физикой часто начинается с использования готовых спрайтов. Но для создания сложных игровых объектов с уникальным поведением необходимо пойти дальше. В этой статье мы разберем, как правильно расширить базовый класс `Phaser.Physics.Arcade.Sprite`, чтобы создавать собственные сущности с предустановленной физикой, анимацией и логикой. Этот подход делает код чище, объекты — управляемее, а их создание — проще.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class SpaceShip extends Phaser.Physics.Arcade.Sprite
{
constructor (scene, x, y)
{
super(scene, x, y, 'ship');
this.play('thrust');
// You can either do this:
scene.add.existing(this);
scene.physics.add.existing(this);
// Or this, the end result is the same
// scene.sys.displayList.add(this);
// scene.sys.updateList.add(this);
// scene.sys.arcadePhysics.world.enableBody(this, 0);
// Set some default physics properties
this.setScale(2);
this.setBounce(1, 1);
this.setCollideWorldBounds(true);
this.body.onWorldBounds = true;
this.setVelocity(0, -200);
}
}
function onWorldBounds (body)
{
body.gameObject.toggleFlipY();
}
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('ship', 'assets/games/lazer/ship.png', { frameWidth: 16, frameHeight: 24 });
}
create ()
{
this.anims.create({
key: 'thrust',
frames: this.anims.generateFrameNumbers('ship', { frames: [ 2, 7 ] }),
frameRate: 20,
repeat: -1
});
for (let i = 0; i < 32; i++)
{
new SpaceShip(this, Phaser.Math.Between(64, 736), Phaser.Math.Between(100, 500));
}
this.physics.world.on('worldbounds', onWorldBounds);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
pixelArt: true,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
debug: false,
gravity: { y: 0 }
}
},
scene: Example
};
const game = new Phaser.Game(config);
Зачем расширять Arcade.Sprite?
Стандартный способ добавить спрайт с физикой — создать объект и вручную настраивать его свойства. Однако при создании десятков однотипных объектов (например, врагов или астероидов) этот код быстро становится громоздким и трудно поддерживаемым.
Создание собственного класса, наследующего от Phaser.Physics.Arcade.Sprite, позволяет:
* **Инкапсулировать логику:** Вся настройка физики, анимаций и начального состояния объекта находится в одном месте — его конструкторе.
* **Упростить создание:** Новый объект создается одной строкой new SpaceShip(...), а не десятком вызовов API.
* **Повторно использовать код:** Класс становится готовым строительным блоком для всей игры.
Исходный пример демонстрирует создание класса космического корабля SpaceShip, который при появлении на сцене уже обладает физикой, анимацией и начальным движением.
Анатомия кастомного класса SpaceShip
Ключевой момент — вызов super() для инициализации родительского класса. В него передаются сцена, координаты и ключ текстуры.
constructor (scene, x, y)
{
super(scene, x, y, 'ship');
Сразу после этого можно работать с объектом. В примере запускается анимация 'thrust'.
this.play('thrust');
Далее объект необходимо добавить в системы отображения и физики сцены. В примере показаны два эквивалентных способа. Первый — рекомендуемый и лаконичный.
// Рекомендуемый способ
scene.add.existing(this);
scene.physics.add.existing(this);
// Альтернативный "ручной" способ
// scene.sys.displayList.add(this);
// scene.sys.updateList.add(this);
// scene.sys.arcadePhysics.world.enableBody(this, 0);
После интеграции со сценой настраиваются физические свойства объекта через методы, унаследованные от Arcade.Sprite.
this.setScale(2);
this.setBounce(1, 1); // Идеальный отскок от всех поверхностей
this.setCollideWorldBounds(true); // Не вылетать за границы мира
this.body.onWorldBounds = true; // Критически важно для события worldbounds
this.setVelocity(0, -200); // Начальная скорость вверх
Интеграция класса в сцену и обработка событий
Класс SpaceShip — это заготовка. Чтобы его использовать, нужно подготовить ассеты в сцене и создать экземпляры. В методе preload загружается спрайтшип, а в create создается анимация.
this.anims.create({
key: 'thrust', // Ключ, который используется в `this.play('thrust')`
frames: this.anims.generateFrameNumbers('ship', { frames: [ 2, 7 ] }),
frameRate: 20,
repeat: -1 // Бесконечное повторение
});
Создание 32 кораблей в случайных позициях становится тривиальной задачей:
for (let i = 0; i < 32; i++)
{
new SpaceShip(this, Phaser.Math.Between(64, 736), Phaser.Math.Between(100, 500));
}
Отдельно настраивается глобальная реакция на столкновение любого физического тела с границами мира. Событие 'worldbounds' генерируется только у тел, у которых свойство body.onWorldBounds установлено в true (мы это сделали в конструкторе).
function onWorldBounds (body)
{
body.gameObject.toggleFlipY(); // Переворачиваем спрайт по вертикали
}
// В create() сцены:
this.physics.world.on('worldbounds', onWorldBounds);
Конфигурация игры и итоговая картина
Конфигурация игры стандартна для использования Arcade Physics. Важный нюанс — pixelArt: true, который отключает сглаживание для пиксельной графики, и нулевая гравитация.
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
pixelArt: true,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
debug: false,
gravity: { y: 0 } // Космос, гравитации нет
}
},
scene: Example
};
В результате получается сцена, где 32 корабля летают, отскакивают от границ экрана и переворачиваются при каждом таком столкновении. Вся бизнес-логика корабля изолирована в его классе, а сцена отвечает только за создание анимации, управление группами объектов и глобальные обработчики событий.
Что попробовать дальше
Расширение Phaser.Physics.Arcade.Sprite — мощный паттерн для создания сложных, самодостаточных игровых объектов. Он превращает спагетти-код в сцене в модульную и понятную архитектуру.
**Идеи для экспериментов:**
1. Добавьте в класс SpaceShip метод takeDamage() и свойство health.
2. Сделайте так, чтобы корабли при создании получали случайную скорость и цветовой оттенок.
3. Перенесите обработчик onWorldBounds внутрь класса SpaceShip в виде метода и подписывайтесь на событие индивидуально для каждого тела.
4. Создайте второй класс, например, Asteroid, и настройте столкновения между ними и SpaceShip с помощью this.physics.add.collider().
