О чем этот пример
Создание отзывчивых и богатых визуальных эффектов — ключ к захватывающему геймплею. В этой статье мы разберем готовый пример из официального репозитория Phaser, который реализует красочный след из частиц, следующий за курсором мыши. Вы научитесь управлять несколькими эмиттерами одновременно, связывать их поведение и оптимизировать производительность, что пригодится для создания эффектов огня, магии, выхлопов или любого динамического фона.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.55.2.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
t = 0;
move = false;
spark1 = null;
spark0 = null;
whiteSmoke = null;
fire = null;
darkSmoke = null;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('dark-smoke', 'assets/particles/smoke-puff.png');
this.load.image('white-smoke', 'assets/particles/smoke0.png');
this.load.image('fire', 'assets/particles/muzzleflash3.png');
this.load.image('spark0', 'assets/particles/blue.png');
this.load.image('spark1', 'assets/particles/red.png');
}
create ()
{
this.spark0 = this.add.particles('spark0').createEmitter({
x: 400,
y: 300,
speed: { min: -500, max: 500 },
angle: { min: -120, max: -60 },
scale: { min: 0.05, max: 0 },
alpha: { min: 1, max: 0 },
gravityY: 500,
lifespan: 1
});
this.spark0.reserve(1000);
this.spark1 = this.add.particles('spark1').createEmitter({
x: 400,
y: 300,
speed: { min: -100, max: 100 },
angle: { min: -120, max: -60 },
scale: { start: 0, end: 0.4 },
alpha: { start: 1, end: 0, ease: 'Expo.easeIn' },
blendMode: 'SCREEN',
gravityY: 500,
lifespan: 1000
});
this.spark1.reserve(1000);
this.fire = this.add.particles('fire').createEmitter({
x: 400,
y: 300,
speed: { min: 100, max: 200 },
angle: { min: -85, max: -95 },
scale: { start: 0, end: 1, ease: 'Back.easeOut' },
alpha: { start: 1, end: 0, ease: 'Quart.easeOut' },
blendMode: 'SCREEN',
lifespan: 1000
});
this.fire.reserve(1000);
this.whiteSmoke = this.add.particles('white-smoke').createEmitter({
x: 400,
y: 300,
speed: { min: 20, max: 100 },
angle: { min: 0, max: 360},
scale: { start: 1, end: 0},
alpha: { start: 0, end: 0.5},
lifespan: 2000
// active: false
});
this.whiteSmoke.reserve(1000);
this.darkSmoke = this.add.particles('dark-smoke').createEmitter({
x: 400,
y: 300,
speed: { min: 20, max: 100 },
angle: { min: 0, max: 360},
scale: { start: 1, end: 0},
alpha: { start: 0, end: 0.1},
blendMode: 'ADD',
lifespan: 2000
// active: false
});
this.darkSmoke.reserve(1000);
this.fire.onParticleDeath(particle =>
{
this.darkSmoke.setPosition(particle.x, particle.y);
this.whiteSmoke.setPosition(particle.x, particle.y);
this.darkSmoke.emitParticle();
this.whiteSmoke.emitParticle();
});
this.input.on('pointermove', pointer =>
{
this.darkSmoke.setPosition(pointer.x, pointer.y);
this.fire.setPosition(pointer.x, pointer.y);
});
}
update ()
{
this.spark0.x = this.fire.x;
this.spark0.y = this.fire.y;
this.spark1.x = this.fire.x;
this.spark1.y = this.fire.y;
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#000',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Базовый принцип: создание менеджера частиц
Phaser предоставляет систему частиц через фабрику this.add.particles(). Этот метод создает менеджер (ParticleEmitterManager), который управляет текстурами и эмиттерами. Каждый эмиттер (ParticleEmitter) настраивается отдельно, определяя, как именно будут появляться, двигаться и исчезать частицы.
В примере в методе preload загружаются пять отдельных спрайтов для частиц: два вида искр (spark0, spark1), огонь (fire), белый и темный дым (white-smoke, dark-smoke). Именно эти изображения будут использоваться эмиттерами.
this.load.image('dark-smoke', 'assets/particles/smoke-puff.png');
this.load.image('white-smoke', 'assets/particles/smoke0.png');
this.load.image('fire', 'assets/particles/muzzleflash3.png');
this.load.image('spark0', 'assets/particles/blue.png');
this.load.image('spark1', 'assets/particles/red.png');
Настройка отдельных эмиттеров
В методе create создается пять эмиттеров. Каждый эмиттер конфигурируется объектом с параметрами. Давайте разберем ключевые параметры на примере первого эмиттера spark0.
* speed и angle: Задают вектор движения. Здесь speed от -500 до 500 пикселей в секунду, а angle от -120 до -60 градусов (вверх и влево-вправо).
* scale и alpha: Контролируют размер и прозрачность частицы на протяжении ее жизни (lifespan). Можно задавать min/max или start/end.
* gravityY: Симулирует гравитацию, заставляя частицы падать вниз.
* blendMode: Режим наложения, например 'SCREEN' или 'ADD', для создания эффекта свечения.
Важный этап — вызов reserve(1000). Он предварительно выделяет память под 1000 частиц для этого эмиттера, предотвращая лаги в момент их создания во время работы эффекта.
this.spark0 = this.add.particles('spark0').createEmitter({
x: 400,
y: 300,
speed: { min: -500, max: 500 },
angle: { min: -120, max: -60 },
scale: { min: 0.05, max: 0 },
alpha: { min: 1, max: 0 },
gravityY: 500,
lifespan: 1
});
this.spark0.reserve(1000);
Взаимодействие эмиттеров: цепные реакции
Самый интересный аспект примера — связь между эмиттерами. Эмиттер fire (огонь) привязан к слушателю события pointermove, поэтому он следует за курсором мыши.
Более того, на эмиттере fire регистрируется обработчик события onParticleDeath. Каждый раз, когда частица огня завершает свой жизненный цикл и исчезает, этот обработчик срабатывает. Внутри него мы берем координаты умершей частицы (particle.x, particle.y) и устанавливаем на это же положение эмиттеры дыма (darkSmoke и whiteSmoke), после чего принудительно испускаем по одной новой частице дыма с помощью emitParticle(). Это создает эффект, будто дым появляется там, где только что погас огонек.
this.fire.onParticleDeath(particle =>
{
this.darkSmoke.setPosition(particle.x, particle.y);
this.whiteSmoke.setPosition(particle.x, particle.y);
this.darkSmoke.emitParticle();
this.whiteSmoke.emitParticle();
});
this.input.on('pointermove', pointer =>
{
this.darkSmoke.setPosition(pointer.x, pointer.y);
this.fire.setPosition(pointer.x, pointer.y);
});
Динамическое обновление в игровом цикле
Чтобы эффект выглядел целостно, эмиттеры искр (spark0 и spark1) должны постоянно следовать за основным эмиттером огня (fire). Поскольку их положение не обновляется в обработчике мыши напрямую, эта задача выполняется в методе update, который вызывается каждый кадр.
Здесь мы просто присваиваем координатам искр текущие координаты огня. Это гарантирует, что источник искр всегда будет находиться в той же точке, что и визуальный центр эффекта, даже когда он движется.
update ()
{
this.spark0.x = this.fire.x;
this.spark0.y = this.fire.y;
this.spark1.x = this.fire.x;
this.spark1.y = this.fire.y;
}
Что попробовать дальше
Разобранный пример демонстрирует мощный паттерн: использование нескольких связанных эмиттеров для создания сложного составного эффекта. Вы можете экспериментировать: измените параметры speed, angle или gravityY, чтобы искры летели в другую сторону, а дым был тяжелее. Попробуйте заменить текстуры на свои, чтобы создать след из магических runes или технологических искр. Отключите следование за мышью и привяжите систему частиц к игровому персонажу для создания постоянного ауры или шлейфа.
