Предисловие
Эффект огня, субъективно, одна из самых распространённых задач для художника по эффектам. Думаю, почти у каждого, кто работает в индустрии от года и более, в портфолио есть эффекты, включающие в себя огонь.
Как следствие и уроков на эту тему в сети не мало. Однако для фреймворка Phaser таких материалов не хватает, на мой взгляд. Вот почему я решил заполнить эту нишу и написать урок о том, как создать красивый эффект огня, используя Phaser.
Весь код и ассеты которые я буду использовать в уроке, доступны по ссылке.
Начальная настройка эмиттера
Давайте сначала создадим простой эмиттер с одним активным параметром — прозрачностью. Альфа-канал у частиц будет меняться от 1 до 0 в течение их жизни.
Все спрайты я предварительно добавил в атлас и в эффекте буду использовать кадры из него.
emitter_v1();
{
this.fireEmitter = this.add.particles(150, 150, 'fire_atlas', {
lifespan: 1500,
frequency: 1000,
frame: ['fire_1'],
alpha: { start: 1, end: 0 },
});
}
Настройка параметров частиц в зависимости от их времени жизни
У предыдущего сетапа есть один важный недостаток: частицы появляются слишком резко. При рождении их альфа сразу равна единице, что не является хорошей практикой. Настройки эмиттера лучше изменить так, чтобы частица рождалась с альфа-значением 0, затем становилась непрозрачной и плавно исчезала к концу своей жизни.
Для этого нам потребуется использовать калбэк onUpdate
и обновлять значение альфы каждый кадр соответственно формуле.
emitter_v2();
{
this.fireEmitter = this.add.particles(150, 150, 'fire_atlas', {
lifespan: 1500,
frequency: 3000,
frame: ['fire_1'],
alpha: {
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI);
},
},
});
}
Добавление скорости и новых кадров для частиц.
Так как мы создаем эффект огня, частицам нужно добавить небольшое движение вверх. Также сейчас хороший момент, чтобы добавить больше кадров в эмиттер.
emitter_v3();
{
this.fireEmitter = this.add.particles(150, 150, 'fire_atlas', {
lifespan: 1000,
frequency: 200,
frame: ['fire_2', 'fire_3'],
alpha: {
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI);
},
},
accelerationY: -100,
sortOrder: 'ascending',
});
}
Можно заметить что я добавил больше кадров, я взял их из аталаса который приготовил заранее. Так же я немного изменил частоту появления частиц и их время жизни. Но более важно что я добавил небольшое ускорение вверх с помошью параметра accelerationY. А так же поменял очередь отрисовки частиц с помощью параметра sortOrder. Теперь новые частицы появляются перед старыми.
Что ж это все больше начинает выглядеть как огонь. Но есть еще большое пространство для улучшения.
Добавление рандома в параметры
Одним из ключевых преимуществ системы частиц является её вариативность. Вы можете сделать красивый разлет частиц в любом редакторе анимаций и вставить в вашу игру. Но он всегда будет выглядеть одинакого, и иногда такой подход более уместен и даже необходим.
Но если вы сделаете тот же разлет с помощью частиц, то благодаря небольшому рандому ваш эффект будет выглядеть уникально какждый раз.
Так что давайте добавим такой рандом в наш эмиттер.
emitter_v4();
{
this.fireEmitter = this.add.particles(150, 150, 'fire_atlas', {
lifespan: 1000,
frequency: 200,
frame: ['fire_2', 'fire_3'],
alpha: {
onEmit: (particle) => {
particle.randomAlpha = 0.5 + Math.random() * 0.5;
},
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI) * particle.randomAlpha;
},
},
speedY: -70,
scaleX: {
onEmit: (particle) => {
particle.randomFlip = Math.random() < 0.5 ? -1 : 1;
},
onUpdate: (particle, key, t, value) => {
return 0.3 + Math.sin(t * Math.PI) * 0.7 * particle.randomAlpha;
},
},
scaleY: {
onUpdate: (particle, key, t, value) => {
return 0.5 + Math.pow(t, 2);
},
},
sortOrder: 'descending',
});
}
Мда... Эмиттер начинает сильно усложнятся. Но сейчас мы разберем все изменения и все снова станет понятным.
Правки альфа канала
На самом старте мы обошлись простой формулой синуса и меняли значение альфы от 0 до 1 и обратно.
alpha: {
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI);
}
},
Но теперь нам этого недостаточно. Теперь мы ввели новую переменную randomAlpha
, эта переменная генерируется при рождении частицы и дает нам рандомное значение от 0.5 до 1. И наша частица меняет свою альфу от 0 до этого рандомного значения и обратно. Таким образомы частицы выглядят менее однородно.
alpha: {
onEmit: (particle) => {
particle.randomAlpha = 0.5 + Math.random() * 0.5;
},
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI) * particle.randomAlpha;
}
},
Правки масштаба
Двигаемся дальше и переходим к настройкам масштаба. В предыдущих итерациях мы его не трогали, но теперь нам нужно добавить эти настройки. Так как нам нужно усилить ощущение того что частицы стремятся вверх, мы будем настраивать оси масштаба по отдельности. Ось Y у нас будет увеличивать не линейно а по более крутой траектории
scaleY: {
onUpdate: (particle, key, t, value) => {
return 0.5 + Math.pow(t, 2);
}
},
Ось X в свою очередь мы настроим отдельно. Изинг роста сделаем более мягким, а главное, добавим переменную которая рандомно будет разворачивать (отражать) частицы по оси X. Тем самым, мы уменьшим ощущение повторяемости частиц. Опять эффект будет выглядеть менее однородно.
scaleX: {
onEmit: (particle) => {
particle.randomFlip = Math.random() < 0.5 ? -1 : 1;
},
onUpdate: (particle, key, t, value) => {
return 0.3 + (Math.sin(t * Math.PI) * 0.7) * particle.randomAlpha;
}
},
Добавление аддитива и мелких искр
К сожалению редко какой эффект удается сделать с помощью одного эмиттера. Как правило нам нужно сочетать несколько эмиттеров для того что бы создавать пропорции крупных, средних и мелких тел. А так же создавать разные по яркости сегменты.
Сейчас мы вышли на финишную прямую и следующим шагом мы добавим еще один эмиттер в режиме ADD
this.fireAddEmitter = this.add.particles(150, 180, 'fire_atlas', {
lifespan: 700,
frequency: 500,
frame: ['fire_1'],
alpha: {
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI) * 0.3;
},
},
speedY: -10,
scaleX: { start: 1, end: 0.5 },
scaleY: { start: 0.5, end: 0.5 },
blendMode: 'ADD',
});
this.fireAddEmitter.onParticleEmit((particle) => {
particle.frame.halfHeight = 190;
});
Это более простая версия основного эмиттера, большиство настроек идентичны. Я уменьшил скорость подъема частиц а так же включил режим наложения ADD (Как в Photoshop).
Но есть важная деталь, я изменил пивот.
this.fireAddEmitter.onParticleEmit((particle) => {
particle.frame.halfHeight = 190;
});
Я сделал это для того что бы скейл частицы происходил не из центра, а от нижнего края. 190 это размер частицы. Так скейл выглядит более натурально как это бывает у языков пламени.
Искры
Финальным штрихом, добавим огню искр. Тут подробно расписывать не буду, подход к настройкам точно такой же, но добавлены новые параметры, вращения и горизонтальной скорости. Отдельно можно обратить внимание на область спавна частиц, теперь это прямоугольник , а не точка как раньше.
this.fireSparks = this.add.particles(150, 180, 'fire_atlas', {
lifespan: 1000,
frame: ['spark_1', 'spark_2', 'spark_3'],
speedY: { min: -50, max: -100 },
speedX: { min: -10, max: 10 },
scale: { start: 0.1, end: 0.5 },
alpha: {
onUpdate: (particle, key, t, value) => {
return Math.sin(t * Math.PI);
},
},
angle: { min: -15, max: 15 },
rotate: { min: -180, max: 180 },
frequency: 100,
blendMode: 'ADD',
emitZone: {
type: 'random',
source: new Phaser.Geom.Rectangle(-40, -70, 80, 5),
},
});
Заключение
Я надеюсь что вам понравился этот урок и вы нашли в нем полезную информацию. Я хочу поблагодарить всю команду Phaser за такой чудесный инструмент как Phaser Sandox. Благодаря ему вы можете легко протестировать все настройки написанные в этом уроке.
Если вас есть вопросы ко мне лично, вы можете написать мне в твиттер или телеграм. Так же приглашаю вас в русскоязычное сообщество разработчиков на Phaser.
Если вы решите поддержать меня материально - ссылка на донат.