Robotics Simulation: Gazebo & Webots Setup for 2026

Listen to this article · 20 min listen

Building and testing robots in the real world is a nightmare of cost, safety risks, and painfully slow iteration. That’s why robotics simulation software is a non-negotiable part of my toolkit. It gives you a virtual sandbox to design, test, and tune robot behaviors before you ever touch a piece of hardware. Being able to quickly throw complex algorithms and hardware designs into a controlled digital space just radically cuts down development time and risk. So, how do you actually get started with the big players like Gazebo and Webots for your own work?

Key Takeaways

  • To get Gazebo running on Ubuntu 22.04 for a ROS 2 Humble setup, add its repo and then run sudo apt-get install ros-humble-gazebo-ros-pkgs.
  • For Webots, you’ll grab the installer (like webots-R2026b_amd64.deb) for your OS and make sure Python 3.10+ is your default interpreter so you can write controllers.
  • You can build a simple robot model in Gazebo with URDF or SDF, defining your links and joints with their specific inertial properties and visual shapes.
  • Writing a basic Python controller in Webots involves getting sensor handles with robot.getDevice('sensor_name') and driving motors with motor.setVelocity().
  • To make ROS 2 talk to either simulator, use the gazebo_ros_pkgs for Gazebo or the webots_ros2 package for Webots. This is how you pass messages and call services between your code and the sim.

1. Setting Up Your Development Environment: Gazebo Installation

You need a stable sim environment before you even think about designing a robot. For Gazebo, which I almost always use with ROS, the setup on Ubuntu is pretty painless. I always stick with the latest LTS release (Ubuntu 22.04 right now) for the best compatibility and support, and the steps are built right in if you’re using ROS 2 Humble.

First, get your system updated: sudo apt update && sudo apt upgrade -y. Then, you add the Gazebo repository key. This just authenticates the packages so you don’t install something sketchy. Run sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" > /etc/apt/sources.list.d/gazebo-stable.list'. After that, grab the public key: wget http://packages.osrfoundation.org/gazebo.key -O - | sudo apt-key add -.

Once the repo is set up, update your package list one more time with sudo apt update. Now you can install Gazebo and all its ROS 2 integration bits. This is the part that matters, because it installs the bridge packages that let your ROS nodes actually talk to the simulation. Run this command: sudo apt-get install ros-humble-gazebo-ros-pkgs -y. This pulls down Gazebo 11 (the classic version everyone still uses with ROS 2) and all the bridge packages you’ll need. Check that it worked by typing gazebo in your terminal. You should see a GUI window pop up with an empty world. If it fails, your environment variables like GAZEBO_MODEL_PATH are probably messed up, though the ROS installer is supposed to handle that for you.

Pro Tip: Always, always install Gazebo through the ROS metapackages if you’re using ROS. Don’t try to install Gazebo on its own and then bolt on ROS later. It’s a recipe for version conflicts and hours of debugging hell.

2. Setting Up Your Development Environment: Webots Installation

Webots is different. It’s a standalone app with its own IDE, so the install process feels less integrated. It runs on Windows, macOS, and Linux. For Ubuntu 22.04, you just download the .deb package from the official Cyberbotics website. The current version might be something like webots-R2026b_amd64.deb in a few years, so grab that and save it to your ~/Downloads folder.

Once it’s downloaded, pop open a terminal and get to that directory: cd ~/Downloads. Install it with sudo dpkg -i webots-R2026b_amd64.deb. You’ll probably hit some dependency errors, that’s normal for .deb files, so just run sudo apt-get install -f to have apt go fetch and install whatever’s missing. After that, you can launch Webots from your application menu or just by typing webots in the terminal.

Webots pretty much runs on Python for controllers, so make sure you have Python 3.10 or a newer version installed and set as the default. You can check with python3, version. If you’re juggling multiple Python versions, a tool like pyenv is a lifesaver. Webots can also use C++, Java, or MATLAB for controllers, but Python is honestly the fastest way to get a prototype up and running.

