Flocking simulation in Python

Flocking simulation is a type of computer simulation that models the behavior of a group of autonomous agents, such as birds, fish, or insects, as they move and interact with one another in a flock or swarm. The simulation is based on the principles of emergent behavior, where the overall behavior of the group emerges from the individual interactions between each agent.

Flocking simulations can be used in a variety of applications, including robotics, computer graphics, and game development, to simulate the behavior of crowds, traffic, and other complex systems.

Boid flocking particle effect

Imports

import tkinter
import random
import math
import Boid

We import the required libraries. tkinter is a standard Python library for creating graphical user interfaces. math provides mathematical functions and constants. Boid is a custom module defined in a separate Python file that contains all functions needed to control individual boids.

Canvas initialisation

def initialise_canvas(window, screen_size):
    canvas = tkinter.Canvas(window, width=screen_size, height=screen_size)
    canvas.pack()
    window.resizable(True, True)
    return canvas

initialise_canvas takes two parameters:

  1. window — a tkinter window object.
  2. screen_size — an integer representing the desired canvas size in pixels.

The function creates a new canvas using tkinter.Canvas() with the specified width and height, adds it to the window via pack(), marks the window as resizable in both directions, and returns the canvas object.

Creating boids

def create_boids(canvas, no_of_boids):
    list_of_boids = []
    for n in range(no_of_boids):
        boid = Boid.Boid("boid" + str(n))
        list_of_boids.append(boid)
        boid.draw_boid(canvas)
    return list_of_boids

create_boids takes two parameters:

  1. canvas — the canvas object on which boids will be drawn.
  2. no_of_boids — the number of boid objects to create.

The function initialises an empty list_of_boids, loops no_of_boids times, creates a new Boid with a unique name on each iteration, appends it to the list, draws it on the canvas, and returns the completed list.

Separation

def separation(nearest_neighbour, boid):
    if nearest_neighbour is not None and boid.euclidean_distance(nearest_neighbour) < 35:
        if nearest_neighbour.x - boid.x == 0.0:
            angle = math.atan((nearest_neighbour.y - boid.y) / 0.0001)
        else:
            angle = math.atan((nearest_neighbour.y - boid.y) / (nearest_neighbour.x - boid.x))
        boid.angle -= angle

separation takes nearest_neighbour and boid as arguments. It calculates the angle between the boid and its nearest neighbour, then steers the boid in the opposite direction.

The condition checks whether a nearest neighbour exists and whether the distance to it is less than 35 units. If met, the angle is computed from the positional difference.

Euclidean distance

The Euclidean distance between two points in Euclidean space is the length of the line segment connecting them. It is derived from the Cartesian coordinates of the points using the Pythagorean theorem, which is why it is sometimes called the Pythagorean distance.

If the nearest neighbour is directly above or below the boid, the function uses math.atan((nearest_neighbour.y - boid.y) / 0.0001) to avoid division by zero. The resulting angle is then subtracted from the boid’s current angle, steering it away from the neighbour.

Division by zero

When a number is divided by zero, the result is mathematically infinite and cannot be represented physically. Python raises a ZeroDivisionError: division by zero exception in this case.

Alignment

def alignment(neighbours, boid):
    average_neighbours_angle = 0.0
    if neighbours:
        for neighbour_boid in neighbours:
            average_neighbours_angle += neighbour_boid.angle
        average_neighbours_angle /= len(neighbours)
        boid.angle -= (average_neighbours_angle - boid.angle) / 100.0
        boid.angle = average_neighbours_angle

alignment steers the boid to match the heading of its neighbours by nudging its angle toward their average. The average angle is computed by summing all neighbour angles and dividing by the neighbour count. The boid’s angle is then adjusted by (average_neighbours_angle - boid.angle) / 100.0 and finally set to the average.

Cohesion

def cohesion(neighbours, boid):
    if neighbours:
        avg_x = 0.0
        avg_y = 0.0
        for neighbour_boid in neighbours:
            avg_x += neighbour_boid.x
            avg_y += neighbour_boid.y
        avg_x /= len(neighbours)
        avg_y /= len(neighbours)
        if avg_x - boid.x == 0.0:
            angle = math.atan((avg_y - boid.y) / 0.00001)
        else:
            angle = math.atan((avg_y - boid.y) / (avg_x - boid.x))
        boid.angle -= angle / 20.0

