news 2026/9/20 11:09:49

Python植物大战僵尸源代码:从零搭建到修改金币

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python植物大战僵尸源代码:从零搭建到修改金币

简介:Python植物大战僵尸源代码是一份面向编程学习者与游戏开发入门者的经典练手项目,基于pygame库用面向对象方式复现了植物、僵尸、场地等核心机制,适合希望掌握塔防游戏事件循环、碰撞检测与状态管理基础技能的读者。压缩包内共8个文件,其中1个game.py主程序负责初始化、主循环和事件处理,7张png图片分别提供草地、地图、向日葵、豌豆射手、子弹及僵尸等素材,整包仅42KB,体积虽小但结构清晰。目前已有12458人学习浏览,不少读者借此入门游戏开发。通过研读代码可以掌握pygame窗口创建、精灵管理、图片加载与绘制流程,也能理解植物攻击、僵尸移动和路径规划等策略模块如何协作,甚至可进一步扩展新植物、新关卡,是一份轻量但完整的小型游戏开发教学样本。

1. 用一份 Python 植物大战僵尸源代码,入门 2D 游戏的最短路径

很多人在搜索引擎里输入“植物大战僵尸 源代码”或者“Python植物大战僵尸源代码”,下载下来的压缩包要么缺素材,要么把几十个.py文件堆在一起没有入口。你真正缺的不是“能跑的代码”,而是一套改得动、拆得开、加得了新功能的游戏骨架。如果你已经装好 Python,并且知道pip install是什么意思,那这篇文章可以直接照着做。

我不打算带你逐行抄一份完整项目,而是用常见做法把这类源代码里最关键的三块讲清楚:主循环怎么搭、植物和僵尸的交互判定怎么写、金币这类数值从哪里改。你不需要先学会 Pygame 的全部 API,只需要理解while循环、类的基础写法,以及事件队列的消费顺序。等你把一个最小副本跑起来,再回头去看网上下载的那些完整源码,会发现每个文件的作用都清清楚楚。

2. 先搭代码骨架:场景、精灵与主循环怎么分离

《植物大战僵尸》的玩法层级并不复杂:按格种植物,植物按冷却时间发射子弹,僵尸沿直线前进,子弹碰到僵尸就扣血。难点在于一旦你在一整份.py文件里同时处理这些逻辑,后续每加一个新植物都要改五六处代码。常见做法是把目录结构分成三层:主循环负责调度,场景层管理草坪与种植网格,精灵层只处理各自的动画、血量和攻击状态。

2.1 目录结构:把“素材”和“逻辑”分开

一份结构合格的 Python 植物大战僵尸源代码,通常长得像是这样:

pvz/ ├── main.py # 程序入口,负责主循环 ├── settings.py # 所有可调参数集中在这里 ├── classes/ │ ├── __init__.py │ ├── plant.py │ ├── zombie.py │ └── projectile.py ├── assets/ # 素材目录 │ ├── images/ # 所有 PNG 图片 │ ├── sounds/ │ └── music/ └── saves/ └── save.json # 金币、关卡进度的存档

这样拆的好处是,植物和僵尸各自的逻辑可以在plant.pyzombie.py里单独测试,不需要为了让一个豌豆射手跑起来而加载整个游戏界面。素材目录独立出来以后,换图片也不用改代码,只要保持文件名不变即可。

我在实际阅读各种开源版本时发现,很多人把素材也打进代码仓库,但图片路径写的是绝对路径,换一台电脑就报错。为了避免这种问题,你可以在settings.py里用os.path来拼接路径:

import os BASE_DIR = os.path.dirname(__file__) IMAGE_DIR = os.path.join(BASE_DIR, "assets", "images") FONT_DIR = os.path.join(BASE_DIR, "assets", "fonts")

os.path.join而不是字符串拼接,是为了自动适应 Windows 和 Linux 不同的分隔符。后续所有加载代码都引用IMAGE_DIR,即使你把整个项目文件夹移动到别的地方,路径也不会断。

2.2 用最小主循环建立帧率基准

主循环的作用不是把游戏做完,而是先把“窗口、事件、刷新”这三件事跑通。一个干净的最小循环大致如下:

