ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pymoveit2 实战指南:Python 高效控制机械臂的 MoveIt2 封装库

pymoveit2 实战指南:Python 高效控制机械臂的 MoveIt2 封装库 1. 为什么我最终选择了 pymoveit2 而不是原生 MoveIt2 C 接口机械臂控制这个领域做过的人都知道MoveIt2 是 ROS 2 生态里绕不开的规划框架。但真正上手的时候很多人会卡在一个很现实的问题上MoveIt2 官方教程几乎全是 C 的Python 开发者想快速验证一个抓取逻辑得先啃完几百行 C 样板代码编译一次等好几分钟调试起来更是痛苦。我最初也是硬着头皮写 C 节点后来在一个协作机械臂项目里被逼着找 Python 方案才认真研究了 pymoveit2 这个库。pymoveit2 本质上是对 MoveIt2 C 接口的一层 Python 绑定封装它把 MoveGroupInterface、PlanningSceneInterface 这些核心类通过 pybind11 暴露给 Python同时补上了很多 C 里需要手动处理的细节比如关节状态订阅、碰撞对象管理、笛卡尔路径规划的参数传递。换句话说它让你用 Python 的语法写 MoveIt2 的逻辑但底层跑的还是同一套规划器不存在“Python 版性能差”的问题——规划本身在 C 层执行Python 只负责调用和数据处理。这个库解决的核心痛点有三个。第一是开发效率Python 写机械臂逻辑比 C 快至少三倍尤其是需要频繁调整参数、试错规划路径的阶段。第二是生态融合Python 在视觉、深度学习、数据分析方面的库太丰富了你可以在同一个节点里用 OpenCV 处理图像、用 PyTorch 做位姿估计、再用 pymoveit2 执行抓取不用跨语言通信。第三是学习曲线对于刚接触 ROS 2 和机械臂的开发者Python 的容错性和可读性明显更友好。适合读这篇内容的人我大致分三类。一类是已经有 ROS 2 基础、想快速上手机械臂控制的 Python 开发者一类是做过 MoveIt1 或 ROS 1 机械臂项目、现在要迁移到 ROS 2 Humble 的工程师还有一类是高校里做机器人研究、需要快速搭建实验平台的研究生。如果你连 ROS 2 的 topic、service 概念都还不清楚建议先去补一下基础否则后面讲规划组配置和关节状态订阅时会比较吃力。我下面会从环境搭建、核心接口拆解、实操控制流程、常见问题排查几个维度展开中间会穿插我在实际项目里踩过的坑和验证过的参数。所有代码都基于 ROS 2 Humble Ubuntu 22.04 环境这是目前最稳定的组合Jazzy 虽然更新但部分机械臂驱动还没完全适配。2. 环境搭建从零到能跑通第一个规划请求2.1 ROS 2 Humble 与 MoveIt2 的安装细节Ubuntu 22.04 上装 ROS 2 Humble 的流程网上教程很多但 MoveIt2 的安装有几个容易忽略的点。首先MoveIt2 在 Humble 里的包名和 Foxy 时代不一样二进制安装用sudo apt install ros-humble-moveit这个命令会拉取 moveit_core、moveit_ros_planning、moveit_ros_planning_interface 等一整套依赖。装完之后验证是否成功ros2 pkg list | grep moveit你应该能看到十几个 moveit 相关的包。如果只看到两三个说明安装不完整大概率是 apt 源没更新或者网络中断导致部分包没下下来。接下来是 pymoveit2 的安装。这个库没有发布到 apt 源需要从源码编译。我试过直接 pip install不行因为它依赖 ROS 2 的 ament 构建系统。正确做法是cd ~/ros2_ws/src git clone https://github.com/AndrejOrsula/pymoveit2.git cd ~/ros2_ws rosdep install --from-paths src --ignore-src -r -y colcon build --symlink-install source install/setup.bash这里有个关键点--symlink-install一定要加否则你修改 Python 文件后每次都要重新 build调试效率极低。另外如果你用的是 zsh 而不是 bashsource 命令要改成对应的 setup.zsh。注意编译 pymoveit2 之前确保你的 ROS 2 环境已经 source 过。我见过有人在新终端里直接 colcon build结果找不到 moveit_core 的头文件报一堆 CMake 错误。养成习惯每个新终端先source /opt/ros/humble/setup.bash。2.2 机械臂 URDF 与 MoveIt 配置包的准备pymoveit2 本身不包含任何机械臂模型你需要有自己的 URDF 和 MoveIt 配置包。如果你手头没有真实机械臂可以用 MoveIt2 自带的 demo 机器人先练手ros2 launch moveit2_tutorials demo.launch.py这个命令会启动一个 Panda 机械臂的 RViz 仿真环境。但注意这个 demo 用的是 MoveIt2 的 C 接口我们要用 pymoveit2 控制它需要自己写一个 Python 节点通过 MoveGroupInterface 连接到同一个规划组。如果你有自己的机械臂比如 UR5、Franka、Aubo 等通常厂商会提供 ROS 2 的 description 包和 moveit_config 包。以 UR5 为例你需要确认 moveit_config 里的config/ur5.srdf文件中定义了规划组名称比如manipulator或ur5_arm。这个名称后面在 Python 代码里要用到写错了会直接报 “Planning group not found”。我建议在正式写代码前先用 RViz 的 MotionPlanning 面板手动拖拽一下机械臂确认规划组能正常工作、关节限位合理、碰撞检测没有误报。这一步花十分钟能省掉后面调试 Python 代码时一半的困惑。2.3 Python 虚拟环境与依赖管理虽然 ROS 2 的 Python 包通常直接装在系统环境里但我强烈建议用 venv 隔离项目依赖。原因很简单pymoveit2 依赖 numpy、scipy 这些科学计算库而系统 Python 环境里可能已经有其他版本混在一起容易出问题。创建虚拟环境的命令python3 -m venv ~/venvs/moveit_env source ~/venvs/moveit_env/bin/activate pip install numpy scipy transforms3d但这里有个坑ROS 2 的 Python 包如 rclpy不在 PyPI 上venv 里默认访问不到。解决办法是在激活 venv 后手动把 ROS 2 的 Python 路径加到 PYTHONPATHexport PYTHONPATH/opt/ros/humble/lib/python3.10/site-packages:$PYTHONPATH或者更优雅的方式用--system-site-packages参数创建 venvpython3 -m venv --system-site-packages ~/venvs/moveit_env这样 venv 里既能用 pip 装的包也能访问系统 ROS 2 的包。实测下来第二种方式更省心推荐使用。3. pymoveit2 核心接口拆解与参数详解3.1 MoveIt2 类的初始化与规划组选择pymoveit2 的核心类是MoveIt2位于pymoveit2/moveit2.py。初始化时最重要的两个参数是node和joint_names。node是 rclpy 的 Node 对象joint_names是机械臂所有关节的名称列表顺序必须和 URDF 里定义的一致。from pymoveit2 import MoveIt2 import rclpy rclpy.init() node rclpy.create_node(moveit2_control) joint_names [ shoulder_pan_joint, shoulder_lift_joint, elbow_joint, wrist_1_joint, wrist_2_joint, wrist_3_joint, ] moveit2 MoveIt2( nodenode, joint_namesjoint_names, base_link_namebase_link, end_effector_nametool0, group_nameur_manipulator, )这里的base_link_name和end_effector_name决定了笛卡尔空间规划时的参考坐标系。如果你只做关节空间规划这两个参数不影响但一旦调用move_to_pose它们就至关重要。group_name必须和 SRDF 文件里定义的规划组名称完全一致大小写敏感。我踩过的一个坑是URDF 里关节名称带了前缀比如robot1_shoulder_pan_joint但我在 Python 里写的是shoulder_pan_joint结果初始化不报错但规划时一直失败日志里只显示 “Joint state not received”。排查了半天才发现是名称不匹配。所以初始化后最好打印一下moveit2.joint_names确认。3.2 关节空间规划move_to_configuration 的使用要点关节空间规划是最基础也最稳定的控制方式。pymoveit2 提供了move_to_configuration方法传入目标关节角度列表即可target_joints [0.0, -1.57, 1.57, -1.57, -1.57, 0.0] moveit2.move_to_configuration(target_joints) moveit2.wait_until_executed()wait_until_executed()是阻塞调用会一直等到规划执行完成或失败。如果你不想阻塞主线程可以用moveit2.execute()配合回调但新手建议先用阻塞方式逻辑简单。这里的关键参数是target_joints的顺序必须和初始化时的joint_names一致。另外角度单位是弧度不是度。我见过有人传了[0, -90, 90, -90, -90, 0]结果机械臂直接撞限位。养成习惯所有角度先用math.radians()转换。还有一个隐藏参数max_velocity和max_acceleration在move_to_configuration里可以通过**kwargs传入moveit2.move_to_configuration( target_joints, max_velocity0.5, max_acceleration0.5, )这两个值默认是 1.0表示最大速度的百分比。实际调试时我建议先从 0.2 开始确认路径安全后再逐步提高。尤其是大型机械臂全速运行时的惯性很大急停容易触发保护。3.3 笛卡尔空间规划move_to_pose 的参数与坐标系陷阱笛卡尔空间规划是 pymoveit2 最常用的功能也是坑最多的部分。核心方法是move_to_posefrom geometry_msgs.msg import PoseStamped pose PoseStamped() pose.header.frame_id base_link pose.pose.position.x 0.3 pose.pose.position.y 0.1 pose.pose.position.z 0.4 pose.pose.orientation.x 0.0 pose.pose.orientation.y 0.707 pose.pose.orientation.z 0.0 pose.pose.orientation.w 0.707 moveit2.move_to_pose(pose) moveit2.wait_until_executed()第一个陷阱是frame_id。它必须是机械臂基座坐标系通常是base_link或world。如果你写的是tool0规划器会尝试把目标位姿转换到基座坐标系但转换结果可能完全不是你想要的。我建议在 RViz 里先确认基座坐标系的名称再填进去。第二个陷阱是四元数的归一化。上面的orientation如果没归一化规划器可能报 “Quaternion not normalized”。pymoveit2 内部会做一次检查但最好自己用transforms3d或scipy.spatial.transform处理from scipy.spatial.transform import Rotation as R quat R.from_euler(xyz, [0, 1.57, 0]).as_quat() pose.pose.orientation.x quat[0] pose.pose.orientation.y quat[1] pose.pose.orientation.z quat[2] pose.pose.orientation.w quat[3]第三个陷阱是规划失败时的重试。move_to_pose默认只规划一次如果失败就返回 False。实际项目中我通常会写一个重试循环每次微调目标位姿或增加规划时间for attempt in range(3): success moveit2.move_to_pose(pose, planner_idRRTConnect) if success: break pose.pose.position.z 0.01planner_id可以指定规划器常用的有RRTConnect、RRTstar、PRM。RRTConnect 速度最快适合大多数场景RRTstar 路径更优但耗时更长。3.4 碰撞对象管理与场景更新pymoveit2 提供了add_collision_box、add_collision_mesh等方法用于在规划场景里添加障碍物。这在抓取任务里非常关键否则规划器会认为空间是空的生成的路径可能穿过桌面或货架。moveit2.add_collision_box( idtable, size[1.0, 1.0, 0.05], position[0.5, 0.0, -0.025], quat_xyzw[0.0, 0.0, 0.0, 1.0], )size是长宽高单位米position是中心点坐标quat_xyzw是四元数旋转。添加后规划器会自动把这块区域视为不可通行。如果你发现规划路径绕得很奇怪先检查碰撞对象是不是加多了或者位置偏了。移除碰撞对象用remove_collision_object(id)。在动态场景里比如传送带上的物体位置会变你需要先移除旧的再添加新的。注意频繁添加移除会影响规划性能建议批量操作。提示碰撞对象的id必须唯一。如果重复添加同一个 idpymoveit2 会先移除旧的再添加新的但日志里会有警告。我习惯用object_前缀加时间戳避免冲突。4. 完整实操用 pymoveit2 控制 UR5 完成一次抓取4.1 场景搭建与节点初始化假设你已经有一个 UR5 的 MoveIt 配置包并且能在 RViz 里正常规划。我们写一个完整的 Python 节点流程是初始化、添加桌面碰撞对象、移动到预抓取位姿、直线下降到抓取位姿、闭合夹爪、提升、移动到放置位姿、松开夹爪。先创建节点和 MoveIt2 对象import rclpy from rclpy.node import Node from pymoveit2 import MoveIt2 from geometry_msgs.msg import PoseStamped from scipy.spatial.transform import Rotation as R import math class UR5GraspNode(Node): def __init__(self): super().__init__(ur5_grasp_node) self.joint_names [ shoulder_pan_joint, shoulder_lift_joint, elbow_joint, wrist_1_joint, wrist_2_joint, wrist_3_joint, ] self.moveit2 MoveIt2( nodeself, joint_namesself.joint_names, base_link_namebase_link, end_effector_nametool0, group_nameur_manipulator, ) self.moveit2.max_velocity 0.3 self.moveit2.max_acceleration 0.3这里我把max_velocity和max_acceleration设成 0.3因为抓取任务对精度要求高速度太快容易在接近物体时产生振动。实测下来0.3 是一个兼顾效率和安全的经验值。4.2 预抓取位姿的计算与规划预抓取位姿是物体上方 10 厘米的位置姿态和抓取时一致。假设物体在(0.4, 0.1, 0.05)抓取姿态是工具朝下def make_pose(self, x, y, z, roll, pitch, yaw): pose PoseStamped() pose.header.frame_id base_link pose.pose.position.x x pose.pose.position.y y pose.pose.position.z z quat R.from_euler(xyz, [roll, pitch, yaw]).as_quat() pose.pose.orientation.x quat[0] pose.pose.orientation.y quat[1] pose.pose.orientation.z quat[2] pose.pose.orientation.w quat[3] return pose pre_grasp self.make_pose(0.4, 0.1, 0.15, math.pi, 0, 0) self.moveit2.move_to_pose(pre_grasp) self.moveit2.wait_until_executed()注意这里的rollmath.pi表示工具绕 X 轴旋转 180 度让夹爪朝下。如果你的 URDF 里 tool0 的默认方向不同这个值需要调整。我建议先在 RViz 里手动设置一个目标位姿看四元数是多少再反推欧拉角。规划到预抓取位姿时如果失败大概率是因为路径经过奇异点或者碰撞。UR5 的奇异点通常在肘部完全伸直或腕部对齐时。解决办法是调整预抓取位姿的 Y 坐标让机械臂稍微偏离奇异区域。4.3 直线下降与夹爪控制从预抓取位姿到抓取位姿最好用直线规划避免机械臂走弧线撞到物体。pymoveit2 提供了move_to_pose的cartesian参数grasp_pose self.make_pose(0.4, 0.1, 0.05, math.pi, 0, 0) self.moveit2.move_to_pose(grasp_pose, cartesianTrue, cartesian_max_step0.01) self.moveit2.wait_until_executed()cartesian_max_step0.01表示每步最大 1 厘米步长越小路径越平滑但规划时间越长。实测 0.01 在 UR5 上表现稳定再小就明显卡顿。夹爪控制不在 pymoveit2 的范围内需要单独发布话题或调用服务。以 Robotiq 2F-85 为例通常是往/gripper/command发一个 Float64 消息from std_msgs.msg import Float64 self.gripper_pub self.create_publisher(Float64, /gripper/command, 10) self.gripper_pub.publish(Float64(data0.0)) # 闭合这里data0.0表示完全闭合data1.0表示完全张开。不同夹爪的协议不一样具体看厂商文档。我建议在抓取前先测试夹爪的开合范围确认不会把物体夹变形。4.4 提升与放置的路径规划抓取完成后先直线提升到预抓取位姿再规划到放置位姿self.moveit2.move_to_pose(pre_grasp, cartesianTrue, cartesian_max_step0.01) self.moveit2.wait_until_executed() place_pose self.make_pose(0.2, -0.3, 0.15, math.pi, 0, 0) self.moveit2.move_to_pose(place_pose) self.moveit2.wait_until_executed() self.gripper_pub.publish(Float64(data1.0)) # 松开放置位姿的规划可以用非笛卡尔模式因为空中路径不需要严格直线。但如果放置区域上方有障碍物还是建议用笛卡尔模式。整个流程跑下来从初始化到完成抓取UR5 大约需要 15 到 20 秒具体取决于路径长度和规划器。如果发现某一步特别慢可以在 RViz 里打开 Planning 面板看规划时间花在哪里。5. 常见问题排查与性能优化实录5.1 规划失败从日志到根因的排查路径规划失败是 pymoveit2 使用中最常见的问题。错误信息通常很模糊比如 “Failed to plan” 或 “No motion plan found”。我的排查顺序是这样的第一步看 RViz 里的目标位姿是否可达。如果目标位姿在机械臂工作空间外规划器直接放弃。UR5 的臂展约 850 毫米但实际可达范围受关节限位影响通常只有 700 毫米左右。第二步检查碰撞对象。如果场景里有桌面、货架等障碍物目标位姿可能被包围。临时移除所有碰撞对象再规划一次如果成功说明是碰撞问题。第三步换规划器。RRTConnect 在狭窄空间里容易失败换成 RRTstar 或 PRM 试试。pymoveit2 支持通过planner_id参数指定self.moveit2.move_to_pose(pose, planner_idRRTstar)第四步增加规划时间。默认规划时间是 5 秒复杂场景下不够self.moveit2.move_to_pose(pose, planning_time10.0)我整理了一个排查速查表现象可能原因解决方法规划失败无详细日志目标不可达在 RViz 里手动拖拽验证规划失败日志提示碰撞碰撞对象阻挡移除或调整碰撞对象规划成功但执行失败控制器未连接检查 ros2 control 状态路径绕远规划器选择不当换 RRTstar 或调整代价权重执行时抖动速度加速度过高降低 max_velocity 到 0.25.2 关节状态丢失与 TF 变换异常pymoveit2 依赖/joint_states话题获取当前关节角度。如果这个话题没有数据move_to_configuration会一直等待。检查方法ros2 topic hz /joint_states如果频率是 0说明机械臂驱动没启动或者 joint_state_publisher 没运行。真实机械臂通常由驱动节点发布仿真环境由 joint_state_publisher_gui 发布。TF 变换异常通常表现为 “Lookup would require extrapolation into the future”。这是因为目标位姿的时间戳比当前 TF 缓存的时间晚。解决办法是把pose.header.stamp设为self.get_clock().now().to_msg()或者直接留空让规划器用最新时间。5.3 性能优化让规划从 5 秒降到 1 秒规划速度直接影响用户体验。我通过以下几个调整把 UR5 的平均规划时间从 5 秒降到了 1 秒左右。第一简化碰撞对象。用 box 代替 meshbox 的碰撞检测计算量小得多。如果必须用 mesh先用工具简化面数。第二调整规划器的采样分辨率。在ompl_planning.yaml里把longest_valid_segment_fraction从 0.05 改成 0.1减少碰撞检测次数。第三预热规划器。在正式规划前先发一个当前位姿的规划请求让 OMPL 加载状态空间。这个技巧在第一次规划特别慢的时候很有效。第四用多线程。pymoveit2 的move_to_pose是阻塞的但你可以把规划放到单独的线程里主线程继续处理传感器数据。不过要注意线程安全MoveIt2 的接口不是完全线程安全的。注意优化规划速度时不要牺牲安全性。我见过有人把longest_valid_segment_fraction调到 0.3结果机械臂直接穿过薄板障碍物。0.1 是一个比较稳妥的上限。5.4 从仿真到真机的迁移注意事项仿真里跑通的代码直接放到真机上大概率会出问题。我总结了几个必须检查的点。第一关节限位。仿真模型的限位通常比真机宽松真机上撞限位会触发急停。把 URDF 里的限位参数和真机手册核对一遍必要时在 pymoveit2 里加软限位。第二速度限制。仿真里可以用 1.0 的速度真机上建议从 0.1 开始逐步加到 0.3。大型机械臂的惯性很大急停时的冲击可能损坏减速器。第三夹爪延迟。仿真里夹爪是瞬间开合的真机有 0.5 到 1 秒的延迟。抓取流程里要在夹爪命令后加time.sleep(1.0)否则机械臂会在夹爪还没闭合时就提升物体直接掉落。第四坐标系标定。仿真里 base_link 和 world 是重合的真机上需要标定。用激光跟踪仪或手眼标定法把 base_link 到 world 的变换矩阵写进 URDF 或 TF 发布节点。我在一个 UR5 真机项目里因为忽略了夹爪延迟连续掉了三次物体后来加了 1.5 秒等待才稳定。这个坑很隐蔽因为仿真里完全没问题。6. 进阶技巧用 pymoveit2 做视觉引导抓取6.1 手眼标定与点云处理视觉引导抓取的核心是把相机坐标系下的物体位姿转换到机械臂基座坐标系。假设你用 RealSense D435 装在腕部需要先做手眼标定得到tool0到camera_link的变换矩阵。标定可以用 easy_handeye2 或 moveit_calibration 包。标定完成后把结果写进 URDF 或发布为静态 TF。然后在 Python 里订阅点云话题用 Open3D 或 PCL 做平面分割和聚类提取物体的位姿。import open3d as o3d from sensor_msgs.msg import PointCloud2 import sensor_msgs_py.point_cloud2 as pc2 def cloud_callback(self, msg): points list(pc2.read_points(msg, field_names(x, y, z), skip_nansTrue)) pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) plane_model, inliers pcd.segment_plane(distance_threshold0.01, ransac_n3, num_iterations100) object_cloud pcd.select_by_index(inliers, invertTrue) centroid object_cloud.get_center() return centroid这段代码从点云里分割出桌面剩下的就是物体。centroid是物体在相机坐标系下的中心点再通过 TF 转换到base_link就可以传给move_to_pose。6.2 动态抓取中的实时位姿更新如果物体在传送带上移动你需要实时更新目标位姿。pymoveit2 的move_to_pose是单次规划不适合动态目标。解决办法是用move_to_pose的异步版本或者自己写一个循环每 100 毫秒更新一次目标位姿。while not self.reached: centroid self.get_object_centroid() target_pose self.transform_to_base(centroid) self.moveit2.move_to_pose(target_pose, cartesianTrue, cartesian_max_step0.005) self.moveit2.wait_until_executed() if self.distance_to_target() 0.01: self.reached True这个循环的缺点是每次都要等规划执行完实际频率可能只有 2 到 3 赫兹。如果物体移动速度快需要更高级的视觉伺服方案但那超出了 pymoveit2 的范围。6.3 多规划组与双臂协同pymoveit2 支持多个 MoveIt2 实例每个实例对应一个规划组。双臂机器人可以创建两个实例分别控制左臂和右臂left_arm MoveIt2(node, left_joints, base_link, left_tool0, left_arm) right_arm MoveIt2(node, right_joints, base_link, right_tool0, right_arm)两个实例共享同一个节点和规划场景但规划组独立。协同任务里需要手动处理碰撞避免比如左臂规划时把右臂的当前位姿作为碰撞对象加入场景。这个逻辑比较复杂建议先用单臂跑通再扩展到双臂。7. 我踩过的那些坑和最后分享的几个技巧第一个坑是 pymoveit2 的版本兼容性。GitHub 上的 main 分支更新很快有时候会引入不兼容的改动。我建议在项目里锁定一个 commit比如git checkout commit_hash避免某天 pull 之后代码跑不起来。我遇到过move_to_pose的参数名从target_pose改成pose导致整个项目报错。第二个坑是 Python 的 GIL 和 ROS 2 回调的交互。pymoveit2 内部用了 rclpy 的 spin如果你在主线程里做耗时计算回调会阻塞导致关节状态更新延迟。解决办法是把耗时计算放到单独的线程或者用MultiThreadedExecutor。第三个坑是日志级别。pymoveit2 默认的日志级别是 INFO规划失败时只打印一行 “Failed to plan”。把日志级别调到 DEBUG能看到 OMPL 的详细规划过程对排查问题很有帮助rclpy.logging.set_logger_level(moveit2, rclpy.logging.LoggingSeverity.DEBUG)最后分享一个实用技巧用moveit2.compute_cartesian_path做直线规划时如果中间有障碍物规划会失败。这时候可以分段规划先规划到障碍物前方再绕过障碍物最后到目标点。pymoveit2 没有直接提供分段接口但你可以手动调用多次move_to_pose每次传一个中间点。还有一个技巧是保存和加载规划场景。pymoveit2 支持get_planning_scene和set_planning_scene可以把当前场景序列化成字符串下次启动时直接加载省去重新添加碰撞对象的时间。这在固定工位的抓取任务里很实用。我在实际项目里从第一次接触 pymoveit2 到稳定控制 UR5 完成抓取大约花了两周时间其中一半时间在踩环境配置和坐标系转换的坑。如果你刚开始建议先用 Panda demo 跑通关节空间规划再逐步加笛卡尔规划和碰撞对象最后上视觉。每一步都确认稳定后再往下走比一次性写完所有代码再调试要快得多。
返回列表