1. 为什么需要硬件级模拟?
在游戏自动化领域,很多开发者首先想到的可能是pyautogui这样的软件工具。我最初做云顶之弈自动化脚本时也是这么想的,但实际测试发现,像《英雄联盟》这样的游戏对软件层面的自动化操作有着严格的检测机制。pyautogui虽然能模拟鼠标移动,但在游戏内点击操作完全失效,这就是典型的软件层防护。
这时候CH9329模块的价值就体现出来了。这个硬件模块本质上是一个USB HID设备模拟器,通过串口接收指令后,会以真实硬件设备的方式向系统上报输入事件。从系统层面看,这就是一个真实的键盘鼠标在操作,游戏客户端很难区分这是人工操作还是自动化脚本。
2. CH9329模块工作原理详解
CH9329是沁恒微电子推出的一款USB键盘鼠标协议转换芯片,核心功能是通过串口协议模拟USB HID设备。它的工作流程可以分为三个关键阶段:
首先是通信协议层。模块通过TTL串口与主控设备(比如我们的Python脚本)通信,波特率可调(常用115200bps)。每次通信以0x57 0xAB开头,后面跟着指令类型、数据长度和具体数据。
其次是事件转换层。模块收到串口数据后,会将其转换为标准的USB HID报告描述符。比如鼠标移动指令会被转换为HID鼠标的位移报告,键盘按键则转换为HID键盘的键码报告。
最后是设备枚举层。当模块连接到电脑时,会像普通键鼠设备一样完成USB枚举过程。在设备管理器中可以看到"USB输入设备",与真实键鼠无异。
3. 硬件连接与初始化配置
要使用CH9329模块,我们需要准备以下硬件:
- CH9329模块(某宝约15元)
- USB转TTL模块(如CH340,约7元)
- 杜邦线若干
连接方式很简单:
- 将CH340的TXD接CH9329的RXD
- CH340的RXD接CH9329的TXD
- 共接GND
- CH9329的VCC接3.3V
在Python中初始化串口连接时,需要注意几个关键参数:
import serial mserial = serial.Serial( port='COM3', # 根据设备管理器中的实际端口号修改 baudrate=115200, # 需与模块设置一致 timeout=1 # 读写超时时间 )4. 核心指令封装实战
理解协议后,我们可以封装几个关键函数。首先是鼠标绝对移动点击函数:
def hard_click(x, y, clicks=1, button='left'): # 将屏幕坐标转换为模块接受的绝对坐标(0-4096) nx = int(x * 4096 / 1920) # 假设屏幕宽度1920 ny = int(y * 4096 / 1080) # 假设屏幕高度1080 # 构建指令帧 cmd = [0x57, 0xAB, 0x00, 0x04, 0x07, 0x02] # 头码+指令类型 button_val = 1 if button == 'left' else 2 # 左键/右键 # 坐标分解为高低字节 low_x = nx & 0xFF high_x = (nx >> 8) & 0xFF low_y = ny & 0xFF high_y = (ny >> 8) & 0xFF # 计算校验和 data = [button_val, low_x, high_x, low_y, high_y, 0x00] checksum = (sum(cmd) + sum(data)) & 0xFF # 组合完整指令 press_cmd = cmd + data + [checksum] release_cmd = cmd + [0x00] + data[1:] + [checksum] # 执行点击 for _ in range(clicks): mserial.write(bytes(press_cmd)) time.sleep(0.05) mserial.write(bytes(release_cmd)) time.sleep(0.05)键盘按键的封装稍微复杂些,需要处理组合键:
key_map = { 'a': 0x04, 'd': 0x07, 'f': 0x09, 'esc': 0x29, 'enter': 0x28 } def hard_key_press(key): # 处理组合键 keys = key.split('+') modifiers = 0x00 key_codes = [] for k in keys: if k in ['ctrl', 'shift', 'alt']: modifiers |= 1 << ['ctrl', 'shift', 'alt'].index(k) else: key_codes.append(key_map.get(k.lower(), 0x00)) # 构建指令帧 cmd = [0x57, 0xAB, 0x00, 0x02, 0x08] data = [modifiers, 0x00] + key_codes[:6] data += [0x00] * (6 - len(key_codes)) # 补全6个键码 # 发送按下指令 press_cmd = cmd + data + [sum(cmd + data) & 0xFF] mserial.write(bytes(press_cmd)) time.sleep(0.05) # 发送释放指令 release_cmd = cmd + [0x00]*8 + [0x0C] mserial.write(bytes(release_cmd))5. 游戏自动化策略设计
在云顶之弈自动化脚本中,我采用了状态机设计模式,将整个流程划分为多个状态:
匹配等待状态:
- 使用pyautogui的locateOnScreen检测"寻找对局"按钮
- 发现按钮后调用hard_click点击
- 鼠标移出检测区域避免遮挡
对局接受状态:
- 循环检测"接受对局"按钮
- 同时监控游戏进程是否启动
- 处理可能出现的拒绝情况
游戏进行状态:
- 通过图像识别检测特定回合(如3-2)
- 执行预设操作序列:
- 随机移动鼠标防止挂机检测
- 间隔购买英雄、刷新商店
- 定时升级小小英雄
投降结算状态:
- 精准点击投降按钮三步曲
- 等待游戏结束
- 检测结算界面并开始下一局
关键技巧在于每个状态间的平滑过渡,以及合理的随机化操作。比如在游戏进行状态中,我设计了这样的行为模式:
def in_game_behavior(): actions = [ (hang_out, 0.4), # 40%概率随机移动 (buy_champion, 0.3), # 30%概率购买英雄 (refresh_shop, 0.1), # 10%概率刷新商店 (upgrade, 0.2) # 20%概率升级 ] # 根据权重随机选择动作 rand = random.random() cumulative = 0 for action, prob in actions: cumulative += prob if rand < cumulative: action() break6. 图像识别优化技巧
pyautogui的locateOnScreen虽然方便,但在实际使用中有几个常见坑点:
分辨率适配问题:
- 游戏客户端可能以不同分辨率运行
- 解决方案:获取窗口实际大小后动态计算坐标
def get_scaled_position(base_x, base_y, window_size): scale_x = window_size[0] / 1600 # 基准分辨率1600x900 scale_y = window_size[1] / 900 return (int(base_x * scale_x), int(base_y * scale_y))图像匹配失败:
- 界面元素可能被遮挡或变形
- 解决方案:
- 使用confidence参数降低匹配阈值
- 对关键按钮截取多个角度的样本
- 添加备用检测逻辑
性能优化:
- 全屏搜索耗时较长
- 解决方案:限定搜索区域
button_region = (x1, y1, x2, y2) # 按钮大致区域 pg.locateOnScreen(image, region=button_region)
7. 稳定性保障方案
要让脚本长时间稳定运行,必须处理好以下问题:
异常恢复机制:
- 网络波动导致匹配失败
- 游戏崩溃或意外退出
- 解决方案:设置超时和重试逻辑
日志记录系统:
- 记录每个关键步骤的时间戳
- 保存异常时的屏幕截图
def save_debug_screenshot(): timestamp = time.strftime("%Y%m%d_%H%M%S") pg.screenshot(f'debug_{timestamp}.png')防检测策略:
- 操作间隔加入随机延迟
- 鼠标移动轨迹采用贝塞尔曲线
- 行为模式避免完全规律
经过实测,这套系统可以稳定运行12小时以上,平均每局耗时15-18分钟,远胜纯软件方案。不过要注意的是,任何自动化操作都应遵守游戏规则,建议仅用于测试和学习目的。