О чем этот пример
Частицы в играх — это не просто точки или статичные спрайты. Это дым, огонь, магические эффекты и летящие листья. Стандартный эмиттер Phaser хорошо справляется с движением, но что, если нужно, чтобы каждая частица проигрывала собственную анимацию? В этой статье мы разберём пример создания пользовательского класса частиц, который позволяет каждой из них быть мини-спрайтом с независимой анимацией. Этот подход открывает двери для создания сложных и живых визуальных эффектов, которые значительно украсят вашу игру.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.55.2.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class AnimatedParticle extends Phaser.GameObjects.Particles.Particle
{
constructor (emitter)
{
super(emitter);
this.t = 0;
this.i = 0;
}
update (delta, step, processors)
{
var result = super.update(delta, step, processors);
this.t += delta;
if (this.t >= anim.msPerFrame)
{
this.i++;
if (this.i > 17)
{
this.i = 0;
}
this.frame = anim.frames[this.i].frame;
this.t -= anim.msPerFrame;
}
return result;
}
}
let config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000',
parent: 'phaser-example',
scene: {
preload: preload,
create: create
}
};
let anim;
var game = new Phaser.Game(config);
function preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('mummy', 'assets/animations/mummy37x45.png', { frameWidth: 37, frameHeight: 45 });
}
function create ()
{
let config = {
key: 'walk',
frames: this.anims.generateFrameNumbers('mummy'),
frameRate: 18,
repeat: -1
};
anim = this.anims.create(config);
let particles = this.add.particles('mummy');
let emitter = particles.createEmitter({
x: 100,
y: 100,
frame: 0,
quantity: 1,
frequency: 200,
angle: { min: 0, max: 30 },
speed: 200,
gravityY: 100,
lifespan: { min: 1000, max: 2000 },
particleClass: AnimatedParticle
});
}
Зачем нужны кастомные частицы?
По умолчанию ParticleEmitter в Phaser управляет такими свойствами частиц, как позиция, скорость, масштаб и альфа-канал. Однако, если вы попытаетесь использовать кадры анимации через конфиг эмиттера, все частицы будут синхронно переключать кадры, что выглядит неестественно для эффектов вроде разлетающихся искр или падающих листьев.
Решение — создать собственный класс, унаследованный от Phaser.GameObjects.Particles.Particle, и переопределить его метод update. Это даст вам полный контроль над логикой жизни каждой отдельной частицы, включая смену её кадров.
Создаём класс AnimatedParticle
Класс AnimatedParticle расширяет функциональность стандартной частицы. В его конструкторе мы инициализируем два счётчика: `tдля отслеживания времени иi` для индекса текущего кадра анимации.
class AnimatedParticle extends Phaser.GameObjects.Particles.Particle
{
constructor (emitter)
{
super(emitter);
this.t = 0;
this.i = 0;
}
Ключевой метод — update. Он вызывается на каждом кадре для каждой частицы. Сначала мы вызываем родительский метод super.update(), который обновляет физику (позицию, скорость) частицы. Затем добавляем свою логику для анимации.
update (delta, step, processors)
{
var result = super.update(delta, step, processors);
this.t += delta;
if (this.t >= anim.msPerFrame)
{
this.i++;
if (this.i > 17)
{
this.i = 0;
}
this.frame = anim.frames[this.i].frame;
this.t -= anim.msPerFrame;
}
return result;
}
}
Логика проста: мы накапливаем время this.t. Как только оно превышает время показа одного кадра анимации (anim.msPerFrame), мы переходим к следующему кадру (this.i++), сбрасываем индекс после последнего кадра и, что самое важное, присваиваем свойству this.frame новый кадр. Это заставляет частицу отрисовываться с другим спрайтом из листа.
Подготовка анимации и создание эмиттера
Перед использованием частиц необходимо создать анимацию, кадры которой будут использоваться. В методе preload загружается спрайтшит, а в create — создаётся анимация walk.
function create ()
{
let config = {
key: 'walk',
frames: this.anims.generateFrameNumbers('mummy'),
frameRate: 18,
repeat: -1
};
anim = this.anims.create(config);
Обратите внимание, что глобальная переменная anim сохраняет ссылку на созданную анимацию. Это нужно, потому что наш класс AnimatedParticle обращается к anim.msPerFrame и anim.frames.
Далее создаётся система частиц (this.add.particles) и эмиттер. Ключевой параметр — particleClass.
let particles = this.add.particles('mummy');
let emitter = particles.createEmitter({
x: 100,
y: 100,
frame: 0, // Стартовый кадр
quantity: 1,
frequency: 200,
angle: { min: 0, max: 30 },
speed: 200,
gravityY: 100,
lifespan: { min: 1000, max: 2000 },
particleClass: AnimatedParticle // Указываем наш класс
});
}
Указав particleClass: AnimatedParticle, мы сообщаем эмиттеру, что вместо стандартных частиц нужно создавать экземпляры нашего класса. Теперь каждая выпущенная частица будет независимо проигрывать анимацию с нуля.
Как это работает в деталях
Важно понимать поток данных:
1. Эмиттер создаёт новую частицу, вызывая конструктор AnimatedParticle. Ей присваивается начальный кадр (frame: 0 из конфига эмиттера).
2. На каждом игровом кадре для каждой "живой" частицы вызывается её метод update.
3. В update сначала выполняется базовое движение (через super.update()).
4. Затем наша логика проверяет, не пришло ли время сменить кадр, используя anim.msPerFrame. Переменная anim должна быть доступна в глобальной области видимости для этого примера.
5. При смене кадра мы напрямую меняем свойство this.frame частицы. Это свойство отвечает за то, какой прямоугольник из текстуры будет отрисован.
Таким образом, мы получаем поток частиц, где каждая "мумия" бежит в своём ритме, создавая хаотичный и живой эффект.
Что попробовать дальше
Создание собственного класса частиц — мощный инструмент для тонкой настройки визуальных эффектов в Phaser. Показанный пример с анимацией — лишь отправная точка. Вы можете экспериментировать: добавлять в класс частицы случайную начальную фазу анимации (this.i = Phaser.Math.Between(0, 17)), привязывать смену кадра не ко времени, а к пройденному расстоянию, или менять не только кадр, но и, например, режим блендинга (this.blendMode). Это открывает возможности для создания сложных эффектов вроде бегущей по воде ряби, мерцающих звёзд или рассыпающихся на анимированные осколки предметов.
