引言

        在机器人开发领域,ROS(Robot Operating System)作为行业标准框架,与Unity游戏引擎的结合正在开创全新的可能性。这种融合不仅让机器人仿真更加逼真,还为实时监控和控制提供了强大工具。本文将详细介绍如何配置ROS 2与Unity之间的通信系统,实现对宇树G1人型机器人电机位置和速度的实时获取。

一、系统架构概览

        系统配置:ubuntu24.04(本次项目使用ROS2与Unity进行通信,ROS也同样支持)。ROS版本为Jazzy。人型机器人模型为宇树开源模型G1(自己的模型也同样可以)。

二、环境配置

        本项目对于ROS2的安装,NIVIDA显卡驱动等不做详细叙述,详情请参考鱼香肉丝帖子进行ROS2的安装。

        Unity:对于Unity的安装以及使用和训练,参考青龙人型机器人的开源项目,链接:https://github.com/loongOpen/Unity-RL-Playgroundhttps://github.com/loongOpen/Unity-RL-Playground

该github项目详细叙述了Unity的安装以及训练方法,涵盖了多种人型机器人模型的训练以及urdf的文件描述。同时支持导入本地机器人进行强化训练开发。该项目相较于issacgym等,训练参数调整较少,训练时长明细降低。我训练400000step只花了8分钟时间,且行走效果较好。具体可参考青龙github开源项目,readme.md中有具体开发过程。

三、ROS-Unity通信配置

        ROS与Unity之间的通信采用的是TCP通讯,其项目链接:https://github.com/Unity-Technologies/Unity-Robotics-Hub/tree/main/tutorials/ros_unity_integrationhttps://github.com/Unity-Technologies/Unity-Robotics-Hub/tree/main/tutorials/ros_unity_integration可以根据项目完成ROS与Unity之间通信,这里主要来详细去介绍怎么解决运行过程中的报错,首先文件夹的存放路径需要注意,在主目录下一共存在ros2_ws(自己创建的工作空间包含ROS-TCP-Endpoint-main-ros2、unity_robotics_demo_msgs、unity_robotics_demo)、ROS_unity(包含ROS-TCP-Connector,用来在unity中导入ROS-TCP-Connector软件包)、Unity-Robotics-Hub-main(包含ros2功能包用来获取UNITY消息unity_robotics_demo_msgs、unity_robotics_demo)。

报错总结:UNITY:1.没有播放按钮,无法进行训练以及演示。 解决:显卡驱动安装失败,无法调用显卡,重新安装显卡驱动(建议使用自带的软件与更新去下载附加。版本需要选open-kernel)。

2.可以进行播放,但是演示结果机器人并没有进行走路。解决:首先查看是否导入了训练后的ONNX,如果导入之后还是这样的话,估计会显示报错:DilNotFoundException: libdl.so assembly:<unknown assembly> type:<unknown type> member:(null)
Assimp.Unmanaged.UnmanagedLibrary+UnmanagedLinuxLibraryImplementation.NativeLoadLibrary。解决办法:

安装必需系统库​

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install libc6-dev

# CentOS/RHEL
sudo yum install glibc-devel

修复符号链接​

# 查找现有库
ls -l /usr/lib/x86_64-linux-gnu/libdl.so*

# 创建必要链接
sudo ln -sf /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/libdl.so

完成以上步骤即可解决。

3.解决报错ImportError: libpython3.13.so.1.0: cannot open shared object file: No such ......

参考博客安装Issacgym后测试examples报错:ImportError: libpython3.8.so.1.0: cannot open shared object file: No such fi-CSDN博客

解决办法为:终端命令行查找lib位置

find / -name libpython3.8.so.1.0 2>/dev/null

终端输出如下:

/home/user_name/miniconda3/envs/unitree-rl/lib/libpython3.8.so.1.0
/home/user_name/miniconda3/pkgs/python-3.8.20-he870216_0/lib/libpython3.8.so.1.

根据查找到的位置,终端输入命令(替换 你的库文件路径 为实际找到的libpython3.8.so.1.0 文件的目录)

