简介这份Python版植物大战僵尸源码包专为Python初学者和游戏开发入门者准备可作为课程设计、实训项目或兴趣练手的完整参考。资源同时包含可直接运行的代码与全部配套图像素材下载解压后即可启动游戏省去自己绘制素材或搭建环境的环节能直观体验豌豆射手、向日葵与僵尸的经典对战玩法。包内共10个文件包括1个Python主程序、8张PNG素材图和1个Markdown说明文档PNG素材覆盖草地、地图、豌豆子弹、射手、向日葵及僵尸等关键视觉对象代码与资源分离方便对照学习整体压缩后仅42KB轻巧便携。当前已有1786人在CSDN浏览学习内容虽精简却完整覆盖了游戏循环、事件处理、碰撞检测以及简单的敌人AI等核心机制读者既可通读源码理解整体架构也可以替换素材或调整关卡参数进行二次创作。整体而言这是一份上手门槛低、可扩展性佳的实战样例适合用短时间跑通一个功能完整的Python小游戏。1. 植物大战僵尸 Python 源码为什么你该跑通它而不是只看别人摆弄这份基于 Python 的植物大战僵尸源码其实是一套用 pygame 实现的完整关卡逻辑不是那种只渲染一屏图片的 Demo。我最初拿到这套源码时以为最复杂的应该是植物攻击频率和子弹轨道真跑起来才发现翻车最多的反而是僵尸移动、碰撞判定和资源路径这三类基础问题。项目适合两类人一类是刚学完 Python 语法、想看看一个 2D 游戏到底怎么组织代码的人另一类是已经能写出小工具但还没碰过游戏对象循环和精灵管理的人。这套源码最大的价值不是让你“一键运行”然后看着植物打僵尸而是它把所有游戏元素都拆成了可独立修改的模块。你会看到阳光、植物、僵尸、子弹、地块、波次各自都有对应的数据结构和更新函数。只要把这些对象之间的关系理清楚后面不管是调难度、换皮肤还是新增一种植物都只是改几个数值和类的问题。下面我从启动开始把从“能跑”到“会改”的过程完整过一遍。2. 从环境到运行先把这个 Python 源码跑起来再说2.1 安装 Python 与 pygame版本选对后面少撞墙我建议直接用 Python 3.7 以上的版本我自己常用 3.10 来跑这套源码。源码里用到的语法基本都是类定义、继承、字典配置和常用的 pygame 常量还没有碰到需要 3.11 之后新特性的地方。如果你机器里同时装了多个 Python 版本打开终端先输入python --version确认一下当前指向哪个版本避免后面出现“我明明装了 pygame 却仍然提示找不到模块”的尴尬。pygame 的安装命令在绝大多数场景下用下面这段就够了# 使用 python -m pip 而不是 pip防止装到另一个解释器里 python -m pip install -U pygame安装完成后做个 10 秒钟的验证直接打印版本号python -c import pygame; print(pygame.version.ver)如果能看到类似2.1.2的输出说明依赖已经就位。这套源码在 pygame 1.x 和 2.x 下都能跑但 2.x 对文字渲染和透明贴图的支持更好遇到窗口黑屏的概率低很多所以我默认会先把 pygame 升到 2.1 以上。有些源码会附带requirements.txt里面写死了一组依赖和版本范围。碰到这种情况直接在项目目录下执行python -m pip install -r requirements.txt就能一次装齐。不推荐顺手装一整套游戏引擎或者任何额外 GUI 库分支越少后面排查问题越容易。2.2 启动入口与目录结构第一次跑起来的三个命令拿到源码后先别急着双击某个文件。我习惯先看一遍目录结构确认入口文件在哪、资源文件夹叫什么。常见的布局是main.py放在根目录所有资源放在resources/images和resources/sounds下游戏代码则拆成plant.py、zombie.py、bullet.py这些模块。这种结构的好处是你以后想单独测试碰撞逻辑不用每次都把整个游戏跑起来。假设你已经把项目放在某台机器的某个目录下第一次启动只需要三个命令cd ~/src/pvz_python python -m pip install -U pygame python main.py运行后如果能看到游戏窗口弹出并进入主菜单说明核心逻辑没问题。如果窗口弹出后又立刻消失最常见的原因有两个一是main.py里没有写while running主循环或者主循环被某个异常打断二是图片资源加载失败代码在初始化阶段就抛了异常。这里有个很容易忽略的点入口文件叫什么不重要但它所在的目录决定了所有相对路径怎么计算。很多源码写的是pygame.image.load(images/sun.png)这种写法依赖你当前终端所在的路径。如果运行python main.py时不在项目根目录就会直接报文件找不到。我一般会先看一眼入口文件前 20 行确认它是否用os.path.dirname(__file__)拼出了绝对路径。如果没有第一件要做的事就是把路径修正否则后面每次换目录启动都会撞同一个坑。3. 读懂源码骨架六个类、三个精灵组和一个主循环的协作3.1 角色类如何写继承 Sprite 带来的复用与坑这套植物大战僵尸源码的核心对象基本都是pygame.sprite.Sprite的子类。植物有Plant基类僵尸有Zombie基类子弹往往也有一个独立的Bullet类。把游戏对象写成精灵的好处非常直接你可以把它们加入精灵组然后用精灵组自带的update()和draw()方法统一更新和绘制不用手写一堆 for 循环来管理对象生死。以下是这套源码里常见的植物类写法注意它的属性和方法划分import pygame class Plant(pygame.sprite.Sprite): def __init__(self, pos, cost50, health100): super().__init__() self.image pygame.image.load(images/plant.png).convert_alpha() self.rect self.image.get_rect(topleftpos) self.cost cost self.health health self.cooldown 30 # 攻击间隔单位帧 self.timer 0 def update(self, zombies, bullets): self.timer 1 if self.timer self.cooldown: self.shoot(bullets) self.timer 0 def shoot(self, bullets): bullet Bullet(self.rect.right, self.rect.centery) bullets.add(bullet)关键点在于update方法接收了外部依赖僵尸列表和子弹组。这样设计是为了让植物在更新时能检测前方是否有僵尸而不是无脑发射。cooldown的单位是帧默认 30 意味着每秒发射一次游戏帧率 60 时。如果你看到某份源码把cooldown写成秒那说明它内部用dt做了帧数换算两种写法都行但参数含义完全不同。这里有一个容易理解错的地方self.image是每次加载一张图片如果资源文件很多建议在__init__里临时加载一次就够了不要每帧都重新load。有的人会觉得 Sprite 子类必须像圣旨一样把draw方法写在类里实际上Group.draw()会自动读取self.image和self.rect你只需要保证这两个属性存在且正确。3.2 碰撞检测与子弹命中groupcollide 的参数会让你晕一会碰撞检测是这类塔防游戏里最容易出“玄学问题”的地方。植物对僵尸的伤害、僵尸啃咬植物、子弹命中敌人都需要判定。pygame 提供了现成的碰撞函数源码里最常用的组合是groupcollide和spritecollide# 子弹组与僵尸组的碰撞 hit_dict pygame.sprite.groupcollide(bullets, zombies, True, False) # 僵尸组与植物的碰撞 attacking_zombies pygame.sprite.spritecollide(zombie, plants, False)groupcollide的后两个参数不是可选项它们控制碰撞后的生死状态。第一个True表示子弹碰到僵尸后立刻消失第二个False表示僵尸不会被子弹直接消灭只是标记为被命中。复数僵尸的精美血量会被外部扣减逻辑处理。这里最常踩的坑是把第二个参数也设为True结果一颗子弹刷掉一整只僵尸泄得毫无乐趣可言。至于僵尸啃植物用的是spritecollide第三个参数是False意思是碰撞后植物不消失而是由僵尸的attack方法逐步扣血。碰撞检测默认用矩形rect图上透明区域也会参与碰撞所以如果图片本身留白很大就会出现“子弹还没碰到僵尸却爆炸”的怪问题。源码级别想修正要么给每张图写更精确的碰撞矩形要么用mask检测但这套代码里通常不会上 mask因为性能吃不消。我的建议是先把这几种碰撞的“生死”语义理清。在正式调手感之前你要记住一个结论——子弹大多一击消失僵尸大多按血量消失植物在“被攻击”时应该保留实体。把这三条原则套到每个groupcollide的布尔参数里就不会改出诡异的无敌僵尸。4. 五个必调参数把植物大战僵尸源码改成你想要的手感4.1 数值集中在哪里先找到配平参数再谈修改很多第一次改这个源码的人会直接去每个类的__init__里改动数字。你会发现改完植物类僵尸攻击力还没变四处分散的数值让人越改越乱。更合理的做法是先找到项目里是否有一个专门的参数配置文件有的源码叫constants.py有的叫config.py也有的直接把参数全部定义在main.py的头部。我拿到的这套“植物大战僵尸-python源码”模拟项目X参数就集中在项目根目录下的config.py里。常见的配置看起来是这个样子# 阳光相关 SUN_PRICE 50 SUNFLOWER_PRICE 50 PEASHOOTER_PRICE 100 # 子弹相关 BULLET_SPEED 8 BULLET_DAMAGE 20 # 僵尸相关 ZOMBIE_SPEED 1 ZOMBIE_HP 100 # 关卡相关 WAVE_COUNT 10看到这种配置文件不要直接手贱改一堆先做一次小改动然后跑一轮确认这个数值被哪些逻辑引用。比如你把BULLET_SPEED改成 12立刻去第一关看看子弹是不是更快落地。如果你改完之后没有任何效果那说明源码的类里直接写死了数值并没有读取config.py。这种情况就得回到类的初始化方法把一个常量赋给实例属性。4.2 五个参数改法阳光值、冷却、速度、波次和植物生命下面这套参数是实践下来最影响“手感”的组合。我会把每个参数的原意、改后效果、以及会影响哪些底层逻辑说明白。# 把豌豆射手价格降半前期更容易建立防线 PEASHOOTER_PRICE 50 # 攻击冷却从 30 帧降到 15 帧秒变加特林 PEASHOOTER_COOLDOWN 15 # 僵尸移动速度降半给新手留更多反应时间 ZOMBIE_SPEED 0.5 # 波次从 10 加到 20用来测试后期压力 WAVE_COUNT 20 # 植物基础生命从 100 调成 80僵尸啃两下就红血 PLANT_HEALTH 80先解释PEASHOOTER_PRICE。价格本质只影响玩家种植物的时机不会改变运行时的任何逻辑所以改完不用重启直接重新开局就能生效。PEASHOOTER_COOLDOWN是帧数前提是update每帧调用一次如果你发现实机效果和预期差一倍大概率是原来代码里用了FPS / 秒数来换算你要去看看主循环里clock.tick(60)的帧率设置不要单看帧数。ZOMBIE_SPEED的坑在于它经常是浮点数。因为僵尸每帧移动的像素如果小于 1pygame 的rect坐标是整数移动量可能会被截成 0。遇到这种问题一般有两个处理一是把 ZOMBIE_SPEED 保留为浮点数然后在更新时累积到self.pos speed最后用self.rect.x int(self.pos)赋值二是干脆把速度设成 1 或 2 这种整数避开截断。我推荐第二种简单直接。WAVE_COUNT要小心它不只是一个数字还会影响僵尸生成调度。很多源码会把波次写成列表每一波里包含僵尸类型和数量。如果你只改WAVE_COUNT 20但生成逻辑里没有为第 11 波到第 20 波准备配置就可能出现后面波次重复前面敌人的现象这不算 bug但会让节奏变得单调。改这一项的同时最好把每波的间隔时间也一起观察一下。PLANT_HEALTH的修改会直接影响僵尸攻击事件的扣血数值如果源码里僵尸攻击力是 10那植物只能扛 8 次。这里很容易出现“植物莫名其妙消失但没有爆炸动画”的现象多半是因为掉血为 0 时只走了移除逻辑没播动画。想保留动画效果就得在血量为 0 的位置补一个死亡状态判断。以上五个参数建议一次只改一处跑完一轮再改第二处这样能准确感知每个数值的改善幅度。5. 换素材与踩坑排查五个最容易翻车的现象和解决顺序5.1 替换图片与音效资源路径、格式与窗口大小的三个坑如果你想把这个源码改成自己风格的小游戏第一个动作几乎都是换图片。这里有个容易被忽略的问题替换素材不只是改文件名还要保证图片尺寸和原图一致否则rect会变形碰撞位置也会偏移。假设原来的植物是 80x80你换成一张 120x120 的大图那植物的撞击框就明显变大看起来会提前吃到僵尸的攻击。路径问题更隐蔽。源码里的资源加载一般用相对路径但相对的是当前工作目录不是源码所在的目录。我自己的习惯是一开始就把图片加载全部改成基于文件位置的绝对路径比如在某个模块顶部写import os BASE_DIR os.path.dirname(__file__) IMAGE_PATH os.path.join(BASE_DIR, images, plant.png)音效方面最常见的坑是格式。pygame 加载 MP3 在某些环境上会有延迟问题换成 OGG 或 WAV 就稳定很多。如果你替换音频后出现音量极其离谱、或者一播放就卡顿先检查是不是音频采样率太高。常见做法是把音效转成 22050 Hz 的 WAV体积小而且播放稳定。窗口尺寸也是隐藏的坑。假如源码是按照 960x600 的地图尺寸设计的你替换一张 1920x1080 的背景图只会被拉伸变形而不是自动扩大地图。想扩大地图需要同步调整网格的起始坐标和地块大小这一步牵涉到的代码比换图多得多。5.2 五条高频报错从闪退到僵尸不动按这个顺序查以下五条踩坑记录基本覆盖了这套源码在本地运行时会遇到的高频问题我每条都按“现象→原因→解决”顺序说明。第一条窗口弹出后瞬间闪退。现象是运行python main.py后窗口闪一下就不见了。原因通常是图片加载失败或入口代码没有捕获异常。解决方法是先在终端看报错栈如果看到FileNotFoundError就按上一节说的路径修正方式处理。如果没报错直接退出就去检查主循环是否被提前执行了pygame.quit()。第二条点击植物后无法种到地块上。现象是卡片能选中但鼠标放到画布上没有任何提示。原因往往是地块列表里的坐标范围跟地图矩形不匹配或者代码只用了get_clicked_cell来判断但那个函数逻辑又依赖点击位置在网格的左上角。解决方法是打印点击点的像素坐标和网格行列对比地块起点。常见做法是把判断逻辑改成col (mouse_x - grid_left) // cell_width而不是硬编码从 0 开始。第三条僵尸走到植物面前却不动。现象是僵尸和植物已经重叠但植物血量一直不减。这是因为碰撞检测只在僵尸的update里调用了一次spritecollide但实际攻击指令被放在了attack方法里而attack没有被调用。解决方法是把“检测到碰撞”和“执行攻击”放在同一个if分支里或者确保attack每帧都触发。第四条游戏越玩越卡内存持续上涨。现象是前期流畅到第 5 波以后明显掉帧。原因大多是子弹和植物被移除时精灵组没有清理干净或者图片被反复加载。解决方法是检查子弹在出界后是否有self.kill()并且图片加载只保留在__init__中。如果代码里用了convert_alpha()在旧版 pygame 上反复调用也可能带来额外开销。第五条修改了音效文件但游戏里还是旧音效。现象是文件已经替换耳朵里还是原来的声音。原因大概率是资源缓存或者目录下存在同名但后缀不同的音频文件。解决方法是先删除生成过的__pycache__然后把音效文件放到绝对路径下并确认游戏里没有第二个音效加载点。这种问题最容易在“同名不同后缀”的情况下出现多查一眼就能排除。6. 把 Python 源码变成自己的作品新增植物和 BOSS 僵尸的套路6.1 新增一个角色照着现有类抄扩展自定义样式做到这一步你已经能跑通源码、调过参数、替换过素材了。接下来最有成就感的玩法就是在不重写主循环的前提下新增一种植物和一个僵尸变种。新增植物最好的起点是找到现有的Peashooter类把它复制一份改成你的新植物。以加一个“寒冰射手”为例最简实现是新增一个子弹类并让植物发射并附加减速效果class IceBullet(Bullet): def __init__(self, x, y): super().__init__(x, y, imageimages/ice_bullet.png) self.speed 8 self.damage 15 class IcePea(Plant): def __init__(self, pos): super().__init__(pos, cost150) self.cooldown 40 self.image pygame.image.load(images/ice_plant.png).convert_alpha() self.rect self.image.get_rect(topleftpos) def shoot(self, bullets): bullet IceBullet(self.rect.right, self.rect.centery) bullets.add(bullet)接下来把新的IcePea注册到植物卡片系统里。通常这套源码会有一个植物类型字典用来把按钮名字映射到类对象。在main.py或配置区里找到类似这样的结构然后加入一行PLANT_TYPES { sunflower: SunFlower, peashooter: Peashooter, icepea: IcePea, }注册之后还要解决两个细节一是植物的卡片按钮是否出现在选卡菜单里这往往由另一个菜单列表控制需要去游戏初始化代码里加入icepea的名称和价格二是你希望它出现在第几关这通常写在解锁关卡配置里可以直接默认全关卡可用。只要类型注册好了就算菜单没显示代码里也能通过调用Plant创建逻辑临时生成它。6.2 验证最终体验跑满一轮后观察的三项指标加完新角色和 BOSS不要急着发视频先跑完整的一轮观察三件事。第一新植物的子弹是否会造成游戏崩溃。由于碰撞函数通常只关心子弹的damage属性只要IceBullet继承了父类的damage并正确赋值就不会出错。第二BOSS 僵尸出现后玩家是否还有机会应对。如果你把 BOSS 血量设成 10000但没调整植物伤害那整场就是一场绝望的运动会。常见做法是给 BOSS 增加一个减速抗性避免被新植物无限硬控。第三也是最容易忽略的新植物是否会造成阳光经济崩盘。比如你把冰豌豆定向 150 阳光但整局产出阳光的量还是按原来三个植物设计的玩家就会觉得新角色是个鸡肋。这部分可以在参数配置里同步调整阳光产出的间隔或单次产量。我那一次给自己加新植物时只顾着让子弹带减速忘了看基础伤害值结果 BOSS 全程原地踏步植物无伤过关反而让整个挑战失去趣味。后来我养成一个习惯每新增一个角色先按“价格、伤害、冷却、特效”四个维度写一张自制平衡表再跑一局。这比在代码里瞎调数字、靠一遍遍重开游戏来试要好得多。玩到最后的经验是不要迷信网上现成的“加强版”源码那只会让你习惯别人的数值偏好。自己从头拆一遍这套植物大战僵尸的 Python 源码把每个类、每个参数为什么存在搞清楚你才能把它真正改成自己的作品。希望这篇笔记能帮你少走那几段弯路早点让第一株自己写的植物开火。本文还有配套的精品资源点击获取