尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

强化学习实战-用强化学习打跑酷游戏 GreatWallRun 第三节 强化学习层构建 临时笔记

强化学习实战-用强化学习打跑酷游戏 GreatWallRun 第三节 强化学习层构建 临时笔记
📅 发布时间:2026/7/27 8:13:26

强化学习层技术笔记 — GreatWallRun

基于 Stable Baselines3 PPO 的手游自动化 RL 训练系统
核心代码:ppo_acting.py/game_env.py/train_ppo.py


一、整体架构:三层分离 + 多线程通信

1.1 设计哲学

系统严格遵循“感知层只感知,决策层只决策”的职责分离原则。三层之间通过共享内存变量和同步原语通信,不允许跨层直接调用。

┌─────────────────────────────────────────────────────────────────┐ │ train_ppo.py │ │ (训练编排层) │ │ PPO("MlpPolicy", env, tensorboard_log=...) │ │ model.learn(total_timesteps=100000) │ │ 断点续训 / 最优模型保存 / Ctrl+C 中断保护 │ └──────────────────────────┬──────────────────────────────────────┘ │ env.step(action) │ env.reset() ▼ ┌─────────────────────────────────────────────────────────────────┐ │ game_env.py │ │ (环境决策层 — Master) │ │ │ │ gymnasium.Env 接口: │ │ • observation_space: Box(207,) — 200 terrain + 7 state │ │ • action_space: Discrete(3) — 0:待机 1:跳 2:射 │ │ │ │ 内部逻辑: │ │ • _calculate_reward(action) — 8 项奖励/惩罚 │ │ • _get_obs() — 构建 207 维观测向量 │ │ • _update_terrain() — 200 维地形采样 │ │ • _update_run_stats(action) — 当局统计 │ │ │ │ 与 Worker 通信: │ │ • queue.put(action) → 发送动作指令 │ │ • action_done.wait() ← 等待执行完成 │ │ • 读取共享状态变量 ← 感知数据 │ │ • death_triggered / env_restart_order ⇄ 死亡重开握手 │ └──────────────────────────┬──────────────────────────────────────┘ │ queue.Queue(maxsize=1) │ threading.Event (action_done) │ 共享变量 (共享内存) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ ppo_acting.py │ │ (感知执行层 — Worker) │ │ │ │ Thread 1 — 帧抓取线程 (全速 30 FPS): │ │ cap.read() → _latest_frame (共享缓冲,Lock 保护) │ │ │ │ Thread 2 — 主循环 (YOLO + 渲染 + 动作执行): │ │ _latest_frame → 悬崖检测 → YOLO → RuleEngine → 更新共享状态 │ │ → 处理动作队列 → 渲染 3 窗口 → cv2.waitKey(1) │ │ │ │ Thread 3 — OCR 线程 (每 0.3s): │ │ _latest_frame → Tesseract → shared_lives/arrows/coins/distance │ └─────────────────────────────────────────────────────────────────┘

1.2 为什么不用 OpenAI Gym 标准的单进程模式?

标准 Gym 中env.step()内部直接执行动作并返回下一状态。但在本项目中:

  1. 动作延迟不可忽略— ADB 点击到游戏画面变化有 300-500ms 延迟,必须等待
  2. 感知必须持续运行— YOLO 推理不能因为step()没被调用就停止,否则画面滞后
  3. 多消费者— OCR、YOLO、game_env 三者都需要最新的游戏画面

因此采用了独立 daemon 线程 + 队列同步的方案:

  • ppo_acting的感知循环永远运行,不依赖step()调用
  • game_env.step()只是一个"下单 → 等结果"的同步点
  • 这种模式类似于异步环境或client-server RL architecture

1.3 同步原语设计

原语类型方向语义
env_action_queuequeue.Queue(maxsize=1)Master → Worker动作指令,容量为 1 强制同步
action_donethreading.EventWorker → Master动作已执行 + 冷却完成 + 感知已更新
perception_readythreading.EventWorker → Master首帧感知完成,环境可以开始
death_triggeredboolWorker → Master检测到死亡(lives=0 或距离停滞)
env_restart_orderboolMaster → Worker确认死亡,执行重开点击流程
_stats_lockthreading.Lock双向保护训练统计 dict 的读写
_frame_lockthreading.Lock单向保护_latest_frame的并发访问

二、观测空间设计

2.1 总览