export LD_LIBRARY_PATH=/home/user_name/miniconda3/envs/unitree-rl/lib/:$LD_LIBRARY_PATH
source ~/.bashrc  # 或者对应的 shell 配置文件

  我按照以上之后发现,每次打开都需进行替换,所以我们可以将其加入到bashrc文件中。

# 设置unity环境库路径
export LD_LIBRARY_PATH="/home/user_name/miniconda3/envs/unitree-rl/lib/:$LD_LIBRARY_PATH"

4.可以完成tcp连接,但是ros2 topic查看话题时发生报错。

raise UnsupportedTypeSupport(pkg_name)
rosidl_generator_py.import_type_support_impl.UnsupportedTypeSupport: Could not import 'rosidl_typesupport_c' for package 'unity_robotics_demo_msgs'sgs'

大概率是编译出现问题,解决办法:

cd ~/ros2_ws

# 完全清理
rm -rf build install log
# 使用 Python 3.13 重新编译
colcon build \
  --symlink-install \
  --merge-install \
  --cmake-args \
  -DPYTHON_EXECUTABLE=/usr/bin/python3.13 \
  -DPYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.13.so.1.0 \
  -DPYTHON_INCLUDE_DIR=/usr/include/python3.13
# 更新 .bashrc
echo "export PYTHONPATH=/usr/lib/python3.13/site-packages:\$PYTHONPATH" >> ~/.bashrc
source ~/.bashrc

5.报错显示:

   Usage of dash-separated 'script-dir' will not be supported in future
        versions. Please use the underscore name 'script_dir' instead.

        This deprecation is overdue, please update your project and remove deprecated
        calls to avoid build errors in the future.

        See https://setuptools.pypa.io/en/latest/userguide/declarative_config.html for details.

 解决办法:

# 进入包源码目录(路径可能不同)
cd ~/ros2_ws/src/unity_robotics_Hub

找到unity_robotics_demoros_tcp_endpoint包中的 setup.pysetup.cfg文件。

将文件中的短横线改成下横线。

# 不推荐(即将废弃)
setup(install-scripts='bin', script-dir='bin')

# 推荐写法
setup(install_scripts='bin', script_dir='bin')

五、电机速度、位姿脚本编写

和ROS-Unity项目中的创建脚本一样,脚本内容如下所示。

using UnityEngine;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Sensor;
using RosMessageTypes.Std;          // 用于 HeaderMsg
using RosMessageTypes.BuiltinInterfaces; // 用于 TimeMsg
using System.Collections.Generic;
using System.Linq;

public class G1JointStatePublisher : MonoBehaviour
{
    [Header("ROS 连接设置")]
    public string topicName = "/g1/joint_states";
    [Range(10, 100)]
    public float publishFrequency = 50f;
    
    [Header("机器人配置")]
    public GameObject robotRoot;
    public List<ArticulationBody> manualJoints = new List<ArticulationBody>();
    
    private ROSConnection ros;
    private List<ArticulationBody> joints = new List<ArticulationBody>();
    private Dictionary<ArticulationBody, string> jointNameMap = new Dictionary<ArticulationBody, string>();
    private float timeElapsed;
    private float publishInterval;
    
    private static readonly Dictionary<string, string> G1JointNameMapping = new Dictionary<string, string>
    {
        {"LF_HIP", "front_left_hip_joint"},
        {"LF_THIGH", "front_left_thigh_joint"},
        {"LF_CALF", "front_left_calf_joint"},
        {"RF_HIP", "front_right_hip_joint"},
        {"RF_THIGH", "front_right_thigh_joint"},
        {"RF_CALF", "front_right_calf_joint"},
        {"LH_HIP", "rear_left_hip_joint"},
        {"LH_THIGH", "rear_left_thigh_joint"},
        {"LH_CALF", "rear_left_calf_joint"},
        {"RH_HIP", "rear_right_hip_joint"},
        {"RH_THIGH", "rear_right_thigh_joint"},
        {"RH_CALF", "rear_right_calf_joint"}
    };

