Gazebo Harmonic Tutorial 2026World SDF · Robot Model · ROS 2 Bridge
Complete 2026 guide to Gazebo Harmonic (gz-harmonic) — the recommended simulator for ROS 2 Jazzy on Ubuntu 24.04. Covers install, custom world SDF creation, differential-drive robot with lidar and IMU sensors, ros_gz_bridge YAML config, Python launch file, plugin reference, and troubleshooting.
Gazebo Version Matrix 2026
| Gazebo Version | Codename | ROS 2 Pairing | Ubuntu | Support Until |
|---|---|---|---|---|
| Gz 9 ← use this | Harmonic | Jazzy (LTS) | 24.04 | 2028 |
| Gz 8 | Garden | Humble/Iron | 22.04 | 2026 |
| Gz 7 | Fortress | Humble | 22.04 | 2025 EOL |
Install Gazebo Harmonic
Install Gazebo Harmonic
Gazebo Harmonic is the recommended version for ROS 2 Jazzy (Ubuntu 24.04).
# Add Gazebo repo and install Harmonic
sudo apt update
sudo apt install -y curl lsb-release gnupg
sudo curl https://packages.osrfoundation.org/gazebo.gpg \
--output /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] \
http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/gazebo-stable.list
sudo apt update
sudo apt install -y gz-harmonic
# Verify
gz sim --versionInstall ROS 2 ↔ Gazebo Bridge
ros_gz_bridge connects ROS 2 topics to Gazebo transports bidirectionally.
sudo apt install -y \
ros-jazzy-ros-gz \
ros-jazzy-ros-gz-bridge \
ros-jazzy-ros-gz-sim \
ros-jazzy-ros-gz-interfaces
# Test: start Gazebo empty world, bridge /clock
source /opt/ros/jazzy/setup.bash
gz sim -r empty.sdf &
ros2 run ros_gz_bridge parameter_bridge /clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock✅ ros_gz replaces the old ros_ign_* packages. Use ros-jazzy-ros-gz for Harmonic.
Create a World SDF
Create a Basic World SDF
SDF (Simulation Description Format) defines worlds, models, and plugins in Gazebo.
<?xml version="1.0" ?>
<sdf version="1.10">
<world name="my_world">
<!-- Physics engine -->
<physics name="1ms" type="ignored">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
</physics>
<!-- Plugins required for Gazebo Harmonic -->
<plugin filename="gz-sim-physics-system"
name="gz::sim::systems::Physics"/>
<plugin filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>
<plugin filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster"/>
<plugin filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands"/>
<!-- Lighting -->
<light name="sun" type="directional">
<cast_shadows>true</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
</light>
<!-- Ground plane -->
<model name="ground_plane">
<static>true</static>
<link name="link">
<collision name="collision">
<geometry><plane><normal>0 0 1</normal></plane></geometry>
</collision>
<visual name="visual">
<geometry><plane><normal>0 0 1</normal><size>100 100</size></plane></geometry>
<material>
<ambient>0.8 0.8 0.8 1</ambient>
<diffuse>0.8 0.8 0.8 1</diffuse>
</material>
</visual>
</link>
</model>
<!-- Include an obstacle box -->
<model name="box">
<pose>3 0 0.5 0 0 0</pose>
<link name="link">
<collision name="collision">
<geometry><box><size>1 1 1</size></box></geometry>
</collision>
<visual name="visual">
<geometry><box><size>1 1 1</size></box></geometry>
<material><ambient>1 0 0 1</ambient></material>
</visual>
</link>
</model>
</world>
</sdf>⚡ Save as my_world.sdf. Launch with: gz sim my_world.sdf -r
Robot Model with Sensors
A complete differential-drive robot SDF with wheel joints, IMU, 2D lidar, and DiffDrive plugin — ready to receive /cmd_vel.
Differential-Drive Robot Model
A minimal two-wheeled robot with DiffDrive plugin, IMU sensor, and lidar.
<?xml version="1.0" ?>
<sdf version="1.10">
<model name="diff_bot">
<pose>0 0 0.16 0 0 0</pose>
<!-- Base link -->
<link name="base_link">
<inertial>
<mass>5.0</mass>
<inertia><ixx>0.1</ixx><iyy>0.1</iyy><izz>0.1</izz></inertia>
</inertial>
<visual name="base_visual">
<geometry><box><size>0.4 0.3 0.1</size></box></geometry>
<material><ambient>0 0.5 1 1</ambient></material>
</visual>
<collision name="base_collision">
<geometry><box><size>0.4 0.3 0.1</size></box></geometry>
</collision>
<!-- IMU sensor -->
<sensor name="imu" type="imu">
<always_on>true</always_on>
<update_rate>100</update_rate>
<topic>imu</topic>
</sensor>
<!-- 2D Lidar -->
<sensor name="lidar" type="gpu_lidar">
<pose>0.2 0 0.1 0 0 0</pose>
<topic>scan</topic>
<update_rate>10</update_rate>
<always_on>true</always_on>
<lidar>
<scan>
<horizontal>
<samples>640</samples>
<resolution>1</resolution>
<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>
</horizontal>
</scan>
<range>
<min>0.12</min>
<max>10.0</max>
<resolution>0.015</resolution>
</range>
</lidar>
</sensor>
</link>
<!-- Left wheel -->
<link name="left_wheel">
<pose>0 0.175 -0.06 1.5708 0 0</pose>
<inertial><mass>1.0</mass></inertial>
<visual name="visual"><geometry><cylinder><radius>0.1</radius><length>0.04</length></cylinder></geometry></visual>
<collision name="collision"><geometry><cylinder><radius>0.1</radius><length>0.04</length></cylinder></geometry></collision>
</link>
<joint name="left_wheel_joint" type="revolute">
<parent>base_link</parent><child>left_wheel</child>
<axis><xyz>0 0 1</xyz></axis>
</joint>
<!-- Right wheel (mirror of left) -->
<link name="right_wheel">
<pose>0 -0.175 -0.06 1.5708 0 0</pose>
<inertial><mass>1.0</mass></inertial>
<visual name="visual"><geometry><cylinder><radius>0.1</radius><length>0.04</length></cylinder></geometry></visual>
<collision name="collision"><geometry><cylinder><radius>0.1</radius><length>0.04</length></cylinder></geometry></collision>
</link>
<joint name="right_wheel_joint" type="revolute">
<parent>base_link</parent><child>right_wheel</child>
<axis><xyz>0 0 1</xyz></axis>
</joint>
<!-- DiffDrive plugin -->
<plugin filename="gz-sim-diff-drive-system"
name="gz::sim::systems::DiffDrive">
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.35</wheel_separation>
<wheel_radius>0.1</wheel_radius>
<odom_publish_frequency>50</odom_publish_frequency>
<topic>cmd_vel</topic>
<odom_topic>odom</odom_topic>
<frame_id>odom</frame_id>
<child_frame_id>base_link</child_frame_id>
</plugin>
<!-- Joint State publisher -->
<plugin filename="gz-sim-joint-state-publisher-system"
name="gz::sim::systems::JointStatePublisher"/>
</model>
</sdf>⚡ gpu_lidar requires a GPU. Use lidar (CPU) type if running in headless/CI environments.
ROS 2 Bridge & Launch File
ROS 2 Bridge Config (YAML)
Bridge Gazebo topics to ROS 2 topics using a config file for complex setups.
# bridge_config.yaml
# Format: gz_topic@ros_type[direction]gz_type
# [ = gz → ros (subscribe from gz)
# ] = ros → gz (publish to gz)
# @ = bidirectional
- ros_topic_name: /cmd_vel
gz_topic_name: /cmd_vel
ros_type_name: geometry_msgs/msg/Twist
gz_type_name: gz.msgs.Twist
direction: ROS_TO_GZ
- ros_topic_name: /odom
gz_topic_name: /odom
ros_type_name: nav_msgs/msg/Odometry
gz_type_name: gz.msgs.Odometry
direction: GZ_TO_ROS
- ros_topic_name: /scan
gz_topic_name: /scan
ros_type_name: sensor_msgs/msg/LaserScan
gz_type_name: gz.msgs.LaserScan
direction: GZ_TO_ROS
- ros_topic_name: /imu
gz_topic_name: /imu
ros_type_name: sensor_msgs/msg/Imu
gz_type_name: gz.msgs.IMU
direction: GZ_TO_ROS
- ros_topic_name: /clock
gz_topic_name: /clock
ros_type_name: rosgraph_msgs/msg/Clock
gz_type_name: gz.msgs.Clock
direction: GZ_TO_ROSLaunch Bridge + Simulation
Python launch file starting Gazebo, spawning the robot, and running the bridge.
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, ExecuteProcess, TimerAction
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
def generate_launch_description():
pkg = get_package_share_directory("my_robot_sim")
# Start Gazebo Harmonic with our world
gz_sim = ExecuteProcess(
cmd=["gz", "sim", "-r",
os.path.join(pkg, "worlds", "my_world.sdf")],
output="screen",
)
# Spawn robot model into running simulation
spawn_robot = TimerAction(
period=3.0, # wait for Gazebo to start
actions=[
ExecuteProcess(
cmd=["gz", "service", "-s", "/world/my_world/create",
"--reqtype", "gz.msgs.EntityFactory",
"--reptype", "gz.msgs.Boolean",
"--timeout", "1000",
"--req",
f'sdf_filename: "{os.path.join(pkg, "models", "diff_bot.sdf")}", name: "diff_bot"'],
output="screen",
)
]
)
# ros_gz_bridge
bridge = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=["--ros-args", "-p",
f"config_file:={os.path.join(pkg, 'config', 'bridge_config.yaml')}"],
output="screen",
)
# robot_state_publisher (reads URDF for TF)
rsp = Node(
package="robot_state_publisher",
executable="robot_state_publisher",
parameters=[{
"robot_description":
open(os.path.join(pkg, "urdf", "diff_bot.urdf")).read()
}],
)
return LaunchDescription([gz_sim, spawn_robot, bridge, rsp])✅ Always add a TimerAction delay before spawning to ensure Gazebo is ready.
Plugin Reference
| Plugin | filename | Use Case |
|---|---|---|
| DiffDrive | gz-sim-diff-drive-system | Differential-drive mobile robot |
| JointTrajectoryController | gz-sim-joint-trajectory-controller-system | Robot arm joint trajectory |
| Physics | gz-sim-physics-system | DART physics engine (required) |
| Sensors | gz-sim-sensors-system | Camera, lidar, IMU, GPS |
| SceneBroadcaster | gz-sim-scene-broadcaster-system | Render scene to GUI (required) |
| RosGzBridge (external) | ros_gz_bridge | Bridge Gz ↔ ROS 2 topics |
| PosePublisher | gz-sim-pose-publisher-system | Publish model poses |
| Thruster | gz-sim-thruster-system | Marine/underwater propulsion |