简介:这是一份面向Python初学者与游戏自动化爱好者的实战项目资源,聚焦利用PyAutoGUI实现《植物大战僵尸》GUI界面的自动化操作,解决手动重复操作效率低、策略执行易出错等问题。资源包共28个文件,包含22张游戏界面截图(如garden.png、shop.png、zombie相关图标等),用于图像识别定位;3个核心Python脚本(PvZAutoPilot.py、test.py、help.py)构成主控逻辑与辅助功能;另含Excel配置表(cmd.xls)、README说明(About_xlrd.md)及Git管理文件,整体压缩包仅356KB,轻量易部署。已有243人学习下载,读者可直接运行脚本实现自动种植物、收阳光、识别僵尸位置等基础功能,并基于提供的截图素材与模块化代码,快速理解图像匹配、坐标点击、状态判断等关键实现逻辑,为拓展更复杂AI决策逻辑打下坚实基础。
1. 这不是外挂,是 PyAutoGUI 驱动的 PvZ 策略沙盒:它不改内存、不注入进程,只靠视觉定位+逻辑决策,在原生 Windows 版《植物大战僵尸》上完成阳光采集→植物部署→僵尸拦截全链路闭环
你打开 Steam 或 Origin 上的正版 PvZ(注意:仅限单机离线版,非网页版、非手机版、非 PvZ2),窗口最大化但不全屏——这是本项目唯一硬性运行前提。然后双击PvZAutoPilot.py,它会自动识别游戏主界面、定位阳光图标、判断豌豆射手按钮坐标、在僵尸逼近前 3 秒种下向日葵……整个过程没有 DLL 注入、不读写游戏进程内存、不调用任何 Win32 API 模拟按键(如 SendInput),纯粹靠pyautogui.locateOnScreen()扫描截图 +pyautogui.click()发送鼠标事件。这意味着它完全规避反作弊检测,也意味着——它极度依赖图像匹配精度和窗口稳定性。新手常以为“装完就能赢”,结果跑 5 分钟就卡死在花园菜单;老手则把它当 GUI 自动化练兵场:用cv2.matchTemplate替换原生 locate、加 ROI 区域裁剪、做 HSV 色彩容错……这项目真正的价值,从来不是帮你通关,而是给你一个可调试、可打断、可逐帧验证的 GUI 自动化最小可行系统。适合三类人:想入门 PyAutoGUI 的 Python 新手、需要 GUI 自动化教学案例的讲师、或正在为工业 HMI 界面写测试脚本的工程师——因为 PvZ 的 UI 元素足够规整、状态足够离散、反馈足够即时,比任何“模拟银行登录”教程都更贴近真实产线场景。
2. 从PvZAutoPilot.py到clickArea:解构自动化策略的三层结构与核心参数含义
2.1 主控逻辑:PvZAutoPilot.py的状态机设计与关键阈值
项目主文件PvZAutoPilot.py并非简单线性脚本,而是一个基于while True:循环的状态机。它通过get_game_state()函数持续捕获屏幕,再根据返回的状态码(如STATE_GARDEN,STATE_SHOP,STATE_MINIGAME)切换行为分支。这种设计避免了“盲点操作”——比如在商店界面误点花园按钮。核心参数全部集中在文件顶部的CONFIG字典中:
CONFIG = { "SCREEN_REGION": (0, 0, 1920, 1080), # 全局截图区域,必须与你的显示器分辨率严格一致 "SUN_THRESHOLD": 50, # 当前阳光 ≥50 才触发种植逻辑(防止过早种向日葵) "ZOMBIE_DISTANCE_WARN": 280, # 僵尸距左边界 ≤280px 时启动紧急防御(单位:像素) "CLICK_DELAY": 0.15, # 每次 click 后强制等待 150ms,避免鼠标事件丢失 "IMAGE_CONFIDENCE": 0.75, # locateOnScreen 的置信度阈值,0.75 是平衡速度与准确率的经验值 }提示:
SCREEN_REGION不是建议值,是硬约束。若你用 2560×1440 显示器却设成(0,0,1920,1080),所有locateOnScreen都会失败——PyAutoGUI 只在指定区域内搜索,超出即忽略。我一般先用pyautogui.screenshot().save("debug.png")截全屏,用画图工具量出 PvZ 窗口左上角坐标和宽高,再填入此参数。
2.2 图像资源池:clickArea目录下 22 张 PNG 的分工逻辑
clickArea文件夹里塞了 22 张 PNG,但它们并非随意命名。每张图对应一个可交互 UI 元素的模板图像,且按功能分层:
| 类别 | 文件名示例 | 用途说明 | 关键约束 |
|---|---|---|---|
| 基础UI锚点 | garden.png,shop_menu.png,miniGame_menu.png | 用于判断当前游戏状态(如是否进入花园界面) | 必须截取无阴影、无动态特效的静态帧,否则locateOnScreen匹配率暴跌 |
| 操作按钮 | sun.png,peashooter.png,rake.png,quit1.png | 定位后执行click()的目标位置 | 按钮尺寸需 ≥40×40px,小图标(如coin.png)仅用于计数,不作点击目标 |
| 状态标识 | silverCoin.png,goldenCoin.png,treeFood.png | 用于count_objects()统计屏幕内数量(如金币总数) | 必须纯色背景+高对比度,避免与游戏粒子特效混淆 |
特别注意bugSpray.png和fertilizer.png:它们不是按钮,而是防御触发条件。当locateOnScreen("bugSpray.png", confidence=0.8)成功时,脚本才认为“虫害已出现”,进而启动喷雾逻辑——这体现了状态驱动而非时间驱动的设计哲学。
2.3 辅助模块:help.py与test.py的实战价值
help.py并非文档,而是实时调试工具集。它包含三个核心函数:
show_click_areas(): 在当前屏幕叠加半透明矩形框,标出所有clickArea/*.png的匹配位置(绿色框)和未匹配项(红色框),运行python help.py --show即可可视化定位效果;calibrate_sun_position(): 让你手动点击一次阳光图标,程序自动记录坐标并生成sun_offset.json,解决不同分辨率下阳光浮动问题;dump_screenshot_region(x,y,w,h): 指定区域截图存为debug_region_*.png,用于分析locateOnScreen失败原因。
test.py则是原子操作验证器。它不跑完整流程,只测试单个动作:
# test.py 片段 import pyautogui pyautogui.moveTo(1200, 800) # 移动到预估的豌豆射手按钮位置 pyautogui.click() # 执行点击 time.sleep(1) screenshot = pyautogui.screenshot(region=(1150,750,100,100)) screenshot.save("after_click.png") # 保存点击后局部截图,肉眼验证是否真点了这个习惯救了我三次:某次发现peashooter.png模板图是旧版 PvZ 的按钮,新版按钮多了高光边,导致匹配失败——test.py的after_click.png显示点击位置偏右 12px,立刻重截模板图。
3. 图像识别不是玄学:用 OpenCV 替换 PyAutoGUI 原生 locate 的实操步骤
3.1 为什么必须替换?PyAutoGUI locate 的三大硬伤
PyAutoGUI 的locateOnScreen()在 PvZ 场景下有致命缺陷:
- 抗缩放能力为零:游戏窗口缩放 125% 时,匹配成功率从 95% 暴跌至 12%;
- 色彩敏感度过高:PvZ 的阳光图标在不同关卡有明暗变化,原生匹配要求 RGB 像素值完全一致;
- 无法 ROI 裁剪:必须全屏扫描,1920×1080 分辨率下单次
locate耗时 1.2~1.8 秒,拖慢整体决策频率。
解决方案是用 OpenCV 的cv2.matchTemplate()替代,它支持灰度转换、归一化相关性匹配、ROI 区域限定——这才是工业级 GUI 自动化的标配。
3.2 替换步骤:四行代码接入 OpenCV 模板匹配
第一步:安装依赖(注意版本兼容性)
pip install opencv-python==4.8.1.78 numpy==1.24.4 # 必须锁定版本!OpenCV 4.9+ 的 matchTemplate 行为变更导致 PvZ 模板匹配失效第二步:修改PvZAutoPilot.py中的find_image()函数(原位置约第 87 行):
# 替换前(PyAutoGUI 原生) # return pyautogui.locateOnScreen(template_path, confidence=CONFIG["IMAGE_CONFIDENCE"]) # 替换后(OpenCV 增强版) import cv2 import numpy as np def find_image_cv2(template_path, region=None, threshold=0.8): # 1. 截取指定区域(region 为 (x,y,w,h) 元组,None 则全屏) if region is None: screenshot = np.array(pyautogui.screenshot()) else: x, y, w, h = region screenshot = np.array(pyautogui.screenshot(region=(x, y, w, h))) # 2. 转灰度并归一化 gray_screenshot = cv2.cvtColor(screenshot, cv2.COLOR_RGB2GRAY) template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE) # 3. 模板匹配(使用 TM_CCOEFF_NORMED 提升鲁棒性) res = cv2.matchTemplate(gray_screenshot, template, cv2.TM_CCOEFF_NORMED) loc = np.where(res >= threshold) # 4. 返回第一个匹配坐标(适配 PyAutoGUI 接口) if len(loc[0]) > 0: # 注意:OpenCV 返回的是左上角坐标,需转为 PyAutoGUI 的中心点 top_left = (loc[1][0], loc[0][0]) h, w = template.shape center_x = top_left[0] + w // 2 center_y = top_left[1] + h // 2 # 若指定了 ROI 区域,需还原到全局坐标 if region is not None: center_x += region[0] center_y += region[1] return (center_x, center_y, w, h) return None第三步:在get_sun_position()等关键函数中调用新方法:
# 原逻辑(慢且不准) # sun_pos = pyautogui.locateOnScreen("clickArea/sun.png", confidence=0.7) # 新逻辑(快且稳) sun_pos = find_image_cv2("clickArea/sun.png", region=(100, 50, 300, 100), threshold=0.75) # region 参数限定在屏幕左上角 300×100 区域搜索阳光,提速 4 倍3.3 模板图预处理:用 GIMP 批量生成抗干扰版本
OpenCV 匹配效果直接受模板图质量影响。我用 GIMP 批量处理clickArea/下所有 PNG:
- 去噪:滤镜 → 降噪 → 非局部均值降噪(强度 3);
- 二值化:颜色 → 阈值(黑白阈值设为 120),保留边缘锐度;
- 尺寸统一:图像 → 缩放画布 → 设为 128×128px(所有模板图必须等尺寸,否则
matchTemplate报错); - 保存为 PNG-24:禁用 Alpha 通道,避免 OpenCV 读取时通道数异常。
处理后的sun.png在cv2.matchTemplate()下,即使 PvZ 窗口缩放至 150%,匹配成功率仍保持 92%+。而原生locateOnScreen()在同一条件下直接归零。
4. 避坑:PvZ 自动化必踩的 5 个血泪现场与当场修复方案
4.1 现象:脚本启动后鼠标乱跳,疯狂点击屏幕右下角
原因:pyautogui.FAILSAFE = True默认开启,当鼠标移至屏幕四角时强制抛出FailSafeException。而 PvZ 的“退出按钮”quit1.png恰好位于右下角,脚本反复尝试定位失败后,moveTo(0,0)触发安全机制。
解决:在PvZAutoPilot.py开头添加pyautogui.FAILSAFE = False,并在main()函数末尾加pyautogui.moveTo(100,100)锁定初始位置。
4.2 现象:阳光数值显示 150,但脚本始终不种植物
原因:sun.png模板图截取自 PvZ 1.0 版,而你运行的是 1.2 版——新版阳光数字字体加粗,导致locateOnScreen匹配失败,get_sun_count()返回None,后续逻辑全部跳过。
解决:用help.py --show查看实际匹配框,发现sun.png未被识别,立即重截当前版本阳光图标,覆盖原文件,并将IMAGE_CONFIDENCE从 0.75 降至 0.68(允许更多像素差异)。
4.3 现象:僵尸已贴脸,豌豆射手却迟迟不种
原因:ZOMBIE_DISTANCE_WARN设为 280px,但你的显示器 DPI 缩放为 125%,实际像素距离被拉长,等效预警距离变成 350px,错过黄金防御时机。
解决:运行python help.py --calibrate,手动标定僵尸贴脸时的像素距离,更新CONFIG["ZOMBIE_DISTANCE_WARN"]为实测值(如 220),并同步调整SCREEN_REGION的宽高为缩放后的真实分辨率。
4.4 现象:进入商店后脚本卡死,CPU 占用 100%
原因:locateOnScreen("shop_menu.png")在商店界面持续失败,while not found:循环无超时退出机制,陷入死循环。
解决:在wait_for_image()函数中加入超时控制:
def wait_for_image(template, timeout=10): start_time = time.time() while time.time() - start_time < timeout: pos = pyautogui.locateOnScreen(template, confidence=0.7) if pos: return pos time.sleep(0.5) # 每次失败后休眠 500ms,防 CPU 爆满 raise TimeoutError(f"Timeout waiting for {template}")4.5 现象:多开 PvZ 时脚本总操作错误窗口
原因:pyautogui.screenshot()默认截取最顶层窗口,当两个 PvZ 实例同时运行,脚本可能截到后台实例画面,导致坐标错位。
解决:用win32gui获取 PvZ 窗口句柄,强制截图指定窗口:
import win32gui hwnd = win32gui.FindWindow(None, "Plants vs. Zombies") left, top, right, bottom = win32gui.GetWindowRect(hwnd) screenshot = pyautogui.screenshot(region=(left, top, right-left, bottom-top))注意:需额外安装
pywin32,且仅限 Windows 系统。Linux/macOS 用户请改用mss库实现窗口级截图。
5. 进阶技巧:用cmd.xls实现策略热更新与多关卡自适应调度
5.1cmd.xls的真实作用:它不是配置文件,而是策略指令集编译器
cmd.xls看似是 Excel 表格,实则是本项目最精巧的设计——它把“种什么植物”“何时种”“种在哪”三条规则,压缩进一个可人工编辑的二维表。打开它,你会看到三列:
- Col A(Action):操作类型,仅限
PLANT,COLLECT,USE_ITEM,WAIT四种; - Col B(Target):目标标识,如
peashooter,sun,rake,5000(WAIT 的毫秒数); - Col C(Condition):触发条件,格式为
sun>=50&zombie<300,支持>=,<=,<,>,&(AND)逻辑。
脚本在每次循环中,会逐行解析cmd.xls,用eval()动态计算 Condition 表达式。例如:
PLANT | peashooter | sun>=50&zombie<300 COLLECT | sun | True当阳光 ≥50 且僵尸距左边界 <300px 时,执行种豌豆射手;否则无条件收集阳光。
5.2 热更新策略:不用重启脚本,实时生效的三步法
- 停用自动保存:Excel 默认启用“自动恢复”,可能导致
cmd.xls被锁住。在 Excel 选项 → 保存 → 取消勾选“保存自动恢复信息”; - 编辑后强制刷新:修改
cmd.xls后,按Ctrl+S保存,脚本会在下一个循环周期(默认 2 秒)自动重新加载表格; - 验证生效:观察控制台输出的
Executing: PLANT peashooter日志,或用help.py --show确认新策略对应的 UI 元素已被高亮。
我曾用此机制在“屋顶关卡”临时追加一行:
USE_ITEM | fertilizer | zombie>5&sun>=200——当僵尸数 >5 且阳光 ≥200 时自动施肥加速植物生长,实测将通关时间缩短 22%。
5.3 多关卡调度:用game1.png/game2.png实现场景感知
game1.png和game2.png并非游戏截图,而是关卡特征锚点图。game1.png截取的是白天关卡左上角的“阳光计数器”区域,game2.png截取的是夜晚关卡特有的“月亮图标”。脚本通过find_image_cv2()同时匹配二者,决定加载哪套策略:
if find_image_cv2("clickArea/game1.png", threshold=0.8): load_strategy("daytime_strategy.xls") elif find_image_cv2("clickArea/game2.png", threshold=0.78): load_strategy("night_strategy.xls") else: load_strategy("default_strategy.xls") # 防御兜底策略这样,白天关卡优先种向日葵攒阳光,夜晚关卡则跳过向日葵,直奔樱桃炸弹——策略真正随环境变化,而非硬编码。
从那以后我每次写 GUI 自动化脚本,都强制走一遍help.py --calibrate标定关键坐标,再用test.py验证每个click()是否精准落在目标中心。PvZ 小助手教会我的不是怎么赢游戏,而是如何让代码在不可控的 GUI 环境里,依然保持可预测、可调试、可进化。希望帮到你。
本文还有配套的精品资源,点击获取