用Python和Pygame从零实现Boids鸟群算法(附完整代码和可视化)

在计算机图形学和人工智能领域,群体智能一直是个令人着迷的话题。想象一下,你正在开发一款自然纪录片风格的模拟游戏,需要让成百上千只鸟在屏幕上形成逼真的群体飞行效果。传统方法是为每只鸟单独编写飞行路径,这不仅耗时耗力,而且难以实现真实的群体互动。这就是Boids算法大显身手的地方。

Boids算法由Craig Reynolds在1986年提出,它通过三条简单规则模拟了鸟群、鱼群等生物群体的集体行为。最神奇的是,这些看似复杂的群体行为,实际上只需要每个个体遵循几个基本行为准则就能实现。本文将带你用Python和Pygame库,从零开始实现这个经典算法,并创建实时可视化的鸟群模拟。

1. 环境准备与基础设置

在开始编码之前,我们需要搭建开发环境。Python 3.6+和Pygame库是本次项目的核心依赖。如果你还没有安装Pygame,可以通过以下命令快速安装:

pip install pygame numpy

为什么选择Pygame?因为它轻量级且易于使用,特别适合2D图形编程和快速原型开发。虽然它不像Unity或Unreal Engine那样功能强大,但对于我们的Boids模拟来说已经绰绰有余。

让我们先创建一个基础的Python文件 boids.py ,并设置基本的Pygame窗口:

import pygame
import numpy as np
import sys

# 初始化Pygame
pygame.init()

# 屏幕设置
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Boids 鸟群模拟")

# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 120, 255)

# 主循环
clock = pygame.time.Clock()
running = True

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    screen.fill(BLACK)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

这段代码创建了一个800x600像素的黑色窗口,帧率设置为60FPS。这是我们Boids模拟的画布,接下来我们将在这个基础上构建整个系统。

2. Boids算法核心原理

Boids算法的精髓在于三个基本原则,它们共同作用产生了复杂的群体行为:

  1. 分离(Separation) :避免与邻近个体相撞
  2. 对齐(Alignment) :与邻近个体的平均飞行方向保持一致
  3. 凝聚(Cohesion) :向邻近个体的平均位置(质心)移动

这三个规则看似简单,但组合起来却能产生令人惊讶的复杂行为。让我们更详细地分析每个规则:

2.1 分离规则实现

分离规则确保Boids不会相互碰撞。每个Boid会检测一定半径内的邻居,并产生一个远离它们的力。这个力的强度通常与距离成反比——邻居越近,排斥力越强。

在代码中,我们可以这样实现分离规则:

def separation(self, boids, separation_distance):
    steer = np.zeros(2)
    total = 0
    
    for other in boids:
        distance = np.linalg.norm(self.position - other.position)
        if 0 < distance < separation_distance:
            diff = self.position - other.position
            diff /= distance  # 标准化
            steer += diff
            total += 1
    
    if total > 0:
        steer /= total
        if np.linalg.norm(steer) > 0:
            steer = steer / np.linalg.norm(steer)  # 标准化
    
    return steer

2.2 对齐规则实现

对齐规则使Boids趋向于与邻近个体保持相同的飞行方向。这通过计算邻近Boids的平均速度向量来实现:

def alignment(self, boids, neighbor_distance):
    avg_velocity = np.zeros(2)
    total = 0
    
    for other in boids:
        distance = np.linalg.norm(self.position - other.position)
        if 0 < distance < neighbor_distance:
            avg_velocity += other.velocity
            total += 1
    
    if total > 0:
        avg_velocity /= total
        if np.linalg.norm(avg_velocity) > 0:
            avg_velocity = avg_velocity / np.linalg.norm(avg_velocity)  # 标准化
    
    return avg_velocity

2.3 凝聚规则实现

凝聚规则让Boids趋向于向邻近个体的中心位置移动,形成群体聚集效果:

def cohesion(self, boids, neighbor_distance):
    center_of_mass = np.zeros(2)
    total = 0
    
    for other in boids:
        distance = np.linalg.norm(self.position - other.position)
        if 0 < distance < neighbor_distance:
            center_of_mass += other.position
            total += 1
    
    if total > 0:
        center_of_mass /= total
        desired = center_of_mass - self.position
        if np.linalg.norm(desired) > 0:
            desired = desired / np.linalg.norm(desired)  # 标准化
    
    return desired