cohesion moves the boid toward the average position of its neighbours by adjusting its heading. The average x and y coordinates of all neighbours are computed, and the angle toward that centroid is calculated — again guarding against division by zero when the boid is directly above or below the average position.

Division by zero

When a number is divided by zero, the result is mathematically infinite and cannot be represented physically. Python raises a ZeroDivisionError: division by zero exception in this case.

The angle is divided by 20 before being subtracted from the boid’s current angle, ensuring the boid moves toward the group centre gradually rather than snapping to it.

Applying behaviours

def boid_behaviours(canvas, list_of_boids, screen_size):
    for boid in list_of_boids:
        neighbours = []
        for b in list_of_boids:
            if boid.euclidean_distance(b) < 75 and (not boid.euclidean_distance(b) == 0):
                neighbours.append(b)
        nearest_neighbour = None
        if neighbours:
            shortest_distance = 999999999
            for neighbour_boid in neighbours:
                d = boid.euclidean_distance(neighbour_boid)
                if d < shortest_distance:
                    shortest_distance = d
                    nearest_neighbour = neighbour_boid

        separation(nearest_neighbour, boid)
        alignment(neighbours, boid)
        cohesion(neighbours, boid)

    for boid in list_of_boids:
        boid.flock(canvas, screen_size)
    canvas.after(100, boid_behaviours, canvas, list_of_boids, screen_size)

boid_behaviours applies separation, alignment, and cohesion to every boid in the list and updates their positions. A neighbour is defined as any other boid within 75 distance units. After all three behaviours are applied, the flock method updates each boid’s position and redraws it. The function schedules itself to run again after 100 milliseconds via canvas.after.

Entry point

def main():
    screen_size = 800
    no_of_boids = 100
    window = tkinter.Tk()
    canvas = initialise_canvas(window, screen_size)
    list_of_boids = create_boids(canvas, no_of_boids)
    boid_behaviours(canvas, list_of_boids, screen_size)
    window.mainloop()

main()

main sets the canvas to 800×800 pixels, spawns 100 boids, kicks off the behaviour loop, and enters the tkinter event loop. The call at the bottom executes the program immediately.

The Boid class

class Boid:
    def __init__(self, label):
        self.x = random.randrange(100, 900)
        self.y = random.randrange(100, 900)
        self.angle = random.uniform(0.0, 2.0 * math.pi)
        self.label = label
        self.color = ["blue", "red", "green"]

    def draw_boid(self, canvas):
        size = 10
        x1 = self.x + size * math.cos(self.angle)
        x2 = self.y + size * math.sin(self.angle)
        canvas.create_line(self.x, self.y, x1, x2, fill='black', arrow='last',
                           arrowshape=(12.8, 16, 4.8), width=2, tags=self.label)

    def flock(self, canvas, screen_size):
        distance = 3
        self.x += distance * math.cos(self.angle)
        self.y += distance * math.sin(self.angle)
        self.x = self.x % screen_size
        self.y = self.y % screen_size
        canvas.delete(self.label)
        self.draw_boid(canvas)

    def euclidean_distance(self, neighbour_boid):
        return math.sqrt(
            (self.x - neighbour_boid.x) ** 2 +
            (self.y - neighbour_boid.y) ** 2
        )

The Boid class is the custom module imported by the main script.

Attributes:

AttributeDescription
xRandom starting x-coordinate (100–900)
yRandom starting y-coordinate (100–900)
angleRandom initial heading in radians (0 to 2π)
labelUnique string identifier for canvas tagging
colorAvailable colours (unused in drawing, reserved for extension)

Methods:

  • __init__ — Initialises the boid with random position and angle.
  • draw_boid — Renders the boid as an arrow line on the canvas.
  • flock — Advances the boid by 3 units along its current heading, wraps it within the screen using modulo arithmetic, and redraws it.
  • euclidean_distance — Returns the straight-line distance to another boid.

Summary

Each boid in the simulation has three possible movements:

  • Separation — move away from the nearest neighbour to avoid collision.
  • Alignment — orient toward the average heading of nearby neighbours.
  • Cohesion — steer toward the average position of the group.

Together, these three simple rules produce the emergent flocking behaviour seen in the simulation.


For queries or suggestions, feel free to write to subkamble@gmail.com.