observation_space = Box(0, 1, shape=(207,), dtype=float32) ┌─────────────────────┬──────────┬──────────────────────────┐ │ 部分 │ 维度 │ 来源 │ ├─────────────────────┼──────────┼──────────────────────────┤ │ 地形向量 │ 200 │ 像素级地面颜色扫描 │ │ 最近障碍物距离 │ 1 │ RuleEngine.danger_list │ │ 最近敌人距离 │ 1 │ RuleEngine.enemy_list │ │ 最近 Soul 距离 │ 1 │ RuleEngine.soul_list │ │ 最近金币距离 │ 1 │ RuleEngine.coin_list │ │ 箭矢数 │ 1 │ OCR shared_arrows │ │ 金币数 (累计) │ 1 │ OCR shared_coins │ │ 生命值 │ 1 │ OCR shared_lives │ │ 总计 │ 207 │ │ └─────────────────────┴──────────┴──────────────────────────┘

2.2 地形向量 (200 维)

采样方法:

从主角马匹的右边界horse_pos[2]开始,向右每隔 3 像素采样一次,固定 200 个点。每个采样点读取画面底部往上 15 像素处的颜色,与GROUND_COLORS对比判定是否为地面。

forxinrange(start_x,min(start_x+200*3,w),3):pixel=frame[h-15,x]is_ground=(pixel ≈ GROUND_COLORS[0]orpixel ≈ GROUND_COLORS[1])terrain_slice.append(1.0ifis_groundelse0.0)

归一化:直接为[0, 1]值,无需额外缩放。

为什么采样 200 个点?

  • 1920×1080 分辨率下 200×3 = 600px 足够覆盖马匹前方到屏幕右边缘的全部地面
  • 200 维足够让 MLP 策略网络感知到"前方空洞 = 悬崖"的 pattern
  • 3px 间隔在精度和向量长度间折中

2.3 状态向量 (7 维)

距离归一化:

danger_dist/500# 超过 500px 视为无穷远enemy_dist/500soul_dist/500coin_dist/500

选择 500 作为分母是因为游戏画面中超过 500px 的物体已接近屏幕边缘,Agent 不需要区分 500px 和 800px —— 都"很远"。

HUD 归一化:

arrows/100# 箭矢上限约 99coins/500# 金币可能累积数百lives/10# 生命值通常 1-5

生命值不做上限截断——如果lives > 10也能正确归一化到> 1.0,策略网络会从数值本身学到"生命很多"。


三、动作空间设计

3.1 动作定义

Action名称ADB 操作游戏效果
0待机无角色自然奔跑
1跳跃adb_tap(300, 300)跳过障碍/悬崖/拾取头顶金币
2射击adb_swipe(horse → enemy, offset=300)消灭前方敌人

3.2 为什么只有 3 个动作?

  1. 大招(加速)未加入— 需要消耗金币,决策复杂度过高,先不纳入 RL
  2. 没有方向控制— 游戏是自动奔跑的卷轴跑酷,角色自动前进
  3. 没有"向下滑"— 游戏不提供下蹲/滑铲操作
  4. 射击方向自动瞄准— swipe 的终点根据shoot_target(最近敌人)自动计算

3.3 射击坐标计算

# 从马的位置向敌人方向延伸 SHOOT_OFFSET=300pxhx,hy=horse 中心坐标 ex,ey=最近敌人的中心坐标 dx,dy=ex-hx,ey-hy ratio=SHOOT_OFFSET/dist end_x=hx+dx*ratio end_y=hy+dy*ratio# 远距离射击时加 Y 轴补偿(箭矢抛物线)ifdist>=DISTANCE_SWITCH(240px):end_y+=SHOOT_Y_OFFSET(-50px)# 向上抬,模拟抛物线

3.4 动作冷却

不同动作的执行时间不同,冷却时间也不同:

Action冷却原因
0 待机0.0s无物理操作
1 跳跃0.5stap 瞬间完成,但需要等角色起跳→落地
2 射击0.6sswipe 100ms + 箭矢飞行 + 游戏动画

冷却以非阻塞方式实现:执行动作后记录cooldown_until = now + interval,下一帧信号检查时若冷却已过 + 新帧感知完成 → 才action_done.set()。


四、奖励函数设计

4.1 设计原则

  1. 密集信号优先— 距离增量每步都给,Agent 每步都有反馈
  2. 关键事件高权重— 悬崖跨越 +20、死亡 -50,明确强化/抑制
  3. 防止 OCR 噪声— 所有 OCR 来源的 delta 都做上限裁剪
  4. 引导合理行为— 战斗惩罚引导 Agent 不浪费箭矢、不放任敌人

4.2 完整奖励项

详见REWARD.md,此处只列核心逻辑:

