Система частиц: фейерверк
Сегодня ты научишься управлять не одной частицей, а целой толпой сразу — сложишь их в массив, в одном цикле обновишь и нарисуешь, а потом по клику взорвёшь над цыплёнком настоящий фейерверк.
Система частиц — это множество мелких объектов-частиц, у каждого своя жизнь (позиция, скорость, угасание), а вместе они складываются в один большой эффект: дым, искры, стаю или, как у нас, фейерверк.
Зачем тебе целая толпа частиц
Открой любую игру или сторис в соцсети: салют над замком, искры от меча, конфетти при победе, дымок из выхлопной трубы машинки. Кажется, будто художник нарисовал каждую искорку вручную. На самом деле почти всегда это система частиц — один и тот же крошечный объект, размноженный в сотни копий, каждая из которых живёт сама по себе пару секунд и гаснет.
В прошлом уроке ты собрал класс Particle: у него были методы update() (сдвинуть и чуть погасить) и show() (нарисовать кружок). Ты создавал одну частицу, она летела и угасала — мило, но одиноко. Одна искра — это не салют, это грустная спичка над цыплёнком.
Сегодня мы исправим это раз и навсегда. Представь школьный класс: учитель не зовёт каждого ученика по имени тридцать раз — он говорит «все встали» и командует сразу всем. Массив — это твой журнал с учениками, а цикл — команда «все обновитесь и нарисуйтесь». Не важно, сколько частиц в журнале: десять или триста, код один и тот же.
К концу урока у тебя будет работающий фейерверк: кликаешь мышкой над цыплёнком — и вверх с того места разлетается сноп искр, которые разлетаются во все стороны, тускнеют и пропадают. А цыплёнок снизу будет восторженно наблюдать за салютом в свою честь.
И вот что самое приятное: ты уже знаешь все детали по отдельности. Класс ты собирал в прошлом уроке. Массивы и циклы прошёл ещё в самом начале курса, когда стая цыплят шагала через холст. Сегодня мы просто соединяем знакомые кубики в одну конструкцию — и из скромных деталей вырастает эффект, который не стыдно показать в любой игре. Никакой новой магии, только новая сборка.
Главная идея: журнал частиц и команда «все сразу»
Вся система частиц держится на двух простых вещах, которые ты уже знаешь по отдельности: массив (коробка, куда складываешь много объектов) и цикл (способ пройтись по всей коробке). Соединяем их — и получаем механизм, который тянет хоть тысячу искр.
Разложим по полочкам, что вообще должна уметь система частиц:
- Хранить много частиц. Заводим массив, например
let particles = [];— пока пустой журнал. - Рождать новые частицы. Когда нужно (по клику, каждый кадр, при событии), создаём объект
new Particle(...)и кладём его в массив черезparticles.push(...)— «записать нового ученика в журнал». - Обновлять и рисовать всех. В
draw()пробегаем по массиву циклом и для каждой частицы зовёмupdate()иshow()— «все обновились, все нарисовались». - Хоронить мёртвых. Искра не должна жить вечно: когда её запас жизни кончился, мы вынимаем её из массива, чтобы журнал не разрастался до бесконечности. Об этом будет отдельный разговор — это место, где спотыкаются почти все.
Заметь красоту подхода: твой код ни в одном месте не привязан к конкретному числу искр. Ты нигде не пишешь «нарисуй частицу номер 1, потом номер 2, потом номер 3». Ты говоришь циклу «пройдись по всем, кто есть в массиве» — и он сам разберётся, сколько их сейчас: ноль, десять или пятьсот. Добавил искр — цикл стал длиннее. Убрал — короче. Один и тот же код тянет и одинокую искорку, и грандиозный салют. Именно поэтому профессиональные эффекты в играх устроены ровно так же, как твой учебный скетч, — отличается лишь количество и красота отдельной частицы.
Запомни ритм системы частиц: рождаем — кладём в массив — в цикле обновляем и рисуем. Этот же ритм потом приведёт нас к дыму, стае и снегу. Меняется только то, как выглядит и живёт одна частица, а каркас остаётся.
Освежаем класс Particle
Чтобы дальше говорить предметно, держим перед глазами частицу из прошлого урока — слегка причешем её под фейерверк. Главное новшество — поле life (запас жизни), которое тает с каждым кадром.
class Particle {
constructor(x, y) {
this.pos = createVector(x, y);
// случайная скорость во все стороны
this.vel = createVector(random(-3, 3), random(-3, 3));
this.life = 255; // запас жизни и заодно прозрачность
}
update() {
this.pos.add(this.vel); // летим
this.life -= 4; // потихоньку гаснем
}
show() {
noStroke();
fill(255, 220, 60, this.life); // жёлтая искра, alpha = life
circle(this.pos.x, this.pos.y, 8);
}
}Результат: сам по себе этот код ничего не рисует — это чертёж одной искры. Но он уже умеет всё, что нужно: помнит, где находится (pos), куда летит (vel) и сколько ей осталось жить (life). Поле life мы хитро используем дважды: и как прозрачность искры (четвёртый параметр fill), и как таймер её жизни. Когда life дойдёт до нуля — искра станет полностью прозрачной и исчезнет.
Разбираем на примерах
Пример 1. Фонтан искр из массива
Начнём с фонтанчика: каждый кадр рождаем одну искру в центре снизу и сразу складываем в массив, а потом в цикле обновляем и рисуем всех.
let particles = [];
function setup() {
createCanvas(400, 400);
}
function draw() {
background(20, 24, 60); // тёмное ночное небо
// 1. рождаем новую искру и кладём в массив
particles.push(new Particle(200, 300));
// 2. в цикле обновляем и рисуем КАЖДУЮ
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].show();
}
}Результат: на тёмно-синем небе из точки внизу бьёт фонтан жёлтых искр: они разлетаются веером во все стороны, тускнеют по дороге и растворяются. Поток непрерывный — салют без остановки.
Разберём по косточкам. Массив particles объявлен снаружи функций, чтобы жить между кадрами и не обнуляться. В setup() мы его не трогаем — он стартует пустым. Главное происходит в draw(): сначала particles.push(new Particle(200, 300)) добавляет в конец массива свежую искру с центром (200, 300). Потом цикл for идёт по индексам от 0 до particles.length (длина массива — сколько искр сейчас в журнале) и для каждой зовёт сперва update() (сдвинуть и погасить), затем show() (нарисовать). Обрати внимание: цикл не знает заранее, сколько частиц — он каждый кадр спрашивает particles.length. Добавили искру — цикл стал на один шаг длиннее. Вот она, сила связки «массив + цикл».
Пример 2. Взрыв по клику — настоящий фейерверк
Фонтан красивый, но фейерверк — это залп: много искр рождается разом в одной точке. Сделаем так, чтобы по клику мышкой в месте клика вспыхивал сноп из полусотни искр.
let particles = [];
function setup() {
createCanvas(400, 400);
}
function draw() {
background(20, 24, 60);
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].show();
}
}
function mousePressed() {
// залп: рождаем сразу 50 искр в точке клика
for (let i = 0; i < 50; i++) {
particles.push(new Particle(mouseX, mouseY));
}
}Результат: небо спокойное и тёмное, пока ты не кликнешь. В точке клика мгновенно вспыхивает шар из пятидесяти жёлтых искр — они разлетаются во все стороны и быстро гаснут, точь-в-точь как разрыв салюта. Кликаешь ещё раз в другом месте — новый залп.
Здесь два цикла в двух разных местах, и важно их не путать. Цикл рождения живёт в mousePressed() — это встроенная функция p5.js, которая срабатывает один раз на каждый клик мышью. Внутри неё мы пятьдесят раз делаем push(new Particle(mouseX, mouseY)), где mouseX и mouseY — координаты курсора в момент клика. Так за один клик в массив прилетает сразу полсотни искр из одной точки. А цикл показа по-прежнему в draw() и каждый кадр обновляет-рисует всех, кто сейчас в массиве. Рождение и показ разнесены: mousePressed() наполняет журнал, draw() крутит его кадр за кадром.
Пример 3. Фейерверк над цыплёнком + уборка мёртвых искр
Соберём всё вместе и добавим героя. Внизу сидит цыплёнок, а ты салютуешь в его честь. И решим важную проблему: мёртвые искры (у которых life упал до нуля) надо убирать из массива, иначе он будет расти бесконечно и скетч начнёт тормозить.
let particles = [];
function setup() {
createCanvas(400, 400);
}
function draw() {
background(20, 24, 60);
drawChick(); // наш герой снизу
// идём ОТ КОНЦА к началу, чтобы безопасно удалять
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].show();
if (particles[i].life <= 0) {
particles.splice(i, 1); // выкинуть мёртвую искру
}
}
}
function mousePressed() {
for (let i = 0; i < 50; i++) {
particles.push(new Particle(mouseX, mouseY));
}
}
function drawChick() {
noStroke();
fill(255, 221, 51); // жёлтое тело
circle(200, 360, 70);
fill(255, 140, 0); // оранжевый клюв смотрит вверх
triangle(200, 330, 194, 345, 206, 345);
}Результат: внизу по центру сидит жёлтый цыплёнок с клювом, задранным к небу. Кликаешь повыше — над ним вспыхивает залп искр и осыпается вниз, к нему. Сколько бы залпов ты ни запускал, скетч не тормозит: погасшие искры тихо исчезают из массива, и в журнале всегда только живые.
Ключевая хитрость этого примера — цикл задом наперёд: for (let i = particles.length - 1; i >= 0; i--). Почему не как обычно, с нуля вперёд? Потому что мы удаляем элементы прямо во время прохода методом splice(i, 1) («вырезать 1 элемент начиная с индекса i»). Когда удаляешь элемент, все, кто стоял правее, сдвигаются на одну позицию влево. Если идти вперёд, после удаления ты перескочишь через соседа и пропустишь его. А двигаясь от конца к началу, ты трогаешь только те индексы, что уже позади, — сдвиг впереди тебя не задевает. Это золотое правило: удаляешь из массива в цикле — иди с конца. Функцию drawChick() мы вынесли отдельно просто для чистоты: рисуем героя, а вся возня с частицами остаётся в своём цикле.
Частые ошибки и подводные камни
Системы частиц спотыкаются об удивительно одинаковые грабли. Пробеги глазами — сэкономишь себе час отладки.
1. Объявили массив внутри draw()
Классика. Человек пишет let particles = []; прямо в начале draw().
function draw() {
let particles = []; // обнуляется КАЖДЫЙ кадр!
particles.push(new Particle(200, 300));
// ...рисуем...
}Результат: вместо фонтана видна одна-единственная искорка, которая дёргается на месте. Причина: на каждом кадре массив рождается заново пустым, старые искры стираются, и в нём всегда ровно одна свежая частица. Объявляй массив снаружи функций — тогда он живёт между кадрами и копит частицы.
2. Забыли убирать мёртвые частицы
Если не делать splice(), искры с нулевой жизнью остаются в массиве навсегда — невидимые (прозрачные), но цикл всё равно их обновляет и рисует каждый кадр. Через минуту в журнале тысячи призраков, и скетч начинает заметно тормозить и дёргаться. Лекарство — проверять life <= 0 и вырезать такие искры, как в примере 3.
3. Удаляют в цикле, который идёт вперёд
Если оставить обычный цикл с нуля (for (let i = 0; i < particles.length; i++)) и внутри звать splice(i, 1), ты будешь через одну пропускать частицы: после удаления соседи сдвинулись, а i всё равно вырос. Часть искр будет мигать или жить дольше положенного. Правило простое: удаляешь по индексу — иди циклом от конца к началу.
3+. push() кладёт ОДНУ искру, когда хотел залп
Чтобы получить взрыв, push надо вызвать в цикле много раз. Если написать particles.push(new Particle(mouseX, mouseY)) один раз в mousePressed(), по клику родится всего одна искорка — грустный «фейерверк» из единственной точки. Заверни push в цикл for (let i = 0; i < 50; i++), и получишь полноценный сноп.
4. Перепутали, где рождать, а где рисовать
Если случайно засунуть push в draw() вместо mousePressed(), искры будут литься непрерывно, а не по клику. А если, наоборот, цикл показа окажется в mousePressed(), частицы нарисуются один раз и замрут. Держи в голове разделение труда: рождение — там, где наступает событие; обновление и показ — всегда в draw().
Мини-проект: салют в честь цыплёнка
Теперь твоя очередь. Возьми пример 3 за основу и доведи салют до праздничного. Чек-лист:
- Запусти базу: цыплёнок снизу, клик рождает залп из 50 искр, мёртвые искры убираются. Убедись, что после десятка залпов скетч не тормозит.
- Разноцветные искры. Передавай в конструктор
Particleещё и цвет (например, случайный из набора праздничных) и используй его вfill(). Один залп — один цвет, как у настоящего салюта. - Гравитация. Добавь искрам лёгкое притяжение вниз: в
update()каждый кадр чуть увеличивайthis.vel.y(например, на 0.1). Искры будут красиво осыпаться вниз, к цыплёнку, а не разлетаться идеальным шаром. - Авто-салют. Сделай, чтобы раз в пару секунд (по
frameCount) залп запускался сам в случайной точке неба — даже когда ты не кликаешь. - Супербонус — реакция цыплёнка. Пусть в момент залпа цыплёнок чуть подпрыгивает или его клюв ещё выше тянется к небу. Герой радуется салюту в свою честь.
Запусти и устрой себе настоящее шоу. Заметь, как мало кода ушло на эффект, который выглядит дорого: один класс, один массив и два цикла — рождение и показ. Меняешь правила жизни одной частицы — меняется весь эффект.
Итоги
Сегодня ты перешёл от одной частицы к управляемой толпе. Главное:
- Система частиц — это массив объектов плюс цикл, который обновляет и рисует всех сразу. Размер толпы коду безразличен.
- Рождение — это
particles.push(new Particle(...)): добавить новую частицу в журнал. Для залпа зовиpushв цикле много раз. - Показ — это цикл по массиву в
draw(), где для каждой частицы вызываешьupdate()иshow(). - Мёртвые частицы надо убирать через
splice(), а цикл с удалением вести от конца к началу, иначе пропустишь соседей. - Поле вроде
lifeслужит и таймером жизни, и прозрачностью — частица угасает естественно.
Сейчас все искры одинаковые и предсказуемые — летят по прямой, гаснут по расписанию. В следующем уроке мы вдохнём в систему частиц настоящую жизнь: добавим шум Перлина и силы, чтобы искры закручивались, как дым на ветру, и каждая стая выглядела неповторимо. Готовь небо — будет красиво. До встречи!