这三个规则共同作用时,会产生非常自然的群体行为。关键在于如何平衡它们的权重——分离通常需要最强的权重,其次是凝聚,最后是对齐。

3. 完整Boid类实现

现在我们将所有部分组合起来,创建一个完整的Boid类。每个Boid都有位置、速度和加速度属性,以及应用上述规则的方法:

class Boid:
    def __init__(self, x, y):
        self.position = np.array([x, y], dtype=float)
        self.velocity = np.random.uniform(-1, 1, 2)
        self.acceleration = np.zeros(2)
        self.max_speed = 3
        self.max_force = 0.05
        self.perception = 50
        
    def update(self):
        self.position += self.velocity
        self.velocity += self.acceleration
        # 限制最大速度
        if np.linalg.norm(self.velocity) > self.max_speed:
            self.velocity = self.velocity / np.linalg.norm(self.velocity) * self.max_speed
        self.acceleration = np.zeros(2)
        
    def apply_rules(self, boids):
        separation = self.separation(boids, 25)
        alignment = self.alignment(boids, self.perception)
        cohesion = self.cohesion(boids, self.perception)
        
        # 应用权重
        separation *= 1.5
        alignment *= 1.0
        cohesion *= 1.0
        
        self.acceleration += separation
        self.acceleration += alignment
        self.acceleration += cohesion
    
    def edges(self):
        if self.position[0] > WIDTH:
            self.position[0] = 0
        elif self.position[0] < 0:
            self.position[0] = WIDTH
            
        if self.position[1] > HEIGHT:
            self.position[1] = 0
        elif self.position[1] < 0:
            self.position[1] = HEIGHT
    
    def draw(self, screen):
        # 计算三角形顶点(指向飞行方向)
        angle = np.arctan2(self.velocity[1], self.velocity[0])
        points = [
            self.position + np.array([np.cos(angle), np.sin(angle)]) * 10,
            self.position + np.array([np.cos(angle + 2.5), np.sin(angle + 2.5)]) * 5,
            self.position + np.array([np.cos(angle - 2.5), np.sin(angle - 2.5)]) * 5
        ]
        pygame.draw.polygon(screen, BLUE, points)

这个Boid类包含了我们之前讨论的所有规则,以及一些辅助方法:

  • update() :更新位置和速度
  • edges() :处理屏幕边界(让Boids从一边穿到另一边)
  • draw() :在屏幕上绘制Boid(作为三角形)

4. 群体模拟与可视化

现在我们已经有了Boid类,可以创建一群Boids并观察它们的行为了。让我们修改主循环来创建和管理Boids群体:

# 创建Boids群体
num_boids = 50
boids = [Boid(np.random.uniform(0, WIDTH), 
              np.random.uniform(0, HEIGHT)) for _ in range(num_boids)]

# 主循环
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
    
    screen.fill(BLACK)
    
    # 更新和绘制所有Boids
    for boid in boids:
        boid.apply_rules(boids)
        boid.update()
        boid.edges()
        boid.draw(screen)
    
    pygame.display.flip()
    clock.tick(60)

运行这段代码,你会看到50个蓝色三角形在屏幕上飞行,逐渐形成群体行为。它们会自然地聚集成群,避免碰撞,并保持相似的飞行方向——所有这些都源自我们实现的三个简单规则。

5. 参数调优与高级功能

基本的Boids模拟已经完成,但我们可以进一步优化和扩展它。以下是几个改进方向:

5.1 参数调优

Boids行为对参数非常敏感。通过调整以下参数,你可以获得完全不同的群体行为:

参数 描述 典型值范围
max_speed Boids的最大速度 2-5
max_force Boids能施加的最大转向力 0.02-0.1
perception Boids能感知其他Boids的距离 30-100
分离权重 分离规则的相对重要性 1.0-2.0
对齐权重 对齐规则的相对重要性 0.5-1.5
凝聚权重 凝聚规则的相对重要性 0.5-1.5

你可以尝试不同的参数组合,观察群体行为的变化。例如,增加分离权重会使群体更加分散,而增加凝聚权重会使群体更加紧密。

5.2 添加障碍物避让

真实的鸟群需要避开树木、建筑物等障碍物。我们可以扩展Boids算法来实现这一点:

def avoid_obstacles(self, obstacles, avoid_distance):
    steer = np.zeros(2)
    
    for obstacle in obstacles:
        distance = np.linalg.norm(self.position - obstacle.position)
        if distance < avoid_distance:
            diff = self.position - obstacle.position
            diff /= distance  # 标准化
            steer += diff
    
    if np.linalg.norm(steer) > 0:
        steer = steer / np.linalg.norm(steer)  # 标准化
    
    return steer