import pygame from settings import SCREEN_WIDTH, SCREEN_HEIGHT, FPS pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) clock = pygame.time.Clock() running = True while running: dt = clock.tick(FPS) / 1000.0 # 单位是秒,1 秒约等于 60 帧 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN: # 先保存点击坐标,不在事件循环里直接更新游戏数据 print(pygame.mouse.get_pos()) # 更新场景与精灵 scene.update(dt) # 绘制所有画面 scene.draw(screen) pygame.display.flip() pygame.quit()

这里重点关注dt变量。clock.tick(FPS)会控制每秒最多刷新多少次,除以 1000 以后得到一个浮点数,表示上一帧到这一帧经过了多少秒。生物移动速度、攻击冷却时间都应该用dt来计算,而不是直接假设每帧固定过 16.67 毫秒,否则在低帧率电脑上植物会整体变慢。

事件处理也应该避免展开太重逻辑。在pygame.event.get()里做分发的原则是:能记录状态就记录状态,把实际的对象操作留到scene.update(dt)阶段。这样做的理由很简单,事件循环里如果去创建精灵或修改血量,就会出现同一帧里多次点击导致重复种植的问题,后面会在第五章专门讲这个坑。

2.3 资源加载策略:图片、声音与配置文件

游戏源代码里最常见的报错有三类:图片不存在、音量文件格式不认识、按钮坐标对不上。统一做法是加载素材后缓存到一个字典里,而不是每次都从磁盘读取:

import os import pygame from settings import IMAGE_DIR _image_cache = {} def load_image(name, alpha=True): if name not in _image_cache: path = os.path.join(IMAGE_DIR, name) image = pygame.image.load(path).convert_alpha() _image_cache[name] = image return _image_cache[name]

素材加载完成后,统一用name访问,而不是在代码里写assets/images/plant_pea.png这样的完整路径。这个封装的额外好处是,游戏结束后你还可以统计哪些图片被加载过,排查加载不必要资源的浪费。

与此同时,像种植冷却时间、子弹速度、僵尸血量这些数值都放进settings.py。下面这张表是我通常会在源码里保留的参数:

参数名建议初始值作用
PLANT_COOLDOWN5.0普通植物两次种植之间的秒数
SUN_POINT_INTERVAL10.0天上掉落阳光的间隔
PEA_SPEED300豌豆子弹每秒移动像素数
PEA_DAMAGE20每颗豌豆造成的伤害
ZOMBIE_HP200普通僵尸初始血量
ZOMBIE_SPEED20僵尸每秒前进像素数

这样设计的原因很直接:当你调试时觉得“豌豆太弱”或者“僵尸太快”,只需要改settings.py,不需要翻找散落在十个文件里的魔法数字。所谓源代码好不好改,首先看数值是否集中管理。

3. 植物与僵尸的核心交互:碰撞、攻击间隔与种植逻辑

游戏可玩性主要集中在这一章。植物与僵尸的交互绕不开三件事:如何判定“子弹打到了僵尸”、如何控制攻击频率不失控、以及如何让“种植植物”这个动作不产生歧义。我会用pygame.sprite.Sprite子类来说明,这也是主流开源版本里最接近“标准答案”的写法。

3.1 用精灵组管理实体

在一个项目中,与其自己实现一个列表来保存所有植物和僵尸,更稳妥的做法是使用pygame.sprite.Group。它会自动处理更新和绘制,也方便批量遍历。你可以在classes/plant.py里定义一个基础植物:

import pygame from settings import PEA_DAMAGE, PLANT_COOLDOWN class Plant(pygame.sprite.Sprite): def __init__(self, pos, groups, name="peashooter"): super().__init__(groups) self.name = name self.image = pygame.image.load(f"assets/images/{name}.png") self.rect = self.image.get_rect(topleft=pos) self.hp = 100 self.attack_timer = 0.0 self.cooldown = PLANT_COOLDOWN def update(self, dt): # 攻击计时器每帧减少,归零后才能发射下一发 self.attack_timer -= dt