    void Start()
    {
        publishInterval = 1f / publishFrequency;
        ros = ROSConnection.GetOrCreateInstance();
        ros.RegisterPublisher<JointStateMsg>(topicName);
        ConfigureJoints();
        
        if (joints.Count == 0)
        {
            Debug.LogError("未配置关节! 请手动分配关节或检查机器人模型");
        }
        else
        {
            Debug.Log($"已配置 {joints.Count} 个关节");
        }
    }

    void FixedUpdate()
    {
        timeElapsed += Time.fixedDeltaTime;
        
        if (timeElapsed >= publishInterval)
        {
            PublishJointStates();
            timeElapsed = 0;
        }
    }
    
    private void ConfigureJoints()
    {
        joints.Clear();
        jointNameMap.Clear();
        
        if (manualJoints.Count > 0)
        {
            foreach (var joint in manualJoints)
            {
                if (joint != null)
                {
                    joints.Add(joint);
                    jointNameMap[joint] = FormatJointName(joint.gameObject.name);
                }
            }
            return;
        }
        
        if (robotRoot == null)
        {
            Debug.LogWarning("未指定机器人根对象,尝试自动查找...");
            robotRoot = GameObject.Find("G1") ?? GameObject.Find("UnitreeG1");
        }
        
        if (robotRoot != null)
        {
            var allJoints = robotRoot.GetComponentsInChildren<ArticulationBody>()
                .Where(j => j.jointType != ArticulationJointType.FixedJoint)
                .ToList();
            
            foreach (var joint in allJoints)
            {
                joints.Add(joint);
                
                if (TryGetStandardJointName(joint.gameObject.name, out string standardName))
                {
                    jointNameMap[joint] = standardName;
                }
                else
                {
                    jointNameMap[joint] = FormatJointName(joint.gameObject.name);
                }
            }
        }
    }
    
    private bool TryGetStandardJointName(string gameObjectName, out string jointName)
    {
        foreach (var kvp in G1JointNameMapping)
        {
            if (gameObjectName.Contains(kvp.Key))
            {
                jointName = kvp.Value;
                return true;
            }
        }
        
        jointName = null;
        return false;
    }
    
    private string FormatJointName(string originalName)
    {
        return originalName
            .Replace("(Clone)", "")
            .Replace(" ", "_")
            .ToLower();
    }
    
    private void PublishJointStates()
    {
        var jointStates = new JointStateMsg
        {
            header = new HeaderMsg
            {
                // 使用 BuiltinInterfaces.TimeMsg
                stamp = new TimeMsg
                {
                    sec = (int)Time.timeAsDouble,
                    nanosec = (uint)((Time.timeAsDouble - (int)Time.timeAsDouble) * 1e9)
                },
                frame_id = "g1_base_link"
            }
        };
        
        int count = joints.Count;
        jointStates.name = new string[count];
        jointStates.position = new double[count];
        jointStates.velocity = new double[count];
        jointStates.effort = new double[count];
        
        for (int i = 0; i < count; i++)
        {
            var joint = joints[i];
            jointStates.name[i] = jointNameMap[joint];
            jointStates.position[i] = joint.jointPosition[0] * Mathf.Deg2Rad;
            jointStates.velocity[i] = joint.jointVelocity[0] * Mathf.Deg2Rad;
            jointStates.effort[i] = GetJointEffort(joint);
        }
        
        ros.Publish(topicName, jointStates);
    }
    
    private double GetJointEffort(ArticulationBody joint)
    {
        return 0.0;
    }
    
    [ContextMenu("自动查找关节")]
    public void FindJointsInEditor()
    {
        ConfigureJoints();
        if (joints.Count > 0)
        {
            manualJoints = new List<ArticulationBody>(joints);
            Debug.Log($"找到 {joints.Count} 个关节并添加到手动列表");
        }
    }
}

创建完脚本后,将其拖拽到G1当中,并且在inspector对G1的关节进行添加。

在右边的manual joints中选者机器人的关节,可以先在左边点击具体关节,再确定测试的关节后将关节导入进脚本。并进行运行。重新打开终端输入:

建立通信

ros2 run ros_tcp_endpoint default_server_endpoint --ros-args -p ROS_IP:=198.168.122.1 -p ROS_TCP_PORT:=10000

发布话题后接受话题消息

ros2 topic echo /g1/joint_states

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