ROS Noetic:从零到一搭建 Gazebo 小车模型
本文将完整记录在 Ubuntu 20.04 + ROS Noetic 环境下,从创建 URDF 模型到实现 Gazebo 物理仿真+键盘控制的全过程,适合 ROS 入门学习者参考。

一、整体方案概述
我们将完成以下核心目标:
- 编写小车 URDF 模型:定义底盘、车轮等部件及物理属性
- 配置 Gazebo 仿真环境:将小车模型加载到物理场景中
- 实现 C++ 键盘控制节点 :通过
/cmd_vel话题驱动小车运动 - 编写 Launch 文件:一键启动可视化与仿真
二、环境准备
1. 安装依赖包
Bash
# 核心 ROS 依赖
sudo apt-get install ros-noetic-urdf ros-noetic-robot-state-publisher ros-noetic-joint-state-publisher-gui ros-noetic-rviz
# Gazebo 仿真依赖
sudo apt-get install ros-noetic-gazebo-ros ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-plugins
2. 创建功能包
Bash
cd ~/catkin_ws/src
catkin_create_pkg car_urdf_cpp roscpp urdf rviz geometry_msgs sensor_msgs
cd ~/catkin_ws && catkin_make
source devel/setup.bash
三、核心文件编写

1. 小车 URDF 模型(car_model.urdf)
路径:~/catkin_ws/src/car_urdf_cpp/urdf/car_model.urdf
XML
<?xml version="1.0"?>
<robot name="cpp_car">
<!-- 虚拟根链接,避免 KDL 警告 -->
<link name="dummy_link"/>
<joint name="dummy_joint" type="fixed">
<parent link="dummy_link"/>
<child link="base_link"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
</joint>
<!-- 底盘 base_link -->
<link name="base_link">
<inertial>
<mass value="2.0"/>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<inertia ixx="0.01" ixy="0.0" ixz="0.0"
iyy="0.02" iyz="0.0" izz="0.03"/>
</inertial>
<visual>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<geometry><box size="0.5 0.3 0.1"/></geometry>
<material name="blue"><color rgba="0 0 1 1"/></material>
</visual>
<collision>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<geometry><box size="0.5 0.3 0.1"/></geometry>
</collision>
</link>
<!-- 左前轮 -->
<link name="left_front_wheel">
<inertial>
<mass value="0.2"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0"
iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
<material name="black"><color rgba="0 0 0 1"/></material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
</collision>
</link>
<!-- 右前轮 -->
<link name="right_front_wheel">
<inertial>
<mass value="0.2"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0"
iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
<material name="black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
</collision>
</link>
<!-- 左后轮 -->
<link name="left_back_wheel">
<inertial>
<mass value="0.2"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0"
iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
<material name="black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
</collision>
</link>
<!-- 右后轮 -->
<link name="right_back_wheel">
<inertial>
<mass value="0.2"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0"
iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
<material name="black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry><cylinder length="0.05" radius="0.08"/></geometry>
</collision>
</link>
<!-- 底盘-左前轮关节 -->
<joint name="left_front_joint" type="continuous">
<parent link="base_link"/>
<child link="left_front_wheel"/>
<origin xyz="0.2 0.15 0.05" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit effort="100" velocity="100"/>
</joint>
<!-- 底盘-右前轮关节 -->
<joint name="right_front_joint" type="continuous">
<parent link="base_link"/>
<child link="right_front_wheel"/>
<origin xyz="0.2 -0.15 0.05" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit effort="100" velocity="100"/>
</joint>
<!-- 底盘-左后轮关节 -->
<joint name="left_back_joint" type="continuous">
<parent link="base_link"/>
<child link="left_back_wheel"/>
<origin xyz="-0.2 0.15 0.05" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit effort="100" velocity="100"/>
</joint>
<!-- 底盘-右后轮关节 -->
<joint name="right_back_joint" type="continuous">
<parent link="base_link"/>
<child link="right_back_wheel"/>
<origin xyz="-0.2 -0.15 0.05" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit effort="100" velocity="100"/>
</joint>
<!-- Gazebo 物理材质 -->
<gazebo reference="base_link">
<material>Gazebo/Blue</material>
</gazebo>
<gazebo reference="left_front_wheel">
<material>Gazebo/Black</material>
</gazebo>
<gazebo reference="right_front_wheel">
<material>Gazebo/Black</material>
</gazebo>
<gazebo reference="left_back_wheel">
<material>Gazebo/Black</material>
</gazebo>
<gazebo reference="right_back_wheel">
<material>Gazebo/Black</material>
</gazebo>
</robot>
2. Gazebo 启动 Launch 文件(car_gazebo.launch)
路径:~/catkin_ws/src/car_urdf_cpp/launch/car_gazebo.launch
XML
<?xml version="1.0"?>
<launch>
<!-- 1. 启动 Gazebo 空世界 -->
<include file="$(find gazebo_ros)/launch/empty_world.launch">
<arg name="gui" value="true"/>
<arg name="paused" value="false"/>
<arg name="use_sim_time" value="true"/>
</include>
<!-- 2. 加载 URDF 到参数服务器 -->
<param name="robot_description" command="cat $(find car_urdf_cpp)/urdf/car_model.urdf"/>
<!-- 3. 发布机器人状态(TF 变换) -->
<node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" output="screen"/>
<!-- 4. 将小车模型 spawn 到 Gazebo -->
<node name="spawn_model" pkg="gazebo_ros" type="spawn_model" output="screen"
args="-urdf -model my_car -param robot_description -x 0 -y 0 -z 0.2"/>
</launch>
3. C++ 键盘控制节点(car_teleop.cpp)
路径:~/catkin_ws/src/car_urdf_cpp/src/car_teleop.cpp
C++
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <cmath>
// 速度限制
#define MAX_LIN_VEL 0.5 // 最大线速度 (m/s)
#define MAX_ANG_VEL 1.0 // 最大角速度 (rad/s)
#define LIN_VEL_STEP 0.1 // 线速度步长
#define ANG_VEL_STEP 0.1 // 角速度步长
// 非阻塞检查按键
int kbhit(void) {
struct termios oldt, newt;
int ch;
int oldf;
tcgetattr(STDIN_FILENO, &oldt);
newt = oldt;
newt.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
ch = getchar();
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
fcntl(STDIN_FILENO, F_SETFL, oldf);
if(ch != EOF) {
ungetc(ch, stdin);
return 1;
}
return 0;
}
// 读取按键
char getKey() {
if(kbhit()) {
switch(getchar()) {
case 'w': return 'w';
case 's': return 's';
case 'a': return 'a';
case 'd': return 'd';
case 'x': return 'x';
case 'q': return 'q';
default: return '\0';
}
}
return '\0';
}
int main(int argc, char** argv) {
ros::init(argc, argv, "car_teleop_cpp_node");
ros::NodeHandle nh;
ros::Publisher vel_pub = nh.advertise<geometry_msgs::Twist>("/cmd_vel", 10);
ros::Rate loop_rate(10);
double linear_vel = 0.0;
double angular_vel = 0.0;
geometry_msgs::Twist vel_cmd;
ROS_INFO("===== C++ 小车键盘控制 =====");
ROS_INFO("w:前进 s:后退 a:左转 d:右转");
ROS_INFO("x:停止 q:退出");
while(ros::ok()) {
char key = getKey();
switch(key) {
case 'w': linear_vel += LIN_VEL_STEP; angular_vel = 0.0; break;
case 's': linear_vel -= LIN_VEL_STEP; angular_vel = 0.0; break;
case 'a': angular_vel += ANG_VEL_STEP; break;
case 'd': angular_vel -= ANG_VEL_STEP; break;
case 'x': linear_vel = 0.0; angular_vel = 0.0; break;
case 'q':
ROS_INFO("退出控制");
linear_vel = 0.0; angular_vel = 0.0;
vel_cmd.linear.x = linear_vel;
vel_cmd.angular.z = angular_vel;
vel_pub.publish(vel_cmd);
return 0;
default:
linear_vel *= 0.9;
angular_vel *= 0.9;
if(fabs(linear_vel) < 0.01) linear_vel = 0.0;
if(fabs(angular_vel) < 0.01) angular_vel = 0.0;
break;
}
// 限制速度范围
linear_vel = std::max(-MAX_LIN_VEL, std::min(linear_vel, MAX_LIN_VEL));
angular_vel = std::max(-MAX_ANG_VEL, std::min(angular_vel, MAX_ANG_VEL));
vel_cmd.linear.x = linear_vel;
vel_cmd.angular.z = angular_vel;
vel_pub.publish(vel_cmd);
loop_rate.sleep();
}
// 停止小车
vel_cmd.linear.x = 0.0;
vel_cmd.angular.z = 0.0;
vel_pub.publish(vel_cmd);
return 0;
}
4. CMakeLists.txt 配置
路径:~/catkin_ws/src/car_urdf_cpp/CMakeLists.txt
CMake
cmake_minimum_required(VERSION 3.0.2)
project(car_urdf_cpp)
add_compile_options(-std=c++11)
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
roscpp
rviz
sensor_msgs
urdf
robot_state_publisher
joint_state_publisher
)
catkin_package(
CATKIN_DEPENDS geometry_msgs roscpp rviz sensor_msgs urdf
)
include_directories(${catkin_INCLUDE_DIRS})
# 编译键盘控制节点
add_executable(car_teleop_node src/car_teleop.cpp)
target_link_libraries(car_teleop_node ${catkin_LIBRARIES})
# 安装可执行文件与资源
install(TARGETS car_teleop_node
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
install(DIRECTORY launch urdf rviz
DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)
四、运行步骤
1. 编译工作空间
Bash
cd ~/catkin_ws
catkin_make
source devel/setup.bash
2. 启动 Gazebo 仿真
Bash
roslaunch car_urdf_cpp car_gazebo.launch
此时 Gazebo 界面会打开,并在场景中加载蓝色底盘+黑色车轮的小车模型。
3. 启动键盘控制节点(新开终端)
Bash
source ~/catkin_ws/devel/setup.bash
rosrun car_urdf_cpp car_teleop_node
4. 控制小车
w:前进s:后退a:左转d:右转x:停止q:退出控制
五、常见问题与解决
- URDF 解析失败 :使用
check_urdf car_model.urdf检查语法错误,确保 Gazebo 标签格式正确。 - Gazebo spawn 超时:等待 Gazebo 界面完全加载后再启动 launch 文件,或调整 spawn 时的 z 轴高度(避免模型穿透地面)。
- 键盘无响应:确保终端窗口处于激活状态,且 Python/C++ 节点有执行权限。
- RViz 不显示模型 :将
Fixed Frame设置为dummy_link,并添加RobotModel显示插件。
六、总结
本文完整实现了从 URDF 建模 → Gazebo 物理仿真 → C++ 键盘控制 的小车开发流程,覆盖了 ROS 机器人建模与控制的核心知识点。通过这个案例,你可以快速理解 ROS 中模型描述、仿真环境与控制节点的协作方式,为后续更复杂的机器人开发打下基础。