用Python和Pygame复刻简化版《植物大战僵尸》:从数学建模到游戏开发的保姆级教程
用Python和Pygame复刻简化版《植物大战僵尸》:从数学建模到游戏开发的保姆级教程
当数学建模遇上游戏开发,会碰撞出怎样的火花?本文将带你用Python和Pygame,从零开始实现一个简化版的《植物大战僵尸》游戏。不同于传统教程,我们将特别关注如何将数学模型转化为可运行的代码,让抽象的数学参数"活"起来。无论你是想学习游戏开发,还是对数学建模感兴趣,这个项目都能让你获得双重收获。
1. 游戏框架搭建
1.1 Pygame基础配置
首先确保已安装Pygame库:
pip install pygame
创建一个基础游戏窗口:
import pygame
import sys
# 初始化pygame
pygame.init()
# 设置窗口尺寸
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("植物大战僵尸简化版")
# 游戏主循环
clock = pygame.time.Clock()
FPS = 60
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 填充背景色
screen.fill((135, 206, 235)) # 天蓝色背景
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()
1.2 游戏场景设计
我们需要设计一个网格化的战场,这是游戏的核心场景。根据数学建模题目,我们设置9个格子:
class GameMap:
def __init__(self):
self.grid_size = 80
self.rows = 5
self.cols = 9
self.plants = [[None for _ in range(self.cols)] for _ in range(self.rows)]
self.zombies = []
def draw(self, surface):
# 绘制网格线
for row in range(self.rows + 1):
pygame.draw.line(surface, (0, 100, 0),
(0, row * self.grid_size),
(self.cols * self.grid_size, row * self.grid_size), 2)
for col in range(self.cols + 1):
pygame.draw.line(surface, (0, 100, 0),
(col * self.grid_size, 0),
(col * self.grid_size, self.rows * self.grid_size), 2)
# 绘制植物和僵尸
for row in range(self.rows):
for col in range(self.cols):
if self.plants[row][col]:
self.plants[row][col].draw(surface)
for zombie in self.zombies:
zombie.draw(surface)
2. 游戏角色实现
2.1 植物类设计
根据数学模型,我们需要实现两种植物:向日葵和豌豆射手。
class Plant:
def __init__(self, x, y):
self.x = x
self.y = y
self.health = 100
self.rect = pygame.Rect(x, y, 80, 80)
def draw(self, surface):
surface.blit(self.image, (self.x, self.y))
def update(self):
pass
class Sunflower(Plant):
def __init__(self, x, y):
super().__init__(x, y)
self.image = pygame.Surface((80, 80))
self.image.fill((255, 255, 0)) # 黄色代表向日葵
self.sun_produce_timer = 0
self.sun_produce_interval = 400 # 根据数学模型,僵尸走4格时间
def update(self):
self.sun_produce_timer += 1
if self.sun_produce_timer >= self.sun_produce_interval:
self.sun_produce_timer = 0
return True # 产生阳光
return False
class Peashooter(Plant):
def __init__(self, x, y):
super().__init__(x, y)
self.image = pygame.Surface((80, 80))
self.image.fill((0, 255, 0)) # 绿色代表豌豆射手
self.shoot_timer = 0
self.shoot_interval = 100 # 根据数学模型,与僵尸步频相等
def update(self, zombies):
self.shoot_timer += 1
if self.shoot_timer >= self.shoot_interval:
self.shoot_timer = 0
# 检查右侧是否有僵尸
for zombie in zombies:
if zombie.row == self.row and zombie.col > self.col:
return True # 可以发射豌豆
return False
2.2 僵尸类实现
僵尸的行为需要严格遵循数学模型中的参数:
class Zombie:
def __init__(self, row):
self.row = row
self.col = 8 # 从最右侧出现
self.x = 8 * 80
self.y = row * 80
self.speed = 1
self.health = 900 # 需要9颗豌豆才能消灭
self.step_counter = 0
self.steps_per_cell = 3 # 僵尸3步走一格
self.image = pygame.Surface((80, 80))
self.image.fill((100, 100, 100)) # 灰色代表僵尸
self.rect = pygame.Rect(self.x, self.y, 80, 80)
self.eating = False
self.eat_timer = 0
def update(self, plants):
if self.eating:
self.eat_timer += 1
if self.eat_timer >= 3 * self.steps_per_cell: # 吃植物需要走3步的时间
self.eating = False
self.eat_timer = 0
return
self.step_counter += 1
if self.step_counter >= self.steps_per_cell:
self.step_counter = 0
self.col -= 1
self.x -= 80
# 检查是否碰到植物
for plant_row in range(len(plants)):
for plant_col in range(len(plants[plant_row])):
plant = plants[plant_row][plant_col]
if plant and plant_row == self.row and plant_col == self.col:
self.eating = True
return
def draw(self, surface):
surface.blit(self.image, (self.x, self.y))
3. 游戏机制实现
3.1 阳光经济系统
根据数学模型,阳光系统需要实现以下规则:
- 向日葵每僵尸走4格时间产生1阳光
- 阳光不点击会在僵尸走1格时间后消失
- 种植向日葵需要2阳光,豌豆射手需要4阳光
class SunSystem:
def __init__(self):
self.sun_count = 6 # 初始阳光数
self.active_suns = [] # 屏幕上可点击的阳光
self.font = pygame.font.SysFont(None, 36)
def add_sun(self, x, y):
self.active_suns.append({
'x': x,
'y': y,
'timer': 0,
'lifetime': 100, # 僵尸走1格的时间
'rect': pygame.Rect(x, y, 50, 50)
})
def update(self):
# 更新现有阳光的生命周期
for sun in self.active_suns[:]:
sun['timer'] += 1
if sun['timer'] >= sun['lifetime']:
self.active_suns.remove(sun)
def draw(self, surface):
# 绘制阳光数量
sun_text = self.font.render(f"阳光: {self.sun_count}", True, (0, 0, 0))
surface.blit(sun_text, (10, 10))
# 绘制可收集的阳光
for sun in self.active_suns:
pygame.draw.circle(surface, (255, 255, 0), (sun['x'], sun['y']), 25)
def check_click(self, pos):
for sun in self.active_suns[:]:
if sun['rect'].collidepoint(pos):
self.sun_count += 1
self.active_suns.remove(sun)
return True
return False
3.2 游戏主逻辑
整合所有系统,实现游戏主循环:
class Game:
def __init__(self):
self.map = GameMap()
self.sun_system = SunSystem()
self.zombie_spawn_timer = 0
self.zombie_spawn_interval = 300 # 僵尸生成间隔
self.game_over = False
self.font = pygame.font.SysFont(None, 72)
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # 左键
# 检查是否点击了阳光
if self.sun_system.check_click(event.pos):
continue
# 转换为网格坐标
col = event.pos[0] // 80
row = event.pos[1] // 80
# 检查是否可以种植植物
if 0 <= row < self.map.rows and 0 <= col < self.map.cols:
if not self.map.plants[row][col]:
# 检查阳光是否足够
if self.sun_system.sun_count >= 2: # 向日葵价格
self.map.plants[row][col] = Sunflower(col * 80, row * 80)
self.sun_system.sun_count -= 2
elif self.sun_system.sun_count >= 4: # 豌豆射手价格
self.map.plants[row][col] = Peashooter(col * 80, row * 80)
self.sun_system.sun_count -= 4
return True
def update(self):
if self.game_over:
return
# 更新植物
for row in range(self.map.rows):
for col in range(self.map.cols):
plant = self.map.plants[row][col]
if plant:
if isinstance(plant, Sunflower):
if plant.update(): # 产生阳光
self.sun_system.add_sun(plant.x + 40, plant.y + 40)
elif isinstance(plant, Peashooter):
if plant.update(self.map.zombies):
# 发射豌豆的逻辑
pass
# 更新僵尸
for zombie in self.map.zombies[:]:
zombie.update(self.map.plants)
if zombie.col < 0: # 僵尸到达最左侧
self.game_over = True
# 生成新僵尸
self.zombie_spawn_timer += 1
if self.zombie_spawn_timer >= self.zombie_spawn_interval:
self.zombie_spawn_timer = 0
row = random.randint(0, self.map.rows - 1)
self.map.zombies.append(Zombie(row))
# 更新阳光系统
self.sun_system.update()
def draw(self, surface):
surface.fill((135, 206, 235)) # 天蓝色背景
self.map.draw(surface)
self.sun_system.draw(surface)
if self.game_over:
game_over_text = self.font.render("游戏结束!", True, (255, 0, 0))
surface.blit(game_over_text, (SCREEN_WIDTH//2 - 150, SCREEN_HEIGHT//2 - 36))
def run(self):
clock = pygame.time.Clock()
running = True
while running:
running = self.handle_events()
self.update()
self.draw(pygame.display.get_surface())
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()
4. 数学模型与游戏参数的对应关系
4.1 时间系统的转换
游戏中的时间系统需要与数学模型严格对应:
| 数学模型描述 | 游戏代码实现 | 参数值 |
|---|---|---|
| 僵尸3步走一个格 | Zombie.steps_per_cell | 3 |
| 豌豆发射频率与僵尸步频相等 | Peashooter.shoot_interval | 100帧 |
| 豌豆飞行6格时间僵尸走一步 | 豌豆速度计算 | 需根据帧率调整 |
| 僵尸被9粒豌豆打中立即消亡 | Zombie.health | 900 (每颗豌豆伤害100) |
| 僵尸用走3步的时间吃掉植物 | Zombie.eat_timer比较 | 3 * steps_per_cell |
4.2 碰撞检测优化
精确的碰撞检测是游戏运行的关键:
def check_collision(pea, zombie):
# 简单的矩形碰撞检测
if (pea.x < zombie.x + zombie.width and
pea.x + pea.width > zombie.x and
pea.y < zombie.y + zombie.height and
pea.y + pea.height > zombie.y):
return True
return False
# 更精确的像素级碰撞检测(性能消耗更大)
def precise_collision(pea, zombie):
pea_mask = pygame.mask.from_surface(pea.image)
zombie_mask = pygame.mask.from_surface(zombie.image)
offset = (zombie.x - pea.x, zombie.y - pea.y)
collision_point = pea_mask.overlap(zombie_mask, offset)
return collision_point is not None
4.3 游戏平衡性调整
根据数学模型的最优解,我们可以预设一些平衡参数:
# 根据问题3的最优解:最少种5棵豌豆荚
INITIAL_PLANTS = [
(0, 0, 'peashooter'),
(1, 0, 'peashooter'),
(2, 0, 'peashooter'),
(3, 0, 'peashooter'),
(4, 0, 'peashooter')
]
# 根据问题4的最佳方案:1向日葵+1豌豆射手
BEST_STRATEGY = [
(2, 4, 'sunflower'),
(2, 0, 'peashooter')
]
5. 高级功能扩展
5.1 游戏存档与读档
实现游戏状态的保存与加载:
import pickle
def save_game(game_state, filename):
with open(filename, 'wb') as f:
pickle.dump(game_state, f)
def load_game(filename):
with open(filename, 'rb') as f:
return pickle.load(f)
# 游戏状态应包括:
game_state = {
'plants': [(row, col, type) for row in range(ROWS) for col in range(COLS) if map.plants[row][col]],
'zombies': [(zombie.row, zombie.col, zombie.health) for zombie in map.zombies],
'sun_count': sun_system.sun_count,
'score': score
}
5.2 音效与动画增强
为游戏添加音效和简单动画:
# 加载音效
try:
shoot_sound = pygame.mixer.Sound('peashoot.wav')
sun_sound = pygame.mixer.Sound('sun.wav')
zombie_sound = pygame.mixer.Sound('zombie.wav')
except:
print("音效文件缺失,游戏将继续但没有音效")
# 在相应位置播放音效
def play_sound(sound):
if pygame.mixer.get_init(): # 检查音效系统是否初始化
sound.play()
5.3 性能优化技巧
对于大量游戏对象的优化:
# 使用精灵组管理游戏对象
all_sprites = pygame.sprite.Group()
plants = pygame.sprite.Group()
zombies = pygame.sprite.Group()
peas = pygame.sprite.Group()
# 在绘制时使用精灵组的绘制方法
all_sprites.draw(screen)
# 使用脏矩形技术优化渲染
dirty_rects = []
for sprite in changed_sprites:
dirty_rects.append(sprite.rect)
pygame.display.update(dirty_rects)
6. 数学建模问题的代码实现
6.1 问题2的解决方案
"场地只在最左边的1个格内有豌豆荚,没有向日葵和阳光。问最小多大间隔产生1个僵尸,计算机永远不会赢?"
def calculate_min_interval():
pea_damage = 100 # 每颗豌豆伤害
hits_to_kill = 9 # 需要9颗豌豆消灭僵尸
total_damage_needed = pea_damage * hits_to_kill
# 僵尸移动参数
steps_per_cell = 3
pea_interval = steps_per_cell # 豌豆发射频率
# 计算僵尸从出现到被消灭能走多远
# 每step受到的伤害
damage_per_step = pea_damage / pea_interval
# 计算僵尸在被消灭前能走的步数
steps_to_kill = total_damage_needed / damage_per_step
# 计算僵尸能走的格子数
cells_to_kill = steps_to_kill / steps_per_cell
# 确保僵尸在被消灭前不会走到最左侧(0格)
max_allowed_cells = 8 # 从最右侧(8)到豌豆荚位置(0)有8格
# 最小间隔 = 僵尸生成间隔要保证前一个僵尸被消灭后再生成新的
min_interval = steps_to_kill
return min_interval
6.2 问题3的解决方案
"最少种几棵豌豆荚,使产生僵尸的间隔最小,而计算机永远不会赢。"
def find_optimal_peashooter_count():
max_zombie_speed = 1 # 僵尸移动速度
pea_damage = 100
hits_to_kill = 9
# 测试不同数量的豌豆射手
for peashooter_count in range(1, 9):
total_damage_per_step = peashooter_count * (pea_damage / 3) # 每步总伤害
steps_to_kill = (pea_damage * hits_to_kill) / total_damage_per_step
cells_to_kill = steps_to_kill / 3
if cells_to_kill < 8: # 僵尸在被消灭前走不到最左侧
min_interval = steps_to_kill
return peashooter_count, min_interval
return None
7. 游戏测试与调试
7.1 单元测试示例
为关键游戏组件编写测试:
import unittest
class TestZombieBehavior(unittest.TestCase):
def setUp(self):
self.zombie = Zombie(0)
self.plant = Peashooter(0, 0)
def test_zombie_movement(self):
initial_x = self.zombie.x
for _ in range(3): # 3步移动一格
self.zombie.update([])
self.assertEqual(self.zombie.x, initial_x - 80)
def test_zombie_plant_collision(self):
plants = [[None]*9 for _ in range(5)]
plants[0][0] = self.plant
self.zombie.col = 0
self.zombie.update(plants)
self.assertTrue(self.zombie.eating)
if __name__ == '__main__':
unittest.main()
7.2 性能分析
使用Python内置工具分析游戏性能:
import cProfile
def run_game():
game = Game()
game.run()
# 性能分析
profiler = cProfile.Profile()
profiler.runcall(run_game)
profiler.print_stats(sort='time')
7.3 常见问题解决
开发过程中可能遇到的问题及解决方案:
-
游戏卡顿
- 原因:每帧绘制过多对象
- 解决:使用精灵组和脏矩形技术优化渲染
-
碰撞检测不准确
- 原因:矩形碰撞检测不够精确
- 解决:实现像素级碰撞检测或调整碰撞框大小
-
游戏节奏不稳定
- 原因:帧率不一致
- 解决:使用
clock.tick(FPS)稳定帧率,或使用基于时间的移动
8. 项目扩展方向
8.1 添加更多植物和僵尸类型
扩展游戏内容,增加多样性:
class WallNut(Plant):
def __init__(self, x, y):
super().__init__(x, y)
self.health = 400 # 更高的生命值
self.image = pygame.Surface((80, 80))
self.image.fill((139, 69, 19)) # 棕色代表坚果墙
class ConeheadZombie(Zombie):
def __init__(self, row):
super().__init__(row)
self.health = 1800 # 需要更多豌豆才能消灭
self.image.fill((150, 150, 150)) # 不同的颜色
8.2 实现关卡系统
设计渐进式难度关卡:
class LevelSystem:
def __init__(self):
self.current_level = 1
self.level_data = {
1: {'zombie_count': 5, 'interval': 300},
2: {'zombie_count': 8, 'interval': 250},
3: {'zombie_count': 12, 'interval': 200}
}
def get_current_level_settings(self):
return self.level_data.get(self.current_level, {})
def level_up(self):
self.current_level += 1
return self.get_current_level_settings()
8.3 网络多人对战
添加简单的网络功能:
import socket
import threading
class NetworkManager:
def __init__(self, is_host=False):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.is_host = is_host
def start_host(self, port):
self.socket.bind(('0.0.0.0', port))
self.socket.listen(1)
conn, addr = self.socket.accept()
self.connection = conn
def connect_to_host(self, ip, port):
self.socket.connect((ip, port))
def send_game_state(self, state):
data = pickle.dumps(state)
self.socket.sendall(data)
def receive_game_state(self):
data = self.socket.recv(4096)
return pickle.loads(data)
9. 项目部署与打包
9.1 使用PyInstaller打包
将游戏打包为可执行文件:
pip install pyinstaller
pyinstaller --onefile --windowed plant_vs_zombie.py
9.2 资源文件处理
确保资源文件正确打包:
# 获取资源文件的正确路径
def resource_path(relative_path):
""" 获取打包后资源的绝对路径 """
try:
# PyInstaller创建的临时文件夹
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# 使用示例
image_path = resource_path('assets/images/sunflower.png')
9.3 跨平台注意事项
处理不同平台的兼容性问题:
# 路径分隔符处理
if os.name == 'nt': # Windows
config_dir = os.path.join(os.getenv('APPDATA'), 'PlantVsZombie')
else: # Linux/Mac
config_dir = os.path.join(os.path.expanduser('~'), '.config', 'PlantVsZombie')
# 创建配置目录
if not os.path.exists(config_dir):
os.makedirs(config_dir)
10. 数学建模与游戏开发的结合思考
在完成这个项目的过程中,最有趣的部分是如何将抽象的数学模型参数转化为具体的游戏行为。例如,当数学模型说"僵尸3步走一个格",我们需要考虑:
- 时间单位的统一:将"步"转换为游戏帧数
- 节奏控制:确保所有角色的行为频率协调一致
- 平衡性验证:通过实际游戏测试验证数学模型的有效性
这种结合不仅让数学建模更加直观,也让游戏开发更有理论依据。在实际项目中,我经常需要调整参数来找到游戏趣味性和数学严谨性之间的平衡点。比如,严格按数学模型实现可能会让游戏太难,这时就需要在不破坏核心规则的前提下进行微调。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)