groups参数允许同一个精灵同时被添加到多个精灵组,例如“所有植物组”和“当前屏幕碰撞检测组”。这样在生成子弹时就不需要遍历场景里的所有对象,只拿出固定的一个组来判交叠即可,性能表现在地图格子多的时候差别相当明显。

3.2 用攻击间隔与伤害参数控制游戏节奏

发射豌豆的正确方式不是“按下就发射”,而是先检查攻击计时器是否已经归零:

def try_shoot(self, projectiles_group): if self.attack_timer <= 0: Projectile(self.rect.center, projectiles_group) self.attack_timer = 1.0 # 攻击间隔,单位秒

注意我没有在try_shoot里直接读取鼠标或键盘状态。原因是植物面向的是“自身所在行”,当本行内没有僵尸时它就不需要发射。一个比较好的派生逻辑是把检测僵尸是否在射程内的任务交给assert_zombie_in_line函数:

def assert_zombie_in_line(self, zombies_group): for zombie in zombies_group: if (self.rect.y == zombie.rect.y and zombie.rect.x > self.rect.x and zombie.rect.x < self.rect.x + 500): return True return False

这里的判定条件是横向五百像素范围内有僵尸,而且两者y坐标相同。严格来说《植物大战僵尸》的“行”判定要看格子的row_index是否一致,而不是像素 y 坐标,因为不同植物的图片高度并不一致。若使用精灵rect.y直接比较,在植物或僵尸图片底部有透明留白时容易漏判。更严谨的方法是给两个类都加一个row_index属性,在创建时由场景层根据种植网格赋值。

子弹本身的类较短,但要妥善处理销毁逻辑:

class Projectile(pygame.sprite.Sprite): def __init__(self, pos, groups): super().__init__(groups) self.image = pygame.Surface((16, 16)) self.rect = self.image.get_rect(center=pos) self.speed = 300 self.damage = PEA_DAMAGE def update(self, dt): self.rect.x += int(self.speed * dt) if self.rect.x > 1200: self.kill() # 移出屏幕后销毁,避免无效对象越积越多

self.kill()将子弹从所有已加入的精灵组中移除,同时pygame.sprite.Group自身的长度也会自动减少,后续任何遍历都不会再把已销毁的子弹算进来。这是一个值得在源码里反复强调的细节,因为新手常犯的错误是只把对象从列表中移除,却忘了销毁精灵组里的引用,导致内存不断增长。

3.3 僵尸的血量、移动与受击反馈

僵尸类的关键属性主要是hpspeedrow_index。受击逻辑放到take_damage方法里会更清晰,避免在碰撞检测处直接改hp属性:

class Zombie(pygame.sprite.Sprite): def __init__(self, pos, groups): super().__init__(groups) self.hp = 200 self.speed = 20 self.alive = True def take_damage(self, damage): self.hp -= damage if self.hp <= 0: self.alive = False self.kill()

然后将碰撞检测放在场景更新阶段,而不是精灵自身内部:

def _handle_collisions(self): for bullet in self.bullets: hit_zombies = pygame.sprite.spritecollide(bullet, self.zombies, False) for zombie in hit_zombies: zombie.take_damage(bullet.damage) bullet.kill() break

pygame.sprite.spritecollide默认使用每个精灵的rect进行轴对齐矩形碰撞检测。这颗子弹如果同时打中两个重叠的僵尸,也只让第一个受击,然后销毁子弹。这里用break结束内层循环,避免一颗子弹同一帧把一整排僵尸同时打死,那会直接毁掉整个游戏的平衡性。

还有一个常见误区是直接在Projectile.update中执行碰撞判断。这会造成子弹和僵尸相互引用,后续要新增一种带穿透效果的寒冰射手时,就必须同时修改子弹类和碰撞场景。把碰撞留在场景层,可以让新植物只替换子弹的属性和外观,不用管谁在检测碰撞。

4. 把“修改金币”做在源代码里:从启动常量到存档解析

搜索记录里经常出现“植物大战僵尸修改金币”相关词组。如果你拿到的是 Python 源码版本,修改金币的方式和改普通游戏存档完全不同——你不需要修改游戏内存,只要在源代码里找到金币初始值,或者修改存档文件即可。这也正是学习源码的额外好处:能看清游戏数值到底存在哪里。

