用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_cell3
豌豆发射频率与僵尸步频相等Peashooter.shoot_interval100帧
豌豆飞行6格时间僵尸走一步豌豆速度计算需根据帧率调整
僵尸被9粒豌豆打中立即消亡Zombie.health900 (每颗豌豆伤害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 常见问题解决

开发过程中可能遇到的问题及解决方案:

  1. 游戏卡顿

    • 原因:每帧绘制过多对象
    • 解决:使用精灵组和脏矩形技术优化渲染
  2. 碰撞检测不准确

    • 原因:矩形碰撞检测不够精确
    • 解决:实现像素级碰撞检测或调整碰撞框大小
  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步走一个格",我们需要考虑:

  1. 时间单位的统一:将"步"转换为游戏帧数
  2. 节奏控制:确保所有角色的行为频率协调一致
  3. 平衡性验证:通过实际游戏测试验证数学模型的有效性

这种结合不仅让数学建模更加直观,也让游戏开发更有理论依据。在实际项目中,我经常需要调整参数来找到游戏趣味性和数学严谨性之间的平衡点。比如,严格按数学模型实现可能会让游戏太难,这时就需要在不破坏核心规则的前提下进行微调。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