Simulate before you build
Write a controller in the browser simulator, then learn what the real world adds.
Day 10 · 60 min
After this lesson
- You can write and test a controller in the robot simulator lab.
- You can explain the sim-to-real gap and how to design around it.
- You can name the professional simulators and what each is for.
Why simulate, and what simulation gets wrong
Why simulate first
In the browser lab a run takes seconds and resets instantly. On the real robot each test costs a battery, a reset, a rescue from under the sofa and now and then a broken servo horn. A simulator also makes runs repeatable: the same start pose and the same code give the same result, so two controllers can be compared fairly and every bug can become a regression test. Industry leans on this hard. Waymo reported more than 20 billion simulated miles by 2021, roughly a thousand for every mile driven on public roads, and Rudin and colleagues trained an ANYmal quadruped to walk in under 20 minutes on one GPU by simulating 4,096 robots in parallel in NVIDIA Isaac Gym.
The sim-to-real gap
A simulator is a model, and every model leaves something out. The usual suspects:
- Friction and slip. Simulated wheels never slip. Real tires grip tile and carpet very differently and cannot deliver accelerations the motors can.
- Motor dynamics. In simulation speed jumps to the command. A real N20 takes 50 to 100 ms to respond and does not turn at all below roughly 15% duty.
- Noise and dropouts. An HC-SR04 jitters by about a centimeter, misses echoes from soft or angled surfaces and measures only every 60 ms. Reflectance sensors drift with sunlight, which is full of infrared.
- Sensor geometry. A simulated range sensor is a thin ray. A real ultrasonic beam is a cone roughly 30° wide that reports the nearest thing anywhere inside it.
- Latency. Sensing, filtering, computing and the motors' response add up to 100 to 200 ms. At 40 cm/s, 150 ms is 6 cm driven blind.
- Power. A sagging battery slows the motors and, as lesson 26 showed, can reset the brain.
What the lab models
The robot simulator lab models the course robot as a differential drive with a 12 cm wheel base, a 7 cm radius and a top wheel speed of 40 cm/s, integrated exactly along arcs. It has three ultrasonic rays at +45°, 0° and −45°, measured from the robot's edge to the first wall or obstacle (the canvas draws rays beyond the 150 cm rated range in red as open, but the readings go all the way to the far wall); two line sensors 6 cm ahead and 5 cm apart; wheel encoders at 10 ticks per cm; and a yaw rate. Touching a wall or an obstacle stops the robot and sets crashed. It deliberately leaves out noise, cones, latency, slip and motor lag, so you learn the logic first; margins for the real world are your job.
Structure the controller: read, estimate, decide, act
Every tick does four things. Read: copy the sensor values. Estimate: filter them into a belief about the world, such as a median of the last three ranges, the time since the line was last seen, or a pose integrated from the encoders. Decide: a state machine picks a behavior. Act: the behavior's desired speed and turn rate become wheel commands, clamped to −1…1. Keep each step a small function and all state in the lab's memory object. The same shape ports straight to the firmware in lesson 26, where control(sensors, memory) becomes controlStep().
Testing and logging
Turn behaviors into scenarios with pass criteria: no crash in 60 s, a lap in under 45 s, never closer than 5 cm to a wall. Then randomize: start pose ±5 cm and ±10°, sensor noise, latency. A controller that passes 100 of 100 randomized runs is far more likely to survive the floor than one tuned on a single perfect run. Log every tick (time, sensors, state, commands) as CSV with the same columns the robot's telemetry uses, so you can overlay a simulated run and a real one and see exactly where they diverge.
Professional simulators
- Gazebo (Open Robotics): the standard ROS 2 companion. It loads URDF and SDF models and simulates lidar, cameras and IMUs; Gazebo Classic reached end of life in January 2025 in favor of the new Gazebo releases such as Harmonic.
- Webots (Cyberbotics): open source since 2018, with a large library of ready robot models; popular in teaching.
- MuJoCo (DeepMind, open-sourced in 2022): fast, accurate contact dynamics; the standard for reinforcement learning on legs and hands.
- NVIDIA Isaac Sim: PhysX physics and RTX ray-traced cameras and lidar for photorealistic synthetic data, with Isaac Lab for massively parallel RL.
- PyBullet: Python bindings for the Bullet engine, quick to script for research prototypes.
URDF basics
ROS tools and most simulators describe a robot with URDF, an XML tree of link elements (visual, collision and inertial properties) joined by joint elements (fixed, continuous, revolute or prismatic), each with a parent, a child, an origin in meters and radians and an axis. The course robot is a base_link, two continuous wheel joints 6 cm either side of center and fixed joints for the caster and sensors. Gazebo extends it with SDF, MuJoCo uses its own MJCF format, and xacro macros keep the left and right wheels from being written twice.
<robot name="course_robot">
<link name="base_link">
<visual><geometry><cylinder radius="0.07" length="0.04"/></geometry></visual>
<collision><geometry><cylinder radius="0.07" length="0.04"/></geometry></collision>
<inertial><mass value="0.45"/><inertia ixx="6e-4" iyy="6e-4" izz="1.1e-3" ixy="0" ixz="0" iyz="0"/></inertial>
</link>
<link name="left_wheel"><visual><geometry><cylinder radius="0.0215" length="0.012"/></geometry></visual></link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/><child link="left_wheel"/>
<origin xyz="0 0.06 0" rpy="-1.5708 0 0"/> <!-- 6 cm to the left: wheel base 12 cm -->
<axis xyz="0 0 1"/>
</joint>
<!-- right_wheel: the same with y = -0.06 -->
<link name="sonar_link"/>
<joint name="sonar_joint" type="fixed">
<parent link="base_link"/><child link="sonar_link"/>
<origin xyz="0.07 0 0.08" rpy="0 0 0"/> <!-- front edge, 8 cm up -->
</joint>
</robot>Interactive: tour the arenas, then open the lab
The simulator's three arenas
The lab's own arenas and sensor model: the readings below are exactly what your controller would get at the robot's pose. Here the robot follows a scripted path; in the lab, your code drives it.
Three round obstacles in an empty room: practice obstacle avoidance and wall following.
sensors.ultrasonic: left 168.8 cm (open) · front 52.1 cm · right 61.9 cm
Rays beyond the 150 cm rated range are drawn red, as in the lab; their readings still run to the far wall.
A live copy of the simulator running the obstacle-avoid preset in the open room. Press Run, watch the rays, then open the full lab to edit the controller.
Robot simulator
Obstacle avoid: Cruise, slow down when something is ahead, and turn away from the nearest short ray.
Ready. Press Run.
Lab goal: 0 / 100 cm without a crash
Ready to drive it yourself?
The lab runs your JavaScript control(sensors, memory) function 100 times a second against the same three arenas, with telemetry and the sensor rays drawn live.
In practice
Practice
Obstacle avoidance, then line following, then a lap without a crash
- Open the robot simulator lab, choose the open room and write an obstacle avoider. Pass: 60 s without a crash.
- Switch to the line track and implement the line-following state machine from lesson 25. Pass: one full lap without leaving the line for more than 1 s and without a crash.
- Make it realistic: put your avoider behind the latency wrapper below (300 ms) and press Run five or six times; every run starts from a random heading. Then fix the controller without removing the latency.
Worked answer, part 1. Braitenberg-style steering on the difference between the side rays, plus an escape spin that commits to one direction with hysteresis, so the robot neither stalls between two obstacles nor dithers in corners:
// Part 1 - obstacle avoidance for the open-room arena.
// sensors.ultrasonic.{left, front, right}: cm from the robot's edge to the first wall or obstacle along each ray.
// Return wheel commands in -1..1 (1 = 40 cm/s).
function control(sensors, memory) {
const { left, front, right } = sensors.ultrasonic;
const p = (d) => Math.max(0, 1 - d / 60); // proximity: 0 beyond 60 cm, 1 at contact
// Blocked ahead: spin toward the more open side. Pick the side once and keep it until the front
// reads more than 35 cm (hysteresis: in at 25, out at 35), so the robot cannot flip back and forth.
if (front < 25 || (memory.spin && front < 35)) {
if (!memory.spin) memory.spin = left > right ? 1 : -1; // +1 = turn left
return { left: -0.5 * memory.spin, right: 0.5 * memory.spin };
}
memory.spin = 0;
// Cruise, slow down when something is ahead, and steer away from the closer side. Steering on
// the difference keeps the robot moving when both sides are equally close, instead of stalling.
const base = front < 60 ? 0.35 : 0.6;
const turn = 0.6 * (p(right) - p(left)); // obstacle on the right: turn left
return { left: base - turn, right: base + turn };
}Part 2 is the state machine from lesson 25, unchanged: follow with P control on the line error, recover toward the side the line was last seen, stop after 3 s of searching. If it oscillates on the straights, lower the steering gain; if it loses the line on the corners, lower the base speed first.
// Part 3 - run a controller on delayed sensors. Rename your control() to avoid() and paste it above this.
// The lab steps at 100 Hz, so the delay is LATENCY_MS / 10 ticks. Each run first spins for a random
// time, so every press of Run starts from a different heading: count crashes over several runs.
const LATENCY_MS = 300;
function control(sensors, memory) {
if (memory.spinFor === undefined) memory.spinFor = Math.random() * 1.9; // up to one full turn
if (sensors.t < memory.spinFor) return { left: -0.5, right: 0.5 };
memory.queue = memory.queue || [];
memory.queue.push(JSON.parse(JSON.stringify(sensors)));
const delayed = memory.queue.length > LATENCY_MS / 10 ? memory.queue.shift() : memory.queue[0];
return avoid(delayed, memory);
}Part 3, what you should see. At 0 and 150 ms nothing visible changes. At 300 ms roughly one run in four ends with Crashed: yes within a minute (270 of 1,000 randomized runs in our tests), and always the same way: the robot cruises toward the small obstacle near the top-right corner, and steering that reacts 0.3 s late cannot get it clear. The escape spins overshoot too: at a command of 0.5 the robot turns at 191°/s, so it keeps spinning about 57° past the gap it saw. Pause and step through a spin while watching θ in the telemetry to see it.
The fix follows the margin rule: every gain inside a delayed loop has to shrink, and every margin has to grow with speed × delay. Slow the cruise from 0.6 to 0.4, halve the spin rate and the steering gain, and widen the thresholds:
// Part 3 answer - the Part 1 avoider retuned for 300 ms of latency (keep the wrapper above it).
function avoid(sensors, memory) {
const { left, front, right } = sensors.ultrasonic;
const p = (d) => Math.max(0, 1 - d / 70); // react from 70 cm instead of 60
if (front < 30 || (memory.spin && front < 45)) { // wider margins: in at 30, out at 45
if (!memory.spin) memory.spin = left > right ? 1 : -1;
return { left: -0.25 * memory.spin, right: 0.25 * memory.spin }; // half the spin rate
}
memory.spin = 0;
const base = front < 70 ? 0.25 : 0.4; // two thirds of the speed
const turn = 0.3 * (p(right) - p(left)); // half the steering gain
return { left: base - turn, right: base + turn };
}In our tests this version did not crash once in 1,000 randomized runs at each of 0, 150, 300 and 500 ms. One surprise is worth knowing: the Part 1 answer also survives 500 ms from this start, because whether a late reaction hits something depends on where the robot happens to be when it is late. That is exactly why teams randomize latency over a range instead of testing one value.
Real-world scenario
Real-world scenario
Perfect in simulation, crashes on the real floor
A team's wall-following controller passed 200 out of 200 runs in their simulator at 40 cm/s with a 15 cm front threshold. On the real robot, in the office hallway, it clips the wall at about one corner in three and has twice driven into the base of a whiteboard stand.
Symptoms
- The real robot starts its corner turn 6 to 9 cm later than the simulated robot does
- Logs show the front range updating only every 60 ms, with occasional 400 cm readings next to a wall angled at 45°
- At 20 cm/s the real robot almost never touches a wall
- The thin metal legs of the whiteboard stand are invisible to it until it is too late
Interview questions
screenWhat is the sim-to-real gap? Give three concrete sources.
screenName three robotics simulators and say what each is best for.
screenWhat is URDF, and what goes into one?
deepYour robot must stop before obstacles at 0.5 m/s. How do you choose the detection threshold?
deepHow would you use domain randomization so a controller trained or tuned in simulation works on hardware?
Quiz
References and further reading
Where the ideas in this lesson come from, and what to read next. Books are the standard texts; papers are the original sources; standards are what employers work to.
- Paper
Read: Why randomizing the simulator makes controllers transfer.
- Bookfree online
Read: The simulation exercises.
- Book
Sebastian Thrun, Wolfram Burgard and Dieter Fox (2005). Probabilistic Robotics. MIT Press.
Read: The motion and measurement model chapters, which are what a simulator implements.