4.1 定位初始化金币的入口点

在大多数用 Python 写的复刻版里,金币并不会直接写在main.py里,而是放在存档加载模块或settings.py中。常见的写法有两类:

# 写法一:硬编码在玩家类初始化里 class Player: def __init__(self): self.coins = 1000 # 写法二:从存档读取 def load_player(): data = load_save() return Player(coins=data.get("coins", 0))

你要找的是coinssun这个变量名首次被赋值的位置。如果项目使用了save.json,大概率会在load_save函数附近看到这段代码:

import json from settings import SAVE_PATH def load_save(): try: with open(SAVE_PATH, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {"coins": 100, "level": 1, "unlocked_plants": ["peashooter"]}

注意json.load(f)读取后返回的是 Python 的字典。若存档文件损坏或者路径不存在,代码会返回一个默认字典,即新玩家的初始状态。理解这个流程后,改动就有了很多入口。

4.2 修改 JSON 存档的具体步骤

先用文本编辑器打开saves/save.json,内容通常是:

{ "coins": 100, "level": 1, "unlocked_plants": [ "peashooter", "sunflower" ] }

coins字段从100改成99999,保存。随后启动游戏前确认load_save用的路径与当前目录一致。常见错误是启动时工作目录不在项目根目录,导致saves/save.json找不到,于是程序走了FileNotFoundError分支,又回到了默认的 100 金币。

我用一个简单的工作目录探测法来判断:

import os current_path = os.path.abspath(__file__) project_root = os.path.dirname(current_path) SAVE_PATH = os.path.join(project_root, "saves", "save.json")

这样无论你在哪个终端路径下执行python main.py,存档路径都不会跑偏。对于从网盘下载源码的人来说,这条极为实用——下载解压后放在任何文件夹都能直接玩。

4.3 调整金币后要同步检查的数值边界

仅仅修改金币数字还不够。如果你在源码里把所有“购买价格”判断成if self.coins >= cost,那么修改存档后还需要注意三件事:

第一,不能让金币低于任何植物的售价,否则会出现“买不了植物但金币显很多”的错觉。第二,不要把存档里的coins改成负数或极大数,超过 Python 整数范围会导致 JSON 序列化失败。第三,有些版本会在购买植物时扣减金币,却在种植失败时把金币退回,这就会出现重复扣钱或刷钱漏洞。

我在调试一个开源版本时遇到过这样的判定逻辑:

def buy_plant(self, plant_name): cost = self.PLANT_COST[plant_name] if self.coins >= cost: self.coins -= cost return self.plant_grid.add_plant(plant_name) return False

这段代码先扣钱再种植物,如果add_plant因为格子被占而失败,钱已经被扣掉了。更合理的是先验证种植位置是否有空位,再执行扣款。这种边界问题在“修改金币”后会被放大,因为一旦金币充足,你反而难以发现刷钱漏洞。当你通读源码时,遇到if条件里同时有“扣钱”和“创建对象”的情况,建议把验证逻辑和变更逻辑拆开,并按以下顺序执行:

def buy_plant(self, plant_name): cost = self.PLANT_COST[plant_name] if self.coins < cost: return False, "金币不足" if not self.plant_grid.has_empty_cell(plant_name): return False, "没有可用格子" self.coins -= cost self.plant_grid.add_plant(plant_name) return True, None

这里的核心思想是,所有会修改数据的操作,都应该先经过一次只读校验。这个习惯在改存档、加新植物时比什么都管用。

5. 用事件队列缓存、碰撞掩码与渲染剔除验证你的源码副本

到了这一步,你的 Python 植物大战僵尸源代码已经能跑,金币也改完了,接下来要解决的是“游戏在后期卡顿”以及“种植植物偶尔失败”这两个最影响体验的问题。这一章的技巧可以直接套用到几乎所有 Pygame 项目里。

5.1 用掩码碰撞代替矩形碰撞,解决“看着像打中其实没打中”

pygame.sprite.spritecollide默认用rect做矩形碰撞,但豌豆子弹是圆的,僵尸图像也有大片透明区域,视觉上子弹已经穿模,实际碰撞盒还差几十像素。升级方案是使用pygame.mask.Mask

mask = pygame.mask.from_surface(self.image)

然后在碰撞检测时把spritecollide换成:

if pygame.sprite.collide_mask(bullet, zombie): zombie.take_damage(bullet.damage) bullet.kill()

collide_mask会基于两个精灵图像的非透明像素逐位比较,精度更高,但代价是每帧多个对象同时触发时 CPU 占用上升。常见做法是先用矩形碰撞粗筛一次,只有矩形相交才进入掩码精确检测:

hit_zombies = pygame.sprite.spritecollide(bullet, self.zombies, False) for zombie in hit_zombies: if pygame.sprite.collide_mask(bullet, zombie): zombie.take_damage(bullet.damage) bullet.kill() break

当场上同时存在五十只僵尸和二十颗子弹时,矩形粗筛能减少约八成不必要的掩码计算。

5.2 用帧时间定位卡顿,而不是靠感觉

游戏卡顿要先量化。在主循环里记录上一帧耗时到列表:

import time frame_times = [] while running: frame_start = time.perf_counter() # 原有更新与绘制逻辑... frame_end = time.perf_counter() frame_times.append(frame_end - frame_start) if len(frame_times) > 120: frame_times.pop(0)

当帧时间持续超过 30 毫秒时,说明瓶颈在绘制或碰撞。具体可以注释掉screen.blit只保留精灵更新,看帧率是否恢复。若恢复,说明图片过大或绘制数量过多;若没有恢复,再去检查update函数里是否有重复遍历所有精灵组。

5.3 种植植物不生效:先检查事件队列的消费顺序

很多“种不上”的问题并非逻辑错误,而是事件消费顺序不对。pygame.event.get()每次调用都会清空事件队列,如果你在多个地方分别调用这个函数,后一次调用拿不到前面的MOUSEBUTTONDOWN事件。常见做法是在主循环只能出现一次事件获取,所有 UI 点击和地图种植都共用这一批事件数据:

events = pygame.event.get() for event in events: if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: pending_click_pos = event.pos # 所有剩余逻辑统一读取 pending_click_pos

pending_click_pos只在下一帧开始时被新值覆盖,这样做的好处是,当你同时开启种植预览、阳光收取和暂停菜单时,三套逻辑不会互相争抢事件。如果你的源代码里在场景更新时又调用了pygame.event.get(),就应该把事件集中到主循环入口处统一处理。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 11:09:20

纯HTML+Canvas实现高性能大屏动态地图看板

简介&#xff1a;这是一份面向数据可视化开发者、前端工程师及智慧城市项目实施人员的地图数据可视化大屏模板&#xff0c;专为HTML大屏展示场景设计&#xff0c;解决地理空间数据动态呈现与多源指标联动分析难题&#xff0c;适用于交通监控、城市治理、商业热力分析等实时决策…

作者头像 李华
网站建设 2026/9/20 11:06:37

CC Switch 指向 TaoToken:GLM 5.3 Flash 与 Kimi K2.7 Code 的切换结果

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:03:56

LLVM编译器基础设施全解析:架构、IR与源码构建实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:03:51

Autocut:用文本编辑器剪视频,3 条命令出片

Autocut&#xff1a;用文本编辑器剪视频&#xff0c;3 条命令出片 【免费下载链接】autocut 用文本编辑器剪视频 项目地址: https://gitcode.com/GitHub_Trending/au/autocut Autocut 是一款本地视频剪辑工具。它先把视频的音频转成带时间戳的字幕文本&#xff0c;你再删…

作者头像 李华
网站建设 2026/9/20 11:03:20

DeepSeek-V3 上了 LiveCodeBench:用 TaoToken 复现同一把 Key 的跑分链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:00:04

给Homebrew套上图形界面:BrewUI开发实践与踩坑记录

日常开发里&#xff0c;包管理器就是个“默认动作”&#xff1a;装个依赖敲 brew install&#xff0c;升级全局工具敲 brew upgrade&#xff0c;清理磁盘空间再敲一遍 brew cleanup。我自己的终端里存了几十条 Homebrew 相关命令的别名&#xff0c;但每次帮同事配开发机、给新同…

作者头像 李华