Skip to main content

Flocking/Swarm Behavior

Emergent Group Behaviors

Create mesmerizing swarm behaviors with simple rules! Learn Craig Reynolds' boids algorithm, steering behaviors, and how separation, alignment, and cohesion create lifelike flocking patterns! ๐Ÿฆ๐ŸŸ๐Ÿฆ‹

Understanding Flocking

๐Ÿฆ The Bird Flock Analogy

Think of flocking like a murmuration of starlings:

graph TD A["Flocking System"] --> B["Core Rules"] A --> C["Steering Behaviors"] A --> D["Advanced Features"] B --> E["Separation"] B --> F["Alignment"] B --> G["Cohesion"] C --> H["Seek/Flee"] C --> I["Arrive"] C --> J["Wander"] D --> K["Obstacle Avoidance"] D --> L["Leader Following"] D --> M["Predator/Prey"]
Three panels, each showing a focal boid (amber triangle) among blue neighbour triangles inside a dashed perception circle, with a green steering arrow. Separation: neighbours crowd close and the arrow steers the focal boid away from them. Alignment: neighbours all point the same way and the arrow turns the focal boid to match their average heading. Cohesion: neighbours are spread out with a marked centre of mass, and the arrow steers the focal boid toward that centre.
Flocking looks like coordinated intelligence, but there is no coordinator. Each boid senses only its nearby neighbours and blends three simple steering rules — separation (don’t crowd), alignment (match their heading), and cohesion (drift toward their centre). Weight and add those three little pushes every frame and the swirling, splitting, rejoining murmuration emerges on its own. This is the classic example of emergent behaviour: complex group motion from thousands of agents each running the same tiny bit of local math.

Interactive Flocking Demo

Click to attract boids, right-click to repel! Watch emergent behaviors form!

Spawn Swarms:

Boids: 0 | Avg Speed: 0 | Obstacles: 0 | Predators: 0

FPS: 60 | Groups Formed: 0

Flocking Implementation

import pygame
import math
import random
from typing import List, Tuple

class Vector2D:
    """2D Vector helper class"""
    def __init__(self, x: float = 0, y: float = 0) -> None:
        self.x: float = x
        self.y: float = y
    
    def add(self, other: 'Vector2D') -> 'Vector2D':
        return Vector2D(self.x + other.x, self.y + other.y)
    
    def subtract(self, other: 'Vector2D') -> 'Vector2D':
        return Vector2D(self.x - other.x, self.y - other.y)
    
    def multiply(self, scalar: float) -> 'Vector2D':
        return Vector2D(self.x * scalar, self.y * scalar)
    
    def divide(self, scalar: float) -> 'Vector2D':
        if scalar != 0:
            return Vector2D(self.x / scalar, self.y / scalar)
        return Vector2D(0, 0)
    
    def magnitude(self) -> float:
        return math.sqrt(self.x ** 2 + self.y ** 2)
    
    def normalize(self) -> 'Vector2D':
        mag = self.magnitude()
        if mag > 0:
            return self.divide(mag)
        return Vector2D(0, 0)
    
    def limit(self, max_val: float) -> 'Vector2D':
        if self.magnitude() > max_val:
            return self.normalize().multiply(max_val)
        return self
    
    def distance(self, other: 'Vector2D') -> float:
        return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
    
    def angle(self) -> float:
        return math.atan2(self.y, self.x)