然后在 apply_rules 方法中添加对新规则的应用:

def apply_rules(self, boids, obstacles):
    # 原有规则...
    avoidance = self.avoid_obstacles(obstacles, 50)
    avoidance *= 2.0  # 避障通常需要更高的权重
    self.acceleration += avoidance

5.3 优化性能

当Boids数量增加时,性能可能成为问题。因为每个Boid需要检查所有其他Boid,算法复杂度是O(N²)。对于大量Boids,我们可以使用空间分区技术来优化:

from scipy.spatial import KDTree

def update_boids(boids):
    positions = np.array([boid.position for boid in boids])
    tree = KDTree(positions)
    
    for i, boid in enumerate(boids):
        # 只查询附近一定距离内的Boids
        neighbors_idx = tree.query_ball_point(boid.position, boid.perception)
        neighbors = [boids[j] for j in neighbors_idx if j != i]
        boid.apply_rules(neighbors)
        boid.update()
        boid.edges()

这种方法可以显著减少不必要的距离计算,特别是当Boids数量很大时。

6. 完整代码与交互功能

以下是整合了所有功能的完整代码,还包括了鼠标交互(点击添加Boids,右键添加障碍物):

import pygame
import numpy as np
import sys
from scipy.spatial import KDTree

# 初始化
pygame.init()
WIDTH, HEIGHT = 1000, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Boids 鸟群模拟 - 完整版")

# 颜色
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 120, 255)
RED = (255, 50, 50)
GREEN = (50, 255, 50)

class Boid:
    def __init__(self, x, y):
        self.position = np.array([x, y], dtype=float)
        self.velocity = np.random.uniform(-1, 1, 2)
        self.acceleration = np.zeros(2)
        self.max_speed = 3
        self.max_force = 0.05
        self.perception = 50
        
    def update(self):
        self.position += self.velocity
        self.velocity += self.acceleration
        if np.linalg.norm(self.velocity) > self.max_speed:
            self.velocity = self.velocity / np.linalg.norm(self.velocity) * self.max_speed
        self.acceleration = np.zeros(2)
        
    def apply_rules(self, boids, obstacles):
        separation = self.separation(boids, 25)
        alignment = self.alignment(boids, self.perception)
        cohesion = self.cohesion(boids, self.perception)
        avoidance = self.avoid_obstacles(obstacles, 50)
        
        separation *= 1.5
        alignment *= 1.0
        cohesion *= 1.0
        avoidance *= 2.0
        
        self.acceleration += separation
        self.acceleration += alignment
        self.acceleration += cohesion
        self.acceleration += avoidance
    
    def separation(self, boids, separation_distance):
        steer = np.zeros(2)
        total = 0
        for other in boids:
            distance = np.linalg.norm(self.position - other.position)
            if 0 < distance < separation_distance:
                diff = self.position - other.position
                diff /= distance
                steer += diff
                total += 1
        if total > 0:
            steer /= total
            if np.linalg.norm(steer) > 0:
                steer = steer / np.linalg.norm(steer)
        return steer
    
    def alignment(self, boids, neighbor_distance):
        avg_velocity = np.zeros(2)
        total = 0
        for other in boids:
            distance = np.linalg.norm(self.position - other.position)
            if 0 < distance < neighbor_distance:
                avg_velocity += other.velocity
                total += 1
        if total > 0:
            avg_velocity /= total
            if np.linalg.norm(avg_velocity) > 0:
                avg_velocity = avg_velocity / np.linalg.norm(avg_velocity)
        return avg_velocity
    
    def cohesion(self, boids, neighbor_distance):
        center_of_mass = np.zeros(2)
        total = 0
        for other in boids:
            distance = np.linalg.norm(self.position - other.position)
            if 0 < distance < neighbor_distance:
                center_of_mass += other.position
                total += 1
        if total > 0:
            center_of_mass /= total
            desired = center_of_mass - self.position
            if np.linalg.norm(desired) > 0:
                desired = desired / np.linalg.norm(desired)
            return desired
        return np.zeros(2)
    
    def avoid_obstacles(self, obstacles, avoid_distance):
        steer = np.zeros(2)
        for obstacle in obstacles:
            distance = np.linalg.norm(self.position - obstacle.position)
            if distance < avoid_distance:
                diff = self.position - obstacle.position
                diff /= distance
                steer += diff
        if np.linalg.norm(steer) > 0:
            steer = steer / np.linalg.norm(steer)
        return steer
    
    def edges(self):
        if self.position[0] > WIDTH:
            self.position[0] = 0
        elif self.position[0] < 0:
            self.position[0] = WIDTH
        if self.position[1] > HEIGHT:
            self.position[1] = 0
        elif self.position[1] < 0:
            self.position[1] = HEIGHT
    
    def draw(self, screen):
        angle = np.arctan2(self.velocity[1], self.velocity[0])
        points = [
            self.position + np.array([np.cos(angle), np.sin(angle)]) * 10,
            self.position + np.array([np.cos(angle + 2.5), np.sin(angle + 2.5)]) * 5,
            self.position + np.array([np.cos(angle - 2.5), np.sin(angle - 2.5)]) * 5
        ]
        pygame.draw.polygon(screen, BLUE, points)