Common Mistake: Forgetting to point Webots to the right Python interpreter. Go to Tools > Preferences > Python and make sure the path is correct. If Webots can’t find your Python installation, your controllers just… won’t run.

3. Creating a Basic Robot Model in Gazebo

Gazebo works with two main file formats for robots: URDF (Unified Robot Description Format) and SDF (Simulation Description Format). URDF is great for defining a single robot’s physical structure, but SDF is the real powerhouse, letting you describe an entire world with multiple robots, lights, and physics properties like gravity. For a simple mobile robot, though, URDF is more than enough and a lot easier to get started with.

Let’s make a basic two-wheeled robot. First, make a new ROS 2 package: ros2 pkg create my_robot_description, build-type ament_cmake. Inside, create a urdf folder. Now create a file at my_robot_description/urdf/my_robot.urdf to define the robot. A minimal URDF for a simple wheeled robot needs a base_link for the chassis, two more links for the wheels, and the joints to connect them. For example:


<robot name="my_diff_robot"> <link name="base_link"> <visual> <geometry><box size="0.2 0.15 0.05"/></geometry> <material name="blue"><color rgba="0 0 1 1"/></material> </visual> <collision> <geometry><box size="0.2 0.15 0.05"/></geometry> </collision> <inertial> <mass value="1.0"/> <inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/> </inertial> </link> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel_link"/> <origin xyz="0.0 -0.08 -0.025" rpy="1.57079632679 0 0"/> <axis xyz="0 0 1"/> </joint> <link name="left_wheel_link"> <visual> <geometry><cylinder radius="0.03" length="0.02"/></geometry> <material name="black"><color rgba="0 0 0 1"/></material> </visual> <collision> <geometry><cylinder radius="0.03" length="0.02"/></geometry> </collision> <inertial> <mass value="0.1"/> <inertia ixx="0.0001" ixy="0.0" ixz="0.0" iyy="0.0001" iyz="0.0" izz="0.0001"/> </inertial> </link> <!, Similar definition for right_wheel_link and right_wheel_joint, >
</robot>

Each link is a rigid body with a visual mesh, a collision mesh, and inertial properties (mass and inertia tensor). If you want realistic physics, your inertial properties have to be accurate, garbage in, garbage out. Joints connect the links and define how they move. The visual geometry is for display, but the collision geometry is what the physics engine actually interacts with. It’s usually a simplified version of the visual mesh to save on computation.

To get this robot into Gazebo, you’ll write a ROS 2 launch file that uses the spawn_entity.py script from gazebo_ros. Put this in my_robot_description/launch/spawn_robot.launch.py:


