Tutorial · Multi-Agent · Intermediate
📅 March 2026⏱ 45 min read🎓 Intermediate⚙️ JavaScript / Three.js

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);
}
Known limitation of APF: Drones can get trapped in local minima. Solutions: (1) add random perturbation when velocity ≈ 0 for N frames, (2) switch to A* pathfinding when stuck, (3) use RRT (Rapidly-exploring Random Trees) for complex environments.

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
Performance: The naive O(N²) neighbor search bottlenecks at N ≈ 500. For larger swarms, use a spatial hash grid (divide 3D space into cells, only check drones in nearby cells) to reduce complexity to roughly O(N). At N = 1000 (60 FPS target), a cell size equal to VIEW_RADIUS gives ~50 candidates per drone vs ~1000 — a 20× speedup.

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.