前言
植物大战僵尸(Plants vs. Zombies)是一款经典的塔防游戏。本文将深入解析如何使用Python的Pygame库,从零构建一个功能完整的简化版PVZ游戏。这个项目不仅适合游戏开发初学者学习,也展示了面向对象设计在游戏开发中的应用。
一、项目架构概览
1.1 技术栈选择
-
Python 3.x:简洁的语法,丰富的库生态
-
Pygame:跨平台的游戏开发库,处理图形、音效和输入
-
类型提示(Type Hints):提升代码可读性和可维护性
-
Dataclass & Enum:现代化的Python特性,清晰的代码结构
1.2 核心类设计
项目采用经典的**实体组件系统(ECS)**思想,主要包含以下核心类:
Game (游戏主循环)
├── Plant (植物基类)
│ ├── Sunflower (向日葵)
│ ├── Peashooter (豌豆射手)
│ ├── WallNut (坚果墙)
│ ├── CherryBomb (樱桃炸弹)
│ └── SnowPea (寒冰射手)
├── Zombie (僵尸类)
├── Bullet (子弹/投射物)
├── Sun (阳光资源)
└── PlantCard (UI卡片)
二、核心机制实现
2.1 网格系统与坐标映射
塔防游戏的核心是网格系统。我们定义了草坪区域和单元格大小:
# 游戏区域定义
LAWN_X = 250 # 草坪左上角X坐标
LAWN_Y = 100 # 草坪左上角Y坐标
CELL_WIDTH = 81 # 单元格宽度
CELL_HEIGHT = 97 # 单元格高度
GRID_COLS = 9 # 列数
GRID_ROWS = 5 # 行数
屏幕坐标到网格坐标的转换是关键算法:
def get_grid_pos(self, mouse_pos: Tuple[int, int]) -> Optional[Tuple[int, int, int, int]]:
"""将鼠标位置转换为网格坐标,返回(x, y, row, col)"""
x, y = mouse_pos
if LAWN_X <= x < LAWN_X + GRID_COLS * CELL_WIDTH and \
LAWN_Y <= y < LAWN_Y + GRID_ROWS * CELL_HEIGHT:
col = int((x - LAWN_X) // CELL_WIDTH)
row = int((y - LAWN_Y) // CELL_HEIGHT)
# 计算单元格中心点坐标(用于植物放置)
grid_x = LAWN_X + col * CELL_WIDTH + CELL_WIDTH // 2
grid_y = LAWN_Y + row * CELL_HEIGHT + CELL_HEIGHT // 2
return (grid_x, grid_y, row, col)
return None
技术要点:
-
使用整数除法
//确保网格索引为整数 -
返回中心点坐标保证植物在单元格内居中显示
-
边界检查防止越界种植
2.2 游戏对象继承体系
使用枚举类型定义植物和僵尸种类,保证类型安全:
class PlantType(Enum):
SUNFLOWER = 0
PEASHOOTER = 1
WALLNUT = 2
CHERRY_BOMB = 3
SNOW_PEA = 4
class ZombieType(Enum):
NORMAL = 0
CONE = 1
BUCKET = 2
FLAG = 3
Plant类的初始化展示了如何根据类型动态设置属性:
def __init__(self, x: int, y: int, plant_type: PlantType, grid_row: int, grid_col: int):
# 基础属性
self.x = x
self.y = y
self.type = plant_type
self.grid_row = grid_row # 所在行,用于碰撞检测
self.grid_col = grid_col
# 根据类型设置特定属性(工厂模式思想)
if plant_type == PlantType.SUNFLOWER:
self.health = 80
self.produce_interval = 8000 # 8秒产生阳光
elif plant_type == PlantType.PEASHOOTER:
self.shoot_interval = 1500 # 1.5秒射击
# ... 其他类型
三、游戏循环与状态更新
3.1 主游戏循环结构
def run(self):
running = True
while running:
# 1. 事件处理(输入)
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
self.handle_click(event.pos)
# ... 其他事件
# 2. 游戏状态更新(逻辑)
self.update()
# 3. 渲染(输出)
self.draw()
# 4. 帧率控制
self.clock.tick(FPS)
这是标准的游戏循环模式:输入→更新→渲染。
3.2 对象池与生命周期管理
游戏中使用对象列表管理动态实体,并在更新时清理死亡对象:
def update(self):
# 更新植物
for plant in self.plants[:]: # 使用切片复制列表,避免遍历时修改
if not plant.alive:
self.plants.remove(plant)
continue
new_sun = plant.update(current_time, self.zombies, self.bullets)
if new_sun:
self.suns.append(new_sun)
# 类似地更新僵尸、子弹、阳光...
注意 :self.plants[:] 创建列表的浅拷贝,这样可以在遍历过程中安全地删除元素。
四、碰撞检测系统
4.1 行优先的碰撞检测优化
由于游戏是横向卷轴的,我们利用**网格行(grid_row)**进行空间分割,大幅减少碰撞检测次数:
# 僵尸检测是否遇到植物
for plant in plants:
if plant.grid_row == self.grid_row and plant.alive: # 先检查同行
if abs(self.x - plant.x) < 40: # 再检查距离
self.eating = True
plant.take_damage(self.damage / FPS)
这比遍历所有植物高效得多,时间复杂度从 O(N×M) 降到 O(N)。
4.2 子弹命中检测
def update(self, zombies: List[Zombie]):
self.x += self.speed
for zombie in zombies:
# 同行检测 + 距离检测
if zombie.grid_row == self.row and zombie.alive:
if abs(self.x - zombie.x) < 30 and abs(self.y - zombie.y) < 40:
zombie.take_damage(self.damage, self.slow)
self.alive = False # 子弹销毁
return
五、特殊机制实现
5.1 樱桃炸弹的AOE伤害
樱桃炸弹需要在种植后延迟爆炸,并造成范围伤害:
def update(self, current_time: int, zombies: List['Zombie'], bullets: List['Bullet']):
if self.type == PlantType.CHERRY_BOMB:
if current_time - self.plant_time > self.explode_delay: # 1秒后
self.explode(zombies)
self.alive = False # 植物死亡
def explode(self, zombies: List['Zombie']):
"""3x3范围伤害"""
for zombie in zombies:
# 行范围:±1行
if abs(zombie.grid_row - self.grid_row) <= 1:
col = int((zombie.x - LAWN_X) // CELL_WIDTH)
# 列范围:±1列
if abs(col - self.grid_col) <= 1 and zombie.alive:
zombie.take_damage(500) # 高额伤害
5.2 减速效果系统
寒冰射手的减速效果通过状态计时器实现:
class Zombie:
def __init__(self):
self.slow_timer = 0 # 减速帧数计时器
def update(self, plants: List[Plant]):
# 应用减速
current_speed = self.speed
if self.slow_timer > 0:
current_speed *= 0.5 # 速度减半
self.slow_timer -= 1 # 每帧递减
self.x -= current_speed # 移动
def take_damage(self, damage: int, slow: bool = False):
self.health -= damage
if slow:
self.slow_timer = 180 # 3秒(60fps)
5.3 阳光的物理与交互
阳光有两种产生方式:自然掉落和植物产出,具有不同的行为:
class Sun:
def __init__(self, x: int, y: int, target_y: Optional[int] = None, falling: bool = True):
self.falling = falling
self.speed = 2 if falling else 0
self.collected = False
def update(self):
if self.falling and self.y < self.target_y:
self.y += self.speed # 自然掉落
elif self.collected:
# 飞向阳光计数器的动画(线性插值)
self.x += (100 - self.x) * 0.1
self.y += (30 - self.y) * 0.1
收集动画 使用了简单的线性插值(Lerp),让阳光平滑飞向UI角落。
六、UI与游戏平衡
6.1 卡片冷却系统
防止玩家无限种植强力植物:
class PlantCard:
def __init__(self):
self.cooldown = 0
self.max_cooldown = 0 # 根据植物类型设置
def update(self, sun_amount: int):
if self.cooldown > 0:
self.cooldown -= 1000 / FPS # 每帧减少(毫秒)
def can_select(self, sun_amount: int) -> bool:
return sun_amount >= self.cost and self.cooldown <= 0
def draw(self, screen, sun_amount: int):
# 绘制冷却遮罩(半透明黑色)
if self.cooldown > 0:
cooldown_height = int(CARD_HEIGHT * (self.cooldown / self.max_cooldown))
s = pygame.Surface((CARD_WIDTH, cooldown_height), pygame.SRCALPHA)
s.fill((0, 0, 0, 200))
screen.blit(s, (self.x, self.y))
6.2 动态难度调整
僵尸生成速度随波次增加:
def spawn_zombie(self):
current_time = pygame.time.get_ticks()
# 波次越高,生成间隔越短(最低5秒)
spawn_delay = max(5000, 20000 - self.wave * 1000)
if current_time - self.last_zombie_spawn > spawn_delay:
# 根据波次决定僵尸类型概率
z_type = ZombieType.NORMAL
if self.wave > 2 and random.random() < 0.3:
z_type = ZombieType.CONE
if self.wave > 4 and random.random() < 0.2:
z_type = ZombieType.BUCKET
七、渲染技巧
7.1 动态视觉效果
阳光旋转动画使用三角函数计算顶点:
def draw(self, screen: pygame.Surface):
points = []
current_time = pygame.time.get_ticks()
for i in range(8):
angle = math.pi * 2 * i / 8 + current_time / 500 # 旋转
r = self.radius + 5 * math.sin(current_time / 200 + i) # 脉动
px = self.x + math.cos(angle) * r
py = self.y + math.sin(angle) * r
points.append((px, py))
pygame.draw.polygon(screen, YELLOW, points) # 绘制星形
7.2 半透明覆盖层
游戏结束时的暗色遮罩:
s = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
s.fill((0, 0, 0, 180)) # RGBA,180为透明度
self.screen.blit(s, (0, 0))
八、可扩展性设计
8.1 添加新植物的步骤
以添加"双发射手"为例:
-
在PlantType枚举中添加类型
-
在Plant.init中设置属性
-
在update中实现射击逻辑
-
在PlantCard颜色映射中添加颜色
-
在Game.reset_game中设置花费和冷却
8.2 潜在优化方向
-
精灵图(Sprite Sheet):使用真实图片替代绘制几何图形
-
音效系统:添加pygame.mixer处理射击、爆炸音效
-
存档系统:使用json或pickle保存游戏进度
-
粒子系统:爆炸时的粒子效果
-
寻路算法:更复杂的僵尸移动路径
九、总结
这个项目展示了如何用Python构建一个完整的游戏,涵盖了:
| 技术点 | 实现方式 |
|---|---|
| 游戏循环 | 标准输入-更新-渲染模式 |
| 碰撞检测 | 网格空间分割优化 |
| 状态管理 | 对象池 + 生命周期管理 |
| 资源管理 | 阳光经济系统 |
| UI系统 | 卡片冷却 + 选中状态 |
| 特效系统 | 三角函数动画 + 半透明渲染 |
感谢友友们的支持,需要完整代码的可以私信我,适合作为游戏开发的入门项目。通过类型提示和枚举,代码具有良好的自文档性和可维护性。

项目亮点:
-
✅ 完整的游戏循环和状态管理
-
✅ 五种植物 + 四种僵尸的差异化设计
-
✅ 优化的碰撞检测(行分割)
-
✅ 流畅的动画和视觉效果
-
✅ 平衡的经济和冷却系统