DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

There is no flock in the code — only an array of points, and three little vectors per bird make a whole murmuration appear

A flocking simulation looks like it wants an AI that plans the swarm — some leader steering the murmuration, some choreographer deciding where the cloud folds next. It needs none of that. On screen there is no flock at all, only data: a boid is a point with a velocity, and the flock is a plain array of them. Craig Reynolds worked this out in 1986, and the whole thing fits in about 150 lines of vanilla JavaScript on one canvas. Here's how I built it.

A boid is a point with a velocity

There's no "flock" object. Each boid is a position and a velocity; the array is the world. A few shared weights and a perception radius live in one params object, so a slider can retune every boid at once.

const P = { sep:1.6, ali:1.0, coh:0.9, perc:60, speed:3.2, count:200 };
function makeBoid(){
  const a = Math.random()*Math.PI*2, s = P.speed*(0.5+Math.random()*0.5);
  return { x:Math.random()*W, y:Math.random()*H,
           vx:Math.cos(a)*s, vy:Math.sin(a)*s, ax:0, ay:0 };
}
Enter fullscreen mode Exit fullscreen mode

Find the neighbours a boid can see

A boid reacts only to birds inside its perception radius; everyone else is invisible. Once a frame I scan the array and keep the close ones — and because the sky wraps, I measure to the nearest image of each neighbour, across the seam if that's shorter, so a flock slides off one edge and reappears on the other with no tear. Every rule below is just a different sum over this same list.

function neighbours(b){
  const out = [], perc2 = P.perc * P.perc;
  for (const o of boids){
    if (o === b) continue;
    let dx = b.x - o.x, dy = b.y - o.y;
    if (dx >  W/2) dx -= W; else if (dx < -W/2) dx += W;  // wrap to
    if (dy >  H/2) dy -= H; else if (dy < -H/2) dy += H;  // nearest image
    if (dx*dx + dy*dy < perc2) out.push({ o, dx, dy });
  }
  return out;
}
Enter fullscreen mode Exit fullscreen mode

Three rules, each a vector

Separation keeps birds from piling up: for each too-close neighbour add a vector pointing away, weighted by 1/d², so the nearer someone is the harder you shove off. Without it the whole flock collapses to a dot.

Alignment makes it flow: average the neighbours' velocities — their headings, not positions — and steer toward that. A turn begun by a few ripples outward until the group banks as one.

Cohesion stops fraying: average the neighbours' positions and steer toward that local centre. Since dx,dy point from neighbour to you, the average of -dx,-dy is the way to the centre.

Reynolds steering turns a direction into a bank

Each rule hands back a raw direction, but a bird can't teleport onto it — it has to turn. Reynolds' trick: normalise the direction to top speed to get the desired velocity, subtract the current velocity to get the steering, then clamp that to a small max force so the bird banks instead of snapping. Steer all three, scale each by its slider weight, sum into one acceleration.

function steer(dx, dy, b){
  const m = Math.hypot(dx,dy) || 1e-6;
  const desX = dx/m*P.speed, desY = dy/m*P.speed;   // desired velocity
  let sx = desX - b.vx, sy = desY - b.vy;           // desired − current
  const sm = Math.hypot(sx,sy), maxF = P.speed*0.08;
  if (sm > maxF){ sx = sx/sm*maxF; sy = sy/sm*maxF; }  // cap the turn
  return { x:sx, y:sy };
}
b.ax = s.x*P.sep + a.x*P.ali + c.x*P.coh;   // weighted blend
b.ay = s.y*P.sep + a.y*P.ali + c.y*P.coh;
Enter fullscreen mode Exit fullscreen mode

Integrate on a frozen snapshot, then wrap

The one subtlety: I compute every boid's acceleration first, in one pass, so each reads the same frozen snapshot — otherwise a boid reacts to a neighbour that already moved this frame. A second pass integrates: velocity gains the acceleration, clamp it between a floor (nobody freezes) and top speed, advance the position, wrap the toroidal edges. Two passes over the array and the motion is done. Draw each boid as a triangle pointing along its velocity, hue by heading, so you can see alignment happen — and it all runs inside one requestAnimationFrame loop.

Nowhere in any of this is the word "flock". The swirling, splitting, re-merging murmuration is emergent — global order that no line of code describes, arising purely from every boid obeying the same three local rules. Drag your mouse through to become an attractor the birds chase or a predator they flee, tune the three weights live, and watch order fall out of arithmetic:

https://dev48v.infy.uk/game/day50-boids.html

Top comments (0)