Drone Swarm Simulation — Flocking + Formation Control
Real drone swarms (Amazon Prime Air, Starling Murmuration drones, military OFFSET program) combine two layers: a bottom-up Boids flocking layer that provides collision avoidance, and a top-down formation control layer that assigns position targets. This tutorial builds both from scratch in JavaScript.
1. Agent Architecture
Each drone is a lightweight data object. Keep physics and rendering separate — the physics loop runs at a fixed timestep (e.g., 60 Hz) while Three.js renders at whatever rate the GPU allows.
class Drone {
constructor(id, x, y, z) {
this.id = id;
this.pos = new THREE.Vector3(x, y, z);
this.vel = new THREE.Vector3();
this.acc = new THREE.Vector3();
this.target = new THREE.Vector3(); // formation slot
this.state = 'FLOCK'; // FLOCK | FORM | AVOID | RETURN
this.maxSpeed = 5.0;
this.maxForce = 0.3;
this.mesh = null; // assigned by renderer
}
applyForce(f) {
this.acc.add(f);
}
update(dt) {
this.vel.addScaledVector(this.acc, dt);
this.vel.clampLength(0, this.maxSpeed);
this.pos.addScaledVector(this.vel, dt);
this.acc.set(0, 0, 0);
}
}
2. Layer 1: Boids Flocking
The Boids layer handles collision avoidance between drones and gives the swarm organic movement. Three steering forces are combined — the same rules as the classic Reynolds (1987) algorithm, but tuned for 3D:
function boidsForces(drone, drones) {
const SEP_RADIUS = 4; // personal space (metres equivalent)
const VIEW_RADIUS = 12;
const W_SEP = 1.8;
const W_ALI = 1.0;
const W_COH = 0.8;
const sep = new THREE.Vector3();
const ali = new THREE.Vector3();
const coh = new THREE.Vector3();
let ns = 0, nv = 0;
for (const other of drones) {
if (other === drone) continue;
const d = drone.pos.distanceTo(other.pos);
if (d < SEP_RADIUS) {
sep.addScaledVector(
new THREE.Vector3().subVectors(drone.pos, other.pos).normalize(),
1 / d); // weight by inverse distance
ns++;
}
if (d < VIEW_RADIUS) {
ali.add(other.vel);
coh.add(other.pos);
nv++;
}
}
const total = new THREE.Vector3();
if (ns > 0) total.addScaledVector(
seek(drone, drone.pos.clone().add(sep)), W_SEP);
if (nv > 0) {
ali.divideScalar(nv).normalize().multiplyScalar(drone.maxSpeed);
total.addScaledVector(steer(drone, ali), W_ALI);
coh.divideScalar(nv);
total.addScaledVector(seek(drone, coh), W_COH);
}
return total;
}
// Seek: steer toward target at max speed
function seek(drone, target) {
const desired = new THREE.Vector3()
.subVectors(target, drone.pos).normalize()
.multiplyScalar(drone.maxSpeed);
return steer(drone, desired);
}
function steer(drone, desired) {
return desired.sub(drone.vel).clampLength(0, drone.maxForce);
}
3. Layer 2: Formation Patterns
The formation layer assigns each drone a world-space
slot target. The drone then steers toward its slot.
When drone.state === 'FORM', the Boids cohesion weight is
reduced and a strong seek-to-slot force is added.
V-Formation (migratory birds)
function vFormationSlots(n, spacing = 4) {
const slots = [];
for (let i = 0; i < n; i++) {
const side = i % 2 === 0 ? 1 : -1;
const rank = Math.ceil(i / 2);
slots.push(new THREE.Vector3(
side * rank * spacing, // X: left/right wing
0, // Y: same altitude
-rank * spacing * 0.8 // Z: behind leader
));
}
return slots; // slot[0] = leader at origin
}
Circle Formation
function circleFormationSlots(n, radius = 10) {
return Array.from({length: n}, (_, i) => {
const angle = (i / n) * Math.PI * 2;
return new THREE.Vector3(
Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
});
}
Grid Formation
function gridFormationSlots(n, cols = 4, spacing = 5) {
return Array.from({length: n}, (_, i) => {
const col = i % cols, row = Math.floor(i / cols);
return new THREE.Vector3(
(col - (cols - 1) / 2) * spacing,
0,
row * spacing);
});
}
To assign slots to drones, use a greedy
nearest-neighbor assignment to minimize total travel distance — or,
for optimal assignment with up to ~30 drones, solve the
linear sum assignment problem (Hungarian algorithm) via a
JavaScript port of scipy's linear_sum_assignment.
// Greedy slot assignment: each drone takes its nearest slot
function assignSlots(drones, slots) {
const used = new Set();
for (const d of drones) {
let best = -1, bestDist = Infinity;
for (let j = 0; j < slots.length; j++) {
if (used.has(j)) continue;
const dist = d.pos.distanceTo(slots[j]);
if (dist < bestDist) { bestDist = dist; best = j; }
}
d.target.copy(slots[best]);
used.add(best);
}
}
4. Leader-Follower Control
One drone acts as the leader and follows a predefined path (waypoints or user input). Followers maintain a fixed offset from the leader's position and orientation. This is simpler than full formation control and works well for convoys.
function leaderFollowerForce(follower, leader, offsetLocal) {
// Transform the offset from leader's local frame to world frame
const leaderQ = new THREE.Quaternion()
.setFromUnitVectors(
new THREE.Vector3(0, 0, 1),
leader.vel.clone().normalize()
);
const worldTarget = offsetLocal.clone()
.applyQuaternion(leaderQ)
.add(leader.pos);
// Arrival steering: slow down as we approach the slot
const d = follower.pos.distanceTo(worldTarget);
const SLOW_RADIUS = 6;
const speed = d < SLOW_RADIUS
? follower.maxSpeed * (d / SLOW_RADIUS)
: follower.maxSpeed;
const desired = worldTarget.clone()
.sub(follower.pos).normalize().multiplyScalar(speed);
return desired.sub(follower.vel).clampLength(0, follower.maxForce);
}
5. Obstacle Avoidance — Artificial Potential Fields
Obstacles generate a repulsive potential field. Targets generate an attractive field. The drone follows the net gradient. This is fast (O(1) per obstacle) and works well for convex obstacles.
function obstacleAvoidForce(drone, obstacles) {
const OBS_RADIUS = 8; // influence radius
const force = new THREE.Vector3();
for (const obs of obstacles) {
const diff = drone.pos.clone().sub(obs.center);
const d = diff.length() - obs.radius; // clearance distance
if (d < OBS_RADIUS && d > 0) {
// APF: F = k * (1/d - 1/OBS_RADIUS) * (1/d²) * unitVec
const k = 50;
const mag = k * (1 / d - 1 / OBS_RADIUS) * (1 / (d * d));
force.addScaledVector(diff.normalize(), mag);
}
}
return force.clampLength(0, drone.maxForce * 3);
}
6. Agent State Machine
Each drone's behavior is governed by a finite state machine. This cleanly separates the different behavior modes:
| State | Active Forces | Transition To |
|---|---|---|
| FLOCK | Separation + Alignment + Cohesion | FORM (user command), AVOID (obstacle detected) |
| FORM | Separation (high W) + Seek slot (high W) | FLOCK (form dissolved), AVOID |
| AVOID | Obstacle repulsion + Boids separation | FLOCK/FORM (obstacle cleared) |
| RETURN | Seek home base + Separation | LAND (reached base) |
function updateDrone(drone, drones, obstacles, formation, dt) {
const obsForce = obstacleAvoidForce(drone, obstacles);
const inDanger = obsForce.length() > 0.5;
if (inDanger) {
drone.state = 'AVOID';
} else if (drone.state === 'AVOID') {
drone.state = formation ? 'FORM' : 'FLOCK';
}
switch (drone.state) {
case 'FLOCK':
drone.applyForce(boidsForces(drone, drones));
break;
case 'FORM':
drone.applyForce(boidsForces(drone, drones).multiplyScalar(0.3));
drone.applyForce(seek(drone, drone.target).multiplyScalar(2.0));
break;
case 'AVOID':
drone.applyForce(obsForce);
drone.applyForce(boidsForces(drone, drones).multiplyScalar(1.5));
break;
}
drone.applyForce(obsForce); // always applies
drone.update(dt);
}
7. Three.js Rendering with InstancedMesh
For 100+ drones, use THREE.InstancedMesh to render all
drones in a single draw call. Each drone's position and quaternion are
written into the instance matrix every frame.
// Create instanced mesh for N drones
const geo = new THREE.ConeGeometry(0.3, 1.0, 6);
geo.rotateX(Math.PI / 2); // point along Z axis
const mat = new THREE.MeshStandardMaterial({ color: 0x34d399 });
const mesh = new THREE.InstancedMesh(geo, mat, drones.length);
scene.add(mesh);
const dummy = new THREE.Object3D();
const _up = new THREE.Vector3(0, 1, 0);
function syncInstancedMesh() {
for (let i = 0; i < drones.length; i++) {
const d = drones[i];
dummy.position.copy(d.pos);
if (d.vel.lengthSq() > 0.001) {
dummy.quaternion.setFromUnitVectors(_up, d.vel.clone().normalize());
}
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
}
8. Tuning Guide
| Parameter | Default | Effect of Increasing |
|---|---|---|
SEP_RADIUS |
4 | Drones spread farther apart — less density, more spacing |
VIEW_RADIUS |
12 | Larger neighborhood → more uniform direction, slower to split |
W_SEP |
1.8 | Harder collision avoidance, less cohesion |
W_ALI |
1.0 | More disciplined direction alignment |
W_COH |
0.8 | Tighter clustering around group center |
maxForce |
0.3 | More responsive turns but can cause oscillation |
maxSpeed |
5.0 | Faster swarm — may need larger separation radius |
Frequently Asked Questions
What will I learn in this tutorial?
Build a drone swarm simulation with Boids flocking, V-formation, circle and grid formations, leader-follower, and obstacle avoidance using JavaScript and Three.js.
What topics are covered in this tutorial?
This tutorial covers: Agent Architecture, Layer 1: Boids Flocking, Layer 2: Formation Patterns, Leader-Follower Control, Obstacle Avoidance (Potential Fields), Agent State Machine, three.js Rendering, Tuning Guide.
What tools and technologies does this tutorial use?
This tutorial uses JavaScript, Three.js.
How long does this tutorial take?
This tutorial takes approximately 45 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.