1. 项目概述打通工业视觉与虚拟世界的实时桥梁最近在做一个工业检测项目客户现场用的是康耐视的VisionPro视觉系统检测结果需要实时显示在Unity做的3D虚拟产线看板上。这个需求听起来挺常见但真做起来你会发现VisionPro和Unity是两个完全不同的生态一个跑在Windows上用C#做二次开发另一个是跨平台的游戏引擎。怎么让检测数据毫秒级地从VisionPro“飞”到Unity里我第一时间排除了写文件、共享内存这些笨办法最终用C#写了个TCP/IP通信方案把两边的数据通道彻底打通了。简单说这个方案就是在VisionPro端作为服务端把检测结果比如圆心坐标、缺陷标志、尺寸数据打包成特定格式的字符串或字节流通过Socket发出去Unity端作为客户端开个线程持续监听这个端口收到数据后立刻解析并更新到UI或3D模型上。整个过程延迟可以控制在10毫秒以内完全满足实时监控的需求。如果你也在搞机器视觉和数字孪生、虚拟调试的集成或者单纯想用Unity做个炫酷的实时数据可视化大屏这套代码框架可以直接拿去用。2. 核心思路与方案选型为什么是TCP/IP面对VisionPro到Unity的数据传输其实有好几条路可以走。我最初也考虑过其他方案但逐一分析后TCP/IP成了最合适的选择。2.1 备选方案分析与淘汰理由首先想到的是中间文件。比如VisionPro把结果写成CSV或TXTUnity定时去读。这个方案实现简单但问题很大。频繁的磁盘I/O是性能杀手在高频检测比如每秒几十次场景下文件读写会成为瓶颈而且很难保证数据的实时性和同步性还容易遇到文件被锁定的问题。其次是数据库。把数据扔进MySQL或SQLite两边都去访问。这比文件方式稍好但引入了额外的系统复杂度。你需要部署和维护数据库通信延迟受数据库性能影响对于简单的点对点实时数据流来说属于“杀鸡用牛刀”太重了。然后是共享内存或命名管道。这在同一台PC上的进程间通信速度极快。但我们的项目有潜在需求未来VisionPro工控机和Unity可视化服务器可能是两台独立的机器。共享内存无法跨机器直接否决。命名管道虽然支持网络但配置和跨平台兼容性不如Socket直观。最后是现成的通信中间件比如MQTT、ZeroMQ甚至ROS。它们功能强大适合复杂的分布式系统。但对于我们这个“点对点、低延迟、简单数据”的需求来说引入这些框架学习成本高增加了系统的不必要依赖和调试复杂度。2.2 TCP/IP方案的优势与考量相比之下TCP/IP Socket通信的优势就非常明显了通用与跨平台TCP/IP是网络通信的基石VisionPro的C#环境和Unity的C#环境都原生支持无需第三方库。无论是本机回环地址通信还是跨网络通信代码几乎不用改。可靠有序TCP协议保证数据包按序、可靠地送达。对于检测数据这种不能丢失、顺序不能乱的关键信息这点至关重要。实时性高在局域网甚至本机环境下TCP通信的延迟极低经过优化完全可以做到毫秒级响应。灵活性好数据格式完全自定义可以传字符串、JSON、二进制流甚至序列化的对象适应各种复杂数据结构。连接稳定一旦建立连接通道可以长期保持适合持续不断的检测数据流。当然选择TCP也需要处理一些事比如连接断开重连、数据粘包处理、异步操作防止阻塞主线程等。但这些都有成熟的编程模式可以解决后文会给出具体的代码实现和避坑指南。2.3 整体架构设计整个系统的架构非常清晰VisionPro端作为TCP服务器在检测流程比如在CogToolBlock的Ran事件中中将CogPMAlignTool、CogCaliperTool等工具的结果对象序列化成字节数据通过一个持续监听的TcpListener发送给已连接的客户端。Unity端作为TCP客户端在MonoBehaviour如DataReceiver的Start方法中开启一个异步任务连接服务器。成功连接后在一个独立的线程或异步循环中接收数据解析后通过UnityEngine.Debug.Log或赋值给公共变量驱动UI Text、Image或3D模型变换。这个架构的扩展性也很强。一个VisionPro服务器可以同时向多个Unity客户端广播数据实现多屏监控反过来也可以让Unity作为服务器接收来自多个视觉工位的数据进行汇总展示。3. VisionPro服务端从工具结果到网络字节流VisionPro端的核心任务就两个一是从视觉工具里准确抓取数据二是把这些数据高效、无误地通过网络送出去。这里面的坑主要集中在数据转换和网络稳定性上。3.1 搭建一个稳健的TCP服务器首先我们得在VisionPro的C#脚本里比如一个独立的工具块或应用创建一个TCP服务端。这里我强烈建议使用异步编程避免阻塞VisionPro的主线程导致界面卡死或检测流程停滞。using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; public class VisionProDataServer { private TcpListener _listener; private TcpClient _connectedClient; private NetworkStream _stream; private bool _isRunning false; private readonly int _port 8080; // 自定义端口确保防火墙开放 public async Task StartServerAsync() { _isRunning true; IPAddress localAddr IPAddress.Parse(127.0.0.1); // 本机测试。实际部署改为工控机IP如“192.168.1.100” _listener new TcpListener(localAddr, _port); _listener.Start(); Console.WriteLine($VisionPro 数据服务器已启动监听 {localAddr}:{_port}); // 异步等待客户端连接 _connectedClient await _listener.AcceptTcpClientAsync(); _stream _connectedClient.GetStream(); Console.WriteLine(Unity客户端已连接。); // 这里可以开始发送心跳包或等待发送数据 } public void StopServer() { _isRunning false; _stream?.Close(); _connectedClient?.Close(); _listener?.Stop(); } }注意AcceptTcpClientAsync是异步方法它会挂起等待连接不会阻塞。你需要在一个按钮事件或工具块的初始化事件里调用StartServerAsync并且不要用Wait()或Result去同步等待它否则还是会卡住。可以用_ StartServerAsync();这种“丢弃任务”的方式启动或者用async void事件处理函数。3.2 封装与发送检测数据VisionPro工具的结果比如CogPMAlignTool的Results里的GetPose()转换成的X,Y,Theta或者CogCaliperTool的Results里边的宽度都是.NET对象。我们需要把它们转换成字节。这里推荐两种格式简单字符串CSV格式适合数据量小、结构固定的场景。例如Tool1,OK,123.45,67.89,0.5|Tool2,NG,200.00,150.00。用特定字符如逗号、竖线分隔不同字段和不同工具的结果。JSON字符串适合数据结构复杂、可能变化的场景。使用Newtonsoft.Json库序列化可读性好Unity端解析也方便。下面以CSV格式为例展示如何在工具运行后发送数据// 假设在CogToolBlock的Ran事件处理函数中 private async void CogToolBlock_Ran(object sender, EventArgs e) { if (!_isRunning || _stream null) return; // 1. 从工具中提取数据 var pmAlignTool myToolBlock.Tools[CogPMAlignTool1] as CogPMAlignTool; var caliperTool myToolBlock.Tools[CogCaliperTool1] as CogCaliperTool; string resultCode pmAlignTool.Results.Count 0 ? OK : NG; double centerX pmAlignTool.Results.Count 0 ? pmAlignTool.Results[0].GetPose().TranslationX : 0.0; double centerY pmAlignTool.Results.Count 0 ? pmAlignTool.Results[0].GetPose().TranslationY : 0.0; double width caliperTool.Results ! null ? caliperTool.Results.Width : 0.0; // 2. 封装数据为CSV字符串 // 格式工具名,结果状态,数据1,数据2,...|下一个工具... string dataString $PMAlign,{resultCode},{centerX:F2},{centerY:F2}|Caliper,{resultCode},{width:F2}; // 3. 转换为字节并发送 byte[] dataBytes Encoding.UTF8.GetBytes(dataString \n); // 添加换行符作为消息结束符有助于解决粘包 try { await _stream.WriteAsync(dataBytes, 0, dataBytes.Length); await _stream.FlushAsync(); // 确保数据立即发送 } catch (Exception ex) { Console.WriteLine($发送数据失败: {ex.Message}); // 这里可以触发重连逻辑 } }实操心得在数据末尾加一个换行符\n是一个简单有效的“消息边界”标识。Unity端可以按\n来分割接收到的字节流从而区分开一条条独立的消息这是处理TCP粘包问题的初级但有效的方法。对于更复杂的情况可以定义“消息头包含数据长度消息体”的协议。3.3 处理连接中断与重连工业现场网络可能不稳定。必须处理客户端断开的情况。private async Task HandleClientCommunicationAsync(TcpClient client) { using (client) using (var stream client.GetStream()) { byte[] buffer new byte[1024]; try { while (_isRunning) { // 这里主要是为了保持连接和接收可能的控制指令如Unity端请求重发 int bytesRead await stream.ReadAsync(buffer, 0, buffer.Length); if (bytesRead 0) { Console.WriteLine(客户端主动断开连接。); break; // 退出循环等待新的连接 } // 可以解析Unity发来的指令... } } catch (IOException) { Console.WriteLine(连接异常断开。); } catch (Exception ex) { Console.WriteLine($通信错误: {ex.Message}); } } // 连接断开后可以重新进入等待连接状态 Console.WriteLine(等待新的客户端连接...); // 可以在这里重新调用 AcceptTcpClientAsync }在实际项目中我会把服务器设计成自动重试监听状态确保Unity端重启后能重新连上。4. Unity客户端接收、解析与驱动场景Unity端的核心是创建一个稳定的TCP客户端在后台线程中接收数据然后将数据安全地传递到Unity的主线程来更新游戏对象或UI。4.1 创建线程安全的TCP客户端在Unity中所有关于GameObject、Transform、UI的操作都必须在主线程进行。但网络接收是耗时操作必须放在子线程或异步任务中否则会卡死主线程。我们需要用Thread或Task并通过线程安全的方式将数据“抛”给主线程。using UnityEngine; using System.Net.Sockets; using System.Text; using System.Threading; using System.Collections.Concurrent; // 用于线程安全队列 public class UnityTCPClient : MonoBehaviour { public string serverIP 127.0.0.1; public int serverPort 8080; private TcpClient _client; private NetworkStream _stream; private Thread _receiveThread; private bool _isConnected false; // 线程安全队列用于存放从网络线程接收到的原始数据字符串 private ConcurrentQueuestring _dataQueue new ConcurrentQueuestring(); void Start() { ConnectToServer(); } void ConnectToServer() { try { _client new TcpClient(); // 使用异步连接避免超时卡死这里简化用同步实际建议用BeginConnect _client.Connect(serverIP, serverPort); _stream _client.GetStream(); _isConnected true; Debug.Log($成功连接到VisionPro服务器 {serverIP}:{serverPort}); // 启动接收线程 _receiveThread new Thread(new ThreadStart(ReceiveData)); _receiveThread.IsBackground true; // 设为后台线程当Unity退出时自动终止 _receiveThread.Start(); } catch (System.Exception e) { Debug.LogError($连接失败: {e.Message}); _isConnected false; // 可以在这里实现定时重连逻辑 Invoke(ConnectToServer, 3f); // 3秒后重试 } } void ReceiveData() { byte[] buffer new byte[1024]; StringBuilder receivedStringBuilder new StringBuilder(); ASCIIEncoding encoder new ASCIIEncoding(); // 根据服务端编码调整 while (_isConnected _client ! null _client.Connected) { try { int bytesRead _stream.Read(buffer, 0, buffer.Length); if (bytesRead 0) { // 将字节转换为字符串 string receivedData encoder.GetString(buffer, 0, bytesRead); receivedStringBuilder.Append(receivedData); // 按换行符分割消息处理粘包 string allData receivedStringBuilder.ToString(); int newlineIndex; while ((newlineIndex allData.IndexOf(\n)) ! -1) { string oneCompleteMessage allData.Substring(0, newlineIndex); allData allData.Substring(newlineIndex 1); // 将完整消息放入队列供主线程处理 if (!string.IsNullOrEmpty(oneCompleteMessage)) { _dataQueue.Enqueue(oneCompleteMessage); } } receivedStringBuilder.Clear(); receivedStringBuilder.Append(allData); // 剩余的不完整数据留待下次 } else { // 连接已关闭 Debug.LogWarning(服务器断开连接。); _isConnected false; break; } } catch (System.Exception e) { if (_isConnected) // 避免重复打印 { Debug.LogError($接收数据时出错: {e.Message}); _isConnected false; } break; } } Debug.Log(接收线程结束。); } }4.2 主线程更新从队列到GameObject在Update方法中我们从队列里取出数据并处理。Update在主线程运行所以在这里操作Unity对象是安全的。void Update() { // 每帧处理队列中的所有消息 while (!_dataQueue.IsEmpty) { if (_dataQueue.TryDequeue(out string rawData)) { ProcessReceivedData(rawData); } } // 如果连接断开可以尝试重连避免在子线程中调用Unity API if (!_isConnected _client ! null !_client.Connected) { // 可以在这里设置一个标志在OnGUI或协程中触发重连避免每帧都尝试 } } void ProcessReceivedData(string data) { // 解析CSV格式数据 // 示例数据: PMAlign,OK,123.45,67.89|Caliper,OK,15.20 string[] toolResults data.Split(|); foreach (string toolResult in toolResults) { string[] fields toolResult.Split(,); if (fields.Length 2) continue; string toolName fields[0]; string status fields[1]; // 根据工具名和状态更新场景 switch (toolName) { case PMAlign: float posX float.Parse(fields[2]); float posY float.Parse(fields[3]); UpdateObjectPosition(posX, posY); // 更新一个代表位置的3D物体 UpdateUIStatus(定位状态, status); // 更新UI文本 break; case Caliper: float width float.Parse(fields[2]); UpdateWidthDisplay(width); // 更新宽度显示 break; } } } void UpdateObjectPosition(float x, float y) { // 假设有一个GameObject叫“Target”代表视觉定位点 GameObject targetObj GameObject.Find(Target); if (targetObj ! null) { // 注意VisionPro的坐标可能需要转换到Unity世界坐标 // 这里假设做了简单的缩放和偏移映射 targetObj.transform.position new Vector3(x * 0.01f, y * 0.01f, 0); } } void UpdateUIStatus(string label, string status) { // 假设有一个Text组件显示状态 // UnityEngine.UI.Text statusText; // statusText.text ${label}: {status}; // 可以用颜色区分OK/NG }关键技巧ConcurrentQueue是线程安全的Enqueue接收线程和TryDequeue主线程Update同时操作不会出错。这是连接子线程和Unity主线程最经典、最稳定的方式之一。4.3 数据坐标转换与可视化增强直接从VisionPro拿到的像素坐标不能直接扔给Unity。通常需要转换。坐标映射如果Unity场景是1:1模拟真实产线你需要知道VisionPro相机的标定参数像素到物理单位的转换再将物理单位毫米按比例转换成Unity世界单位米。例如VisionPro给出(100,200)像素通过标定得知是(10mm, 20mm)你的Unity场景比例是1单位1毫米那么位置就是(0.01f, 0.02f)。可视化除了移动物体还可以用LineRenderer绘制测量出的宽度。在检测到缺陷NG时实例化一个红色的警示模型如立方体在缺陷位置。将数据实时绘制成图表可以使用UnityEngine.UI.Image填充或第三方插件。5. 核心环节实现自定义协议与性能优化基础通信搭建好后要投入实际工业环境必须在可靠性和性能上下功夫。这就涉及到设计一个更健壮的通信协议并进行针对性优化。5.1 设计一个简单的应用层协议前面用换行符分隔消息在数据量小、频率低时没问题。但为了绝对可靠最好设计一个包含“长度头”的二进制协议。服务端发送时将数据如JSON字符串转换为字节数组dataBytes。计算数据长度length dataBytes.Length。将长度length转换为4字节的整数字节数组BitConverter.GetBytes(length)。先发送这4字节的长度头再发送dataBytes。客户端接收时先读取4字节解析出接下来消息体的长度expectedLength。循环读取直到收满expectedLength字节这才算一条完整消息。解析这expectedLength字节的消息体。这样可以完美解决TCP粘包问题。代码稍复杂但可靠性是质的提升。下面是服务端发送的改进示例private async Task SendDataWithHeaderAsync(string message) { if (_stream null || !_stream.CanWrite) return; byte[] messageBytes Encoding.UTF8.GetBytes(message); byte[] lengthBytes BitConverter.GetBytes(messageBytes.Length); byte[] packet new byte[4 messageBytes.Length]; Buffer.BlockCopy(lengthBytes, 0, packet, 0, 4); Buffer.BlockCopy(messageBytes, 0, packet, 4, messageBytes.Length); try { await _stream.WriteAsync(packet, 0, packet.Length); } catch { /* 处理异常 */ } }5.2 性能优化与资源管理连接池与心跳如果Unity端需要连接多个VisionPro服务端可以考虑使用连接池管理TcpClient。同时建立简单的心跳机制定期发送一个小包以便快速检测断线。数据压缩当传输图像轮廓点集等大数据量时可以在发送前用GZipStream进行压缩接收端解压。二进制序列化对于非常频繁的简单数据如一组浮点数可以跳过JSON/CSV直接使用BinaryWriter写入字节流效率最高。但牺牲了可读性两端必须严格约定格式。Unity对象池如果NG时需要频繁创建警示特效使用对象池ObjectPool来复用GameObject避免频繁的Instantiate和Destroy带来的GC垃圾回收压力。限制更新频率如果VisionPro检测频率是100Hz但Unity界面刷新30FPS就足够。可以在Unity端做个节流比如每3帧处理一次最新数据避免无用的计算。6. 常见问题与排查技巧实录在实际部署中我踩过不少坑。这里把最常见的问题和解决方法列出来希望能帮你节省大量调试时间。6.1 连接失败类问题问题现象可能原因排查步骤与解决方案Unity报SocketException: No connection could be made1. 服务器IP/端口错误。2. VisionPro服务端程序未启动。3. 防火墙阻止了连接。1.Ping测试在Unity运行的机器上打开命令提示符ping [VisionPro机器IP]看网络是否通。2.端口监听检查在VisionPro机器上用netstat -ano连接成功但立即断开1. 服务端Accept只执行了一次处理完第一个连接后就退出了。2. 异常未处理导致线程退出。1.服务端循环接受确保服务端在while循环中持续调用AcceptTcpClientAsync。2.加强异常捕获在ReceiveData和SendData的try-catch中记录详细日志。只有本机127.0.0.1能连其他机器连不上服务端绑定到了127.0.0.1环回地址。将服务端TcpListener的IP地址改为IPAddress.Any0.0.0.0表示监听所有网络接口。注意安全这会暴露端口到整个网络。6.2 数据通信类问题问题现象可能原因排查步骤与解决方案Unity收到乱码或数据截断1. 编码不一致。服务端用UTF8客户端用ASCII。2. 粘包问题多条消息连在一起了。1.统一编码两端都使用Encoding.UTF8。2.实现封包协议采用上文提到的“长度头数据体”协议这是根本解决方案。临时可用ReadLine如果服务端发WriteLine或按\n分割。数据延迟高偶尔卡顿1. Unity主线程被阻塞如解析复杂JSON。2. 网络抖动。3. GC频繁触发。1.主线程减负确保ProcessReceivedData方法执行速度极快。复杂解析可考虑分帧处理。2.使用Ping命令检查网络稳定性。3.性能分析在Unity Profiler中查看CPU和GC情况优化数据结构和避免在Update中频繁分配内存如new字符串、数组。Unity收不到任何数据1. 服务端没成功发送。2. 客户端接收缓冲区大小不够。3. 客户端接收线程已崩溃退出。1.服务端日志在WriteAsync前后加日志确认执行和数据内容。2.增大缓冲区适当增加byte[] buffer的大小如4096。3.检查线程状态在Unity编辑器的Console中查看接收线程的启动和退出日志确保while循环条件正确。6.3 Unity特定问题在Unity编辑器里正常打包后不行很可能是防火墙问题。打包后的应用被视为新程序防火墙规则可能阻止它。需要在打包后的机器上为你的.exe文件添加防火墙允许规则。UnityEngine.Debug.Log在子线程中调用这会导致崩溃或日志不输出。所有Unity Engine API都必须在主线程调用。日志可以通过上文提到的队列机制将日志字符串也放入队列在主线程的Update中统一打印。移动端Android/iOS连接失败移动平台对后台网络线程有更严格的限制。确保在Player Settings中设置了正确的网络权限如Internet Access。对于iOS可能还需要在Info.plist中添加允许任意负载的传输安全设置。6.4 调试技巧先用网络调试助手验证在VisionPro机器上用“TCP服务器模式”的网络调试工具如NetAssist模拟Unity端看VisionPro能否正常发送数据。反之用调试工具模拟服务端看Unity能否接收。这能快速定位问题是出在VisionPro、Unity还是网络环境。在VisionPro端写日志文件将准备发送的数据字符串同时写入一个本地的文本文件确认数据本身是正确的。在Unity端打印原始字节在ReceiveData线程中将收到的byte[]直接以16进制形式打印到控制台或文件检查原始数据流是否正确排除编码和解析问题。这套从VisionPro到Unity的TCP/IP实时通信方案我已经在多个产线数字孪生项目中稳定应用。核心就是理解TCP通信的流程处理好线程安全设计好数据协议。一旦跑通它就成了一个可靠的数据管道无论是传几个坐标还是大批量的点云数据都能灵活应对。