О чем этот пример
Динамичное взаимодействие объектов — основа увлекательного игрового процесса. В этой статье мы разберем, как реализовать эффект взрывной волны, которая отбрасывает физические тела в зависимости от их массы и расстояния до эпицентра. Этот прием полезен для создания спецэффектов (взрывы гранат, магические заклинания) или нестандартных механик, например, притягивающих/отталкивающих полей.
Версия 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/sprites/block.png');
this.load.spritesheet('boom', 'assets/sprites/explosion.png', { frameWidth: 64, frameHeight: 64, endFrame: 23 });
}
create ()
{
this.anims.create({
key: 'explode',
frames: 'boom',
frameRate: 20,
showOnStart: true,
hideOnComplete: true
});
const blocks = this.physics.add.group({
defaultKey: 'block',
bounceX: 1,
bounceY: 1,
collideWorldBounds: true,
dragX: 0.5,
dragY: 0.5,
useDamping: true
});
for (let i = 0; i < 10; i++)
{
const block = blocks.create(Phaser.Math.Between(100, 700), Phaser.Math.Between(100, 500));
block.setMass(Phaser.Math.Between(1, 2));
block.setScale(block.body.mass ** 0.5);
}
const boom = this.add.sprite(0, 0, 'boom').setBlendMode('ADD').setScale(4).setVisible(false);
this.input.on('pointerdown', (pointer) =>
{
boom.copyPosition(pointer).play('explode');
const distance = new Phaser.Math.Vector2();
const force = new Phaser.Math.Vector2();
const acceleration = new Phaser.Math.Vector2();
for (const block of blocks.getChildren())
{
distance.copy(block.body.center).subtract(pointer);
force.copy(distance).setLength(5000000 / distance.lengthSq()).limit(1000);
acceleration.copy(force).scale(1 / block.body.mass);
block.body.velocity.add(acceleration);
}
});
}
}
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);
Подготовка сцены и ресурсов
В методе preload загружаются необходимые ресурсы: спрайт блока и спрайтшит для анимации взрыва. Обратите внимание на параметры загрузки спрайтшита: они определяют размер каждого кадра и их общее количество.
this.load.image('block', 'assets/sprites/block.png');
this.load.spritesheet('boom', 'assets/sprites/explosion.png', { frameWidth: 64, frameHeight: 64, endFrame: 23 });
В create первым делом создается анимация взрыва из загруженного спрайтшита. Ключевые параметры: showOnStart и hideOnComplete автоматически управляют видимостью спрайта, что очень удобно для одноразовых анимаций.
this.anims.create({
key: 'explode',
frames: 'boom',
frameRate: 20,
showOnStart: true,
hideOnComplete: true
});
Создание и настройка физической группы
Вместо создания каждого объекта по отдельности, мы используем физическую группу (this.physics.add.group). Это эффективно для управления множеством однотипных тел. Группе задаются общие свойства: упругость (bounce), границы мира, сопротивление (drag) и использование демпфирования (useDamping) для более плавного замедления.
const blocks = this.physics.add.group({
defaultKey: 'block',
bounceX: 1,
bounceY: 1,
collideWorldBounds: true,
dragX: 0.5,
dragY: 0.5,
useDamping: true
});
В цикле создаются 10 блоков в случайных позициях. Для каждого блока задается масса (setMass) — ключевой параметр для расчета физики. Масштаб блока визуально привязан к массе, чтобы более тяжелые объекты выглядели крупнее.
const block = blocks.create(Phaser.Math.Between(100, 700), Phaser.Math.Between(100, 500));
block.setMass(Phaser.Math.Between(1, 2));
block.setScale(block.body.mass ** 0.5);
Логика взрывной волны на указатель мыши
Создается спрайт взрыва, который изначально невидим. Он будет проигрывать анимацию в точке клика.
const boom = this.add.sprite(0, 0, 'boom').setBlendMode('ADD').setScale(4).setVisible(false);
Обработчик события pointerdown — сердце примера. При клике взрыв визуализируется, а затем для каждого блока в группе рассчитывается и применяется сила.
this.input.on('pointerdown', (pointer) => {
boom.copyPosition(pointer).play('explode');
// ... расчет силы
});
Расчет физической силы и ускорения
Внутри цикла по всем блокам происходит основной расчет. Создаются три временных вектора для промежуточных вычислений, чтобы избежать создания новых объектов в каждом кадре.
1. **Вектор расстояния:** Определяется направление от точки взрыва к центру блока (block.body.center).
distance.copy(block.body.center).subtract(pointer);
2. **Вектор силы:** Сила обратно пропорциональна квадрату расстояния (1 / distance.lengthSq()), что моделирует реальный физический закон. Чем дальше объект, тем слабее воздействие. Сила также ограничивается (limit(1000)) для избежания аномальных значений.
force.copy(distance).setLength(5000000 / distance.lengthSq()).limit(1000);
3. **Вектор ускорения:** Согласно второму закону Ньютона, ускорение = сила / масса. Для тяжелых блоков одинаковое усилие создает меньшее ускорение.
acceleration.copy(force).scale(1 / block.body.mass);
4. **Применение:** Рассчитанное ускорение добавляется к текущей скорости тела через block.body.velocity.add().
block.body.velocity.add(acceleration);
Что попробовать дальше
Мы реализовали взрывную волну, физически корректно учитывающую массу объектов и расстояние до эпицентра. Для экспериментов попробуйте изменить закон убывания силы (например, линейный), добавить разный коэффициент сопротивления для блоков или создать не взрыв, а постоянное силовое поле, которое активируется при удержании кнопки мыши.
