☰
植物大战僵尸Python源码解析:跑通、读懂、扩展
2026/10/13 11:03:03 网站建设 项目流程

简介:这份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, cost=50, health=100): super().__init__() self.image = pygame.image.load("images/plant.png").convert_alpha() self.rect = self.image.get_rect(topleft=pos) 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的坑在于它经常是浮点数。因为僵尸每帧移动的像素如果小于 1,pygame 的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, image="images/ice_bullet.png") self.speed = 8 self.damage = 15 class IcePea(Plant): def __init__(self, pos): super().__init__(pos, cost=150) self.cooldown = 40 self.image = pygame.image.load("images/ice_plant.png").convert_alpha() self.rect = self.image.get_rect(topleft=pos) 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 源码,把每个类、每个参数为什么存在搞清楚,你才能把它真正改成自己的作品。希望这篇笔记能帮你少走那几段弯路,早点让第一株自己写的植物开火。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询