class Obstacle:
    def __init__(self, x, y, radius=30):
        self.position = np.array([x, y])
        self.radius = radius
    
    def draw(self, screen):
        pygame.draw.circle(screen, RED, self.position.astype(int), self.radius)

# 创建初始Boids和障碍物
num_boids = 50
boids = [Boid(np.random.uniform(0, WIDTH), np.random.uniform(0, HEIGHT)) for _ in range(num_boids)]
obstacles = []

# 主循环
clock = pygame.time.Clock()
running = True

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            x, y = pygame.mouse.get_pos()
            if event.button == 1:  # 左键添加Boid
                boids.append(Boid(x, y))
            elif event.button == 3:  # 右键添加障碍物
                obstacles.append(Obstacle(x, y))
    
    screen.fill(BLACK)
    
    # 更新和绘制障碍物
    for obstacle in obstacles:
        obstacle.draw(screen)
    
    # 使用KDTree优化邻居查询
    positions = np.array([boid.position for boid in boids])
    if len(positions) > 0:
        tree = KDTree(positions)
    
    # 更新和绘制Boids
    for i, boid in enumerate(boids):
        neighbors_idx = tree.query_ball_point(boid.position, boid.perception)
        neighbors = [boids[j] for j in neighbors_idx if j != i]
        boid.apply_rules(neighbors, obstacles)
        boid.update()
        boid.edges()
        boid.draw(screen)
    
    # 显示信息
    font = pygame.font.SysFont('Arial', 16)
    text = font.render(f'Boids数量: {len(boids)} | 左键添加Boid | 右键添加障碍物 | ESC退出', True, WHITE)
    screen.blit(text, (10, 10))
    
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

这段完整代码提供了交互式体验,你可以:

  • 左键点击添加新的Boids
  • 右键点击添加障碍物
  • 观察Boids如何自然地避开障碍物并保持群体行为
  • 实时查看Boids数量

7. 进一步探索方向

Boids算法虽然简单,但有着广泛的应用和扩展空间。以下是一些你可以尝试的进阶方向:

7.1 3D扩展

我们的实现是2D的,但Boids算法同样适用于3D空间。使用Pygame的3D扩展或切换到PyOpenGL等库,你可以创建更真实的鸟群模拟。3D版本需要考虑额外的维度,但核心规则保持不变。

7.2 捕食者-猎物模型

通过引入捕食者Boids(遵循不同规则),你可以模拟更复杂的生态系统。例如:

  • 普通Boids会逃离捕食者
  • 捕食者Boids会追逐普通Boids
  • 不同行为可以设置不同的视觉范围和速度

7.3 路径规划与目标导向

让Boids群体朝着特定目标移动,同时保持群体行为。这可以通过添加"目标导向"规则实现,类似于我们添加障碍物避让的方式。

7.4 与其他算法结合

Boids可以与其他算法结合创造更复杂的行为:

  • 与A*路径规划结合,让Boids在复杂环境中导航
  • 与神经网络结合,让Boids学习适应不同环境
  • 与遗传算法结合,优化Boids参数

提示:在扩展Boids算法时,保持核心规则的简洁性很重要。复杂行为通常可以通过添加简单规则而非修改现有规则来实现。

Boids算法的魅力在于,简单的规则能够产生令人惊叹的复杂行为。通过这个项目,你不仅学习了一个经典的AI算法,还掌握了如何用Python和Pygame实现可视化模拟。这些技能可以应用于游戏开发、数据可视化、机器人控制等多个领域。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