Chapter 5: Hands-on - Creating a Custom Simulation Environment
⏱️ Estimated Time: 60 minutes
Learning Objectives
By the end of this hands-on tutorial, you will be able to:
- Create a custom Gazebo world file from scratch
- Design and place environmental elements (walls, obstacles, lighting)
- Integrate sensors into a robot model
- Spawn robots programmatically using ROS 2 services
- Test and validate your simulation environment
- Troubleshoot common Gazebo issues
Prerequisites
Required Software
- ✅ ROS 2 Humble installed
- ✅ Gazebo Garden or Fortress
- ✅ Python 3.8+
- ✅ Text editor (VS Code recommended)
Required Knowledge
- Basic ROS 2 concepts (Chapter 1-2 of Module 1)
- URDF/SDF syntax (Module 2, Chapter 3)
- Linux command line basics
Verification
# Verify ROS 2 installation
ros2 --version
# Verify Gazebo installation
gazebo --version
# Check Gazebo plugins
ros2 pkg list | grep gazebo
# Expected output includes:
# gazebo_ros
# gazebo_ros_pkgs
# gazebo_plugins
Project Overview
In this tutorial, we'll create a custom warehouse simulation environment with:
- Custom world: Indoor warehouse with walls and obstacles
- Lighting: Realistic lighting setup
- Robot: Mobile robot with camera and LiDAR sensors
- Spawning script: Python script to programmatically spawn robots
- Testing framework: Validation of sensors and robot movement
Project Structure
~/custom_gazebo_ws/
├── src/
│ └── warehouse_simulation/
│ ├── worlds/
│ │ └── warehouse.world
│ ├── models/
│ │ └── mobile_robot/
│ │ ├── model.sdf
│ │ └── model.config
│ ├── launch/
│ │ ├── warehouse.launch.py
│ │ └── spawn_robot.launch.py
│ ├── scripts/
│ │ └── spawn_robot.py
│ ├── package.xml
│ └── setup.py
└── install/
Part 1: Workspace Setup (5 minutes)
Step 1: Create Workspace
# Create workspace
mkdir -p ~/custom_gazebo_ws/src
cd ~/custom_gazebo_ws/src
# Create package
ros2 pkg create warehouse_simulation \
--build-type ament_python \
--dependencies rclpy gazebo_ros
# Create directory structure
cd warehouse_simulation
mkdir -p worlds models launch scripts
Step 2: Configure Package
Edit package.xml to add dependencies:
<?xml version="1.0"?>
<package format="3">
<name>warehouse_simulation</name>
<version>0.0.1</version>
<description>Custom warehouse simulation environment</description>
<maintainer email="your@email.com">Your Name</maintainer>
<license>MIT</license>
<depend>rclpy</depend>
<depend>gazebo_ros</depend>
<depend>gazebo_ros_pkgs</depend>
<depend>geometry_msgs</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
Update setup.py:
from setuptools import setup
import os
from glob import glob
package_name = 'warehouse_simulation'
setup(
name=package_name,
version='0.0.1',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
(os.path.join('share', package_name, 'worlds'), glob('worlds/*.world')),
(os.path.join('share', package_name, 'models/mobile_robot'), glob('models/mobile_robot/*')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='Your Name',
maintainer_email='your@email.com',
description='Custom warehouse simulation',
license='MIT',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'spawn_robot = warehouse_simulation.spawn_robot:main'
],
},
)
Part 2: Creating the World File (15 minutes)
Step 3: Basic World Structure
Create worlds/warehouse.world:
<?xml version="1.0"?>
<sdf version="1.8">
<world name="warehouse">
<!-- Physics settings -->
<physics type="ode">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
<real_time_update_rate>1000</real_time_update_rate>
</physics>
<!-- Scene settings -->
<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
</scene>
<!-- Sun (directional light) -->
<light type="directional" name="sun">
<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>
<attenuation>
<range>1000</range>
<constant>0.9</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
<direction>-0.5 0.1 -0.9</direction>
</light>
<!-- Overhead warehouse lights -->
<light type="point" name="warehouse_light_1">
<pose>5 5 5 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>0.1 0.1 0.1 1</specular>
<attenuation>
<range>20</range>
<constant>0.5</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
<cast_shadows>false</cast_shadows>
</light>
<light type="point" name="warehouse_light_2">
<pose>-5 5 5 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>0.1 0.1 0.1 1</specular>
<attenuation>
<range>20</range>
</attenuation>
</light>
<!-- Ground plane -->
<model name="ground_plane">
<static>true</static>
<link name="link">
<collision name="collision">
<geometry>
<plane>
<normal>0 0 1</normal>
<size>100 100</size>
</plane>
</geometry>
<surface>
<friction>
<ode>
<mu>100</mu>
<mu2>50</mu2>
</ode>
</friction>
</surface>
</collision>
<visual name="visual">
<geometry>
<plane>
<normal>0 0 1</normal>
<size>100 100</size>
</plane>
</geometry>
<material>
<ambient>0.5 0.5 0.5 1</ambient>
<diffuse>0.5 0.5 0.5 1</diffuse>
</material>
</visual>
</link>
</model>
<!-- Warehouse walls -->
<model name="wall_north">
<static>true</static>
<pose>0 10 1.5 0 0 0</pose>
<link name="link">
<collision name="collision">
<geometry>
<box>
<size>20 0.2 3</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>20 0.2 3</size>
</box>
</geometry>
<material>
<ambient>0.7 0.7 0.7 1</ambient>
<diffuse>0.7 0.7 0.7 1</diffuse>
</material>
</visual>
</link>
</model>
<model name="wall_south">
<static>true</static>
<pose>0 -10 1.5 0 0 0</pose>
<link name="link">
<collision name="collision">
<geometry>
<box>
<size>20 0.2 3</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>20 0.2 3</size>
</box>
</geometry>
<material>
<ambient>0.7 0.7 0.7 1</ambient>
<diffuse>0.7 0.7 0.7 1</diffuse>
</material>
</visual>
</link>
</model>
<model name="wall_east">
<static>true</static>
<pose>10 0 1.5 0 0 0</pose>
<link name="link">
<collision name="collision">
<geometry>
<box>
<size>0.2 20 3</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.2 20 3</size>
</box>
</geometry>
<material>
<ambient>0.7 0.7 0.7 1</ambient>
<diffuse>0.7 0.7 0.7 1</diffuse>
</material>
</visual>
</link>
</model>
<model name="wall_west">
<static>true</static>
<pose>-10 0 1.5 0 0 0</pose>
<link name="link">
<collision name="collision">
<geometry>
<box>
<size>0.2 20 3</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.2 20 3</size>
</box>
</geometry>
<material>
<ambient>0.7 0.7 0.7 1</ambient>
<diffuse>0.7 0.7 0.7 1</diffuse>
</material>
</visual>
</link>
</model>
<!-- Obstacles (boxes on pallets) -->
<model name="obstacle_1">
<static>true</static>
<pose>3 3 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>0.8 0.5 0.2 1</ambient>
<diffuse>0.8 0.5 0.2 1</diffuse>
</material>
</visual>
</link>
</model>
<model name="obstacle_2">
<static>true</static>
<pose>-3 -3 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>0.8 0.5 0.2 1</ambient>
<diffuse>0.8 0.5 0.2 1</diffuse>
</material>
</visual>
</link>
</model>
<!-- ROS 2 time publisher plugin -->
<plugin name="gazebo_ros_state" filename="libgazebo_ros_state.so">
<ros>
<namespace>/gazebo</namespace>
</ros>
<update_rate>10.0</update_rate>
</plugin>
</world>
</sdf>
Part 3: Creating the Robot Model (20 minutes)
Step 4: Robot Model with Sensors
Create models/mobile_robot/model.config:
<?xml version="1.0"?>
<model>
<name>Mobile Robot</name>
<version>1.0</version>
<sdf version="1.8">model.sdf</sdf>
<author>
<name>Your Name</name>
<email>your@email.com</email>
</author>
<description>
Mobile robot with differential drive, camera, and LiDAR
</description>
</model>
Create models/mobile_robot/model.sdf:
<?xml version="1.0"?>
<sdf version="1.8">
<model name="mobile_robot">
<pose>0 0 0.1 0 0 0</pose>
<!-- Base link -->
<link name="base_link">
<inertial>
<mass>10.0</mass>
<inertia>
<ixx>0.166</ixx>
<ixy>0</ixy>
<ixz>0</ixz>
<iyy>0.166</iyy>
<iyz>0</iyz>
<izz>0.166</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<box>
<size>0.5 0.3 0.2</size>
</box>
</geometry>
</collision>
<visual name="visual">
<geometry>
<box>
<size>0.5 0.3 0.2</size>
</box>
</geometry>
<material>
<ambient>0.2 0.2 0.8 1</ambient>
<diffuse>0.2 0.2 0.8 1</diffuse>
</material>
</visual>
<!-- Camera sensor -->
<sensor name="camera" type="camera">
<pose>0.25 0 0.1 0 0 0</pose>
<update_rate>30</update_rate>
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
<format>R8G8B8</format>
</image>
<clip>
<near>0.1</near>
<far>100</far>
</clip>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.007</stddev>
</noise>
</camera>
<plugin name="camera_controller" filename="libgazebo_ros_camera.so">
<ros>
<namespace>/mobile_robot</namespace>
<remapping>camera/image_raw:=camera/image_raw</remapping>
<remapping>camera/camera_info:=camera/camera_info</remapping>
</ros>
<camera_name>camera</camera_name>
<frame_name>camera_link</frame_name>
</plugin>
</sensor>
<!-- LiDAR sensor -->
<sensor name="lidar" type="ray">
<pose>0 0 0.15 0 0 0</pose>
<update_rate>10</update_rate>
<ray>
<scan>
<horizontal>
<samples>360</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.01</resolution>
</range>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</ray>
<plugin name="lidar_controller" filename="libgazebo_ros_ray_sensor.so">
<ros>
<namespace>/mobile_robot</namespace>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
<frame_name>lidar_link</frame_name>
</plugin>
</sensor>
</link>
<!-- Left wheel -->
<link name="left_wheel">
<pose>0 0.2 0.05 -1.5708 0 0</pose>
<inertial>
<mass>0.5</mass>
<inertia>
<ixx>0.001</ixx>
<ixy>0</ixy>
<ixz>0</ixz>
<iyy>0.001</iyy>
<iyz>0</iyz>
<izz>0.001</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<cylinder>
<radius>0.05</radius>
<length>0.05</length>
</cylinder>
</geometry>
<surface>
<friction>
<ode>
<mu>1.0</mu>
<mu2>1.0</mu2>
</ode>
</friction>
</surface>
</collision>
<visual name="visual">
<geometry>
<cylinder>
<radius>0.05</radius>
<length>0.05</length>
</cylinder>
</geometry>
<material>
<ambient>0.2 0.2 0.2 1</ambient>
<diffuse>0.2 0.2 0.2 1</diffuse>
</material>
</visual>
</link>
<!-- Right wheel -->
<link name="right_wheel">
<pose>0 -0.2 0.05 -1.5708 0 0</pose>
<inertial>
<mass>0.5</mass>
<inertia>
<ixx>0.001</ixx>
<ixy>0</ixy>
<ixz>0</ixz>
<iyy>0.001</iyy>
<iyz>0</iyz>
<izz>0.001</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<cylinder>
<radius>0.05</radius>
<length>0.05</length>
</cylinder>
</geometry>
<surface>
<friction>
<ode>
<mu>1.0</mu>
<mu2>1.0</mu2>
</ode>
</friction>
</surface>
</collision>
<visual name="visual">
<geometry>
<cylinder>
<radius>0.05</radius>
<length>0.05</length>
</cylinder>
</geometry>
<material>
<ambient>0.2 0.2 0.2 1</ambient>
<diffuse>0.2 0.2 0.2 1</diffuse>
</material>
</visual>
</link>
<!-- Joints -->
<joint name="left_wheel_joint" type="revolute">
<parent>base_link</parent>
<child>left_wheel</child>
<axis>
<xyz>0 0 1</xyz>
</axis>
</joint>
<joint name="right_wheel_joint" type="revolute">
<parent>base_link</parent>
<child>right_wheel</child>
<axis>
<xyz>0 0 1</xyz>
</axis>
</joint>
<!-- Differential drive plugin -->
<plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so">
<ros>
<namespace>/mobile_robot</namespace>
</ros>
<update_rate>50</update_rate>
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.4</wheel_separation>
<wheel_diameter>0.1</wheel_diameter>
<max_wheel_torque>20</max_wheel_torque>
<max_wheel_acceleration>1.0</max_wheel_acceleration>
<publish_odom>true</publish_odom>
<publish_odom_tf>true</publish_odom_tf>
<publish_wheel_tf>false</publish_wheel_tf>
<odometry_frame>odom</odometry_frame>
<robot_base_frame>base_link</robot_base_frame>
</plugin>
</model>
</sdf>
Part 4: Launch Files and Scripts (10 minutes)
Step 5: Create Launch File
Create launch/warehouse.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_share = get_package_share_directory('warehouse_simulation')
world_file = os.path.join(pkg_share, 'worlds', 'warehouse.world')
# Gazebo launch
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(get_package_share_directory('gazebo_ros'),
'launch', 'gazebo.launch.py')
]),
launch_arguments={'world': world_file}.items()
)
return LaunchDescription([
gazebo
])
Step 6: Robot Spawning Script
Create warehouse_simulation/spawn_robot.py:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from gazebo_msgs.srv import SpawnEntity
from geometry_msgs.msg import Pose
import os
from ament_index_python.packages import get_package_share_directory
class RobotSpawner(Node):
def __init__(self):
super().__init__('robot_spawner')
# Create service client
self.client = self.create_client(SpawnEntity, '/spawn_entity')
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Waiting for /spawn_entity service...')
def spawn_robot(self, robot_name, x=0.0, y=0.0, z=0.1):
"""Spawn robot at specified position"""
# Get model path
pkg_share = get_package_share_directory('warehouse_simulation')
model_path = os.path.join(pkg_share, 'models', 'mobile_robot', 'model.sdf')
# Read SDF file
with open(model_path, 'r') as f:
robot_sdf = f.read()
# Create spawn request
request = SpawnEntity.Request()
request.name = robot_name
request.xml = robot_sdf
# Set initial pose
request.initial_pose = Pose()
request.initial_pose.position.x = x
request.initial_pose.position.y = y
request.initial_pose.position.z = z
# Call service
self.get_logger().info(f'Spawning {robot_name} at ({x}, {y}, {z})...')
future = self.client.call_async(request)
rclpy.spin_until_future_complete(self, future)
if future.result() is not None:
self.get_logger().info(f'Successfully spawned {robot_name}')
return True
else:
self.get_logger().error(f'Failed to spawn {robot_name}')
return False
def main(args=None):
rclpy.init(args=args)
spawner = RobotSpawner()
# Spawn robot
spawner.spawn_robot('mobile_robot', x=0.0, y=0.0, z=0.1)
spawner.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Make it executable:
chmod +x warehouse_simulation/spawn_robot.py
Part 5: Build and Test (10 minutes)
Step 7: Build the Package
# Navigate to workspace root
cd ~/custom_gazebo_ws
# Build
colcon build --packages-select warehouse_simulation
# Source the workspace
source install/setup.bash
Step 8: Launch the Simulation
Terminal 1 - Launch Gazebo:
source ~/custom_gazebo_ws/install/setup.bash
ros2 launch warehouse_simulation warehouse.launch.py
Terminal 2 - Spawn Robot:
source ~/custom_gazebo_ws/install/setup.bash
ros2 run warehouse_simulation spawn_robot
Step 9: Verify Sensors
Check topics:
ros2 topic list
# Expected topics:
# /mobile_robot/camera/image_raw
# /mobile_robot/camera/camera_info
# /mobile_robot/scan
# /mobile_robot/cmd_vel
# /mobile_robot/odom
# /clock
View camera image:
# Install rqt_image_view if needed
sudo apt install ros-humble-rqt-image-view
# View camera
ros2 run rqt_image_view rqt_image_view /mobile_robot/camera/image_raw
View LiDAR scan:
# Install rviz2 if needed
sudo apt install ros-humble-rviz2
# Launch RViz
rviz2
# Add LaserScan display
# Topic: /mobile_robot/scan
# Fixed Frame: odom
Step 10: Test Robot Movement
Send velocity commands:
# Move forward
ros2 topic pub /mobile_robot/cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.5, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"
# Rotate
ros2 topic pub /mobile_robot/cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.5}}"
# Stop
ros2 topic pub /mobile_robot/cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"
Troubleshooting
Issue 1: Gazebo doesn't start
Symptoms: Error messages about missing plugins or world file
Solution:
# Check if world file exists
ls ~/custom_gazebo_ws/install/warehouse_simulation/share/warehouse_simulation/worlds/
# Verify Gazebo can find the world
gazebo ~/custom_gazebo_ws/install/warehouse_simulation/share/warehouse_simulation/worlds/warehouse.world --verbose
Issue 2: Robot spawns but falls through ground
Symptoms: Robot position z < -1.0
Solution:
- Check ground plane collision is defined
- Verify robot initial pose z >= 0.1
- Increase physics solver iterations in world file
Issue 3: Sensors not publishing
Symptoms: No topics for camera or LiDAR
Solution:
# Check Gazebo plugins loaded
ros2 run gazebo_ros spawn_entity.py --help
# Verify sensor plugins in model.sdf
grep -A 20 "sensor" models/mobile_robot/model.sdf
# Check for errors in Gazebo terminal
Issue 4: Robot doesn't move
Symptoms: cmd_vel received but robot stationary
Solution:
- Verify differential drive plugin loaded
- Check wheel joint names match plugin configuration
- Increase max_wheel_torque if robot is too heavy
Issue 5: Poor performance
Symptoms: Low FPS, simulation slower than real-time
Solution:
# Run headless (no GUI)
gazebo --verbose -s libgazebo_ros_init.so ~/custom_gazebo_ws/.../warehouse.world
# Reduce sensor rates in model.sdf
# Camera: 30 Hz → 15 Hz
# LiDAR: 10 Hz → 5 Hz
Extensions and Challenges
Challenge 1: Add More Obstacles
Add 5-10 randomly placed boxes in the warehouse.
Hint: Copy-paste obstacle_1 model, change name and pose.
Challenge 2: Multi-Robot Simulation
Spawn 3 robots at different positions.
Hint: Modify spawn_robot.py to spawn multiple robots with unique names.
Challenge 3: Navigation
Integrate with Nav2 for autonomous navigation.
Steps:
- Install Nav2:
sudo apt install ros-humble-navigation2 - Create map using SLAM
- Configure Nav2 parameters
- Test autonomous navigation
Challenge 4: Custom Sensor
Add a depth camera (RealSense D435i simulation).
Hint: Use gazebo_ros_depth_camera plugin.
Assessment Checklist
✅ I can create a custom Gazebo world file ✅ I can define static obstacles and walls ✅ I can configure lighting in a simulation ✅ I can create a robot model with sensors ✅ I can spawn robots programmatically using ROS 2 services ✅ I can verify sensor data is being published ✅ I can control the robot using velocity commands ✅ I can troubleshoot common Gazebo issues ✅ I understand when to use Gazebo vs other simulators
Summary
Congratulations! You've created a complete custom simulation environment including:
- ✅ Custom warehouse world with walls and obstacles
- ✅ Mobile robot with differential drive
- ✅ Camera sensor integration
- ✅ LiDAR sensor integration
- ✅ Programmatic robot spawning
- ✅ ROS 2 integration and testing
Key Takeaways
- World files define the simulation environment (lighting, physics, static objects)
- Model files define dynamic entities (robots, sensors)
- Plugins provide ROS 2 integration (publishers, subscribers, services)
- Testing is crucial - verify each component incrementally
- Troubleshooting follows a systematic process (check files, plugins, topics)
Next Steps
- Complete Module 3 for advanced simulation techniques
- Explore Isaac Sim for NVIDIA-accelerated simulation
- Learn Nav2 for autonomous navigation
- Integrate computer vision algorithms with simulated sensors
References
🎉 Tutorial Complete! You now have a fully functional custom simulation environment for robotics development!