本文是在开始学习ROS过程中所做的小结,以二话不说上来就干的方式,一步一步亲自动手实现ROS中创建工作空间和package。通过创建,编译和运行节点的方式带大家了解和体会ROS中消息、服务和动作的工作机制。文中动手内容偏多,参考了ROS官方网站和《Learning ROS for Robotics Programming》等相关书籍。水平有限,欢迎批评指正。
(注:系统Ubuntu 16.04,ROS kinetic)
在开始工作之前,我们需要创建自己工作空间,以添加自己的代码。假设我们的工作空间的名称为catkin_ws
, 则创建的步骤如下
~
)中创建/dev/catkin_ws/src
文件夹,也可以在终端输入mkdir –p ~/dev/catkin_ws/src
cd ~/dev/catkin_ws/src
,进入到src目录下catkin_init_workspace
命令对新工作区间进行初始化。完成后会在src文件夹下会新增CmakeLists.txt
文件cd ~/dev/catkin_ws
,或cd ..
(仅当终端路径处于~/dev/catkin_ws/src
目录下)进入~/dev/catkin_ws目录catkin_make
对工作空间进行编译。完成后会在catkin_ws文件夹下生成build和devel文件夹gedit ~/.bashrc
进入.bashrc隐藏文件,并在文件最后新建一行添加source ~/dev/catkin_ws/devel/setup.bash
,或在终端键入echo "source ~/dev/catkin_ws/devel/setup.bash" >> ~/.bashrc
总结:新建终端依次输入的命令为:
mkdir –p ~/dev/catkin_ws/src
cd ~/dev/catkin_ws/src
catkin_init_workspace
cd ..
catkin_make
echo "source ~/dev/catkin_ws/devel/setup.bash" >> ~/.bashrc
工作区间目录说明:
catkin_ws
build :catkin_make产生的中间文件
devel :catkin_make生成的输出文件include :自定义消息、服务和动作的头文件
lib :生成的可执行文件src :源代码存放位置
package_one :包1(英文名叫package,中文姑且叫做包吧? )
action :存放动作(*.action)文件
include :存放库文件(*.hpp, *.cpp, *.h, *.c)
msg :存放消息文件(*.msg)
launch :存放启动文件(*.launch)
scripts :存放python文件(*.py)
src :存放节点源文件(*.cpp, *.c)
srv :存放服务文件(*.srv)
CmakeLists.txt :catkin_make文件
package.xml : 包描述文件package_two :包2
… :与包1相同
ROS中的包,需要存放在工作空间中src文件夹下,假设包的名字为test
,则创建步骤为:
cd ~/dev/catkin_ws/src
,进入到src文件夹下catkin_create_pkg test std_msgs roscpp
创建包在步骤2中使用了catkin_create_pkg
命令创建新的包,其中test
为包的名称,std_msgs roscpp
表示此包依赖ROS标准消息类型和C++编译环境,具体命令结构如下:
Catkin_create_pkg [package_name] [depend1] [depend2] [depend3]
ROS提供了以下命令方便对包的查看
ROS以节点基础,通过建立P2P网络和规范化信息传递方式,实现节点之间的相互通讯。话不多说,运行下面两个节点来感受ROS的通讯机制。
Talker: 在test包中的src文件夹下,创建talker_node.cpp文件,粘贴如下内容
#include "ros/ros.h" // ros头文件
#include "std_msgs/String.h" // 消息头文件
#include // stl字符串头文件
int main(int argc, char **argv)
{
// ROS初始化,名称为talker
ros::init(argc, argv, "talker");
// 创建节点句柄
ros::NodeHandle n;
// 创建消息发布变量,消息类型为std_msgs::String,名称为message,缓存1000字节
ros::Publisher pub = n.advertise<std_msgs::String>("message", 1000);
// 创建发布频率变量,频率为10Hz
ros::Rate loop_rate(10);
// 如果ROS出于开启状态,则保持循环
while (ros::ok()){
// 创建消息变量并,输入信息: I am the talker node
std_msgs::String msg;
std::stringstream ss;
ss << " I am the talker node ";
msg.data = ss.str();
// 发布消息
pub.publish(msg);
// 启动ROS消息回调处理函数
ros::spinOnce();
// 等待
loop_rate.sleep();
}
return 0;
}
Listener: 在test包中的src文件夹下,创建listener_node.cpp文件,粘贴如下内容
#include "ros/ros.h"
#include "std_msgs/String.h"
// 消息回调处理函数
void Callback(const std_msgs::String::ConstPtr& msg)
{
// 将收到的消息打印出来
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle n;
// 创建消息订阅变量,消息名称为message,缓存1000字节,回调函数为CallBack
ros::Subscriber sub = n.subscribe("message", 1000, Callback);
// 启动ROS消息回调处理函数
ros::spin();
return 0;
}
CmakeList.txt最后添加以下内容
# 添加路径搜索目录
include_directories(
include
${catkin_INCLUDE_DIRS}
)
# 添加节点的可执行文件
add_executable(${PROJECT_NAME}_Listener_node src/listener_node.cpp)
add_executable(${PROJECT_NAME}_Talker_node src/talker_node.cpp)
# 添加节点可指定文件作需要链接的库文件
target_link_libraries(${PROJECT_NAME}_Listener_node ${catkin_LIBRARIES})
target_link_libraries(${PROJECT_NAME}_Talker_node ${catkin_LIBRARIES})
在写完源文件和修改完CmakeLists.txt文件后,即可对此包进行编译,在终端输入以下命令:
cd ~/dev/catkin_ws/
catkin_make
编译成功后,新建三个终端,依次在各个终端上运行
roscore
rosrun test test_Listener_node
rosrun test test_Talker_node
在运行listener的终端上会看到,talker节点发出的 I am the talker node信息
launch文件初探
每个终端点运行一个节点,再加上roscore的一个终端,有时十分不方便。ROS中提供了roslaunch函数,可以实现一个终端可同时运行多个节点的目的。此函数会自动运行roscore节点,并通过运行launch文件实现,具体方法为:
<launch>
<node name="Listener_node" pkg="test" type="test_Listener_node" output="screen"/>
<node name="Talker_node" pkg="test" type="test_Talker_node"/>
launch>
roslaunch test node.launch
通过在新终端中键入rosnode list
可以查看当前运行的节点,rostopic list
可一查看发布的消息名称
目前为止我们已经在ROS上运行了两个节点,并完成了数据的收发和处理,并使用launch文件在一个终端下实现了两个节点的运行。接下来学习ROS中三种信息传递的方式:消息、服务和动作。
ROS中可以使用python作为编程语言,其好处是省去了写CmakeLists的麻烦(但需要在CmakeLists中find_packag内添加rospy,然后从新对包编译,已获得对python的支持),并且可以使用丰富的python资源,提高编程效率和代码安全性。
python文件应放在scripts文件夹中,下面使用python对上面两个节点进行重写。
talker_node.py
#!/usr/bin/env python
# 添加ros函数库
import rospy
# 添加std_msgs中String类型
from std_msgs.msg import String
# 初始化节点
rospy.init_node('Talker_node')
# 初始化发布者,话题名称messaage,类型String,缓存1000字节
pub = rospy.Publisher('message', String, queue_size = 1000)
# 发布频率10Hz
rate = rospy.Rate(10)
# 定义发布的信息
msg = String()
msg = ' I am the talker node '
# 循环发布
while not rospy.is_shutdown():
pub.publish(msg)
rate.sleep()
listener_node.py
#!/usr/bin/env python
import rospy
from std_msgs.msg import String
# 话题回调函数
def CallBack(msg):
rospy.info('I heard %s"', msg.data)
rospy.init_node('Listener_node')
# 初始化订阅者,订阅话题为message,类型String,回调函数CallBack
sub = rospy.Subscriber('message', String, CallBack)
rospy.spin()
将上面两个文件保存到制定目录,然后在三个终端中依次运行:
roscore
rosrun test listener_node.py
rosrun test talker_node.py
或者运行launch文件:
<launch>
<node name="Listener_node" pkg="test" type="listener_node.py" output="screen"/>
<node name="Talker_node" pkg="test" type="talker_node.py"/>
launch>
##消息(message)
###自定义消息
在package中的msg文件夹下新建Position_msg.msg文件,粘贴以下内容。
float64 latitude
float64 longitude
float64 altitude
该消息包含了经度、纬度和高度三个信息。
修改CmakeLists.txt文件
例如:
find_package(catkin REQUIRED COMPONENTS
# 其他依赖包(std_msgs, roscpp, rospy等)
message_generation
}
add_message_files(
FILES
Position_msg.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
修改package.xml文件
找到
去掉注释后并保存
<build_depend>message_generationbuild_depend>
<exec_depend>message_runtimeexec_depend>
Publisher :新建gnss_node.cpp文件,内容如下
/*gnss_node.cpp*/
#include "ros/ros.h"
#include "test/Position_msg.h" // 自定义的消息类型
int main(int argc, char **argv)
{
ros::init(argc, argv, "Gnss");
ros::NodeHandle n;
ros::Publisher pub = n.advertise<test::Position_msg>("Position_msg", 1000);
ros::Rate loop_rate(10);
test::Position_msg pos;
pos.latitude = 39.2;
pos.longitude = 40.5;
pos.altitude = 100.6;
while (ros::ok()){
pub.publish(pos);
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}
Subscriber:新建controller_node.cpp文件,内容如下
/*controller_node.cpp*/
#include "ros/ros.h"
#include "test/Position_msg.h"
void Callback(const test::Position_msg::ConstPtr& msg)
{
ROS_INFO("Lat : %f, Lng : %f, Alt : %f",
msg->latitude, msg->longitude, msg->altitude);
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "Control");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("Position_msg", 1000, Callback);
ros::spin();
return 0;
}
修改CmakeLists.txt,添加需要新的节点,并添加节点的依赖关系,本例中添加项为:
include_directories(
include
${catkin_INCLUDE_DIRS}
)
# 添加节点的可执行文件
add_executable(${PROJECT_NAME}_Gnss_node src/gnss_node.cpp)
add_executable(${PROJECT_NAME}_Controller_node src/controller_node.cpp)
# 添加节点依赖关系
add_dependencies(${PROJECT_NAME}_Gnss_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(${PROJECT_NAME}_Controller_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
# 添加节点可指定文件作需要链接的库文件
target_link_libraries(${PROJECT_NAME}_Gnss_node ${catkin_LIBRARIES})
target_link_libraries(${PROJECT_NAME}_Controller_node ${catkin_LIBRARIES})
消息传输原理
ROS话题传输一共分7个步骤进行,分别为:
下图中Tom为发布者,Bob为订阅者,简述了话题传递的原理
消息数据类型
std_msgs文件中的变量类型与C++中的变量类型对应表如下:
msg type | C++ | msg type | C++ |
---|---|---|---|
bool | bool | int64 | long long |
int8 | char | uint64 | unsigned long long |
uint8 | unsigned char | float32 | float |
int16 | short | float64 | double |
uint16 | unsigned short | string | std::string |
int32 | int | time | ros::Time |
uint32 | unsigned int | duration | ros::Duration |
此外msg还可以使用其他包中的消息类型,例如sensor_msgs中的Image.msg、PointCloud2.msg、PointCloud.msg等,或用户自定义的消息类型,此时需要在generate_messages中添加对这些包的依赖。并且在去掉add_dependencies注释。
注:在编译时,如果当前包中用到了其他包中所定义的消息、服务和动作类型,需要添加add_dependencies项。
服务(Server)官方教程
在package中的srv文件夹下新建Add_srv.srv文件,粘贴以下内容。
Add_srv.srv
# request
float64 apple
float64 banana
---
#response
float64 sum
服务文件分请求和回答两个部分,之间以三个横线(—)分隔,上半部分为请求部分,即输入到服务器的数据,下半部分为回答部分,即返回给客户端的数据。
修改CmakeLists.txt文件
例如:
find_package(catkin REQUIRED COMPONENTS
# 其他依赖包(std_msgs, roscpp, rospy等)
message_generation
}
add_service_files(
FILES
Add_srv.srv
)
generate_messages(
DEPENDENCIES
std_msgs
)
修改package.xml文件的方式同上。
Service :新建add_server_node.cpp文件,内容如下
/*add_server_node.cpp*/
#include "ros/ros.h"
#include "test/Add_srv.h"
bool AddServer(test::Add_srv::Request &req, test::Add_srv::Response &res)
{
res.sum = req.apple + req.banana;
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "Add_srv_node");
ros::NodeHandle n;
// 创建一个加法服务,名称为Add_srv,对应函数为AddServer
ros::ServiceServer srv = n.advertiseService("Add_srv", AddServer);
ros::spin();
return 0;
}
Client :新建add_client_node.cpp文件,内容如下
/*add_client_node.cpp*/
#include "ros/ros.h"
#include "test/Add_srv.h"
int main(int argc, char **argv)
{
ros::init(argc, argv, "Add_node");
ros::NodeHandle n;
// 创建客户端,服务类型为test::Add_srv,请求服务的名称为Add_srv
ros::ServiceClient client = n.serviceClient<test::Add_srv>("Add_srv");
ros::Rate loop_rate(1);
// 创建服务变量,并输入需要相加的两个数
test::Add_srv add_srv;
add_srv.request.apple = 100;
add_srv.request.banana = 50;
for (;;){
// 请求服务
if ( client.call(add_srv) ) {
// 打印计算结果
ROS_INFO("Sum : %f", add_srv.response.sum);
} else {
// 请求失败,打印错误信息
ROS_ERROR("Failed to call service");
}
loop_rate.sleep();
}
ros::spin();
return 0;
}
修改CmakeLists.txt部分,添加需要新的节点,并编译。
include_directories(
include
${catkin_INCLUDE_DIRS}
)
# 添加节点的可执行文件
add_executable(${PROJECT_NAME}_AddServer_node src/add_server_node.cpp)
add_executable(${PROJECT_NAME}_AddClient_node src/add_client_node.cpp)
# 添加节点依赖关系
add_dependencies(${PROJECT_NAME}_AddServer_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(${PROJECT_NAME}_AddClient_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
# 添加节点可指定文件作需要链接的库文件
target_link_libraries(${PROJECT_NAME}_AddServer_node ${catkin_LIBRARIES})
target_link_libraries(${PROJECT_NAME}_AddClient_node ${catkin_LIBRARIES})
ROS中服务相当于多线程编程中的同步模型,客户端在发送请求和收到回到的期间,出于等待状态,不能进行下一步操作。
想象一下李婶儿去水果店买水果的过程,挑水果和付钱时李婶儿亲自操作,而称重标价则是由店员完成。在称重的过程中,李婶儿只能等待其完成才可以去收银台付款。此时李婶儿为服务中的客户端,店员为服务端,水果为店员的输入,贴好的签为店员的输出。在服务器执行的过程中客户端出于等待状态。
服务有自己的标准输入输出接口,可以对应多个节点的请求。
动作(Action)官方教程
在package中的ation文件夹下新建Count.srv文件,粘贴以下内容。
# Define the goal
int32 length
---
# Define the result
int32 cnt
---
# Define a feedback message
float64 percent
与srv文件不同,action文件氛围三个部分,从上倒下分别为目标、结果和反馈,各个部分之间同样以三个横线(—)分隔。其中目标为服务端的输入,结果为服务端的输出,动作服务器在运行的过程中可以通过反馈,项客户端发送反馈信息(例如当前运行的百分比)。
修改CmakeLists.txt文件
例如:
find_package(catkin REQUIRED COMPONENTS
#其他依赖
message_generation
genmsg
actionlib_msgs
actionlib
)
add_action_files(
FILES
Count.action
)
generate_messages(
DEPENDENCIES
std_msgs
actionlib_msgs
)
修改package.xml
在package.xml中
标签内,添加下面内容
<build_depend>actionlibbuild_depend>
<build_depend>actionlib_msgsbuild_depend>
<exec_depend>actionlibexec_depend>
<exec_depend>actionlib_msgsexec_depend>
动作服务器:在action文件夹下添加action_server_node.cpp,内容如下:
#include "ros/ros.h"
// 动作服务头文件
#include
// 自定动作义头文件
#include "test/CountAction.h"
// 定义自定义动作服务类型
typedef actionlib::SimpleActionServer<test::CountAction> Server;
/*
* 根据goal中的length值,以10Hz的速度进行计数,并每计数一次,返回进度状态
*/
void Execute(const test::CountGoalConstPtr& goal, Server* as)
{
ros::Rate loop_rate(10);
// 创建反馈变量
test::CountFeedback feedback;
// 创建结果变量
test::CountResult res;
for (int i = 0; i != goal->length; ++i){
ROS_INFO("In Action Server Node");
loop_rate.sleep();
// 计算已完成任务的百分比
feedback.percent = static_cast<float>(i) / goal->length;
// 发布反馈信息
as->publishFeedback(feedback);
}
// 记录运算结果,并发布完成信息
res.cnt = goal->length;
as->setSucceeded(res);
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "count_server");
ros::NodeHandle n;
// 创建动作服务器,名称为count_action,过bind函数适配器,将server变量传入Execute函数,_1为占位符
Server server(n, "count_action", boost::bind(&Execute, _1, &server), false);
// 开启服务器
server.start();
ros::spin();
return 0;
}
动作客户端:在action文件夹下添加action_client_node.cpp,内容如下:
#include "ros/ros.h"
#include
#include "test/CountAction.h"
// 定义自定义动作客户端类型
typedef actionlib::SimpleActionClient<test::CountAction> Client;
/*
* 动作完成时调用,打印结果信息
*/
void Finish(const actionlib::SimpleClientGoalState& state,
const test::CountResultConstPtr& result)
{
ROS_INFO("result : %d", result->cnt);
}
/*
* 动作发出反馈时调用,打印反馈信息
*/
void Feedback(const test::CountFeedbackConstPtr& feedback)
{
ROS_INFO("Got Feedback : %f", feedback->percent);
}
/*
* 动作启动调用,打印启动信息
*/
void Active()
{
ROS_INFO("Goal just went active");
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "count_client");
// 创建客户端变量,目标服务器名称为count_action, ture : 不需要ros::spin()
Client client("count_action", true);
// 等待服务器启动
client.waitForServer();
// 设置目标值
test::CountGoal goal;
goal.length = 100;
/* 发送目标到动作服务器,
* 运行开始执行Active函数,
* 收到反馈信息执行Feedback函数,
* 运行结束执行Finish函数,
* 除了goal之外,其他位置可以使用nullptr代替 */
client.sendGoal(goal, Finish, Active, Feedback);
ros::Rate loop_rate(2);
while (ros::ok()){
ros::spinOnce();
ROS_INFO("In Action Client Node");
loop_rate.sleep();
}
return 0;
}
修改CmakeLists.txt部分,添加需要新的节点,并编译。
include_directories(
include
${catkin_INCLUDE_DIRS}
)
# 添加节点的可执行文件
add_executable(${PROJECT_NAME}_ActionServer_node src/action_server_node.cpp)
add_executable(${PROJECT_NAME}_ActionClient_node src/action_client_node.cpp)
# 添加节点依赖关系
add_dependencies(${PROJECT_NAME}_ActionServer_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(${PROJECT_NAME}_ActionClient_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
# 添加节点可指定文件作需要链接的库文件
target_link_libraries(${PROJECT_NAME}_ActionServer_node ${catkin_LIBRARIES})
target_link_libraries(${PROJECT_NAME}_ActionClient_node ${catkin_LIBRARIES})
ROS中的动作模型相当于多线程编程中的异步模型,动作客户端启动动作后无需等待动作完成,可以继续执行后续工作,中途只需处理一下动作的反馈结果和最终结果即可。
上图以一个生活中简单的例子,讲述异步的运行机制。小新在玩游戏的时候感觉自己饿了,于是在某app上定了一份外卖(sendGoal发送目标请求),之后继续打游戏(不等待,继续执行)。期间,app会发布某些消息以告知外卖的进度(反馈),小新看过一眼后得知外卖按照预定计划执行,所以并没有打客服投诉(反馈处理函数),而是继续打游戏,外卖小哥敲门时(动作执行完毕),小新开门结果外卖开始吃饭(处理执行结果),饭后继续打游戏(继续执行任务)。图中实现表示客户端进程,虚线表示动作服务端进程。
launch文件的基本模版为:
<launch>
<node name=" " pkg=" " type=" "/>
<node name=" " pkg=" " type=" "/>
<node name=" " pkg=" " type=" "/>
<\launch>
node标签代表一个节点,在模版中的节点名称(name),包名称(pkg),和可执行节点名称(type)中输入对应的值即可,按顺序执行每个节点。其中每个节点的name是唯一的,但可以使用相同的type,例如:
<launch>
<node name="Listener1_node" pkg="test" type="test_Listener_node" output="screen"/>
<node name="Listener2_node" pkg="test" type="test_Listener_node" output="screen"/>
<node name="Talker_node" pkg="test" type="test_Talker_node"/>
launch>
传入参数
launch除了可以用于启动多个节点外,还可以设置节点的启动参数,例如:
<launch>
<node name="GNSS_node" pkg="gnss" type="gnss_node">
<param name="port" type="string" value="/dev/ttyS1"/>
<param name="baud_rate" type="int" value="115200"/>
node>
launch>
由于xml语法的原因,如果node标签未能在一行内定义完全则需要使用作为标签的结束符号。在节点中使用
#include "ros/ros.h"
#include
int32 main(int32 argc, int8 **argv)
{
ros::init(argc, argv, "GNSS_node");
ros::NodeHandle nLocal("~");
std::string port = "/dev/ttyS0";
int32 baudRate = 9600;
nLocal.getParam("port", port);
nLocal.getParam("baud_rate", baudRate);
ros::spin()
return 0
}
上述例子简单介绍了launch文件设置节点参数的方法。函数中初始串口为/dev/ttyS0,波特率为9600,经过launch启动重新对端口号和波特率赋值后,串口号和波特率变更为/dev/ttyS1,115200。
有时value中的变量会很长,或很多参数使用同一个值,此时需要arg标签定义变量,然后在param标签中使用,定义方式如下:
<launch>
<arg name="gnss_port" value="/dev/ttyS1">
<arg name="gnss_baud_rate" value="115200">
<node name="GNSS_node" pkg="gnss" type="gnss_node">
<param name="port" type="string" value="$(arg gnss_port)"/>
<param name="baud_rate" type="int" value="$(arg gnss_baud_rate)"/>
node>
launch>
其他参数
output=”screen”
配置了该属性的节点会将标准输出显示在屏幕上而不是记录到日志文档respawn=”true”
,这样当节点停止的时候,roslaunch会重新启动该节点required="true"
,当这个节点退出后,roslaunch会关闭所有的节点,并退出launch-prefix="xterm -e"
让该个节点在单独的终端窗口中启动具体内容请参考官方说明文档,和相关博客等资料。
yaml是一种更加简洁的表达数据的方式,常被当做配置文件,用于参数多精度高等场合,在节点运行的过程中可以多次读取其中数据,以实现对参数的更新和设置。使用yaml文件需要在CmakeLists中对使用该文件节点的target_link_libraries中添加yaml-cpp依赖。
由于yaml文件语言内容比较多,本文仅以一个简单的例子要介绍,具体内容请参阅官方文档和相关博客。
文件名为:config.yaml。
-
name : tank # I am a comment
attack : 1000
defence : 1000
-
name : V3
attack : 2000
defence : 500
通过C++读取yaml文件中的数据
#include "yaml-cpp/yaml.h"
#include
int main(int argc, char **argv)
{
// yaml节点
YAML::Node config = YAML::LoadFile(argv[1]);
std::string name;
for (int i = 0; i != config.size(); ++i){
// 读取第一个数组的name参数
name = config[i]["name"].as<std::string>();
// 打印数组信息
std::cout << name
<< "\n attack : " << config[i]["attack"].as<int>()
<< "\n defence : " << config[i]["defence"].as<int>()
<< std::endl;
}
return 0;
}
输出为:
tank
attack : 1000
defence : 1000
V3
attack : 2000
defence : 500