О чем этот пример
Настройка системы частиц — ключевой элемент для создания атмосферных эффектов в играх: огня, дыма, магии или звёздной пыли. Ручной подбор параметров через код может быть долгим и неудобным. В этой статье мы разберём готовый редактор частиц, который позволяет на лету менять все свойства эмиттера через графический интерфейс и сразу видеть результат. Этот подход ускоряет итерации и делает процесс творческим.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.55.2.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
blendModes = {
NORMAL: Phaser.BlendModes.NORMAL,
ADD: Phaser.BlendModes.ADD,
MULTIPLY: Phaser.BlendModes.MULTIPLY,
SCREEN: Phaser.BlendModes.SCREEN
};
eases = [
'Linear',
'Quad.easeIn',
'Cubic.easeIn',
'Quart.easeIn',
'Quint.easeIn',
'Sine.easeIn',
'Expo.easeIn',
'Circ.easeIn',
'Back.easeIn',
'Bounce.easeIn',
'Quad.easeOut',
'Cubic.easeOut',
'Quart.easeOut',
'Quint.easeOut',
'Sine.easeOut',
'Expo.easeOut',
'Circ.easeOut',
'Back.easeOut',
'Bounce.easeOut',
'Quad.easeInOut',
'Cubic.easeInOut',
'Quart.easeInOut',
'Quint.easeInOut',
'Sine.easeInOut',
'Expo.easeInOut',
'Circ.easeInOut',
'Back.easeInOut',
'Bounce.easeInOut'
].sort();
alphaConfig = {
start: 1, end: 0, ease: 'Linear'
};
scaleConfig = {
start: 1, end: 0, ease: 'Linear'
};
speedConfig = {
min: 0, max: 200
};
angleConfig = {
min: 0, max: 360
};
countText = null;
move = false;
emitter = null;
gui = null;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('spark0', 'assets/particles/blue.png');
this.load.image('spark1', 'assets/particles/red.png');
}
create ()
{
if (typeof dat === 'undefined')
{
this.add.text(16, 16, 'Please [Launch] this example.');
return;
}
this.gui = new dat.GUI();
this.emitter = this.add.particles('spark1').createEmitter({
name: 'sparks',
x: 400,
y: 300,
gravityY: 300,
speed: this.speedConfig,
angle: this.angleConfig,
scale: this.scaleConfig,
alpha: this.alphaConfig,
blendMode: 'SCREEN'
});
this.gui.add(this.emitter, 'name');
this.gui.add(this.emitter, 'on');
this.gui.add(this.emitter, 'blendMode', this.blendModes).name('blend mode').onChange(val => { this.emitter.setBlendMode(Number(val)); });
this.gui.add(this.angleConfig, 'min', 0, 360, 5).name('angle min').onChange(() => { this.emitter.setAngle(this.angleConfig); });
this.gui.add(this.angleConfig, 'max', 0, 360, 5).name('angle max').onChange(() => { this.emitter.setAngle(this.angleConfig); });
this.gui.add({ life: 1000 }, 'life', 100, 5000, 100).onChange(value => { this.emitter.setLifespan(value); });
this.gui.add({ gravityX: 0 }, 'gravityX', -300, 300, 10).onChange(value => { this.emitter.setGravityX(value); });
this.gui.add({ gravityY: 300 }, 'gravityY', -300, 300, 10).onChange(value => { this.emitter.setGravityY(value); });
this.gui.add(this.speedConfig, 'min', 0, 600, 10).name('speed min').onChange(() => { this.emitter.setSpeed(this.speedConfig); });
this.gui.add(this.speedConfig, 'max', 0, 600, 10).name('speed max').onChange(() => { this.emitter.setSpeed(this.speedConfig); });
this.gui.add(this.scaleConfig, 'start', 0, 1, 0.1).name('scale start').onChange(() => { this.emitter.setScale(this.scaleConfig); });
this.gui.add(this.scaleConfig, 'end', 0, 1, 0.1).name('scale end').onChange(() => { this.emitter.setScale(this.scaleConfig); });
this.gui.add(this.scaleConfig, 'ease', this.eases).name('scale ease').onChange(() => { this.emitter.setScale(this.scaleConfig); });
this.gui.add(this.alphaConfig, 'start', 0, 1, 0.1).name('alpha start').onChange(() => { this.emitter.setAlpha(this.alphaConfig); });
this.gui.add(this.alphaConfig, 'end', 0, 1, 0.1).name('alpha end').onChange(() => { this.emitter.setAlpha(this.alphaConfig); });
this.gui.add(this.alphaConfig, 'ease', this.eases).name('alpha ease').onChange(() => { this.emitter.setAlpha(this.alphaConfig); });
this.gui.add(this.emitter, 'killAll');
this.gui.add(this.emitter, 'pause');
this.gui.add(this.emitter, 'resume');
this.gui.add({save: this.saveEmitter.bind(this)}, 'save').name('save JSON');
this.input.on('pointermove', pointer =>
{
if (this.move)
{
this.emitter.setPosition(pointer.x, pointer.y);
}
});
this.input.on('pointerdown', pointer =>
{
this.emitter.setPosition(pointer.x, pointer.y);
this.move = true;
});
this.input.on('pointerup', pointer =>
{
this.move = false;
});
this.countText = this.add.text(0, 0, 'Alive Particles');
}
update ()
{
if (!this.countText) { return; }
this.countText.setText(`Alive Particles: ${this.emitter.getAliveParticleCount()}`);
}
saveEmitter ()
{
this.load.saveJSON(this.emitter.toJSON(), `${this.emitter.name}.json`);
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Инициализация эмиттера частиц
В методе create() создаётся основной объект системы частиц — эмиттер. Мы используем менеджер частиц сцены (this.add.particles) и указываем текстуру для частиц. Конфигурация передаётся объектом с начальными параметрами: позиция, гравитация, скорость, угол разлёта, масштаб, прозрачность и режим наложения.
this.emitter = this.add.particles('spark1').createEmitter({
name: 'sparks',
x: 400,
y: 300,
gravityY: 300,
speed: this.speedConfig,
angle: this.angleConfig,
scale: this.scaleConfig,
alpha: this.alphaConfig,
blendMode: 'SCREEN'
});
Ключевые параметры:
- speed, angle: задаются объектами с полями min и max для случайного диапазона.
- scale, alpha: используют объекты конфигурации с полями start, end и ease для плавной интерполяции.
- blendMode: определяет, как цвет частиц смешивается с фоном. Здесь используется 'SCREEN' для эффекта свечения.
Подключение GUI для интерактивной настройки
Для управления параметрами в реальном времени используется библиотека dat.GUI. Каждый контрол связан с конкретным свойством эмиттера или конфигурационным объектом. При изменении значения в интерфейсе вызывается соответствующий метод set эмиттера, который немедленно применяет новую конфигурацию к частицам.
this.gui.add(this.angleConfig, 'min', 0, 360, 5).name('angle min').onChange(() => {
this.emitter.setAngle(this.angleConfig);
});
Этот код создаёт слайдер для минимального угла. При любом изменении вызывается this.emitter.setAngle(this.angleConfig), который перечитывает обновлённые min и max из объекта this.angleConfig. Аналогично настраиваются скорость, гравитация, прозрачность и масштаб.
Важный момент: для режима наложения (blendMode) используется преобразование строкового значения в число, так как setBlendMode ожидает числовую константу.
this.gui.add(this.emitter, 'blendMode', this.blendModes).name('blend mode').onChange(val => {
this.emitter.setBlendMode(Number(val));
});
Управление эмиттером с помощью мыши
Чтобы визуально тестировать эффекты, эмиттер можно перемещать по сцене. Логика обработки ввода реализована через слушатели событий указателя (pointer).
this.input.on('pointermove', pointer =>
{
if (this.move)
{
this.emitter.setPosition(pointer.x, pointer.y);
}
});
Флаг this.move становится true при нажатии кнопки мыши (pointerdown) и сбрасывается при отпускании (pointerup). Во время движения с зажатой кнопкой эмиттер постоянно обновляет свою позицию, следуя за курсором. Это позволяет оценить, как частицы взаимодействуют с разными областями игрового мира.
Мониторинг и сохранение конфигурации
В методе update() отображается количество «живых» частиц в системе. Это полезно для контроля производительности и балансировки параметров (например, lifespan и frequency).
this.countText.setText(`Alive Particles: ${this.emitter.getAliveParticleCount()}`);
Финальный штрих — возможность экспорта настроек в JSON-файл. Метод saveEmitter использует встроенный метод эмиттера toJSON(), который сериализует все его текущие параметры. Затем this.load.saveJSON сохраняет данные в файл, имя которого основано на свойстве name эмиттера.
saveEmitter ()
{
this.load.saveJSON(this.emitter.toJSON(), `${this.emitter.name}.json`);
}
Сохранённый JSON можно позже загрузить и использовать для воссоздания точно такого же эффекта в другом месте игры, обеспечивая согласованность.
Что попробовать дальше
Встроенный редактор частиц превращает тонкую настройку визуальных эффектов из рутины в увлекательный процесс. Вы можете мгновенно экспериментировать с десятками параметров и находить идеальный вид для огня, взрыва или ауры. Для дальнейших экспериментов попробуйте: заменить текстуру частиц на анимированный спрайт; добавить несколько эмиттеров с разными настройками для сложных эффектов (например, дым + искры); привязать эмиттер к движущемуся игровому объекту; использовать сохранённый JSON для динамической загрузки эффектов в зависимости от уровня.