def_calculate_reward(action):reward=0.0# P1 生存税 — 打破全正奖励reward-=0.005# R1 距离增量 — 核心正向驱动delta_dist=current_dist-prev_distif|delta_dist|<=50:# OCR 防抖reward+=delta_dist*0.1# R2 金币增量delta_coins=current_coins-prev_coinsif0<delta_coins<=10:# OCR 防抖reward+=delta_coins*1.0# R3 障碍越过 — 跟踪 nearest danger 的 x 坐标变化ifnearest_danger_x 换了:reward+=2.0# R4 悬崖跨越 — True → Falseifcliff 解除:reward+=20.0# P2 无目标射击 — action=2 但无敌或 >300pxifaction==2andnotenemy_close:reward-=0.3# P3 有敌不射 — 敌人 <300px 但 action≠2ifaction!=2andenemy_close:reward-=0.8# P4 死亡ifdone:reward-=50.0

4.3 典型一局预算

跑 2000m, 捡 20 币, 过 15 障碍, 跨 2 悬崖, 射 10 次 生存税: -0.005 × 400步 = -2.0 距离: +0.1 × 2000 = +200.0 金币: +1.0 × 20 = +20.0 障碍: +2.0 × 15 = +30.0 悬崖: +20.0 × 2 = +40.0 射击奖惩: ≈ 0.0 ──────────────────────────────── 合计: ~+288.0 死亡: -50.0 (17%)

死亡惩罚约占一局总正奖励的 17%,既能起到威慑作用,又不会让 Agent 过度保守(不敢冒险跳跃)。

4.4 奖励 shaping 的反面:为什么不加"按键惩罚"?

有些 RL 项目对每次行动(action≠0)施加小惩罚,防止 Agent 疯狂按键。本项目不需要:

  1. 冷却机制已经限制频率— Agent 每秒最多 2 次动作
  2. 无效射击已有专门惩罚— 比通用按键惩罚更有针对性
  3. 生存税已经打破全正奖励— 无需额外 per-action 惩罚

五、死亡检测与重开闭环

5.1 死亡判定(双重机制)

主判定:OCR 读到 lives=0 → 立即标记 death_triggered=True └─ 优点:零延迟,不会有"死后还在选动作"的问题 兜底判定:距离停滞 2s → 标记 death_triggered=True └─ 用途:OCR 偶尔漏读 lives=0 的情况

5.2 端到端时序

时间线: T+0.0s lives=0 → death_triggered=True → step() 返回 done T+0.0s PPO 调用 reset() T+1.0s 等待 OCR 稳定(0.3s×3 周期) T+1.0s 采样 dist_before T+3.0s 采样 dist_after T+3.0s dist_before == dist_after → 确认死亡 T+3.0s env_restart_order = True → Worker 收到指令 T+3.0s _death_check_enabled = False ← 锁住死亡检测 T+3.0s tap(300, 300) — 进入死亡页面 T+6.0s tap(960, 1000) — 点击重开 T+8.0s 等待游戏加载 T+8.0s _death_check_enabled = True ← 恢复死亡检测 T+8.0s reset() 返回初始 obs — 新局开始

5.3 关键保护机制

保护机制防止的问题
OCR 滞后误判等 1s 再采样,确保 OCR 稳定lives 归零但距离读数还在更新 → 误判为"还活着"
2s 距离验证两次采样对比OCR 短暂异常读数 → 误判死亡
_death_check_enabled锁重开期间完全关闭死亡检测死画面上 lives=0 持续触发 → 无限循环重启

六、多线程交互时序

6.1 正常 Step 的完整交互

train_ppo game_env ppo_acting 主循环 OCR 线程 │ │ │ │ │ env.step(1) │ │ │ │ ──────────────► │ │ │ │ │ queue.put(1) │ │ │ │ ────────────────────► │ │ │ │ │ │ │ │ action_done.wait() │ │ │ │ (阻塞, 最多 2s) │ │ │ │ │ │ │ │ ┌─────┴─────┐ │ │ │ │ pop 动作 1 │ │ │ │ │ adb_tap │ │ │ │ │ 设 cooldown│ │ │ │ │ pending= │ │ │ │ │ True │ │ │ │ └─────┬─────┘ │ │ │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ │ 继续跑感知循环 │ │ │ │ │ 读帧 → 悬崖 → YOLO │ │ │ │ │ → 更新共享状态 │ │ │ │ └──────────┬──────────┘ │ │ │ │ │ │ │ ┌─────┴─────┐ │ │ │ │ cooldown │ │ │ │ │ 已过? │ │ │ │ │ YES → │ │ │ │ │ action_ │ │ │ │ │ done.set()│ │ │ │ └─────┬─────┘ │ │ │ │ │ │ │ ◄─ action_done 收到 ──┘ │ │ │ │ │ │ │ 读共享状态: │ │ │ │ horse_pos, │ │ │ │ danger_list, │ │ │ │ shared_distance ... │ │ │ │ │ │ │ │ _calculate_reward() │ │ │ │ _get_obs() │ │ │ │ │ │ │ ◄─ (obs, r, done)│ │ │ │ │ │ │

