车间里最怕听到的声音,就是主轴撞上夹具那一瞬间的闷响。更麻烦的是,这种事往往发生在夜里调试新程序的时候——操机师傅盯着屏幕,手已经按上急停,但程序跑得比人眼快,等反应过来,刀头和工件已经抱在一起了。所以我从很早开始就养成了一个习惯:任何程序放到真实机床之前,先在虚拟环境里完整跑一遍。今天想聊的是我这段时间用 Unity 做的一套“机床+机器人联合加工防碰撞仿真”方案,它不是商业软件那种大而全的系统,而是从零手写的一套完整逻辑,包含模型处理、轴运动驱动、碰撞检测、安全联锁和轨迹回放,核心代码都可以拆开讲清楚。适合正在做数字孪生、虚拟调试,或者对机器人加工安全方案感兴趣的工程师参考。
这套东西实际解决什么问题?简单说,就是在把程序下发到机床和机器人控制器之前,先用 Unity 把整个加工过程模拟一遍,让机床的轴和机器人的手臂在虚拟空间里按真实运动轨迹跑起来,实时计算它们之间的距离。一旦逼近安全临界值,就提前报警甚至触发联锁信号。这样既能规避设备损坏风险,也能大幅缩短现场调试时间,还能在方案评审时给非技术人员直观展示加工流程。
1. 为什么坚持用 Unity 做加工防碰撞:选型逻辑与系统边界
1.1 专业 CAM 仿真和 Unity 之间,差的不是功能而是场景
很多同行第一反应是:防碰撞仿真不是有 VERICUT、RobotStudio、DELMIA 这些专业软件吗,为什么还要用 Unity 自己造轮子?
我的实际感受是:专业软件确实强,尤其在刀具路径级碰撞验证、金属切削过程模拟、五轴联动微观干涉检查上,Unity 完全不是对手。但如果你是做整线方案、多设备联合动作预演、给客户演示工艺流程、或者要把仿真数据接入上位机做数字孪生,专业软件往往显得很笨重——价格高、模型格式封闭、二次开发门槛高,想跟 PLC 或自定义算法对接更是麻烦。
Unity 的优势恰恰在这里:模型导入方便,脚本完全可控,界面可以按项目需求定制,性能优化空间大。它更像是“虚拟设备联合调试平台”,而不是“刀具路径验证工具”。这决定了它的使用场景:面向工艺方案验证、生产节拍预演、异常干涉排查、以及向数字化产线做技术输出的底座。
1.2 我把系统拆成四个模块
刚开始我也想着一步到位做一套大系统,后来发现不行,代码会越写越乱。最后我按照职责拆成四个独立模块,每个模块只关心自己的事:
| 模块 | 职责 | 实现方式 |
|---|---|---|
| 模型层 | 机床、机器人、夹具、毛坯、刀具的模型和碰撞体 | FBX 导入 + 层级整理 + Collider 配置 |
| 运动层 | 驱动各轴/关节按 NC 程序或点位轨迹运动 | MachineAxis / RobotArm 脚本 |
| 检测层 | 计算各部件之间的最小距离,输出碰撞风险 | AABB 粗筛 + ClosestPoint 精测 |
| 联锁层 | 根据距离、速度、状态机输出报警/急停信号 | SafetyMonitor 状态机 |
这样的好处是每一层都可以单独调试。比如模型层不对,碰撞检测无从谈起;运动层不对,检测层再准也没意义。我建议你做的时候也先按这个顺序推进,不要一上来就写状态机。
1.3 什么场景下不建议用这套方案
直说结论:如果你的目标是验证一个复杂五轴刀路在真实金属切削中会不会过切、会不会有让刀,那不要用 Unity,老老实实用 VERICUT 或 NX CAM 的仿真模块。Unity 的碰撞检测本质上是几何体之间的距离判断,它不关心切削力学、刀具变形、残余应力这些物理过程。
我自己的做法是两条腿走路:专业 CAM 软件负责“刀路级”的切削验证,Unity 负责“整机联动级”的干涉和节拍验证。前者保证程序切削没问题,后者保证在真实产线上,主轴、转台、机器人、夹具动起来之后不会撞在一起。搞清楚这个边界,你的方案才不会被质疑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 机床与机器人模型导入后的坐标统一与运动骨架搭建
2.1 工业模型的毫米单位问题
从 SolidWorks、UG、CATIA 导出的模型,最常见的问题是单位。CAD 里默认单位是毫米,Unity 里 1 个单位默认等于 1 米。如果你把模型直接拖进 Unity,一个 2500mm 的机床床身变成一个 2500 单位的“巨人”,后面所有尺寸换算都会出问题。
处理方式其实不复杂:在 FBX 导入设置里把 Scale Factor 设为 0.001,这样 1 毫米在 Unity 中就是 0.001 个单位。但这里有个容易忽略的点:如果你的模型在导出前已经做过缩放,或者原点是乱的,光改 Scale Factor 不够。我建议在建模软件里就把模型原点归到机床零点或机器人基座中心,轴向也对齐好,再导出 FBX。原点不对,后面所有运动驱动都会出现“莫名其妙的偏移”。
模型导入后先别急着做碰撞,先把所有模型集中在场景里,目视检查一遍每个部件的原点是否合理。一个小技巧:创建一个空物体代表机床零点,然后把机床模型作为子物体,在 Inspector 里手动把位置对齐到原点,这样最稳妥。
2.2 机床轴运动:用 Transform 还是用刚体?
这是很多 Unity 新手会纠结的问题。我用的是 Transform 直接驱动,不用 Rigidbody,也不开重力。
原因很简单:机床轴是刚性运动,位置完全由数控系统决定,不存在物理碰撞后的反弹、摩擦、惯性这些效果。如果开了刚体和碰撞响应,反而可能因为碰撞体的微小穿透产生“弹开”的假动作,完全不符合实际。所以运动层就老老实实用 Transform,碰撞检测交给独立的距离计算逻辑。
我封装了一个很基础的机床轴类:
csharp复制using UnityEngine;
public class MachineAxis : MonoBehaviour
{
public enum AxisType { Linear, Rotational }
public AxisType axisType = AxisType.Linear;
public Vector3 moveAxis = Vector3.right; // 线性轴移动方向
public float currentValue = 0f;
public float minValue = 0f;
public float maxValue = 1000f;
public void MoveTo(float targetValue)
{
currentValue = Mathf.Clamp(targetValue, minValue, maxValue);
if (axisType == AxisType.Linear)
{
transform.localPosition = moveAxis * currentValue;
}
else
{
transform.localRotation = Quaternion.AngleAxis(currentValue, moveAxis);
}
}
}
这段代码做的事情很简单:给定一个目标坐标值,钳制到行程范围内,然后把 Transform 的 localPosition 或 localRotation 设置到位。注意这里用的是 localPosition,说明轴是挂在父节点下的子物体,这样机床坐标系和机器人基座坐标系才能统一。
实际项目里,NC 程序不是直接给坐标值的,而是 G 代码。我最简实现是写一个 GCodePlayer,解析 G00、G01 和 F 指令,按进给速度把坐标目标值发送给轴。插补不用做得很精细,50ms 一个步长、线性插补就够了,因为防碰撞仿真关心的是“空间位置和相对距离”,不是切削精度。如果要在仿真里体现加减速,可以在步进之间加入速度斜坡。
2.3 机器人 6 轴关节驱动与轨迹回放
机器人的运动驱动和机床轴稍有不同。机床轴是直线/旋转的单轴,机器人是多个旋转关节的串联结构。在 Unity 里,正确做法是把机器人模型按关节层级拆开:基座 -> J1 -> J2 -> J3 -> J4 -> J5 -> J6 -> 末端法兰。
每个关节是一个子物体,旋转方向按机器人实际轴向设置。然后写一个 RobotArm 类来统一驱动:
csharp复制using UnityEngine;
public class RobotArm : MonoBehaviour
{
public Transform[] joints; // 按顺序:J1~J6
public float[] minAngles; // 关节下限
public float[] maxAngles; // 关节上限
private float[] currentAngles;
void Start()
{
int count = joints.Length;
currentAngles = new float[count];
}
public void SetJointAngles(float[] angles)
{
for (int i = 0; i < joints.Length; i++)
{
currentAngles[i] = Mathf.Clamp(angles[i], minAngles[i], maxAngles[i]);
// 简化:默认绕Z轴旋转,实际按每个关节的local axis调整
joints[i].localRotation = Quaternion.Euler(0f, 0f, currentAngles[i]);
}
}
public Vector3 GetEndPosition()
{
// 末端在关节链最后一个子物体下
return joints[joints.Length - 1].position;
}
}
有人可能会问,为什么不做逆运动学?我实际做完发现,做防碰撞仿真时,你根本不需要在 Unity 里从头算 IK。工程上更高效的做法是:从机器人仿真软件或示教器里导出点位数据,每个点位包含 6 个关节角,存成 CSV,然后在 Unity 里按序回放。这正好对应了现场工程师常说的“给 ABB 机器人添加点位”——实际上就是在控制器里记录或生成点位,再连接到动作序列。
这么做的好处是:轨迹完全和真实机器人一致,不需要担心 IK 解算偏差,也不会出现“Unity 里能走,真机走不了”的问题。Delta 并联机器人这类构型,关节驱动逻辑不一样,它的运动是末端平台通过三条主动臂联动,需要解并联运动学的正逆解,不能用上面的串联关节法直接套。这是另一套东西,建议单独处理。
2.4 运动骨架搭建时的关键检查点
模型层级搞对了,运动质感就成功了一半。我在这一步踩过的坑不少,给你列几个检查点:
- 检查父子关系:机床轴要挂在对应的导轨滑座下,机器人关节要按连接顺序嵌套,不能把所有零件平铺在一个层级下。
- 检查旋转中心:每个关节的旋转中心必须对齐真实铰点,否则转起来像脱臼。
- 给关键位置加空锚点:比如主轴端面、机器人末端法兰、夹具夹持点,这些位置后面要做距离检测,有独立锚点会方便很多。
- 大模型一定要拆开:不要整个机床一个 Mesh 挂一个物体,要拆成床身、工作台、主轴箱、刀库等,每个可动部件独立成物体,才能挂独立脚本和碰撞体。
3. 碰撞检测的两层方案:AABB 快速筛选与 ClosestPoint 精确测距
3.1 为什么原生 Collider 做不了工业防碰撞
很多第一次接触这个项目的人会问:Unity 自带 Collider 和 OnCollisionEnter,直接用不就行了吗?
问题在于,原生物理引擎的碰撞是“接触检测”,不是“距离预测”。你要的是在碰撞发生之前就能知道“还有多远会撞”,而不是撞上了再触发事件。另外,高速运动的薄壁物体还可能出现“隧穿效应”——上一帧在壁的一侧,下一帧已经穿到另一侧,物理引擎根本没检测到碰撞。机床加工场景里,主轴进给速度很高,薄壁零件又很常见,隧穿风险很大。
还有一点:MeshCollider 在 Unity 里性能开销很高,而且非凸网格的碰撞计算非常费。机床模型、机器人手臂有很多复杂外形,直接挂 MeshCollider,场景一复杂帧率就崩。所以我最终确定了两层检测方案:先快速筛选可能的干涉对,再对候选对做精确测距。
3.2 快速筛选层:AABB 逐对检测
第一层用 AABB(轴向对齐包围盒),原理很简单:用每个物体在世界空间里的轴对齐包围盒做相交判断,如果两个包围盒距离还很远,那内部物体肯定碰不上,直接跳过,不进入精确检测。
AABB 的好处是计算极快,只有 6 次比较运算。但要注意,Unity 的 Renderer.bounds 返回的是世界空间 AABB,它会包含旋转后的物体范围,所以实际是比物体本身更大一圈。这在快速筛选阶段是可以接受的,因为它的作用就是把“明显安全”的对子剔除掉,宁可多留一些候选对,也不能漏掉真正危险的。
我封装了一个工具类:
csharp复制using UnityEngine;
public class BoundsUtil
{
public static bool AreAabbClose(Bounds a, Bounds b, float threshold)
{
// 在aabb的基础上外扩阈值,判断是否相交
a.Expand(threshold);
b.Expand(threshold);
return a.Intersects(b);
}
public static float AabbDistance(Bounds a, Bounds b)
{
Vector3 delta = Vector3.zero;
for (int i = 0; i < 3; i++)
{
float aMin = a.min[i];
float aMax = a.max[i];
float bMin = b.min[i];
float bMax = b.max[i];
if (aMax < bMin)
delta[i] = bMin - aMax;
else if (bMax < aMin)
delta[i] = aMin - bMax;
else
delta[i] = 0f;
}
return delta.magnitude;
}
}
这个 AabbDistance 返回两个轴对齐包围盒之间的最短距离:两个盒子分开时是正值,相交时是 0。你在 Broad Phase 里用这个值做初筛:如果大于某个安全距离(比如 200mm),说明这对物体还有很大余量,不进入精确检测。
3.3 精确测距层:Collider.ClosestPoint
经过快速筛选后,剩下的候选对需要精确测距。Unity 的 Collider.ClosestPoint(Vector3 point) 方法很实用:它返回碰撞体表面距离传入点最近的点。所以计算两个碰撞体之间的最短距离,只需要:
csharp复制using UnityEngine;
public static class DistanceDetector
{
public static bool TryGetMinDistance(Collider a, Collider b, out float distance)
{
if (a == null || b == null || !a.enabled || !b.enabled)
{
distance = float.MaxValue;
return false;
}
// 用AABB先做快速剔除
Bounds ba = a.bounds;
Bounds bb = b.bounds;
if (BoundsUtil.AabbDistance(ba, bb) > 500f)
{
distance = float.MaxValue;
return false;
}
// 精确测距:互相求最近点,取两个距离的较小值
Vector3 p1 = b.ClosestPoint(a.transform.position);
Vector3 p2 = a.ClosestPoint(b.transform.position);
float d1 = Vector3.Distance(p1, b.transform.position) > Vector3.Distance(p1, a.transform.position)
? Vector3.Distance(p1, a.transform.position)
: Vector3.Distance(p1, b.transform.position);
float d2 = Vector3.Distance(p2, a.transform.position);
distance = Mathf.Min(d1, d2);
return true;
}
}
这里有一个细节值得说明:Collider.ClosestPoint 的性能和 Collider 的类型关系很大。最简单的是 BoxCollider、SphereCollider、CapsuleCollider,计算很快;最贵的是 MeshCollider,尤其非凸的。所以我的实践是:把机床主轴、刀柄、机器人手臂这些关键部位,用若干个 Box 或 Capsule 组合来近似,而不是直接上 MeshCollider。精度损失很小,性能提升巨大。
从现场工程师的角度看,你不需要刀柄上的每个螺纹特征都参与碰撞检测,你只需要知道“刀柄圆柱”和“夹具主体”之间还有多少距离。这个思路一定要清晰。
3.4 场景轮询策略与性能预估
碰撞检测不是每帧都要对全部物体对跑一遍。我的做法是在场景里维护一个“危险部件对”列表,比如:
- 机床主轴包围盒 vs 机器人末端法兰
- 机床工作台夹具 vs 机器人抓手
- 刀库换刀臂 vs 机器人手臂
每个部件对包含两个 Collider。然后每 0.1 秒轮询一次列表,而不是每帧。为什么 0.1 秒就够了?因为加工进给速度再快,100ms 内的相对位移也就是几毫米到几十毫米,不会从“安全距离”直接跳进碰撞区。如果要做实时高速监测,可以动态调整轮询间隔,但没必要每帧都算。
15 个部件对,每对做一次 AABB 判断 + 有限的 ClosestPoint 计算,在 PC 上性能开销可以忽略不计。如果场景很复杂,我可以把距离计算放到 Unity Job System 里并行化。但第一版没必要,先用单线程把逻辑跑通,用 Profiler 看耗时,再决定是否优化。
4. 安全距离的动态计算与状态机联锁:核心防碰撞代码说明
4.1 防碰撞的本质是“临界距离预警”,不是“碰到了再停”
这是整套系统设计理念里最重要的一点。如果等到两个物体接触才报警,那报警毫无意义——已经撞上了。防碰撞系统必须根据当前运动速度提前计算“安全刹车距离”,在距离小于所需刹车距离时发出预警。
工业上有一个简化公式:
所需最小安全距离 = 当前速度 × 系统响应时间 + 制动距离 + 附加裕量
举个例子:主轴进给速度是 2000mm/min,约等于 33.3mm/s;控制系统从检测到报警到最终执行急停的总响应时间是 200ms,那么响应时间内走过的距离就是 6.7mm。再加上机械制动距离 50mm,附加裕量 10mm,最小安全距离就是 66.7mm。也就是说,当检测到主轴与障碍物之间的距离低于 66.7mm 时,必须发出急停信号。
不同轴的速度不一样,所以不能用一个固定距离做全局报警线。这也是我坚持用动态安全距离而不是固定 100mm/50mm 这种一刀切数值的原因。
4.2 可调速的 SafetyMonitor 状态机
基于上面的思路,我写了一个 SafetyMonitor 类,核心是状态机:
csharp复制using UnityEngine;
using UnityEngine.Events;
public class SafetyMonitor : MonoBehaviour
{
public enum SafetyState { Safe, Warning, Danger }
[Header("检测配置")]
public DistanceDetector distanceDetector;
public float warningMargin = 100f; // 警告距离余量(mm)
public float dangerMargin = 50f; // 危险距离余量(mm)
public float maxMoveSpeed = 2000f; // 当前部件最大相对速度(mm/min)
public float responseTime = 0.2f; // 系统响应时间(s)
public float brakeDistance = 30f; // 机械制动距离(mm)
public float extraMargin = 10f; // 附加裕量(mm)
[Header("事件")]
public UnityEvent<SafetyState> onStateChanged;
private SafetyState _currentState = SafetyState.Safe;
public SafetyState CurrentState => _currentState;
float GetDynamicDangerDistance()
{
float speedMmPerSec = maxMoveSpeed / 60f;
return speedMmPerSec * responseTime + brakeDistance + extraMargin;
}
void Update()
{
// 计算最小距离,由外部注入
float minDistance = distanceDetector.GetCurrentMinDistance();
float dynamicDanger = GetDynamicDangerDistance();
float dynamicWarning = dynamicDanger + warningMargin;
SafetyState newState;
if (minDistance <= dynamicDanger)
newState = SafetyState.Danger;
else if (minDistance <= dynamicWarning)
newState = SafetyState.Warning;
else
newState = SafetyState.Safe;
if (newState != _currentState)
{
_currentState = newState;
onStateChanged?.Invoke(_currentState);
Debug.Log($"[SafetyMonitor] 状态变化: {newState}, 当前最小距离: {minDistance:F1}mm");
}
}
}
注意 GetDynamicDangerDistance 里把当前相对速度作为输入,这样当主轴高速进给时,危险距离自动变大;当机器人低速调整时,危险距离自动缩小。这样的状态机不会在低速时频繁误报。
4.3 迟滞区间与连续帧确认:防止临界抖动误报
机械调试时最头疼的问题,是传感器在临界值附近来回跳动,导致报警一会儿亮一会儿灭。解决这个问题有两个常用方法:一是迟滞区间,二是连续帧确认。
迟滞区间的思想是:进入危险状态的距离是 66.7mm,但退出危险状态的距离要大于 70mm,这样状态切换不会因为在 66.7mm 附近微小抖动而来回跳变。我把这个逻辑放到状态机里:
csharp复制// 在状态机内部增加阈值偏移即可
float exitDanger = dynamicDanger + 5f; // 退出危险状态的阈值比进入时高5mm
if (_currentState == SafetyState.Danger)
{
if (minDistance > exitDanger)
newState = SafetyState.Warning;
}
连续帧确认也很简单:不在一帧内改变状态,而是要连续 3~5 帧都满足条件才切换。加一个计数器就可以实现。这样即使有一帧因为数据抖动出现异常偏差,也不会立刻触发误报。两个方法可以同时用,效果更好。
4.4 报警输出与联锁信号:给控制器一个“急停建议”
状态机算出来的结果最终要输出到真实设备。在纯仿真阶段,我用 UnityEvent 把状态通知到 UI 面板,显示红黄绿状态灯,并在控制台打印。到了半实物仿真阶段,就要把状态通过 TCP/Modbus 发给 PLC 或机器人控制器。
这里要特别提醒:Unity 侧发出的应该是一个“建议信号”,不是直接取代设备安全回路的急停信号。真正的安全联锁必须由 PLC 安全回路或机器人安全 IO 来执行,Unity 只能做“虚拟联锁”——在虚拟环境里模拟设备收到急停后的行为。这是一个工业安全领域的基本原则:软件仿真不能替代硬安全回路。
如果你需要把状态发给 PLC,最简单的办法是走 Modbus TCP 或 TCP Socket。下面是一个最简单的 TCP 客户端示例,把状态码发出去:
csharp复制using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class SafetySignalSender : MonoBehaviour
{
private TcpClient client;
private NetworkStream stream;
public void Connect(string ip, int port)
{
client = new TcpClient(ip, port);
stream = client.GetStream();
Debug.Log($"[SafetySignalSender] 已连接 {ip}:{port}");
}
public void SendState(int stateCode)
{
if (stream == null) return;
byte[] data = Encoding.ASCII.GetBytes(stateCode.ToString());
stream.Write(data, 0, data.Length);
}
}
当然,实际项目里要按控制器的具体协议封帧、加校验、做心跳。但核心思路就是这样:状态机输出一个整数状态码(0 安全、1 警告、2 危险),通过网络发送给真实控制器。至于控制器收到后怎么处理,那是 PLC 程序的事。
5. 实测过程踩过的坑:模型精度、性能开销与误报处理
5.1 模型的精度直接决定检测可信度
模型精度是这套系统能不能真正跑起来的关键。实际生产中,车间工程师导模型时往往只导了机床主体和机器人本体,夹具、压板、毛坯、刀具都被忽略了。在他们看来,这些是“小东西”,但恰恰是防碰撞仿真里最需要关注的碰撞对象。
我一个真实的教训:某次项目里只检测了主轴和机器人臂,忽略了排屑器,结果现场排屑器顶住机器人手腕,而仿真里完全没体现,差点把机器人轴搞坏。从那以后,我在项目开始时就跟机械同事确认:所有跟工件、刀具、夹具、外围设备相关的模型,都必须导入到仿真里。毛坯可以用简单的方块模型表示,刀具可以按实际尺寸用圆柱体近似。
5.2 MeshCollider 与运行时卡顿的排查
第一版系统跑起来后,我发现一个奇怪的问题:空载还好,一旦把机床主轴靠近夹具,帧率就掉到十几帧。用 Profiler 一看,耗时全部在碰撞检测上。
原因有两个。一是机床主轴和夹具的 MeshCollider 面数太高,ClosestPoint 的计算量随着面数线性增长。二是部分模型导入后带有大量无意义的面片,比如螺纹孔、圆弧倒角,这些细节在碰撞检测层面完全无用,却拖慢了性能。
解决办法很直接:对参与碰撞检测的模型做减面处理,或者在导入设置里降低 Mesh 精度。更实际的做法是给关键部位替换成组合体碰撞体——主轴用一个圆柱形 CapsuleCollider 加两个 BoxCollider 包络,机器人手臂用若干个 CapsuleCollider 分段包络。这样既保留了关键外形,又把碰撞检测开销降到了极低。
5.3 误报的根源:模型间隙、柔性体与信号延迟
误报,是防碰撞系统在实际调试中最难缠的问题。我觉得根源主要有三类:
第一,模型与实物之间的间隙不一致。CAD 模型里的配合间隙是理想值,真实装配后可能有误差,比如夹具压板稍微偏了几毫米。解决方法是给检测阈值加上一个“工程裕量”,虽然这样会牺牲一点检测精度,但比误报让人失去信任要好。
第二,机械结构的柔性。主轴夹爪、液压缓冲器、橡胶垫片都有柔性,在视觉上看起来会“轻微接触”,但实际没有硬碰撞。如果系统把这种状态判断为危险,就会频繁报警。我的经验是:对明显有柔性缓冲的部件,把额外裕量调大一点。
第三,控制器的速度反馈有延迟。Unity 里模拟的运动速度和真实控制系统里读到的速度可能不一样。如果机器人正在执行加减速,你在仿真里按最大速度算安全距离,就会偏保守;如果不按最大速度算,又可能在高速段不够安全。最终我选择了“模拟速度斜坡”的方式,在仿真里模仿真实控制器的 S 曲线加减速,而不是直接跳变到目标速度。这个细节对报警时机的准确性影响很大。
5.4 从误报到可用:一套调试方法论
这套系统真正达到“可信任”状态,是靠一套分步调试流程调出来的:
- 静态检查:所有轴停在极限位置,检查模型之间是否互相穿插,穿插量是多少。如果静态都穿插了,阈值再大也没用。
- 单轴极限扫描:逐个轴走全行程,在极限位观察是否有干涉。
- 双轴联动:两个轴同时运动,看相对距离曲线是否在安全区间内。
- 全场景联调:机器人+机床+刀库+夹具全部运动,按真实节拍跑一遍。
- 日志回放:把每一对部件的距离输出到 CSV,人工确认每个报警点对应的实际工况是否合理。
这套流程走下来,报警阈值基本能调到让人放心的水平。一定要把“最小距离曲线”可视化出来,不要只看一个报警灯。曲线能告诉你哪些阶段距离在持续缩小、哪个位置最危险,这对于调整工艺参数也很有帮助。
6. 对接真实控制器的信号链路与轨迹回放扩展
6.1 把 Unity 当成一个“虚拟设备”接入控制柜
当仿真稳定之后,一个自然的扩展方向是让 Unity 直接跟真实控制器通信。这样 Unity 就不再只是“离线的模拟器”,而是能实时接收设备状态、回传安全信号的“虚拟数字孪生节点”。
工业控制器常见的对外接口有 Modbus TCP、OPC UA、TCP/UDP 自定义协议等。机床广数、三菱、西门子系统各有各的协议,机器人也类似。做对接时我建议第一版只做“旁路监测”:Unity 只读控制器的轴坐标和状态字,不做任何写操作。等数据链路足够稳定了,再把安全状态作为只读信号传给 PLC,让 PLC 决定是否执行联锁。
下面是一个极其简化的 TCP 数据接收示例,用于从控制器接收轴位置:
csharp复制using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class TcpPositionReceiver : MonoBehaviour
{
private TcpListener listener;
private TcpClient client;
private NetworkStream stream;
public MachineAxis xAxis;
public MachineAxis zAxis;
async void Start()
{
listener = new TcpListener(System.Net.IPAddress.Any, 6000);
listener.Start();
client = await listener.AcceptTcpClientAsync();
stream = client.GetStream();
Debug.Log("[TcpPositionReceiver] 控制器已接入");
}
void Update()
{
if (stream == null || !stream.DataAvailable) return;
byte[] buffer = new byte[64];
int read = stream.Read(buffer, 0, buffer.Length);
string msg = Encoding.UTF8.GetString(buffer, 0, read);
// 假设收到的字符串格式是 "X100.5,Z300.2"
string[] parts = msg.Split(',');
foreach (string part in parts)
{
string[] kv = part.Split(':');
if (kv.Length != 2) continue;
float value = float.Parse(kv[1]);
if (kv[0] == "X") xAxis.MoveTo(value);
if (kv[0] == "Z") zAxis.MoveTo(value);
}
}
}
这只是演示数据链路的基本思路,真实项目里协议解析会复杂得多,但核心始终是“轴位置进来 -> 运动层更新 -> 碰撞检测 -> 安全状态输出”。
6.2 轨迹回放与碰撞事故复盘
联锁做出来之后,另一个非常实用的功能是轨迹回放。现场出了碰撞或者误报警,往往需要排查“那一刻到底发生了什么”。如果没有回放,光靠看代码和日志,效率很低。
我的做法是做一个 TrajectoryRecorder,每 0.05 秒记录一次所有关键轴的坐标、关节角、最小距离和安全状态,存到内存列表里。发生事故或报警时,把最近 5 分钟的轨迹落盘。然后在场景里做一个回放模式,按时间戳逐帧恢复各轴位置,并在 UI 上显示当前距离和状态。
csharp复制using System.Collections.Generic;
using UnityEngine;
public class FrameSnapshot
{
public float timestamp;
public float[] axisValues;
public float[] jointAngles;
public float minDistance;
public int safetyState;
}
public class TrajectoryRecorder : MonoBehaviour
{
public MachineAxis[] machineAxes;
public RobotArm robotArm;
public DistanceDetector distanceDetector;
private List<FrameSnapshot> frames = new List<FrameSnapshot>();
void Start()
{
InvokeRepeating(nameof(RecordFrame), 0f, 0.05f);
}
void RecordFrame()
{
var snap = new FrameSnapshot();
snap.timestamp = Time.time;
snap.axisValues = new float[machineAxes.Length];
for (int i = 0; i < machineAxes.Length; i++)
snap.axisValues[i] = machineAxes[i].currentValue;
snap.jointAngles = robotArm.currentAngles;
snap.minDistance = distanceDetector.GetCurrentMinDistance();
snap.safetyState = (int)distanceDetector.GetCurrentSafetyState();
frames.Add(snap);
}
}
回放时逐帧把 axisValues 和 jointAngles 赋回对应轴,就能精确还原当时的轨迹和报警时序。这个功能在跟现场师傅、设备厂商核对“为什么这里报警”的时候,沟通效率能提高好几倍。
6.3 从防碰撞仿真到数字孪生底座的一步之遥
整套系统跑通之后你会发现,当安全状态、轨迹数据、节拍数据都能实时汇总到 Unity 场景里时,它已经具备了数字孪生底座的雏形。数据流通常有三条:
- 设备 -> Unity:轴坐标、关节角、运行状态、报警信息。
- Unity -> 设备:安全状态、虚拟联锁信号、节拍指令(可选)。
- Unity -> 展示层:UI 状态灯、距离曲线、日志、大屏可视化。
我建议分三个阶段推进:先是离线回放,验证模型和算法;然后半实物仿真,把控制器介入进来;最后才是实时双向通信。不要一上来就做全双工,工业现场的数据风暴一旦处理不好,很容易把仿真系统搞崩。
另外,如果场景里有多个机器人和多台机床同时工作,你还需要考虑多设备之间的路径协调问题。这个方向已经接近多机器人路径规划了,Unity 里的距离检测可以作为一种碰撞约束,配合路径规划算法去生成无碰撞的运动时序。这是一个值得单独展开的话题,但基础还是今天这套“距离计算+状态机”的骨架。
最后再分享一个小经验:我见过不少团队把防碰撞仿真的重心放在“算法多高级”上,其实真正在现场管用的,往往是最简单的“最小距离显示 + 分级报警 + 回放”。你先把这套基础功能做得稳定可靠,让操作师傅愿意信它,再往上叠加更复杂的东西。技术方案可以很酷,但落地的时候,可靠性永远比炫技重要。
