第四部分 · 环境与接口:亲手写一个王者子任务
目标:理解强化学习环境的统一接口(
reset / step / 观测 / 动作 / 奖励 / done), 并亲手写一个可训练的《王者荣耀》子任务环境——「鲁班瞄准」。
有了理论,就该动手了。强化学习需要一个能交互的"世界"——即环境 (Environment)。 业界的通用接口只有几个函数,开悟的 1v1 环境和我们下面要写的子任务,遵循的是同一套接口。 所以只要把接口吃透,从小任务到大环境都一通百通。
4.1 强化学习环境的统一接口
无论是我们自己写的小任务,还是开悟的完整 1v1,核心都是这几个成员:
| 成员 | 作用 | 在王者里 |
|---|---|---|
env.reset() |
重置环境,返回初始观测 obs |
开一局新对战,返回开局战场 |
env.step(action) |
执行动作,返回 (obs, reward, done, info) |
走一帧:给出新战场、这步奖励、是否结束 |
| 动作空间 | 能做哪些动作 | 移动方向 / 技能 / 目标… |
| 观测空间 | 状态长什么样 | 战场快照向量 s |
📌 这套接口是第一课交互循环的代码化。开悟官方 SDK(
HoK1v1)也是reset()/step(),业界标准库 Gymnasium 亦然——接口通用,我们只用王者场景来学它。
4.2 第一个可训练任务:亲手写「鲁班瞄准」环境
回忆第一课 0.45:我们把大任务拆成小任务,第一个就是鲁班瞄准—— 鲁班站桩不动,地图上有个会移动的目标,训练它朝正确方向发射炮弹命中目标。
这个任务小到可以用几十行纯 Python 写出来,却五脏俱全(状态/动作/奖励/结束齐全), 是我们后面训练算法的"练兵场":
import numpy as np
class LubanAimEnv:
"""《王者荣耀》子任务:鲁班站桩,在有限步数里尽量多地命中目标。
命中一个 +1 并立刻刷新下一个目标;未命中 -0.1,目标继续移动。回合到步数上限结束。
- 观测 s: [dx, dz, vx, vz, cd] —— 目标相对位置、目标速度、炮弹是否就绪(5 维)
- 动作 a: Discrete(16) —— 发射方向(把 360° 离散成 16 个扇区,朝扇区中心打)
- 奖励 r: 命中 +1;未命中 -0.1(鼓励又快又准)
"""
N_DIR = 16
def __init__(self, max_steps=20, target_speed=0.05, seed=None):
self.max_steps = max_steps
self.target_speed = target_speed
self.rng = np.random.default_rng(seed)
def _new_target(self):
ang = self.rng.uniform(0, 2 * np.pi) # 目标方向
r = self.rng.uniform(0.3, 1.0) # 目标距离
self.target = np.array([r * np.cos(ang), r * np.sin(ang)])
v_ang = self.rng.uniform(0, 2 * np.pi) # 目标移动方向
self.vel = self.target_speed * np.array([np.cos(v_ang), np.sin(v_ang)])
def reset(self):
self.t = 0
self._new_target()
return self._obs()
def _obs(self):
return np.array([*self.target, *self.vel, 1.0], dtype=np.float32)
def step(self, action):
fire_ang = 2 * np.pi * (action + 0.5) / self.N_DIR # 朝扇区中心发射
tgt_ang = np.arctan2(self.target[1], self.target[0]) % (2 * np.pi)
diff = abs((fire_ang - tgt_ang + np.pi) % (2 * np.pi) - np.pi) # 环形角差
hit = diff < (np.pi / self.N_DIR) # 命中容差=半个扇区
self.t += 1
if hit:
reward = 1.0
self._new_target() # 命中→刷新下一个目标
else:
reward = -0.1
self.target = self.target + self.vel # 未命中→目标继续移动
if np.linalg.norm(self.target) > 1.2: # 简化:撞到边界反弹
self.vel = -self.vel
done = self.t >= self.max_steps
return self._obs(), reward, done, {"hit": hit}
先用随机策略试玩,感受一下"零基础的鲁班"有多菜(我们统计每回合平均命中数与平均回报):
env = LubanAimEnv(seed=0)
total_hits = total_return = 0
for episode in range(200):
obs = env.reset()
done = False
while not done:
action = np.random.randint(env.N_DIR) # 随机乱打
obs, reward, done, info = env.step(action)
total_hits += info["hit"]
total_return += reward
print(f"随机策略:平均每回合命中 {total_hits/200:.2f} 次,平均回报 {total_return/200:.2f}")
# 随机策略:平均每回合命中 1.39 次,平均回报 -0.48 ——20 步里基本靠蒙
随机策略每回合才蒙中 1 次左右、回报是负的。第五课我们训练它后,能做到 20 步命中 20 个、回报 +20!
4.3 王者子任务阶梯(我们的"训练场"清单)
「鲁班瞄准」只是第一级。把 1v1 拆开,可以得到一串由易到难、都能在 CPU 上训的子任务:
| 子任务 | 状态(示意) | 动作 | 学到的能力 | 难度 |
|---|---|---|---|---|
| 鲁班瞄准 | 目标相对位置+速度 | 发射方向(离散) | 打得准 | ⭐ |
| 补刀最后一击 | 小兵血量、我方攻击力、弹道时间 | 何时平A(离散) | 拿经济 | ⭐⭐ |
| 残血风筝 | 我/敌血量、距离、技能CD | 移动方向+攻击(复合) | 边打边退不被抓 | ⭐⭐⭐ |
| 塔下生存 | 到塔距离、塔攻击计时、我方血量 | 进/退(离散) | 懂威胁、不越塔送 | ⭐⭐⭐ |
| 完整 1v1 | 491 维战场向量 | 三元组复合动作 | 综合博弈 | ⭐⭐⭐⭐⭐ |
💡 每一行都是一个独立的强化学习问题。学会最上面几个简单的,就理解了下面复杂的。 完整 1v1(最后一行)就是第七课的开悟环境。
4.4 动作空间与观测空间(都用王者场景理解)
设计算法前,先看清两件事:状态长什么样、动作能选什么。
- 离散动作
Discrete(n):取值0 ~ n-1。- 鲁班瞄准:
Discrete(16)(16 个发射方向); - 补刀:
Discrete(2)(平A / 不平A)。
- 鲁班瞄准:
- 连续/向量观测
Box(shape):实数向量。- 鲁班瞄准:
Box(5,)(相对位置+速度+CD); - 完整 1v1:
Box(491,)。
- 鲁班瞄准:
- 复合动作:完整 1v1 的动作不是一个数,而是"按钮 + 方向 + 目标"的多个子动作头(见第七课 6.4)。
env = LubanAimEnv()
print("动作数量:", env.N_DIR) # 16 个离散发射方向
print("观测维度:", env.reset().shape) # (5,)
📌 选型口诀(第五课细讲):离散动作 → Q-Learning/DQN;复合/大动作空间 → PPO。 鲁班瞄准是离散动作,正好用来练 Q-Learning 和 DQN。
4.5 进阶:把子任务变难一点
理解环境最好的方式是动手改它。给「鲁班瞄准」加点花样,就得到更难的任务:
- 目标闪现:每隔几步,目标以一定概率瞬间"闪现"到随机位置(模拟敌方交闪现);
- 提前量:目标移动更快时,必须预判提前量(朝目标"将要到"的位置打,而不是当前位置);
- 限定弹药/冷却:炮弹有冷却,逼迫智能体挑好时机再发射。
def step(self, action):
# ...命中判定同上...
if not hit and self.rng.random() < 0.1: # 10% 概率触发"闪现"
ang = self.rng.uniform(0, 2 * np.pi)
r = self.rng.uniform(0.3, 1.0)
self.target = np.array([r * np.cos(ang), r * np.sin(ang)])
# ...
🎯 目标一旦会移动/闪现,只看当前位置就不够了——必须用上"速度"信息做预判。 这正是为什么我们的观测里放了
vx, vz:状态要足够,才能支撑好决策(呼应第二部分马尔可夫性)。
4.6 环境包装 (Wrappers):不改原环境地加功能
有时想在不动原环境代码的前提下加点功能,比如限制每局步数、缩放奖励、统计命中率。 可以写一个"包装器"套在外面:
class ScaleReward:
"""把奖励乘一个系数(奖励缩放常能稳定训练)——包装我们的鲁班环境"""
def __init__(self, env, scale=1.0):
self.env, self.scale = env, scale
def reset(self):
return self.env.reset()
def step(self, action):
obs, r, done, info = self.env.step(action)
return obs, r * self.scale, done, info
env = ScaleReward(LubanAimEnv(), scale=0.5) # 奖励缩小一半
开悟环境同样支持在外层自定义观测/奖励的处理(见第七课 6.5的奖励塑形)。
4.7 从子任务到完整开悟:接口不变,难度升级
我们自己写的「鲁班瞄准」和开悟的完整 1v1,用的是同一套接口,只是规模不同:
| 概念 | 鲁班瞄准(本部分自写) | 开悟 1v1(第七课) |
|---|---|---|
| 观测 | Box(5,) |
Box(491,) 战场向量 |
| 动作 | Discrete(16) 发射方向 |
三元组复合动作 + legal_action 合法掩码 |
| 奖励 | 命中 +1 / 未命中 -0.1 | 5 类多维可塑形(击杀/推塔/补刀/经济…) |
| 一步 | env.step(action)(纯 Python) |
env.step(actions)(经 ZMQ 与 gamecore 通信) |
| 复现 | 几十行代码 | Docker 一键(含 Wine 运行 gamecore) |
换句话说:接口不变,难度升级。先在鲁班瞄准上把接口和算法吃透, 第七课去驾驭真实开悟环境自然水到渠成。 👉 第七课 · 开悟《王者荣耀》MOBA 实战
✅ 小结
- 环境是 RL 的"训练场",核心接口只有
reset / step / 动作空间 / 观测空间; - 我们亲手写了「鲁班瞄准」——一个五脏俱全、纯 CPU 可训的王者子任务;
- 把 1v1 拆成一串子任务阶梯:鲁班瞄准 → 补刀 → 风筝 → 塔下生存 → 完整 1v1;
- 动作空间决定算法选型:离散 → Q-Learning/DQN,复合/大空间 → PPO;
- 会写、会改环境(加闪现/提前量),才算真正理解它;开悟用的是同一套接口。
📝 练习
- 跑通
LubanAimEnv的随机策略,统计 200 回合的平均命中数与平均回报(应该很低,为第五课做对照)。 - 把
target_speed设为0(目标站桩),再统计随机的平均命中数——和会动时相比差别大吗?为什么?(提示:随机策略根本没用到位置信息) - 按 4.5 给环境加上"闪现",观察随机策略的平均回报如何变化,说说为什么变难。
- 仿照
ScaleReward,写一个TimeLimit包装器,把每局最多步数限制到 20。 - 思考题:如果把观测里的
vx, vz(目标速度)去掉,只留位置,智能体还能学会打移动目标吗?为什么?(提示:马尔可夫性)
⬅️ 上一部分:第三部分 · 强化学习基础概念 ➡️ 下一部分:第五部分 · 各式各样的强化学习算法 🎮 旗舰实战:第七课 · 开悟《王者荣耀》MOBA 实战