资讯动态

Python复刻植物大战僵尸:pygame塔防游戏源码解析与实战

发布时间:2026/10/17 10:23:05 来源:尧图企业网站定制
简介这是一份以pygame库实现的《植物大战僵尸》Python游戏源码包适合正在学习Python基础与游戏开发的初学者以及希望将面向对象编程运用于实际项目的开发者。代码中涵盖了植物与僵尸的类设计、继承与多态、基于Rectangle的碰撞检测、典型游戏循环、事件处理及随机生成等核心机制README文档则对变量类型、流程控制、函数封装、模块导入等知识点作了配套梳理便于边读代码边理解游戏开发的基本流程。压缩包共10个文件以游戏主程序py文件、8张游戏素材PNG图片地图、向日葵、豌豆射手、僵尸等以及说明文档为主整体大小仅45KB小巧完整、开箱即用。目前已有1002人浏览学习对想通过具体项目快速入门Python游戏开发、并进一步修改扩展玩法的读者来说是一份轻量但覆盖关键知识点的参考资料。1. 植物大战僵尸的 Python 复刻一个能跑通关的 pygame 塔防项目从网上下载“植物大战僵尸 python 游戏源码”的人十有八九遇到过这种尴尬压缩包解压出来文件倒是不少一运行要么缺素材疯狂报错要么主循环写得像天书根本不敢动。这份源码是少数解压后就能直接跑起来的 pygame 复刻阳光、种植冷却、子弹碰撞、波次推进、胜负判定一应俱全主线流程能完整通关。适合刚学完 pygame 基础、想找一个完整项目拆开看的人也适合想在塔防玩法上做二次开发、加新植物新僵尸的开发者。整个工程按场景、精灵、配置分层改一个功能不需要翻遍全部文件这也是我推荐拿它当练手底子的原因。2. 工程结构拆解从 main.py 到精灵类的组织方式拿到一份源码我习惯先看目录再读代码因为目录结构直接决定了后续改起来是轻松还是坐牢。这份项目的组织方式是典型的小型游戏工程分层入口文件只负责启动场景和精灵各归各的文件夹公共配置单独抽出来素材和代码完全分离。2.1 目录结构一份能直接看懂的分层project_root/ ├── main.py ├── requirements.txt ├── config.py ├── game/ │ ├── __init__.py │ ├── app.py │ ├── scene/ │ │ ├── __init__.py │ │ ├── menu_scene.py │ │ ├── select_scene.py │ │ └── battle_scene.py │ ├── sprites/ │ │ ├── __init__.py │ │ ├── plant.py │ │ ├── zombie.py │ │ ├── bullet.py │ │ └── sunshine.py │ └── core/ │ ├── __init__.py │ ├── wave.py │ └── cooldown.py └── assets/ ├── images/ └── sounds/main.py 只有二十来行做的事情就是创建 Game 实例并调用 run()。config.py 集中放屏幕尺寸、帧率、格子行列数、各种冷却时间、植物价格这些数值改平衡性基本不用碰逻辑代码。scene 目录放的是场景类每个场景负责自己的事件处理和绘制sprites 目录是游戏里的实体对象全部继承 pygame.sprite.Spritecore 目录放波次生成和冷却计算这类独立逻辑。assets 目录里的图片和音效按植物、僵尸、卡片、背景分类存放代码里用相对路径引用。这里有一个值得学习的点把 cooldown冷却从植物对象里拆出来单独做成 core 模块而不是在每个植物类里各写一套计时逻辑。因为冷却跟植物实例的生命周期无关它属于玩家操作层面拆开之后种植新植物、切换卡片都只跟这一份冷却表打交道不会出现“这株豌豆射手死了冷却反而重置”这种玄学问题。2.2 主循环与事件分发一帧里发生了什么所有 pygame 游戏的核心都是同一个套路这个项目也不例外。Game.run() 的写法很朴素但是每一帧的职责分得很清楚def run(self): clock pygame.time.Clock() running True while running: dt clock.tick(self.config.fps) / 1000.0 events pygame.event.get() if self.scene: self.scene.handle_events(events) self.scene.update(dt) self.scene.draw(self.screen) pygame.display.flip()clock.tick 控制帧率上限返回值是上一帧消耗的毫秒数除以 1000 转成秒作为 dt 传给所有 update 方法。这样植物动画、僵尸移动、子弹飞行都用 dt 做增量计算不管机器快慢游戏速度都是一致的这也是帧率忽高忽低时游戏不会变快变慢的原因。handle_events 只做一件事把 pygame 的 event 列表交给当前场景处理场景内部自己去判断鼠标点到了哪张卡片、哪个格子。有个细节值得说事件处理用的是“把整个事件列表传进去”的方式而不是在 Game 里写一长串 if event.type MOUSEBUTTONDOWN。因为菜单场景、选关场景、战斗场景对点击的响应逻辑完全不一样把事件分发下沉到场景层新增场景时就不用来回改主循环。2.3 场景管理与切换菜单、选关、战斗怎么串起来场景之间不是互相 new 出来就完事而是做成一个可替换的当前场景指针。BattelScene 结束后要回到菜单菜单里点了开始要进选关切换动作统一走 change_scenedef change_scene(self, scene_name): scene_cls { menu: MenuScene, select: SelectScene, battle: BattleScene, }.get(scene_name) if scene_cls: self.scene scene_cls(self)scene_name 是由上一个场景在某种条件下主动触发的比如 MenuScene 里检测到“开始游戏”按钮被点击就调用 self.game.change_scene(select)。这里用字典做映射的好处是以后想加一个“植物图鉴”场景只需要在字典里加一行不用改任何调用方的代码。场景对象在切换时被整体替换旧的场景对象如果没有其他地方引用就会被回收内存占用不会随着关卡切换越涨越高。3. 核心玩法实现阳光、冷却与碰撞检测的完整链路植物大战僵尸玩起来无非就是四件事攒阳光、种植物、打僵尸、撑过波次。这一章把这条链路从代码层面完整走一遍也是二次开发时最常动刀的地方。3.1 阳光系统从生成、飘落到点击收集阳光是整个游戏的经济系统每关开始时给 50 点向日葵每隔一段时间产阳光天上也会随机掉阳光。Sunshine 类同时承担阳光实体和数值管理class SunShine(pygame.sprite.Sprite): def __init__(self, pos, value25, targetNone): super().__init__() self.image load_image(sunshine.png) self.rect self.image.get_rect(centerpos) self.value value self.target target self.vy 60 # 飘落速度像素/秒 self.lifetime 10.0 # 存活时间秒 def collect(self, game): game.add_sun(self.value) self.kill()生成逻辑有两种向日葵产出的阳光直接出现在花朵附近后缓慢飘落天上掉的阳光则是从屏幕顶部随机 x 坐标落下。update 方法里每天只要做一件事——让 rect.y 按 vy 和 dt 往下移同时把 lifetime 减掉 dt减到 0 就自行消失。玩家点击阳光时BattleScene 的鼠标检测会遍历存活阳光的 rect如果点击点落在 rect 内就调用 collect()。参数说明里值得注意的有三个value 默认 25但向日葵产的和天上掉的有时候是 50这个数值直接在生成处传参覆盖vy 设成 60 是让阳光飘得不太快玩家来得及点lifetime 设 10 秒是为了防止地上堆太多阳光实体拖慢碰撞检测。如果觉得阳光来得太慢把 value 参数调大或者把生成间隔调短属于最无脑也最有效的改平衡性手段。3.2 种植流程卡片选择、格子校验与冷却计算种植是玩法里最容易出 bug 的地方因为要同时检查阳光够不够、格子有没有被占、冷却有没有好、鼠标有没有点进草坪范围。这个项目把校验集中在一个 plant() 方法里def plant(self, plant_id, row, col): plant_cls PLANT_TABLE[plant_id] if self.sun_value plant_cls.price: return False if not self.grid.is_empty(row, col): return False if not self.cooldown.ready(plant_id): return False self.sun_value - plant_cls.price self.cooldown.start(plant_id) pos self.grid.get_pos(row, col) self.plants.add(plant_cls(pos)) return TruePLANT_TABLE 是一张把字符串 id 映射到植物类的表比如 peashooter 对应 Peashooter。把类作为值存进去比用一堆 if elif 干净新增植物时只要在表里加一行。grid.is_empty 查的是 9x5 的二维布尔数组种植成功后对应位置置 True植物死亡时置回 False这样格子占用判断不依赖任何精灵遍历。cooldown.ready 的实现基于 pygame.time.get_ticks()记录每个 plant_id 上次种植的时间戳当前时间减上次时间大于冷却时长就算就绪。要注意冷却是跟植物种类绑定的不是跟格子绑定所以同一个玩家在短时间内不能连续种两株同种植物这也是原版游戏的规则。若玩家想改“每种植物各自独立冷却”把 cooldown 的 key 从 plant_id 改成 (plant_id, row, col) 就行但那样平衡性会崩。3.3 子弹与僵尸从发射到击杀的碰撞链路子弹由植物按固定间隔发射比如豌豆射手每 1.4 秒发射一颗。子弹本身是个匀速向右移动的 Spritex 方向速度取正值遇到屏幕边缘就自杀。碰撞检测用的是 pygame 精灵组的内置方法hits pygame.sprite.groupcollide( self.bullets, self.zombies, True, False, collidedpygame.sprite.collide_mask ) for bullet, zombie_list in hits.items(): for zombie in zombie_list: zombie.take_damage(bullet.damage) if zombie.hp 0: zombie.kill()groupcollide 五个参数的顺序是先被检测的组、被碰撞的组、第一个组是否随碰撞删除、第二个组是否随碰撞删除、碰撞判定函数。这里把子弹设为 dokill1True僵尸设为 dokill2False因为子弹撞到僵尸就该消失但僵尸要扣血而不是被一碰就带走。collide_mask 是按像素精确碰撞比默认的矩形碰撞更接近原版手感代价是要求精灵的图片必须带透明通道否则会报错——这一点在第 5 章避坑里会再次提到。take_damage 只是一个 hp 减法的封装天然适合以后加“减速”“冰冻”效果在 Zombie 基类里加一个 status 字典子弹命中时往字典里写减速倍率和剩余时间update 里按 status 计算实际速度。3.4 波次推进与胜负判定关卡不是无限出兵而是按数据表生成波次。wave.py 里定义了每波的出场时间、僵尸种类、每波数量WAVE_TABLE [ {time: 5.0, zombies: [normal]}, {time: 20.0, zombies: [normal, normal]}, {time: 40.0, zombies: [cone, normal]}, {time: 60.0, zombies: [cone, cone, normal]}, ]BattleScene 的 update 里维护一个 elapsed 累计时间每次进入新波就按表里配置的僵尸列表逐个生成生成间隔可以再包一层循环。胜负判定相对粗暴僵尸的 rect.x 小于等于房子那一列对应的 x 坐标游戏立刻切到失败结算所有波次生成完且场上没有存活僵尸就判定胜利。这种判定方式有个明显的漏洞——漏一只僵尸就没法补救所以很多改版会在房子前再放一条“底线”僵尸踩线后先扣生命值扣完三次才失败算是对玩家的仁慈。4. 运行指南从安装依赖到第一局通关拿到源码第一件事永远是跑起来跑不起来一切都是白搭。这一章按我自己的习惯顺序写建虚拟环境、装依赖、启动然后讲配置参数在哪改、失败时看什么。4.1 运行环境与启动命令项目依赖只有 pygame 一个第三方库理论上 Python 3.8 以上就能跑。建议先建虚拟环境避免污染系统 Pythoncd project_root python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt python main.pyrequirements.txt 里通常就一行 pygame 的版本声明。装依赖这一步最常翻车的就是网络问题如果 pip 超时可以换国内镜像源把下载源临时指到镜像站再装一次。启动后如果窗口正常弹出并进入主界面说明素材路径和初始化都通过了。窗口弹出的瞬间会先黑屏一小会儿那是 pygame 在加载整张背景图和大量精灵图片属于正常现象。4.2 配置参数帧率、分辨率、格子这些在哪里改平衡性数值全部集中在 config.py改完直接重启游戏即可不需要动其他文件。这里列一份最常用的参数表参数名默认值作用FPS60帧率上限调高更流畅但 CPU 占用上升SCREEN_WIDTH / SCREEN_HEIGHT960 / 600窗口尺寸改小后注意格子坐标要跟着调GRID_ROWS / GRID_COLS5 / 9草坪的行列数直接影响可种植数量SUN_START50每关初始阳光SUN_FALL_INTERVAL10.0天上掉阳光的间隔单位秒PEASHOOTER_COOLDOWN7.5豌豆射手两次种植间的冷却单位秒PEASHOOTER_PRICE100种植消耗的阳光大部分数值的单位是“秒”或者“阳光点数”改的时候注意别把冷却写成毫秒否则会出现种完一株立刻能种第二株的鬼畜效果。如果你想把节奏调快优先改 SUN_FALL_INTERVAL 和僵尸的生成间隔这两项对通关难度的影响立竿见影。4.3 素材命名约定让替换图片不踩坑assets/images 下的文件名不是随便起的代码里全部通过固定文件名加载比如 sun_shine.png、peashooter.png、zombie_normal.png、卡片图标则是 card_peashooter.png。替换素材时保持同名、同尺寸就行如果新图片尺寸差太多植物的 rect 中心点会漂移导致看起来种下去的位置和实际逻辑位置对不上。出现这种问题不要慌去 plant.py 里找设置 rect 的那行代码把 center 改成你想要的锚点。资源缺失通常不会在启动时报错而是等植物或僵尸真正被创建时才报 “FileNotFoundError”所以跑起来之后前 30 秒没报错不代表素材齐全最好先打一局到出现僵尸为止。失败时看终端输出pygame.error 开头的错误八成是图片格式问题比如用了不支持的颜色通道或损坏的 PNG。5. 常见问题与避坑运行这个源码时最容易翻车的 5 个地方这个项目我在本机和几台不同配置的机器上都跑过坑基本集中在图片格式、路径写法、碰撞参数这几个固定位置。每条都按“现象 → 原因 → 解决”写照着排查能省不少时间。5.1 窗口黑屏闪退现象运行 main.py 后窗口一闪而过终端报错信息消失太快根本来不及看。原因最常见的是当前工作目录不对。如果你在 project_root 的上一级目录执行 python project_root/main.py代码里相对路径 assets/images 就解析不到。另一个原因是某些机器缺少 pygame 的音频初始化驱动pygame.mixer.init() 抛异常导致整个程序退出。解决先 cd 到项目根目录再运行路径问题立刻解决音频初始化失败的话在 main.py 里把 mixer 初始化包一层 try/except音频设备缺失时不至于让游戏整体崩掉。验证方法是在终端输出一行“mixer skipped”配合调试。5.2 素材加载报错文件名或路径对不上现象游戏能进主菜单一旦开始战斗创建植物时抛 FileNotFoundError提示找不到某某图片。原因assets 目录里文件名和代码里硬编码不一致比如代码写 peashooter.png实际素材叫 pea_shooter.png或者某些二次修改版把图片移动到了子目录代码还在按旧路径找。解决报错信息里会直接写出实际查找的路径顺着路径去 assets 目录比对文件名把文件改名或修正代码引用。改的时候建议在 config.py 里集中维护一个 RESOURCE 字典文件名都作为 key以后替换素材只需改字典不用满文件搜字符串。5.3 植物种下去不攻击现象豌豆射手放好后一直傻站着僵尸走到面前也不吐子弹。原因很多源码把子弹生成逻辑写在了植物的 update 里但只有当植物的 firing 状态被外部事件激活时才触发如果碰撞检测用的 groupcollide 参数写错子弹和僵尸所在的组不在同一个 update 链路上子弹对象创建了却不加入活动精灵组。解决先确认植物的 update 是否被 BattleScene 调用——标准做法是所有植物放进 sprite.Group每次场景 update 时调用 group.update(dt)。然后检查子弹的生成处有没有 self.add(self.bullets) 这行子弹不加入精灵组就不会被渲染和检测这是新手最容易漏的一步。5.4 僵尸走到房子不判负现象僵尸一路走到最左边穿过房子模型继续左移游戏就是不结束。原因胜负判定的坐标写错了要么拿僵尸的 rect.right 和房子线比要么拿 rect.x 和房子的右边界比。帧率波动时 dt 偏大僵尸一帧移动距离超过判定阈值可能直接跳过触发线。解决把判定条件改成 rect.right house_line_x确保整个僵尸身体都越过房子线才判负。为了防跳帧在 update 里可以额外记录上一帧坐标发现僵尸跨线时强制触发失败结算。5.5 游戏越玩越卡帧率明显掉现象进入后期波次僵尸数量一多游戏开始明显卡顿点击响应变慢。原因最常见的不是渲染太慢而是碰撞检测用了嵌套循环遍历所有僵尸和所有子弹数量上去之后复杂度变成 O(mn)另一个常见原因是死亡的僵尸和射击后的子弹没有及时 kill()精灵组里堆积大量无效对象。解决去 sprties 的 update 里检查所有 kill() 分支是否真的执行了——阳光 lifetime 到 0 后要自杀子弹飞出屏幕要自杀僵尸 hp 归零要 kill。如果这些都做了还卡把 groupcollide 的 collided 从 collide_mask 换成默认矩形碰撞碰撞精度略降但性能显著提升。6. 二次开发思路把固定关卡改成数据驱动的关卡编辑器最后一章聊点实在的改造思路。这个项目目前的波次表写在 wave.py 里属于硬编码每次调关卡都得改代码、重启游戏、肉眼测试很痛苦。一个性价比极高的改造是把它抽成 JSON 关卡文件游戏启动时加载当前关卡配置这样改关卡内容完全不需要碰代码。{ level: 1, start_sun: 50, waves: [ {time: 5, zombies: [[normal], [normal], [cone]]}, {time: 25, zombies: [[normal, normal], [cone, normal]]} ] }实现起来只需要在 wave.py 里加一个 load_level(path) 函数把 json 里每个 wave 的 time 和 zombies 数组读出来转成运行时对象BattleScene 的初始化参数里加一个 level_id从固定导入 WAVE_TABLE 改成按 level_id 加载对应文件。以后往工程里加新关卡就是复制一个 json 改数值连游戏代码都不用重新编译。加新植物也是同样的套路。先在 PLANT_TABLE 里注册新类名和显示名再在 assets/images 里放一套同名图片然后在植物基类里定义好 fire_interval 和 damage一个自定义植物就上线了。如果把这种注册制再往前推一步还可以把植物的所有属性写成字典配置由统一的工厂函数创建实例以后加植物连类都不用写只填配置表。另类的冷却是我自己踩过的坑曾经把冷却记录放在战况场景对象里结果一暂停场景就重置冷却玩家疯狂暂停刷植物。后来才把冷却模块改成了全局单例和场景生命周期解耦。从那以后我做任何塔防项目都强制把“全局状态”和“关卡状态”分开免得再吃一遍这种亏。这份源码好就好在坑位明确、结构清晰适合大胆改希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