🏗️ 第一个环境 — 写自己的环境类¶
上一节我们用最简环境跑通了仿真循环。这一节,你将学会如何写一个完整的环境类来控制仿真。
为什么要写环境类?¶
要控制仿真(物理步进、读取状态),你需要继承环境基类。
推荐使用 OrcaGymEulerEnv(Euler 新主路径), OrcaGymLocalEnv 处于维护模式。
一个环境类 = 场景的"驱动程序":
最小环境骨架(,推荐)¶
环境类需要实现 3 个核心方法:
__init__() — 初始化(设置 action_space、observation_space)
step(action) — 执行一步仿真,返回 (obs, reward, terminated, truncated, info)
reset_model() — 重置到初始状态
_get_obs() — 收集观测数据
与 RL 环境的关键区别:
step()返回 5 元组(Gymnasium 标准),不是老版的 4 元组。terminated表示任务完成/失败(如机器人摔倒),truncated表示超时截断(达到 max_episode_steps)。
下面是一个可运行的完整环境(离线模式,无需 Studio):
"""
my_first_env.py — 一个最小的自定义环境
基于 [OrcaPlayground examples/euler/01_hello_euler/](https://github.com/OrcaGym/OrcaPlayground) 的简化版
"""
import numpy as np
from gymnasium import spaces
from orca_gym.environment.euler.orca_gym_euler_env import OrcaGymEulerEnv
class MyFirstEnv(OrcaGymEulerEnv):
"""最简环境:观测 = 关节位置 + 速度,动作 = 力矩控制,奖励 = 0"""
metadata = {"render_modes": ["human", "none"], "version": "0.0.1", "render_fps": 30}
def __init__(self, model_xml_path, **kwargs):
# ── 父类初始化(自主编排生命周期)──
super().__init__(
frame_skip=kwargs.pop("frame_skip", 5),
orcagym_addr=kwargs.pop("orcagym_addr", "localhost:50051"),
agent_names=kwargs.pop("agent_names", ["agent0"]),
time_step=kwargs.pop("time_step", 0.002),
model_xml_path=model_xml_path,
skip_grpc_load=kwargs.pop("skip_grpc_load", True), # 默认离线
**kwargs,
)
# ── 动作空间 ──
self.action_space = spaces.Box(
low=-1.0, high=1.0, shape=(self.model.nu,), dtype=np.float32
)
# ── 观测空间 ──
obs_sample = self._get_obs()
self.observation_space = spaces.Box(
low=-np.inf, high=np.inf, shape=obs_sample.shape, dtype=np.float32
)
# ── 观测获取 ───────────────────────────────────────
def _get_obs(self) -> np.ndarray:
"""返回当前观测。零拷贝直接读 data。"""
return np.concatenate([
self.data.qpos.copy(),
self.data.qvel.copy(),
]).astype(np.float32)
# ── 仿真步进 ───────────────────────────────────────
def step(self, action: np.ndarray):
"""
执行一步仿真。
action: 形状 (nu,),每个值 ∈ [-1, 1]
返回: (obs, reward, terminated, truncated, info) — Gymnasium 5 元组
"""
action = np.asarray(action, dtype=np.float32).reshape(self.model.nu)
# 1. 执行仿真:do_simulation 内部自动同步 data
self.do_simulation(action, self.frame_skip)
# 2. 获取新观测
obs = self._get_obs()
# 3. 奖励 & 终止条件
reward = 0.0 # 此处恒为 0(替换为你的奖励函数)
terminated = False # 此处永不终止(替换为你的终止条件)
truncated = False
info = {"time": float(self.data.time)}
return obs, reward, terminated, truncated, info
# ── 重置 ───────────────────────────────────────────
def reset_model(self):
"""回到初始状态。"""
# 合规写法:通过 set_joint_qpos / set_joint_qvel 设置状态
self.set_joint_qpos(self.init_qpos) # 恢复到 XML 中定义的初始 qpos
self.set_joint_qvel(self.init_qvel) # 恢复到 XML 中定义的初始 qvel
self.mj_forward() # 更新派生量(body 位姿、传感器等)
self._sync_view() # 同步到 DataView(env.data)
return self._get_obs(), {}
# ============================================================
# 使用
# ============================================================
if __name__ == "__main__":
SCENE_XML = "/path/to/your/scene.xml"
env = MyFirstEnv(model_xml_path=SCENE_XML)
obs, _ = env.reset()
print(f"nq={env.model.nq}, nv={env.model.nv}, nu={env.model.nu}")
print(f"obs.shape={obs.shape}")
for i in range(10):
action = env.action_space.sample() # 随机动作
obs, reward, terminated, truncated, _ = env.step(action)
print(f" Step {i}: reward={reward:.3f}, time={_:.4f}s")
env.close()
核心概念拆解¶
do_simulation — 一步到位的仿真步进¶
这一行在 Euler 体系中等价于:
# 内部委托 self._gym 公共方法:
# _gym.step_with_coupling(ctrl, n_frames, dt)
# → _sim.set_ctrl(ctrl) + _sim.step(n_frames)
# _gym.sync_to_view()
# → data 自动同步为最新状态
关键优势:
do_simulation()返回后self.data已自动更新,无需手动update_data()。
self.data — 完整状态只读视图¶
中 self.data 是 OrcaGymDataView,提供零拷贝只读视图:
| 属性 | 含义 | 形状 |
|---|---|---|
self.data.qpos |
广义位置 | (nq,) |
self.data.qvel |
广义速度 | (nv,) |
self.data.qacc |
广义加速度 | (nv,) |
self.data.time |
仿真时间 | 标量 |
self.data.xfrc_applied |
外力(只读) | (nbody, 6) |
⚠️
self.data.qpos是零拷贝视图,直接读即可。若需保存历史值,调用.copy()。
self.model — 模型结构查询¶
| 属性/方法 | 含义 |
|---|---|
self.model.nq |
广义坐标维度 |
self.model.nv |
广义速度维度 |
self.model.nu |
执行器数量 |
self.model.body_name2id(name) |
Body 名称 → ID |
self.model.joint_name2id(name) |
Joint 名称 → ID |
self.model.get_joint_dict() |
所有关节信息字典 |
self.model.get_geom_dict() |
所有几何体信息字典 |
状态写入¶
修改状态后必须调用 mj_forward(),否则派生量(body 位姿、传感器等)不会更新:
# ✅ 正确写法
qpos = self.data.qpos.copy()
qpos[0] = 0.5 # 修改关节 0 的角度
self.set_joint_qpos(qpos) # 合规写入
self.mj_forward() # ← 必须!更新派生量
self._sync_view() # 同步到 DataView
# ❌ 错误:直接写 data.qpos(只读视图)
# self.data.qpos[0] = 0.5 # 违反封装契约
# ❌ 错误:缺少 mj_forward
# self.set_joint_qpos(qpos)
# # 此时读 body_xpos 会是旧值
reset_model 的标准范式¶
def reset_model(self):
qpos = self.init_qpos + self.np_random.uniform(-0.1, 0.1, self.model.nq)
qvel = self.init_qvel + self.np_random.uniform(-0.1, 0.1, self.model.nv)
self.set_joint_qpos(qpos) # 合规写入
self.set_joint_qvel(qvel) # 合规写入
self.mj_forward() # 更新派生量
self._sync_view() # 同步 DataView
return self._get_obs(), {}
self.init_qpos/self.init_qvel:父类在initialize_simulation()后缓存的初始状态self.np_random:父类通过set_seed_value()创建的随机数生成器
环境生命周期¶
MyFirstEnv(model_xml_path=..., skip_grpc_load=True)
└── OrcaGymEulerEnv.__init__()
├── initialize_grpc() # 离线模式 skip
├── pause_simulation()
├── set_time_step(time_step)
├── initialize_simulation() # 加载 model_xml → 创建 MuJoCo 实例
├── reset_simulation() # reset_data + sync_to_view
└── init_qpos_qvel() # 缓存 init_qpos / init_qvel
env.reset() [来自 OrcaGymEnvMixin]
├── reset_simulation() → 恢复初始状态
└── reset_model() → 你的自定义重置逻辑
env.step(action) ← 重复 N 次
├── do_simulation(ctrl, frame_skip) # 步进 + 自动 sync
├── _get_obs()
└── 返回 (obs, reward, terminated, truncated, info)
env.close()
└── 关闭 gRPC 通道(离线模式 no-op)
常见错误¶
| 错误 | 原因 | 解决 |
|---|---|---|
ValueError: Action dimension mismatch |
action.shape ≠ (nu,) |
检查 action.reshape(env.model.nu) |
| 观测数据"不对" | 没在 mj_forward() 后读 data |
reset_model 中确认调用了 mj_forward() |
| 观测全是 NaN | 在 mj_forward() 前读了派生量 |
用 do_simulation() 代替手动操作 |
AttributeError: 'OrcaGymEulerEnv' object has no attribute 'gym' |
用了 API | 没有 env.gym,用 env.data / env.model |
TypeError: step() returns 5 values |
旧代码只解包 4 个值 | Gymnasium 标准:obs, reward, terminated, truncated, info = env.step(action) |
下一步¶
环境类写好了。接下来学习如何读取更多状态信息:📡 读取状态。