MuJoCo 的 Python 绑定是低层 API 风格,和底层 C API 很接近。
基本入口
import mujocom = mujoco.MjModel.from_xml_path("pendulum.xml")d = mujoco.MjData(m)
你也可以直接从字符串加载:
m = mujoco.MjModel.from_xml_string(xml_text)d = mujoco.MjData(m)
为什么 MjData 能读到东西
MjData 不是随便生成的空对象,而是和某个 MjModel 绑定在一起的运行时数据容器。
当你写 d = mujoco.MjData(m) 时,MuJoCo 会根据 m 里定义好的模型结构,提前分配好各种数组和缓存。模型里有多少关节、多少自由度、多少电机、多少传感器,d 里就会有对应大小的空间。
所以 d 之所以“能读到”,不是因为它天生知道答案,而是因为:
- 模型
m 已经告诉了 MuJoCo 这个系统长什么样 mj_forward 或 mj_step 会把当前状态和派生量算出来
以摆杆模型为例
还是用上一章的 simple.xml 来看。这个模型里有一个铰链关节 hinge,一个电机 hinge_motor,还有一个末端标记点 tip。
import mujocom = mujoco.MjModel.from_xml_path("simple.xml")d = mujoco.MjData(m)mujoco.mj_forward(m, d)print("nq, nv, nu:", m.nq, m.nv, m.nu)print("qpos:", d.qpos)print("qvel:", d.qvel)print("ctrl:", d.ctrl)print("hinge angle:", d.joint("hinge").qpos)print("tip position:", d.site("tip").xpos)
在这个例子里,你通常会读到这些信息:
m.nq、m.nv、m.nu:这个模型一共有多少位置自由度、速度自由度、控制输入d.qpos:当前关节位置。对这个摆杆来说,主要就是摆角d.joint("hinge").qpos:按名字直接读关节状态d.site("tip").xpos:摆杆末端 tip 在世界坐标系里的位置
如果以后模型里加了传感器,还可以读 d.sensordata;如果出现碰撞,还能看接触和约束相关信息。也就是说,MjData 里不仅有“状态”,还有很多仿真过程中算出来的中间结果。
你会最常碰到的对象
模型和数据的分工
MjData:当前位置、速度、控制量、传感器读数、仿真时间
简单说,m 更像“说明书”,d 更像“当前状态”。
读状态
print(d.time)print(d.qpos)print(d.qvel)print(d.ctrl)
常见的状态量包括:
这几个字段里,qpos 和 qvel 是最基础的状态。像 site("tip").xpos、body("pole").xpos 这种位置量,通常是根据当前状态再计算出来的,所以刚创建 d 后,最好先调用一次 mj_forward。
命名访问
MuJoCo 很推荐你按名字读对象:
print(m.geom("floor").rgba)print(d.joint("hinge").qpos)print(d.site("tip").xpos)
这比你自己记编号舒服得多。
名字和编号
如果你需要底层编号,也可以用:
idx = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_GEOM, "floor")
一般来说,学习阶段优先用“按名字访问”。
记住一个坑
很多字段返回的是视图,不是拷贝。
xs = []for _ in range(10): mujoco.mj_step(m, d) xs.append(d.body("pole").xpos.copy())
如果不 .copy(),你很可能收集到一堆被后续步骤覆盖掉的同一块内存。
常用函数的区别
mj_forward:根据当前状态刷新派生量。你手动改了 qpos 或 qvel 后,通常要先调用它mj_step:推进一步物理仿真,后面会专门讲仿真循环mj_resetData:把 d 重置到初始状态,常用于重新开始一局仿真,后面也会再展开
先记住一个直觉就够了:forward 是“刷新”,step 是“往前走一步”,resetData 是“回到初始状态”。