6.2 关键时序保证

为什么action_done一定在感知更新后?

主循环的执行顺序保证了:

while True: 1. 读新帧 ← 上轮动作的效果已经体现在画面中 2. 感知 (YOLO + rule_engine) ← 更新所有共享状态 3. 如果 pending_signal + cooldown_done: action_done.set() ← game_env 醒来读到的一定是动作后的状态 4. 如果 queue 非空: 执行动作 ← 标记 pending_signal=True 5. 渲染

当 game_env 被action_done.set()唤醒时,第 2 步已经完成,共享状态是最新的。

为什么选择maxsize=1的 Queue?

  • maxsize=1意味着 game_env 的queue.put()会阻塞直到 Worker 消费了上一个动作
  • 这自然形成了背压机制:Agent 不可能领先 Worker 超过 1 个动作
  • 比无限 queue + 手动同步更简洁健壮

七、PPO 训练配置

7.1 超参数与设计意图

参数值设计意图
policyMlpPolicy207 维观测 → 3 维动作,MLP 足够。不需要 CNN
n_steps256每 256 步做一次策略更新。约等于 2-3 局游戏
batch_size64256 步分 4 个 batch 更新,平衡训练速度和稳定性
n_epochs5每轮数据重放 5 次。RL 环境数据不能过拟合
learning_rate3e-4PPO 标准值,太大了策略震荡,太小了收敛慢
gamma0.99重视长期回报。游戏可以跑很久,远期的生存也很重要
gae_lambda0.95标准值,平衡优势估计的偏差和方差
clip_range0.2PPO 核心:限制每次更新的幅度,防止策略崩溃
ent_coef0.05略高,鼓励探索。游戏有随机性(敌人/障碍随机出现)
vf_coef0.5标准值,价值函数损失权重

7.2 模型保存策略

类型触发用途
ppo_greatwall_N_steps.zip每 10000 步定期 checkpoint,断点续训
ppo_greatwall_best.zipepisode reward 创新高训练中最佳模型
ppo_greatwall_final.zip训练完成最终模型
ppo_greatwall_interrupt.zipCtrl+C中断保护

BestModelCallback是自定义的——监听Monitor包装器在每个 episode 结束时写入infos的episode字段,比较ep_reward。


八、设计决策 FAQ

Q: 为什么 RuleEngine 仍然在 ppo_acting 中运行?RL 不是不应该用规则吗?

RuleEngine 的输出(danger_list,enemy_list等)是作为观测的一部分输入给 RL 的,不是用来做决策的。它本质上是一个特征提取器——把 YOLO 的边界框列表转化为"最近的障碍物距离 230px"这种结构化信息。PPO 策略自己决定跳不跳。

Q: 为什么 terrain 向量用像素采样而不是 YOLO 检测?

悬崖是地面缺失,不是可检测的"物体"。YOLO 不知道地面长什么样。像素扫描是唯一可靠的悬崖检测方式。

Q: 为什么 OCR 读 HUD 而不是让 YOLO 检测数字?

OCR 识别任意数字比训练 YOLO 检测每个数字(0-9)更灵活通用。Tesseract 专门优化了数字识别。

Q: reset() 中的 2s 等待会不会浪费时间?

只在死亡时发生。一局游戏可能几百上千步,一次 3 秒的 reset 开销可忽略。更重要的是它确保了重开可靠性。

占位图
​​
我们挑几张验证效果看看

​

相关新闻

  • 鸡西房屋漏水维修修护宝典(2026 新版):卫生间、厨房、阳台就近派工快速上门勘测 - 金信达
  • 荆州精选口碑瓷砖空鼓维修公司推荐(2026)厨房瓷砖脱落处理 - 屋工匠
  • 多模态模型视觉编码器革新:Penguin-VL的技术突破与应用

最新新闻

  • 基于 W-GAN 的光伏出力场景生成方法研究(Python代码实现)
  • YOLOv8+ByteTrack+MediaPipe 足球赛事全链路实战|全网独家复现球员足球多目标追踪人体姿态估计,助力世界杯赛事视频分析 AI 平台功能落地
  • 112233
  • AI智能体技术栈解析:Agent、推理引擎与大模型协同
  • 港口智能化转型:AI识别技术提升安全与效率
  • C++编程中ASCII码的核心原理与应用实战

日新闻

  • OpenClaw开源智能体网关:AI助手与即时通讯的完美融合
  • 写一个简单的sh脚本
  • 2026年 西安缝隙天线厂家:5G通信与车载天线专业定制供应商深度分析 - 卓企推荐

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号