class Boid:
    """Individual boid in the flock"""
    def __init__(self, x: float, y: float, max_speed: float = 150) -> None:
        self.position: Vector2D = Vector2D(x, y)
        self.velocity: Vector2D = Vector2D(random.uniform(-1, 1), random.uniform(-1, 1))
        self.acceleration: Vector2D = Vector2D(0, 0)
        
        # Movement constraints
        self.max_speed: float = max_speed
        self.max_force: float = 0.2
        
        # Perception
        self.vision_range: int = 50
        self.vision_angle: float = math.radians(270)
        
        # Behavior weights
        self.separation_weight: float = 1.5
        self.alignment_weight: float = 1.0
        self.cohesion_weight: float = 1.0
        
        # Visual
        self.size: int = 5
        self.color: tuple[int, int, int] = (76, 175, 80)
    
    def flock(self, boids: List['Boid']) -> None:
        """Apply flocking rules"""
        neighbors = self.get_neighbors(boids)
        
        # Calculate forces
        sep = self.separation(neighbors)
        align = self.alignment(neighbors)
        coh = self.cohesion(neighbors)
        
        # Weight forces
        sep = sep.multiply(self.separation_weight)
        align = align.multiply(self.alignment_weight)
        coh = coh.multiply(self.cohesion_weight)
        
        # Apply forces
        self.acceleration = self.acceleration.add(sep)
        self.acceleration = self.acceleration.add(align)
        self.acceleration = self.acceleration.add(coh)
    
    def get_neighbors(self, boids: List['Boid']) -> List['Boid']:
        """Find nearby boids within vision"""
        neighbors = []
        
        for other in boids:
            if other == self:
                continue
            
            distance = self.position.distance(other.position)
            
            if distance < self.vision_range:
                # Check if within vision angle
                if self.can_see(other):
                    neighbors.append(other)
        
        return neighbors
    
    def can_see(self, other: 'Boid') -> bool:
        """Check if another boid is within vision cone"""
        to_other = other.position.subtract(self.position)
        angle_to_other = to_other.angle()
        my_angle = self.velocity.angle()
        
        angle_diff = abs(angle_to_other - my_angle)
        if angle_diff > math.pi:
            angle_diff = 2 * math.pi - angle_diff
        
        return angle_diff < self.vision_angle / 2
    
    def separation(self, neighbors: List['Boid']) -> Vector2D:
        """Avoid crowding neighbors (separation)"""
        desired_separation = 25
        steer = Vector2D(0, 0)
        count = 0
        
        for other in neighbors:
            distance = self.position.distance(other.position)
            
            if 0 < distance < desired_separation:
                # Calculate repulsion force
                diff = self.position.subtract(other.position)
                diff = diff.normalize()
                diff = diff.divide(distance)  # Weight by distance
                steer = steer.add(diff)
                count += 1
        
        if count > 0:
            steer = steer.divide(count)
            
            # Implement Reynolds: Steering = Desired - Velocity
            if steer.magnitude() > 0:
                steer = steer.normalize()
                steer = steer.multiply(self.max_speed)
                steer = steer.subtract(self.velocity)
                steer = steer.limit(self.max_force)
        
        return steer
    
    def alignment(self, neighbors: List['Boid']) -> Vector2D:
        """Align with average heading of neighbors"""
        steer = Vector2D(0, 0)
        count = 0
        
        for other in neighbors:
            steer = steer.add(other.velocity)
            count += 1
        
        if count > 0:
            steer = steer.divide(count)
            steer = steer.normalize()
            steer = steer.multiply(self.max_speed)
            steer = steer.subtract(self.velocity)
            steer = steer.limit(self.max_force)
        
        return steer
    
    def cohesion(self, neighbors: List['Boid']) -> Vector2D:
        """Steer towards average position of neighbors"""
        steer = Vector2D(0, 0)
        count = 0
        
        for other in neighbors:
            steer = steer.add(other.position)
            count += 1
        
        if count > 0:
            steer = steer.divide(count)
            return self.seek(steer)
        
        return steer
    
    def seek(self, target: Vector2D) -> Vector2D:
        """Seek a target position"""
        desired = target.subtract(self.position)
        desired = desired.normalize()
        desired = desired.multiply(self.max_speed)
        
        steer = desired.subtract(self.velocity)
        steer = steer.limit(self.max_force)
        
        return steer
    
    def flee(self, target: Vector2D) -> Vector2D:
        """Flee from a target position"""
        desired = self.position.subtract(target)
        desired = desired.normalize()
        desired = desired.multiply(self.max_speed)
        
        steer = desired.subtract(self.velocity)
        steer = steer.limit(self.max_force)
        
        return steer
    
    def update(self, dt: float) -> None:
        """Update boid position"""
        # Update velocity
        self.velocity = self.velocity.add(self.acceleration)
        self.velocity = self.velocity.limit(self.max_speed)
        
        # Update position
        self.position.x += self.velocity.x * dt
        self.position.y += self.velocity.y * dt
        
        # Reset acceleration
        self.acceleration = Vector2D(0, 0)
    
    def edges(self, width: int, height: int) -> None:
        """Wrap around screen edges"""
        if self.position.x < 0:
            self.position.x = width
        elif self.position.x > width:
            self.position.x = 0
        
        if self.position.y < 0:
            self.position.y = height
        elif self.position.y > height:
            self.position.y = 0
    
    def draw(self, screen: pygame.Surface) -> None:
        """Draw the boid"""
        # Calculate angle for rotation
        angle = math.degrees(self.velocity.angle())
        
        # Create triangle points
        points = [
            (self.size, 0),
            (-self.size, -self.size // 2),
            (-self.size, self.size // 2)
        ]
        
        # Rotate and translate points
        rotated_points = []
        for px, py in points:
            # Rotate
            rx = px * math.cos(math.radians(angle)) - py * math.sin(math.radians(angle))
            ry = px * math.sin(math.radians(angle)) + py * math.cos(math.radians(angle))
            # Translate
            rotated_points.append((self.position.x + rx, self.position.y + ry))
        
        # Draw boid
        pygame.draw.polygon(screen, self.color, rotated_points)

Best Practices

โšก Flocking Tips

Key Takeaways

๐Ÿ‹๏ธโ€โ™‚๏ธ Practice Exercise

๐Ÿ‹๏ธโ€โ™‚๏ธ Exercise 1: Reynolds Boids โ€” Three Forces, Local Rules, Emergent Flock in One Pygame Window

Objective: Build a runnable pygame window in roughly 90 lines that shows three orthogonal Reynolds boids disciplines visible per frame on a 768ร—480 play area plus a 320px force-vector sidebar (1088ร—480 total). 30 boids spawn at random positions with random initial velocities and apply three additive steering forces every tick โ€” separation (avoid crowding neighbors via inverse-distance-weighted repulsion), alignment (match neighbor average velocity), and cohesion (steer toward neighbor centroid) โ€” within a vision_range of 50px and vision_angle of 270 degrees, with all forces summed into acceleration then Euler-integrated. (a) Local rules โ†’ emergent global behavior: each boid does ZERO global computation and only sees neighbors within its vision cone; the three additive steering forces each compute a small per-tick adjustment; the global flock pattern emerges from the bottom up with no central FlockManager โ€” multi-agent emergent behavior, the orthogonal axis to an earlier lesson's single-agent utility-AI ranking. (b) Reynolds steering formula steer = desired โˆ’ current_velocity capped by max_force: every rule ends with the same shape โ€” compute desired velocity from neighbors, normalize to max_speed, subtract current velocity, cap magnitude at max_force; the cap is what makes turns gradual over many frames rather than snap-pivots, the same incremental-adjustment-toward-target shape as the platformer_camera smooth-follow exponential decay. (c) Distance-weighted separation via inverse-distance scaling: the separation rule's diff.normalize().divide(distance) makes the steering force grow hyperbolically as boids get closer, so a near-collision pushes much harder than a comfortable-spacing nudge โ€” natural collision-avoidance shape rather than uniform avoidance. Keys 1/2/3 toggle each force ON/OFF independently so each rule's absence is visible (turning off separation makes boids clump into a single point; turning off alignment makes the flock lose direction; turning off cohesion makes the flock disperse). R resets all three to ON. Sidebar shows a representative boid's three force vectors as colored arrows (red=separation, green=alignment, blue=cohesion) plus the resulting velocity in white, scaled for visibility. HUD shows separation/alignment/cohesion ON/OFF state, average flock speed, and the legend โ€” three orthogonal Reynolds boids disciplines visible per frame as toggleable forces and concrete force-vector lengths.

Instructions:

  1. Create a Vector2D helper class with add / subtract / multiply / divide / magnitude / normalize / limit methods (the lesson's Vector2D shape; needed for all three rules' vector arithmetic).
  2. Create a Boid class with position / velocity / acceleration vectors plus a stored last_forces tuple of (separation, alignment, cohesion) for sidebar visualization. Implement a vision-cone neighbors check (within VISION_RANGE=50px and VISION_ANGLE=270ยฐ of the boid's current heading via dot-product comparison).
  3. Implement separation(neighbors) using steer += diff.normalize().divide(distance) for every neighbor inside DESIRED_SEP=25px โ€” the inverse-distance weighting is what makes urgent collisions push harder than comfortable spacings. Average across neighbors, then end with the Reynolds steering pattern steer = (averaged.normalize() * MAX_SPEED) โˆ’ current_velocity, limited to MAX_FORCE.
  4. Implement alignment(neighbors) as the average neighbor velocity, then the same Reynolds steering pattern.
  5. Implement cohesion(neighbors) as steer toward the neighbor centroid, then the same Reynolds steering pattern.
  6. Per-tick update: compute neighbors once, compute the three forces, multiply each by its weight (0 if toggled off, 1.5 / 1.0 / 1.0 default for sep / ali / coh), sum into acceleration, integrate velocity += acceleration; position += velocity * dt, wrap position around play area edges via modulo.
  7. Render: blue boid dots in the play area; sidebar with a representative boid plus its three force-vector arrows (red / green / blue) and resulting velocity arrow (white). HUD shows toggle state and average speed.
  8. Wire keys 1 / 2 / 3 to toggle separation / alignment / cohesion individually; key R to reset all three to ON. Verify by toggling: sep-off boids clump to one point, ali-off boids point random ways, coh-off boids disperse to grid edges.
๐Ÿ’ก Hint

The Reynolds steering pattern repeats three times โ€” extract a helper reynolds(desired, vel) that does desired.normalize().multiply(MAX_SPEED).subtract(vel).limit(MAX_FORCE). The vision-cone check is a dot-product comparison: angle between (other โˆ’ self) and self.velocity must be less than VISION_ANGLE / 2. Keep MAX_FORCE small (~0.2) โ€” that's the per-tick steering cap that produces smooth gradual turns; a larger value snap-pivots the boid each tick. Distance-weighted separation requires you to call .divide(distance) AFTER .normalize() so the weighting is by 1/distance not by 1/distanceยฒ. Position-wrap (position.x %= PLAY_W) keeps boids on screen forever without edge-collision logic.

โœ… Example Solution
import pygame, random, math

W, H, PLAY_W = 1088, 480, 768
N, VISION_RANGE, VISION_ANGLE = 30, 50, math.radians(270)
MAX_SPEED, MAX_FORCE, DESIRED_SEP = 150, 0.2, 25

class V:
    def __init__(s, x: float = 0, y: float = 0) -> None:
        s.x: float = x
        s.y: float = y
    def add(s, o: 'V') -> 'V': return V(s.x + o.x, s.y + o.y)
    def sub(s, o: 'V') -> 'V': return V(s.x - o.x, s.y - o.y)
    def mul(s, k: float) -> 'V': return V(s.x * k, s.y * k)
    def div(s, k: float) -> 'V': return V(s.x / k, s.y / k) if k else V()
    def mag(s) -> float: return math.hypot(s.x, s.y)
    def norm(s) -> 'V':
        m = s.mag()
        return s.div(m) if m else V()
    def limit(s, m: float) -> 'V':
        return s.norm().mul(m) if s.mag() > m else s

def reynolds(desired: 'V', vel: 'V') -> 'V':
    return desired.norm().mul(MAX_SPEED).sub(vel).limit(MAX_FORCE)

class Boid:
    def __init__(s, x: float, y: float) -> None:
        s.p: V = V(x, y)
        s.a: V = V()
        ang = random.uniform(0, 2 * math.pi)
        s.v: V = V(math.cos(ang) * 80, math.sin(ang) * 80)
        s.last: tuple[V, V, V] = (V(), V(), V())

    def can_see(s, o: 'Boid') -> bool:
        d = o.p.sub(s.p); m = d.mag()
        if m == 0 or m >= VISION_RANGE: return False
        if s.v.mag() == 0: return True
        cos_a = (d.x * s.v.x + d.y * s.v.y) / (m * s.v.mag())
        return cos_a > math.cos(VISION_ANGLE / 2)

    def step(s, boids: list['Boid'], ws: list[float], dt: float) -> None:
        ns = [o for o in boids if o is not s and s.can_see(o)]
        sep = V(); n = 0
        for o in ns:
            d = s.p.sub(o.p); m = d.mag()
            if 0 < m < DESIRED_SEP:
                sep = sep.add(d.norm().div(m)); n += 1  # inverse-distance
        sep = reynolds(sep.div(n), s.v) if n else V()
        if ns:
            avg_v = V()
            for o in ns: avg_v = avg_v.add(o.v)
            ali = reynolds(avg_v.div(len(ns)), s.v)
            cen = V()
            for o in ns: cen = cen.add(o.p)
            coh = reynolds(cen.div(len(ns)).sub(s.p), s.v)
        else:
            ali = coh = V()
        sep, ali, coh = sep.mul(ws[0]), ali.mul(ws[1]), coh.mul(ws[2])
        s.last = (sep, ali, coh)
        s.a = sep.add(ali).add(coh)
        s.v = s.v.add(s.a).limit(MAX_SPEED)
        s.p = s.p.add(s.v.mul(dt))
        s.p.x %= PLAY_W; s.p.y %= H

pygame.init()
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
font = pygame.font.SysFont("Courier", 14)
boids = [Boid(random.uniform(0, PLAY_W), random.uniform(0, H)) for _ in range(N)]
on = [True, True, True]
weights = [1.5, 1.0, 1.0]

while True:
    dt = clock.tick(60) / 1000.0
    for e in pygame.event.get():
        if e.type == pygame.QUIT: pygame.quit(); raise SystemExit
        if e.type == pygame.KEYDOWN:
            if e.key == pygame.K_1: on[0] = not on[0]
            if e.key == pygame.K_2: on[1] = not on[1]
            if e.key == pygame.K_3: on[2] = not on[2]
            if e.key == pygame.K_r: on = [True, True, True]
    ws = [weights[i] if on[i] else 0 for i in range(3)]
    for b in boids: b.step(boids, ws, dt)
    screen.fill((20, 30, 50))
    for b in boids:
        pygame.draw.circle(screen, (180, 220, 255), (int(b.p.x), int(b.p.y)), 4)
    pygame.draw.line(screen, (80, 80, 80), (PLAY_W, 0), (PLAY_W, H))
    bx, by = PLAY_W + 160, H // 2
    pygame.draw.circle(screen, (255, 200, 100), (bx, by), 8)
    sep, ali, coh = boids[0].last
    for vec, col in [(sep, (255, 80, 80)), (ali, (80, 255, 80)), (coh, (80, 80, 255))]:
        ex, ey = bx + int(vec.x * 250), by + int(vec.y * 250)
        pygame.draw.line(screen, col, (bx, by), (ex, ey), 3)
    vx, vy = bx + int(boids[0].v.x * 0.4), by + int(boids[0].v.y * 0.4)
    pygame.draw.line(screen, (255, 255, 255), (bx, by), (vx, vy), 3)
    avg = sum(b.v.mag() for b in boids) / N
    lines = [f"sep={'ON' if on[0] else 'OFF'}  ali={'ON' if on[1] else 'OFF'}  coh={'ON' if on[2] else 'OFF'}",
             "1/2/3 toggle  R reset",
             f"avg speed={avg:.0f}",
             "red=sep  grn=ali  blu=coh  wht=v"]
    for i, t in enumerate(lines):
        screen.blit(font.render(t, True, (240, 240, 240)), (PLAY_W + 10, 10 + i * 18))
    pygame.display.flip()

๐ŸŽฏ Quick Quiz

Question 1: In Reynolds boids (separation + alignment + cohesion), how does the global flock pattern arise from the per-boid code?

Question 2: All three boid rules end with the same shape: steer = desired.normalize().multiply(MAX_SPEED).subtract(velocity).limit(MAX_FORCE). What does the limit(MAX_FORCE) step contribute that just returning desired directly would not?

Question 3: The separation rule weights closer neighbors more strongly via diff.normalize().divide(distance). Why this inverse-distance scaling instead of treating every in-range neighbor equally?

What's Next?

Now that you've mastered flocking behaviors, next we'll explore simple decision-making systems for game AI!