Флокинг: имитация стаи (boids)
Сегодня ты заставишь сотню цыплят летать как единая стая — без единого общего командира, только по трём простым правилам, которые каждый цыплёнок применяет к соседям.
Флокинг (boids) — модель поведения стаи, в которой каждая частица следует трём правилам по отношению к соседям: разделение (не толкаться), выравнивание (лететь в общем направлении) и сплочение (держаться кучи). Никакого центрального мозга — слаженность рождается сама.
Откуда берётся стая
Видел, как осенью скворцы собираются в огромное облако и текут по небу, будто это одно живое существо? Облако сжимается, растягивается, огибает дерево, делится надвое и снова сливается — и при этом никто им не дирижирует. Нет главной птицы с микрофоном, которая кричит «все влево!». Каждая птица просто смотрит на пару-тройку ближайших соседей и подстраивается под них. А красивое облако на сотни особей получается само собой.
То же самое ты видел в играх и мультиках: косяки рыб в подводных уровнях, рои пчёл, толпы зомби, которые обтекают препятствия. Почти всё это — один и тот же приём, придуманный ещё в 1986 году программистом Крейгом Рейнольдсом. Он назвал свои искусственные стаи boids (от «bird-oid», «птицеподобные») и доказал крутую вещь: чтобы получить сложное поведение толпы, не нужно прописывать поведение толпы. Достаточно дать каждому отдельному существу несколько простых правил — а сложность вырастет сама. Это называется эмерджентность: целое ведёт себя умнее, чем любая его часть.
В прошлых уроках ты уже подобрался к этому вплотную. Ты делал эмиттеры с жизненным циклом частиц, где каждая частица жила сама по себе. А в уроке про притяжение и отталкивание ты научился толкать частицы силами. Флокинг — это следующий шаг: теперь частицы будут реагировать не на одну точку-магнит, а друг на друга. К концу урока у тебя по холсту понесётся живая стая цыплят, которая кружит, рассыпается и снова собирается в косяк.
Три правила стаи
Вся магия держится на трёх правилах. Представь, что ты — один цыплёнок в стае и летишь в общей куче. Что ты делаешь каждое мгновение? Ровно три вещи, и каждая даёт тебе одну стрелку-силу, куда подвинуться.
- Разделение (separation): не толкайся. Если сосед подлетел слишком близко — отодвинься от него. Это сила, направленная прочь от тех, кто залез в твоё личное пространство. Без неё стая слиплась бы в одну точку.
- Выравнивание (alignment): лети как все. Посмотри, куда в среднем летят соседи, и поверни в ту же сторону. Это сила, которая подгоняет твою скорость под среднюю скорость стаи. Благодаря ей стая течёт единым потоком, а не как попало.
- Сплочение (cohesion): держись кучи. Найди середину между соседями (их «центр масс») и подтянись к ней. Это сила к центру компании. Без неё стая разлетелась бы и распалась на одиночек.
Заметь, как три правила тянут в разные стороны и уравновешивают друг друга. Сплочение тянет цыплят в кучу, разделение не даёт им слипнуться, а выравнивание следит, чтобы они летели согласованно. Это как компания друзей на прогулке: вы держитесь вместе (сплочение), но не наступаете друг другу на пятки (разделение), и идёте примерно в одну сторону, а не разбредаетесь (выравнивание). Три простых желания — и получается живая, текучая толпа.
Главная идея флокинга: никто не управляет стаей. Каждый цыплёнок видит только соседей рядом и считает для себя три маленькие силы. Сложное поведение облака — это сумма множества простых решений.
Как правило превращается в силу
Все три правила выдают одно и то же — вектор-силу: стрелку «толкнись вон туда». Мы складываем три стрелки в одну, прибавляем её к скорости цыплёнка, а скорость прибавляем к позиции — ровно как ты делал с гравитацией. Разница лишь в том, что теперь сила зависит не от одной точки, а от того, кто летает рядом. Поэтому в каждом правиле есть один и тот же шаг: пройтись по всем соседям, кто ближе заданного радиуса, и что-то усреднить.
Разбираем на примерах
Пример 1. Один цыплёнок-boid с классом
Прежде чем делать стаю, соберём одну частицу. Заведём класс Boid с позицией и скоростью (вектора тебе уже родные) и научим его двигаться и отскакивать от краёв. Никаких правил стаи пока нет — просто летающий цыплёнок.
class Boid {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-2, 2), random(-2, 2));
this.acc = createVector(0, 0); // ускорение, копим сюда силы
this.maxSpeed = 3;
}
update() {
this.vel.add(this.acc); // силы меняют скорость
this.vel.limit(this.maxSpeed); // не даём разогнаться до бесконечности
this.pos.add(this.vel); // скорость двигает позицию
this.acc.mult(0); // обнуляем ускорение на следующий кадр
}
edges() {
// вылетел за край — появляется с другой стороны (мир-бублик)
if (this.pos.x > width) this.pos.x = 0;
if (this.pos.x < 0) this.pos.x = width;
if (this.pos.y > height) this.pos.y = 0;
if (this.pos.y < 0) this.pos.y = height;
}
show() {
noStroke();
fill(255, 221, 51);
circle(this.pos.x, this.pos.y, 10);
}
}Результат: заготовка одного цыплёнка-частицы. Сам по себе он пока ничего не делает на экране — это чертёж, по которому мы скоро наделаем целую стаю.
Разберём ключевые места. У каждого boid три вектора: pos (где он), vel (куда летит) и acc — ускорение, копилка для сил этого кадра. В update() происходит вся физика: силы из acc прибавляются к скорости, скорость ограничивается через vel.limit(maxSpeed) (иначе цыплёнок разгонится до космической), скорость двигает позицию, и в конце acc.mult(0) обнуляет копилку — на следующем кадре силы посчитаем заново. Метод edges() делает мир «бубликом»: вылетел справа — влетел слева, как в старой Pac-Man. А show() просто рисует жёлтую точку-тело.
Пример 2. Стая без правил — хаос
Создадим массив из сотни таких цыплят и запустим. Пока без флокинга — чтобы ты своими глазами увидел разницу, когда правила появятся.
let flock = [];
function setup() {
createCanvas(600, 600);
for (let i = 0; i < 100; i++) {
flock.push(new Boid(random(width), random(height)));
}
}
function draw() {
background(135, 206, 235); // небо
for (let boid of flock) {
boid.edges();
boid.update();
boid.show();
}
}Результат: по голубому небу разлетается сотня жёлтых точек-цыплят, каждый летит в свою случайную сторону. Картина похожа на россыпь пикселей или снег в телевизоре без сигнала — движение есть, а порядка нет. Никто ни на кого не смотрит.
Это важная отправная точка. Сейчас каждый цыплёнок живёт сам по себе: его скорость задана один раз в конструкторе и больше не меняется (силы-то мы не считаем, acc всегда нулевое). Запомни эту картинку хаоса — через пару примеров мы добавим три правила, и тот же самый код, те же сто точек вдруг начнут вести себя как стая. Вся разница будет в одном новом методе.
Пример 3. Добавляем три правила
Вот сердце урока. Напишем три метода, каждый возвращает вектор-силу, и метод flock(), который складывает их и кладёт в acc. Добавь это внутрь класса Boid.
separation(boids) {
let perception = 30; // личное пространство
let steer = createVector(0, 0);
let count = 0;
for (let other of boids) {
let d = dist(this.pos.x, this.pos.y, other.pos.x, other.pos.y);
if (other !== this && d < perception) {
let diff = p5.Vector.sub(this.pos, other.pos); // стрелка ОТ соседа
diff.div(d); // чем ближе сосед, тем сильнее толчок
steer.add(diff);
count++;
}
}
if (count > 0) steer.div(count);
return steer;
}
alignment(boids) {
let perception = 50;
let avg = createVector(0, 0);
let count = 0;
for (let other of boids) {
let d = dist(this.pos.x, this.pos.y, other.pos.x, other.pos.y);
if (other !== this && d < perception) {
avg.add(other.vel); // копим скорости соседей
count++;
}
}
if (count > 0) {
avg.div(count); // средняя скорость стаи рядом
avg.sub(this.vel); // насколько надо подвернуть
}
return avg;
}
cohesion(boids) {
let perception = 50;
let center = createVector(0, 0);
let count = 0;
for (let other of boids) {
let d = dist(this.pos.x, this.pos.y, other.pos.x, other.pos.y);
if (other !== this && d < perception) {
center.add(other.pos); // копим позиции соседей
count++;
}
}
if (count > 0) {
center.div(count); // центр масс соседей
center.sub(this.pos); // стрелка К центру
}
return center;
}
flock(boids) {
let sep = this.separation(boids);
let ali = this.alignment(boids);
let coh = this.cohesion(boids);
sep.mult(1.5); // разделение чуть важнее — не дать слипнуться
ali.mult(1.0);
coh.mult(1.0);
this.acc.add(sep);
this.acc.add(ali);
this.acc.add(coh);
}Результат: три новых метода-правила и один сборщик. Сами по себе на экране они ничего не меняют — их надо вызвать в цикле. Но внутри уже спрятано всё поведение стаи.
Смотри, как три метода устроены по одному шаблону: пройтись по всем boids, отобрать тех, кто ближе perception пикселей, и что-то усреднить. Разница только в том, что усредняем. В separation() мы для каждого близкого соседа берём стрелку p5.Vector.sub(this.pos, other.pos) — она смотрит от соседа к нам, то есть «оттолкнись отсюда». И делим её на расстояние d: чем сосед ближе, тем толчок резче. В alignment() копим скорости соседей и усредняем — получаем «куда летит толпа», а потом вычитаем свою скорость, чтобы понять, насколько подвернуть. В cohesion() копим позиции, усредняем — это центр масс компании, — и стрелка к нему тянет нас в кучу.
Обрати внимание на проверку other !== this в каждом цикле: цыплёнок не должен считать соседом самого себя, иначе расстояние до себя ноль и всё ломается. А метод flock() — это дирижёр: он зовёт три правила, домножает их на коэффициенты-важности (mult) и складывает в общую копилку acc. Коэффициенты — твои ручки настройки: усиль сплочение — стая собьётся в плотный комок, усиль разделение — расплывётся пожиже.
Пример 4. Запускаем живую стаю
Осталось вызвать flock() в главном цикле — перед update(), чтобы силы успели посчитаться до движения.
function draw() {
background(135, 206, 235);
for (let boid of flock) {
boid.flock(flock); // считаем три силы по соседям
boid.edges();
boid.update(); // силы двигают цыплёнка
boid.show();
}
}Результат: та же сотня жёлтых цыплят, но теперь они текут по небу единым облаком — то вытягиваются в косяк, то закручиваются спиралью, то делятся на два потока и снова сливаются. Точь-в-точь стая скворцов на закате. Ни один цыплёнок не получил команды «лети туда» — но вся стая движется слаженно и живо.
Вот она, эмерджентность, своими глазами. Мы не написали ни строчки про «облако», «косяк» или «спираль» — мы написали три крошечных правила для одного цыплёнка. А красивое поведение тысячи особей выросло само, из того, что каждый смотрит на соседей. Поменяй perception в правилах — стая станет дальнозоркой и плотной или близорукой и рыхлой. Поиграй с коэффициентами в flock() — и характер стаи поменяется на глазах.
Частые ошибки и подводные камни
Флокинг легко запустить и так же легко сломать. Лови самые частые грабли заранее.
1. Забыли проверку other !== this
Если не исключить самого цыплёнка из соседей, расстояние до себя будет ноль. В separation() деление diff.div(d) превратится в деление на ноль, вектор станет NaN («не-число»), и зачумлённый цыплёнок исчезнет с холста или улетит в бесконечность. Всегда проверяй other !== this внутри цикла по соседям.
2. Не ограничили скорость
Силы каждый кадр прибавляются к скорости, и без this.vel.limit(this.maxSpeed) цыплята разгоняются всё быстрее и быстрее, пока не начнут телепортироваться через весь холст за кадр. Стая превращается в дрожащую кашу. Лимит скорости — обязательная страховка любого boid.
3. Перепутали направление в separation
Разделение должно толкать прочь от соседа. Стрелка считается как p5.Vector.sub(this.pos, other.pos) — «из соседа в меня». Если случайно написать наоборот, p5.Vector.sub(other.pos, this.pos), цыплята начнут притягиваться к тем, кто близко, слипнутся в одну точку и схлопнутся. Правило: вычитаемое — это тот, от кого убегаешь.
4. Вызвали flock() после update()
Порядок важен. Сначала flock() считает силы и кладёт их в acc, и только потом update() применяет их к скорости. Если поменять местами, силы этого кадра обнулятся в update() раньше, чем подействуют, и стая снова рассыплется в хаос, как в примере 2.
5. Слишком большой радиус восприятия на сотнях частиц
Каждый цыплёнок в каждом кадре сравнивает себя со всеми остальными — это сотня на сотню, десять тысяч проверок за кадр. Если ещё и perception огромный, frameRate проседает и анимация начинает тормозить и дёргаться. Для старта держи стаю в пределах 80–150 особей и радиус 30–60. Большие стаи — отдельная тема про оптимизацию.
Мини-проект: стая цыплят CodeChick
Теперь твоя очередь оживить стаю и сделать её узнаваемой. Возьми рабочий код из примеров 1, 3 и 4 за основу и доведи до собственной работы. Чек-лист:
- Запусти базовую стаю на 100 цыплят и убедись, что облако течёт и собирается. Если рассыпается — проверь порядок вызовов и направление в
separation(). - Сделай цыплят цыплятами. В
show()рисуй не просто точку, а маленькое жёлтое тело с оранжевым клювом-треугольником, повёрнутым по направлению скорости. Подсказка: возьми угол черезthis.vel.heading()и поверни систему координат (push/translate/rotate/pop). - Покрути ручки. Поиграй коэффициентами в
flock(): усильcoh— стая собьётся плотнее; усильsep— расплывётся свободнее. Найди настройку, которая нравится именно тебе. - Бонус — вожак-курсор. Добавь четвёртое правило: лёгкую силу к точке мыши (
mouseX,mouseY). Теперь стая будет послушно следовать за твоим курсором, как цыплята за наседкой. - Супербонус — два вида. Заведи половину стаи другого цвета и сделай так, чтобы выравнивание и сплочение действовали только между «своими». Получишь две стаи, которые делят одно небо и обтекают друг друга.
Запусти, отойди и просто понаблюдай минуту. Стая никогда не повторяется дважды — каждый прогон рисует новый танец. В этом и кайф генеративного: ты задаёшь правила, а узор рождается сам.
Итоги
Сегодня ты собрал настоящую живую стаю из простых правил. Главное:
- Флокинг (boids) — это поведение толпы без единого командира: каждая частица смотрит только на близких соседей.
- Три правила дают три силы: разделение (отодвинься от слишком близких), выравнивание (лети как соседи) и сплочение (тянись к центру компании).
- Каждое правило устроено одинаково: пройтись по соседям ближе
perception, усреднить — и вернуть вектор-силу. Силы складываются вacc,accдвигает скорость, скорость — позицию. - Эмерджентность: сложное облако из сотни частиц вырастает само из крошечных правил для одной. Это и есть магия флокинга.
- Коэффициенты важности в
flock()и радиусыperception— твои ручки настройки характера стаи.
Ты прошёл от одной точки в первом модуле до стаи живых цыплят, которые ведут себя почти как настоящие. Осталось собрать всё, что ты умеешь, в одну работу. В следующем — финальном — уроке мы возьмём цвет, анимацию, случайность, частицы и флокинг и соберём генеративный аватар CodeChick: твою личную работу, которую не стыдно опубликовать. До встречи на финише!