import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node def generate_launch_description(): pkg_path = get_package_share_directory('my_robot_description') urdf_file = os.path.join(pkg_path, 'urdf', 'my_robot.urdf') with open(urdf_file, 'r') as infp: robot_desc = infp.read() return LaunchDescription([ IncludeLaunchDescription( PythonLaunchDescriptionSource([os.path.join( get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')]), launch_arguments={'world': 'empty.world'}.items(), ), Node( package='robot_state_publisher', executable='robot_state_publisher', name='robot_state_publisher', output='screen', parameters=[{'robot_description': robot_desc}], ), Node( package='gazebo_ros', executable='spawn_entity.py', arguments=['-topic', 'robot_description', '-entity', 'my_diff_robot'], output='screen' ) ])

This launch file fires up Gazebo with an empty world, runs the robot_state_publisher to broadcast your robot’s TF frames, and then uses spawn_entity.py to drop your model into the sim. Run it with ros2 launch my_robot_description spawn_robot.launch.py.

Pro Tip: Visualize your URDF in RViz *before* you try to spawn it in Gazebo. Seriously. It’ll save you so much time catching bad joint origins or messed-up geometries. Use this command: ros2 launch urdf_tutorial display.launch.py model:=$(ros2 pkg prefix my_robot_description)/share/my_robot_description/urdf/my_robot.urdf.

4. Creating a Basic Robot Model in Webots

Webots has its own scene format that you edit directly in the GUI, which makes building a model pretty straightforward for beginners. Just open Webots and go to File > New Project Directory to start a project. Then go to File > New Robot to get a wizard that will walk you through creating a basic bot, like from a “Wheeled Robot” template.

The scene tree on the left side shows you all the nodes in the world. A node can be a Solid (a rigid body), a Shape (its visual geometry), a Joint, or various sensors and actuators like a DistanceSensor or Motor. You just drag and drop nodes from the “Add Node” menu to build up your robot. For a wheel, you’d add a Solid node, give it a Shape child (like a Cylinder) to make it visible, and then add a Motor node to make it spin.

For a differential drive robot, you start with a main `Solid` for the body. For each wheel, you add a `HingeJoint` connected to the body, and inside that joint, another `Solid` for the wheel itself. Just make sure every wheel `Solid` has a `Motor` node as a child. Give that motor a name (like “left wheel motor”) because you’ll need that exact name to control it from your code. Do the same for any sensors you add, giving them all unique names.

Webots can also import CAD models like URDF, OBJ, and STL files (File > Import > URDF file). Importing models is convenient, but you’ll almost always have to manually tweak joint limits, motor definitions, and sensor placements to get them working right in the Webots physics engine. For anything complex, I’ve had the best results with a hybrid approach: import the base geometry, then manually add the Webots-specific nodes like motors and sensors.

Common Mistake: Not setting the name field for your motors and sensors. If you don’t give them unique names, your controller code won’t be able to find the right device, and you’ll just get runtime errors.

5. Developing a Simple Controller in Webots (Python)

Webots controllers are just separate programs that talk to your simulated robot. Let’s write a simple Python controller to make our robot drive forward. In your Webots project, right-click the robot node in the scene tree and pick Add new > Robot Controller. Choose Python and name it something like my_robot_controller.

This makes a new my_robot_controller.py file in your project’s controllers directory. Open it up. The structure is always the same: initialize the robot, grab the device handles you need, and then run a main loop that reads sensors and sends actuator commands. Here’s a bare-bones example:


from controller import Robot # Create the Robot instance.
robot = Robot() # Get the time step of the current world.
timestep = int(robot.getBasicTimeStep()) # Get motor devices
left_motor = robot.getDevice('left wheel motor') # Use the name you set in Webots
right_motor = robot.getDevice('right wheel motor') # Set motor velocity mode
left_motor.setPosition(float('inf')) # Set to velocity control
right_motor.setPosition(float('inf'))
left_motor.setVelocity(0.0)
right_motor.setVelocity(0.0) # Main loop:
# Perform simulation steps until Webots is terminated.
while robot.step(timestep) != -1: # Read the sensors: # (e.g., distance_sensor_value = distance_sensor.getValue()) # Process sensor data here. # Set motor velocities to drive forward left_motor.setVelocity(5.0) # Radians per second right_motor.setVelocity(5.0) # You can add logic here to change direction, avoid obstacles, etc.

You use robot.getDevice('device_name') to get a handle for any motor, sensor, or other device. For motors, you call setPosition(float('inf')) to switch them to velocity control mode, and then you can use setVelocity(speed) to set their speed in radians per second. The robot.step(timestep) call is what actually moves the simulation forward one step, and you have to call it inside your loop. If you forget it, the simulation will just freeze.

To get this controller running, save the file. Back in Webots, select your robot node, and find the controller field in its properties panel. Set it to my_robot_controller. Now click the “Run” button in the toolbar, and your robot should start moving.

Pro Tip: Use the Webots console (View > Console) for debugging. Any standard print() statement in your Python controller will show up there, which is a lifesaver for figuring out what your code is actually doing.

6. Integrating ROS 2 with Gazebo

The main way to get ROS 2 and Gazebo talking is with the gazebo_ros_pkgs. These packages have plugins that let Gazebo publish sensor data on ROS 2 topics and listen for command topics for your actuators. For our two-wheeled robot, we need a plugin for the differential drive and maybe for publishing odometry.

First, you have to modify your my_robot.urdf file to include the diff_drive_controller plugin. This plugin comes from ros2_control and integrates nicely with Gazebo. Add this block inside your <robot> tags:

 <gazebo> <plugin name="gazebo_ros2_control" filename="libgazebo_ros2_control.so"> <parameters>$(find my_robot_description)/config/my_robot_controllers.yaml</parameters> </plugin> </gazebo>

This just tells Gazebo to load the gazebo_ros2_control plugin and find your controller configs in the specified YAML file. So, now you need to create that file at my_robot_description/config/my_robot_controllers.yaml:


controller_manager: ros__parameters: update_rate: 100 # Hz diff_drive_controller: type: diff_drive_controller/DiffDriveController joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster diff_drive_controller: ros__parameters: left_wheel_name: 'left_wheel_joint' right_wheel_name: 'right_wheel_joint' wheel_separation: 0.16 # meters wheel_radius: 0.03 # meters # Odometry topic publish_odom_tf: true odom_frame_id: 'odom' base_frame_id: 'base_link' pose_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.01] twist_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.01] # Velocity and acceleration limits linear: x: has_velocity_limits: true max_velocity: 0.5 has_acceleration_limits: true max_acceleration: 1.0 angular: z: has_velocity_limits: true max_velocity: 1.0 has_acceleration_limits: true max_acceleration: 2.0

This YAML sets up the diff_drive_controller to manage your wheels and publish odometry data. It maps the controller to the joint names from your URDF. You also need to add <ros2_control> tags in your URDF to define the hardware interface for the joints:

 <ros2_control name="GazeboSystem" type="system"> <hardware> <plugin>gazebo_ros2_control/GazeboSystem</plugin> </hardware> <joint name="left_wheel_joint"> <command_interface name="velocity"> <param name="min">-10</param> <param name="max">10</param> </command_interface> <state_interface name="velocity"/> <state_interface name="position"/> </joint> <joint name="right_wheel_joint"> <command_interface name="velocity"> <param name="min">-10</param> <param name="max">10</param> </command_interface> <state_interface name="velocity"/> <state_interface name="position"/> </joint> </ros2_control>

Last step, you have to tell your launch file to actually load and start these controllers:

 Node( package='controller_manager', executable='spawner', arguments=['diff_drive_controller', ', ros-args', ', node-name', 'diff_drive_spawner'], output='screen', ), Node( package='controller_manager', executable='spawner', arguments=['joint_state_broadcaster', ', ros-args', ', node-name', 'joint_state_broadcaster_spawner'], output='screen', ),

Once you launch everything, you can drive the robot around by publishing to the /diff_drive_controller/cmd_vel topic. For instance: ros2 topic pub /diff_drive_controller/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'. This command makes the robot move forward at 0.2 m/s.

Common Mistake: Mismatching your joint names. The names in the URDF, the YAML config file, and the <ros2_control> block must all be *exactly* the same, or the controller won’t know which joints to control.

7. Integrating ROS 2 with Webots

Webots has really good ROS 2 support through the webots_ros2 package. This package lets you control Webots robots with ROS 2 messages and services while publishing sensor data back out to ROS 2 topics. First, install the package. On ROS 2 Humble, you can just do sudo apt install ros-humble-webots-ros2. Or you can clone the repo and build from source if you need newer features.

To get your Webots robot working with ROS 2, you’ll make a ROS 2 launch file that starts Webots and loads your robot. The webots_ros2 package has a nice helper for this. Create a new ROS 2 package (e.g., my_webots_robot) and put a launch file inside it, like my_webots_robot/launch/robot_launch.py:


import os
import launch
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
from webots_ros2_driver.webots_launcher import WebotsLauncher def generate_launch_description(): package_dir = get_package_share_directory('my_webots_robot') robot_description_path = os.path.join(package_dir, 'resource', 'my_robot.urdf') # If using URDF webots = WebotsLauncher( world=os.path.join(package_dir, 'worlds', 'my_robot_world.wbt') ) robot_driver = Node( package='webots_ros2_driver', executable='webots_ros2_driver', output='screen', parameters=[ {'robot_description': robot_description_path}, # If using URDF {'use_sim_time': True}, {'robot_name': 'my_webots_robot'}, # Name of the robot node in Webots {'set_robot_state_publisher': True}, {'joint_state_topic': 'joint_states'}, {'joint_state_publish_rate': 50.0}, ], remappings=[ ('/cmd_vel', '/my_robot/cmd_vel'), # Remap command velocity topic ] ) return LaunchDescription([ webots, robot_driver, launch.actions.RegisterEventHandler( event_handler=launch.event_handlers.OnProcessExit( target_action=webots, on_exit=[launch.actions.EmitEvent(event=launch.events.Shutdown())], ) ) ])

Back in your Webots project, find your robot’s controller field and set it to <extern>. This tells Webots to hand over control to an external program, which in our case is the ROS 2 driver. The robot_driver node from the webots_ros2_driver package handles all the communication. The driver node connects to the Webots sim, reads the robot’s sensor data, publishes it all to ROS 2 topics, and listens for command topics like `/cmd_vel` to drive the actuators. You just have to make sure the robot_name parameter matches the name you gave your `Robot` node in the Webots world file.

After that, you can drive the robot with a standard ROS 2 command: ros2 topic pub /my_robot/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'. The webots_ros2_driver is smart enough to automatically map that `cmd_vel` message to the right motor commands for a diff drive robot, as long as you named your motors correctly in Webots (like “left wheel motor” and “right wheel motor”).

Pro Tip: On more complex robots, I often write a custom supervisor controller in Webots itself (in Python or C++) to handle the really low-level stuff. That supervisor then communicates with ROS 2 at a higher level of abstraction. This approach offloads some of the heavy lifting from the main ROS 2 driver and gives you finer control over the hardware.

Both Gazebo and Webots are powerful simulators, but they have different strengths. Gazebo’s tight ROS integration and powerful physics engine make it the default choice for heavy-duty academic research or industrial work where ROS is already in place. Webots, on the other hand, gets you prototyping faster with its friendly GUI and huge library of ready-to-go models which is why you see it so often in schools and robotics competitions. Your choice really comes down to your existing toolchain, how complex the project is, and frankly, just your personal workflow. Knowing how to use both gives you a much more flexible toolkit as a robotics engineer. If you care about AI’s wider impact on automation, seeing how AI agents are being built for resilience is worth understanding. Plus, connecting sims to physical hardware forces you to think about complex agentic AI safety problems, especially as these bots become more autonomous.

What’s the difference between Gazebo and Webots?

Gazebo is deeply tied into ROS and is built for advanced physics, custom plugins, and large environments, so it’s a favorite for research and industry. Webots is a standalone tool with a very intuitive GUI and a big library of pre-made robots and parts, making it great for education and getting a prototype working fast.

Can I use ROS 2 with both?

Yep, both have great ROS 2 support. Gazebo uses the gazebo_ros_pkgs and ros2_control framework. Webots uses a dedicated webots_ros2 package. Either way, you can use ROS 2 topics and services to control your simulated robot.

What file formats do they use for robot models?

Gazebo mostly uses URDF for defining a robot and SDF for defining an entire world. Webots has its own format that you edit in the GUI, but it can also import URDF, OBJ, and STL files.

How do I debug a Webots controller?

If you’re writing a Python controller, just use standard print() statements. The output shows up right in the Webots console (View > Console). For C++ controllers, you can attach a debugger like GDB. You can also inspect just about anything in real-time through the scene tree.

Can I simulate multiple robots at once?

Yes, both can handle multi-robot sims. In Gazebo, you just call the spawn_entity.py script multiple times in your launch file, making sure to give each robot its own namespace. In Webots, you just add multiple Robot nodes to your world file in the GUI.

Editorial Team

The Editorial Team at Code & Coffee is a group of writers and editors who cover technology, products, and how teams ship. They research each story and publish under a shared desk byline — experts in their fields, working as one team.