I recently came across an article that mentioned boids, which is essentially a computer simulation of a flock a birds. Boids uses three simple rules, from which complex behaviour emerges. Having studied large scale distributed systems I'm familiar with emergent behaviour, but I haven't seen anything that looks as impressive as boids.
The three rules of the Boid system are:
Rule 1: Don't overcrowd near-by boids Rule 2: Move closer to near-by boids Rule 3: Move in the general direction of the other boids
I thought I'd have a crack at implementing my own boids. I hadn't used PyGame before, but it looked ideal for this project.
First I came up with a basic boid class that stores the boids position and velocity.
class Boid:
def __init__(self, x, y):
self.x = x
self.y = y
self.velocityX = random.randint(1, 10) / 10.0
self.velocityY = random.randint(1, 10) / 10.0
I added a method to move the boid based on its velocity, and a method to detect the distance between two boids. Then I added the code for each of the rules. Rule 1: Don't overcrowd near-by boids.
"Move away from a set of boids. This avoids crowding"
def moveAway(self, boids, minDistance):
if len(boids) < 1: return
distanceX = 0
distanceY = 0
numClose = 0
for boid in boids:
distance = self.distance(boid)
if distance < minDistance:
numClose += 1
xdiff = (self.x - boid.x)
ydiff = (self.y - boid.y)
if xdiff >= 0: xdiff = math.sqrt(minDistance) - xdiff
elif xdiff < 0: xdiff = -math.sqrt(minDistance) - xdiff
if ydiff >= 0: ydiff = math.sqrt(minDistance) - ydiff
elif ydiff < 0: ydiff = -math.sqrt(minDistance) - ydiff
distanceX += xdiff
distanceY += ydiff
if numClose == 0:
return
self.velocityX -= distanceX / 5
self.velocityY -= distanceY / 5
Rule 2: Move closer to near-by boids.
"Move closer to a set of boids"
def moveCloser(self, boids):
if len(boids) < 1: return
# calculate the average distances from the other boids
avgX = 0
avgY = 0
for boid in boids:
if boid.x == self.x and boid.y == self.y:
continue
avgX += (self.x - boid.x)
avgY += (self.y - boid.y)
avgX /= len(boids)
avgY /= len(boids)
# set our velocity towards the others
distance = math.sqrt((avgX * avgX) + (avgY * avgY)) * -1.0
self.velocityX -= (avgX / 100)
self.velocityY -= (avgY / 100)
Rule 3: Move in the general direction of the other boids
"Move with a set of boids"
def moveWith(self, boids):
if len(boids) < 1: return
# calculate the average velocities of the other boids
avgX = 0
avgY = 0
for boid in boids:
avgX += boid.velocityX
avgY += boid.velocityY
avgX /= len(boids)
avgY /= len(boids)
# set our velocity towards the others
self.velocityX += (avgX / 40)
self.velocityY += (avgY / 40)
So that was the boid code sorted. Then all that was required was some code to tie it all together and display the boids:
pygame.init()
size = width, height = 800, 600
black = 0, 0, 0
maxVelocity = 10
numBoids = 50
boids = []
screen = pygame.display.set_mode(size)
ball = pygame.image.load("ball.png")
ballrect = ball.get_rect()
# create boids at random positions
for i in range(numBoids):
boids.append(Boid(random.randint(0, width), random.randint(0, height)))
while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
for boid in boids:
closeBoids = []
for otherBoid in boids:
if otherBoid == boid: continue
distance = boid.distance(otherBoid)
if distance < 200:
closeBoids.append(otherBoid)
boid.moveCloser(closeBoids)
boid.moveWith(closeBoids)
boid.moveAway(closeBoids, 20)
# ensure they stay within the screen space
# if we roubound we can lose some of our velocity
border = 25
if boid.x < border and boid.velocityX < 0:
boid.velocityX = -boid.velocityX * random.random()
if boid.x > width - border and boid.velocityX > 0:
boid.velocityX = -boid.velocityX * random.random()
if boid.y < border and boid.velocityY < 0:
boid.velocityY = -boid.velocityY * random.random()
if boid.y > height - border and boid.velocityY > 0:
boid.velocityY = -boid.velocityY * random.random()
boid.move()
screen.fill(black)
for boid in boids:
boidRect = pygame.Rect(ballrect)
boidRect.x = boid.x
boidRect.y = boid.y
screen.blit(ball, boidRect)
pygame.display.flip()
I'm really impressed with the results! From the interaction of three simple rules some quite complex behaviour emerges.