A self-contained, docker-based MuJoCo simulator for the Booster Robotics K1 humanoid, used by
NUbots for RoboCup development. It replaces both Booster's gated Webots build
and their closed-source mck motion runner with one inspectable, NUClear-based simulator that speaks
the Booster SDK over FastDDS — the same wire protocol as the real robot — so NUbots_K1 binaries drive
it unchanged.
sim/soccer (docker container, NUClear)
MuJoCo physics + servo/mode machine + head camera + GLFW viewer + GameController supervisor
│ Booster SDK over FastDDS (domain 0) │ camera frames → shared memory
▼ ▼
NUbots_K1 (K1WalkPolicy / K1GetUpPolicy, behaviour) input::K1Camera → ImageCompressor → NUsight
▲
│ RoboCup GameController (UDP, direct)
- One process simulates the K1 body — physics, servos, mode machine and fall detection — while the
locomotion policies run on the NUbots side and drive it through CUSTOM mode +
rt/joint_ctrl, exactly as they drive the real robot. No Booster downloads, no separate motion runner. - Vision is rendered by MuJoCo and handed to NUbots' unchanged
input::K1Cameraover shared memory, soCompressedImagereaches NUsight exactly as on the real robot. - GameController is heard directly by NUbots over the network; the sim additionally runs a supervisor that places the ball/robots per game phase.
Requirements: Docker (everything else is baked into the image). Full setup, config reference, and troubleshooting are in docs/K1_MUJOCO_SETUP.md.
./b configure # configure the sim build
./b build # build the sim in docker (first run builds the image)
./b run sim/soccer # launch the soccer sim (viewer + DDS + camera + supervisor)Then drive it from NUbots_K1 exactly as against the real robot:
cd ~/NUbots_K1
./b run keyboardwalk
# e = walk on/off, w/s/a/d = velocity (0.01 m/s per press — tap ~10x), z/x = turn, arrows = headThe Booster SDK requires a FastDDS profiles file (participant profile booster_dds) or it refuses to
create its DDS participant (Failed to create participant). ./b run now defaults
FASTRTPS_DEFAULT_PROFILES_FILE to the repo's copy, so no flag is needed — see
Networking for the caveat if you pass --environment yourself.
The full autonomous stack works too — ./b run nusim/behaviour has the robot find the ball by vision and
dribble it goalward. See docs/K1_MUJOCO_SETUP.md for the end-to-end walkthrough
and the NUbots_K1-side requirements (skill::K1WalkPolicy + skill::K1GetUpPolicy in the role,
VisualMesh.yaml camera entry).
Older revisions of this README told you to mount an OpenVINO overlay over the image, because the NUbots_K1 image was built with
ENABLE_INTEL_CPU=OFFand had no CPU inference device at all (Device with "CPU" name is not registered). Fixed upstream — the image now shipslibopenvino_intel_cpu_plugin.so, and inference prefers TensorRT on the GPU anyway. No overlay, no--volume, noLD_LIBRARY_PATH.
./b wraps the container workflow; everything after the role name passes straight through to the
binary (argparse.REMAINDER), so sim flags are never parsed by ./b.
| Command | Description |
|---|---|
./b configure [-i] [--clean] [--set-role R] [--unset-role R] |
CMake-configure in docker; -i drops into ccmake, --clean wipes the build dir. |
./b build [targets...] |
Ninja build in docker (default: everything; e.g. ./b build sim-soccer). |
./b run <role> [args] |
Exec bin/<role> in the container. Only role today: sim/soccer. |
./b roles |
List sim roles and their enabled/disabled state. |
./b image |
(Re)build the docker toolchain image. |
Parsed in mujoco/shared/CliOptions.hpp; --help prints the same list.
| Flag | Default | Description |
|---|---|---|
--headless |
off | Run without the GLFW viewer window (CI / headless servers). Physics, DDS and the camera bridge all still run. |
--field <name> |
simulation.yaml's field (middle) |
Field to play on, one of simulation.yaml's fields: middle — the RoboCup 2026 Humanoid Soccer League M-Field (14 × 9 m), the field of the Middle Division the K1 plays in; kidsize — RoboCup KidSize under the pre-2026 rules (9 × 6 m). NUbots' FieldDescription.yaml field_type must match for localisation. |
--model <path> |
the --field scene |
MJCF scene to load, relative to mujoco/, overriding --field. Must be a scene (models/k1/k1_scene_robocup_middle.xml, models/k1/k1_scene_robocup.xml, models/k1/k1_scene_flat.xml) — the bare K1_22dof.xml is a component with no floor or lights, so the robot free-falls and the viewer renders black. |
--config-dir <dir> |
mujoco/config |
Config directory to read the YAML from. |
--keyframe <name> |
ready |
Startup keyframe for the main robot (e.g. lying_front to start fallen and exercise the get-up chain). |
--rtf <factor> |
simulation.yaml's real_time_factor |
Real-time factor; 0 = free-run (uncapped, for tests/sweeps). |
--robots <n> |
1 |
Total K1s on the field, 1–20. n−1 extra copies are attached via MuJoCo's mjSpec attach API with subNN_ name prefixes, so the main robot's unprefixed joint/sensor names — and every DDS and shared-memory contract — are untouched. Extras spawn standing on a 5×4 grid clear of the y = 0 main-robot/ball lane and are PD-held at the ready pose: uncontrolled obstacles for dribbling and navigation practice, not extra DDS endpoints. Sim resets re-place them; --keyframe does not apply to them. |
--help, -h |
— | Print the flag list and exit. |
./b run sim/soccer --headless # no viewer window (CI / server)
./b run sim/soccer --field kidsize # the KidSize field instead of the M-Field
./b run sim/soccer --model models/k1/k1_scene_flat.xml # bare robot on a flat floor, no field/ball
./b run sim/soccer --rtf 0 # free-run (uncapped real-time factor)
./b run sim/soccer --keyframe lying_front # start fallen, to exercise GetUp
./b run sim/soccer --robots 5 # 4 extra K1s on the field (max 20 total)| Variable | Effect |
|---|---|
FASTRTPS_DEFAULT_PROFILES_FILE |
Required by the Booster SDK (see Networking). ./b run defaults it to the repo's copy. |
K1SIM_CONFIG_DIR |
Config directory, same as --config-dir (the flag wins). |
K1_DDS_UDP_ONLY |
1 strips the FastDDS shared-memory transport, leaving UDPv4 only — the fallback when the sim and NUbots are on opposite sides of a docker boundary. Equivalent to udp_only: true in config/dds.yaml. |
Standard MuJoCo mouse camera (left-drag rotate, right-drag pan, scroll zoom, ctrl/shift modifiers for perturbation drags), plus:
| Key | Action |
|---|---|
Backspace |
Reset the sim to its startup state (and re-place --robots extras). |
F |
Shove the robot over — a deterministic topple for testing fall detection / GetUp, harder than a mouse-drag perturb, which the push-randomised policy usually rides out. |
Esc |
Close the window. |
Space is not a pause: physics stepping belongs to module::Simulation's 1 kHz thread and no pause
switch is exposed yet.
The sim speaks the Booster SDK wire protocol over FastDDS, so NUbots_K1 binaries connect to it with
no build flag, no shim and no code change — the same code path they use against the real robot.
- DDS domain 0, because NUbots'
platform::Booster::HardwareIOhardcodesChannelFactory::Init(0). Configurable inmujoco/config/dds.yaml, but changing it means changing NUbots too. - Transport is UDPv4 + shared memory by default.
./b rungives the container--network host --ipc hostprecisely so both survive the docker boundary; setK1_DDS_UDP_ONLY=1(orudp_only: true) if only UDP works in your setup. - Topics — sim publishes
rt/low_state(IMU + serial/parallel motor state, 50 Hz),rt/odometer_state,rt/fall_down(on change, ≥1 Hz keepalive) andrt/battery_state(constant SOC); sim subscribes tort/joint_ctrl(LowCmd, PD-tracked at 1 kHz in CUSTOM mode). The RPC pairrt/LocoApiTopicReq/rt/LocoApiTopicRespservesCHANGE_MODE,MOVE,ROTATE_HEAD,LIE_DOWN,GET_UP,GET_UP_WITH_MODE,VISUAL_KICKandGET_MODE; unimplementedapi_ids are accepted with a warning (unknown_api_statusindds.yaml). Full wire details inmujoco/module/SdkBridge/PROTOCOL.md. - FastDDS profiles are mandatory.
ChannelFactory::Init(0)refuses to create its participant unlessFASTRTPS_DEFAULT_PROFILES_FILEpoints at an XML containing a participant profile namedbooster_dds(Failed to create participant)../b runsets it for you. If you pass--environmentyourself, note it takes ONE comma-separated argument and replaces the default — re-include the FastDDS var or all DDS dies. - Vision is not DDS. Head-camera frames are rendered offscreen and written to a Boost.Interprocess
shared-memory segment (
_boostercamera_head_rgb, rgb8 640×480 @ 30 Hz) whose header matches NUbots'input::K1Camerabyte-for-byte; head pose goes to a second segment (_head_pose,K1Sensors"NBPO" layout) and is the only path torso tilt takes into NUbots, i.e. what makes fall detection and the get-up chain work. Segment names must matchK1Camera.yaml/K1Sensors.yamlon the NUbots side. Only the left camera is rendered —K1Camerawarn-retries harmlessly on the right one; stereo is future work.
This is the RoboCup GameController (the UDP match-control broadcast), not a USB gamepad — there is no
joypad/SDL input path in NUSim, and manual driving is done from NUbots_K1's keyboardwalk role.
- NUbots hears the GameController itself, directly and independently over its own socket. That path needs nothing from the sim.
- The sim additionally ships a supervisor (
mujoco/module/Supervisor) that listens to the same broadcast and moves MuJoCo bodies per game phase — ball to the centre circle on kickoff, penalised robots to the sideline — the job Webots' Supervisor role used to do. It parses RoboCup GameControlData protocol version 20 (RGmeheader), receive-only: it never sends a reply. - It is OFF by default (
enabled: falseinmujoco/config/supervisor.yaml). With--network host, NUbots binds GameController port 3838 on the same host; a second binder steals it and breaks NUbots withUnable to bind the UDP socket: Address already in use. Enable the supervisor only when running the sim standalone, with no NUbots GameController on the host. - With no GameController on the network it idles silently — no errors either way.
- Ball placement, per-robot team/player mapping, home and penalty poses, and which events reset the ball
(
finished,goal,half_change) are all configurable insupervisor.yaml. Exact RoboCup per-player penalty spacing is not implemented — the penalty spot is a single configurable pose.
NUSim does one thing: simulate. It neither trains nor runs locomotion policies: the sim is a
servo-command listener (CUSTOM mode + rt/joint_ctrl LowCmd, PD-tracked at 1 kHz), and inference runs on
the NUbots side (NUbots_K1 module/skill/K1WalkPolicy and module/skill/K1GetUpPolicy, OpenVINO,
50 Hz). Policies are trained in the NUbots
mujoco_playground fork (branch feat/k1-training,
K1JoystickFlatTerrain / K1Getup tasks) and exported to ONNX with learning/export_k1_onnx.py. The
walk observation/action interface is pinned in
docs/OBS_ACTION_CONTRACT.md — anything that trains a walk policy for
the K1 must match it.
| Path | Description |
|---|---|
b, mujoco/b.py, mujoco/tools/ |
NUbots-style ./b command dispatcher (run, build, configure, roles, image). |
mujoco/roles/ |
Role files (sim/soccer.role) → bin/<role> binaries. |
mujoco/module/ |
NUClear modules: Simulation, SdkBridge (DDS), Locomotion, Camera, Supervisor, Viewer. |
mujoco/models/k1/ |
Vendored MuJoCo K1 model (BSD-3, booster_assets) + RoboCup/flat scenes. |
mujoco/docker/ |
Toolchain image + k1sim.sh (the container workflow ./b wraps). |
docs/K1_MUJOCO_SETUP.md |
Setup, config reference, end-to-end with NUbots_K1, and troubleshooting. |
docs/OBS_ACTION_CONTRACT.md |
ONNX policy interface contract (obs/action layout the sim expects). |
mujoco/module/SdkBridge/PROTOCOL.md |
Booster SDK DDS wire surface: topics, message layouts, RPC api_ids. |
Forked from NUWebots; the Webots/NUgus simulation has been removed in favour of the MuJoCo path (see git history if you need it).