О чем этот пример
В игровом мире важно, чтобы объекты взаимодействовали друг с другом. Например, игрок должен подбирать аптечки для восстановления здоровья. В движке Phaser за такое взаимодействие отвечает физический движок Arcade, а именно — система перекрытий (overlap). Этот пример наглядно показывает, как настроить сбор предметов, используя статическую группу и обработку столкновений. Вы научитесь создавать игровые механики сбора ресурсов с постоянной угрозой, что является основой для аркадных и roguelike игр.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
timedEvent;
maxHealth = 100;
currentHealth = 100;
cursors;
text;
healthGroup;
sprite;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('cat', 'assets/sprites/orange-cat1.png');
this.load.image('health', 'assets/sprites/firstaid.png');
}
create ()
{
this.sprite = this.physics.add.image(400, 300, 'cat');
this.sprite.setCollideWorldBounds(true);
// Create 10 random health pick-ups
this.healthGroup = this.physics.add.staticGroup({
key: 'health',
frameQuantity: 10,
immovable: true
});
const children = this.healthGroup.getChildren();
for (let i = 0; i < children.length; i++)
{
const x = Phaser.Math.Between(50, 750);
const y = Phaser.Math.Between(50, 550);
children[i].setPosition(x, y);
}
this.healthGroup.refresh();
// So we can see how much health we have left
this.text = this.add.text(10, 10, 'Health: 100', { font: '32px Courier', fill: '#000000' });
// Cursors to move
this.cursors = this.input.keyboard.createCursorKeys();
// When the player sprite his the health packs, call this function ...
this.physics.add.overlap(this.sprite, this.healthGroup, this.spriteHitHealth, null, this);
// Decrease the health by calling reduceHealth every 50ms
this.timedEvent = this.time.addEvent({ delay: 50, callback: this.reduceHealth, callbackScope: this, loop: true });
}
update ()
{
if (this.currentHealth === 0)
{
return;
}
this.text.setText(`Health: ${this.currentHealth}`);
this.sprite.setVelocity(0);
if (this.cursors.left.isDown)
{
this.sprite.setVelocityX(-200);
}
else if (this.cursors.right.isDown)
{
this.sprite.setVelocityX(200);
}
if (this.cursors.up.isDown)
{
this.sprite.setVelocityY(-200);
}
else if (this.cursors.down.isDown)
{
this.sprite.setVelocityY(200);
}
}
reduceHealth ()
{
this.currentHealth--;
if (this.currentHealth === 0)
{
// Uh oh, we're dead
this.sprite.body.reset(400, 300);
this.text.setText('Health: RIP');
// Stop the timer
this.timedEvent.remove();
}
}
spriteHitHealth (sprite, health)
{
// Hide the sprite
this.healthGroup.killAndHide(health);
// And disable the body
health.body.enable = false;
// Add 10 health, it'll never go over maxHealth
this.currentHealth = Phaser.Math.MaxAdd(this.currentHealth, 10, this.maxHealth);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#2d6b2d',
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
gravity: 0
}
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и загрузка ресурсов
В классе Example, расширяющем Phaser.Scene, мы объявляем переменные для хранения состояния игры: таймер, здоровье, элементы управления, текстовое поле, группу здоровья и главного спрайта.
Метод preload() отвечает за загрузку изображений. Обратите внимание, что базовый URL задаётся один раз для всех ресурсов.
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('cat', 'assets/sprites/orange-cat1.png');
this.load.image('health', 'assets/sprites/firstaid.png');
Здесь мы загружаем два спрайта: кота (игровой персонаж) и аптечку (предмет для сбора).
Создание мира и настройка физики
В методе create() происходит инициализация игровых объектов. Сначала создаётся физический спрайт кота, который будет сталкиваться с границами мира.
this.sprite = this.physics.add.image(400, 300, 'cat');
this.sprite.setCollideWorldBounds(true);
Далее создаётся статическая группа healthGroup с 10 аптечками. Ключевое свойство immovable: true гарантирует, что предметы не будут сдвигаться при столкновении. После создания все дети группы размещаются в случайных позициях на экране. Метод refresh() обязателен для статических тел после изменения их позиции.
this.healthGroup = this.physics.add.staticGroup({
key: 'health',
frameQuantity: 10,
immovable: true
});
const children = this.healthGroup.getChildren();
for (let i = 0; i < children.length; i++)
{
const x = Phaser.Math.Between(50, 750);
const y = Phaser.Math.Between(50, 550);
children[i].setPosition(x, y);
}
this.healthGroup.refresh();
Затем настраивается текст для отображения здоровья, управление с клавиатуры и самое важное — обработчик перекрытия между спрайтом кота и группой аптечек с помощью this.physics.add.overlap. Последним аргументом передаётся контекст this для корректной работы колбэка. Также запускается таймер, который каждые 50 мс уменьшает здоровье.
this.physics.add.overlap(this.sprite, this.healthGroup, this.spriteHitHealth, null, this);
this.timedEvent = this.time.addEvent({ delay: 50, callback: this.reduceHealth, callbackScope: this, loop: true });
Игровой цикл и управление
Метод update() выполняется каждый кадр. Если здоровье на нуле, обновление игровой логики прекращается. В противном случае обновляется текст и обрабатывается ввод с клавиш-стрелок, чтобы задавать скорость движения спрайту кота.
if (this.currentHealth === 0)
{
return;
}
this.text.setText(`Health: ${this.currentHealth}`);
this.sprite.setVelocity(0);
if (this.cursors.left.isDown)
{
this.sprite.setVelocityX(-200);
}
else if (this.cursors.right.isDown)
{
this.sprite.setVelocityX(200);
}
if (this.cursors.up.isDown)
{
this.sprite.setVelocityY(-200);
}
else if (this.cursors.down.isDown)
{
this.sprite.setVelocityY(200);
}
Обратите внимание: скорость задаётся отдельно по осям X и Y, что позволяет двигаться по диагонали при одновременном нажатии двух клавиш.
Логика здоровья и сбор предметов
Функция reduceHealth(), вызываемая таймером, уменьшает текущее здоровье на единицу. Когда здоровье достигает нуля, спрайт кота возвращается в начальную позицию методом body.reset(), таймер останавливается, а текст меняется на 'RIP'.
this.currentHealth--;
if (this.currentHealth === 0)
{
this.sprite.body.reset(400, 300);
this.text.setText('Health: RIP');
this.timedEvent.remove();
}
Колбэк spriteHitHealth() срабатывает при перекрытии спрайта кота и любого тела из группы аптечек. Аптечка скрывается и деактивируется, а здоровье игрока увеличивается на 10 единиц, но не превышает максимальное значение. Здесь используется удобная функция Phaser.Math.MaxAdd.
this.healthGroup.killAndHide(health);
health.body.enable = false;
this.currentHealth = Phaser.Math.MaxAdd(this.currentHealth, 10, this.maxHealth);
Важно: метод killAndHide() скрывает спрайт визуально, а отключение тела (body.enable = false) предотвращает дальнейшие физические взаимодействия с этим объектом.
Что попробовать дальше
Этот пример демонстрирует ключевые концепции для создания игр на Phaser: управление физическими телами, работу с группами объектов и обработку их взаимодействий через систему перекрытий. Механика постепенной потери здоровья создаёт постоянное напряжение, а сбор аптечек даёт игроку шанс на выживание. Для экспериментов попробуйте: 1. Изменить скорость потери здоровья или количество восстанавливаемых единиц. 2. Добавить звуковые эффекты при сборе аптечки или смерти. 3. Сделать аптечки не статичными, а динамическими, чтобы они отскакивали от стен. 4. Реализовать систему очков за собранные предметы.
