Unity程序嵌入到WPF项目,并通过WPF按钮控制Unity组件属性的方案(Unity程序)

张开发
2026/4/12 3:17:21 15 分钟阅读

分享文章

Unity程序嵌入到WPF项目,并通过WPF按钮控制Unity组件属性的方案(Unity程序)
Unity程序的准备场景搭建我们新建一个unty项目通过包管理器导入game4automation插件(unity项目需要准备game4automation插件进行对象的操控(https://assetstore.unity.com/zh-CN/search#qgame4automation获取))创建一个该插件的场景(该过程可以参考(Unity数字孪生插件Game4Automation的简单应用-1_哔哩哔哩_bilibili)最终的场景如下图所示(仅搭建简单场景做演示)该插件提供了一些功能需要注意的点如下图搭建好场景后进行下一步脚本的编写与挂载通讯控制的脚本如下using UnityEngine; public class DriveBridge : MonoBehaviour { public Component driveComponent; // 使用反射是为了避免直接依赖第三方插件类型。 // 这样只要字段/属性名一致就可以通过字符串桥接控制。 public void SetTargetSpeed(float v) SetField(TargetSpeed, v); public void JogForward(bool on) SetField(JogForward, on); public void JogBackward(bool on) SetField(JogBackward, on); public void TargetStartMove(bool on) SetField(TargetStartMove, on); public void StopDrive(bool on) SetField(StopDrive, on); public void ResetDrive(bool on) SetField(ResetDrive, on); /// summary /// 通过反射动态设置 driveComponent 上的字段或属性值 /// /summary /// param namename字段或属性的名称/param /// param namevalue要设置的值会被自动转换为目标类型/param public void SetField(string name, object value) { // 【防御性检查】 // 运行前最常见的配置错误未绑定 driveComponent // 提前给出明确提示避免在后续反射中出现空引用异常 if (driveComponent null) { Debug.LogWarning( DriveBridge: driveComponent 为空请在 Inspector 中指定 Plugin Drive 组件。 ); return; } // 获取 driveComponent 的实际运行时类型 var t driveComponent.GetType(); try { // 反射绑定标志 // Instance 只查找实例成员非 static // Public 包含公共成员 // NonPublic 包含私有/保护成员 // IgnoreCase 忽略大小写便于容错 var flags System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.IgnoreCase; // 1. 首先尝试查找字段Field var f t.GetField(name, flags); // 如果找到字段则设置其值 if (f ! null) { // 将传入的 value 转换为字段声明的类型 // InvariantCulture 用于避免不同地区的小数点格式差异 f.SetValue( driveComponent, System.Convert.ChangeType( value, f.FieldType, System.Globalization.CultureInfo.InvariantCulture ) ); return; } // 2. 如果没有字段尝试查找属性Property var p t.GetProperty(name, flags); // 确保属性存在且可写有 set if (p ! null p.CanWrite) { p.SetValue( driveComponent, System.Convert.ChangeType( value, p.PropertyType, System.Globalization.CultureInfo.InvariantCulture ) ); return; } // 3. 尝试常见命名变体驼峰命名兼容 // 例如传入 TargetSpeed尝试匹配 targetSpeed if (!string.IsNullOrEmpty(name)) { // 首字母小写其余保持不变 var altName char.ToLowerInvariant(name[0]) name.Substring(1); // 再次尝试字段 f t.GetField(altName, flags); if (f ! null) { f.SetValue( driveComponent, System.Convert.ChangeType( value, f.FieldType, System.Globalization.CultureInfo.InvariantCulture ) ); return; } // 再次尝试属性 p t.GetProperty(altName, flags); if (p ! null p.CanWrite) { p.SetValue( driveComponent, System.Convert.ChangeType( value, p.PropertyType, System.Globalization.CultureInfo.InvariantCulture ) ); return; } } // 4. 再次容错忽略大小写 忽略下划线/空格等符号做“标准化名称匹配” // 例如Stop Drive / stop_drive / stopDrive / StopDrive 都按同一名字比较 var normalizedTarget NormalizeName(name); foreach (var field in t.GetFields(flags)) { if (NormalizeName(field.Name) normalizedTarget) { field.SetValue( driveComponent, System.Convert.ChangeType( value, field.FieldType, System.Globalization.CultureInfo.InvariantCulture ) ); return; } } foreach (var prop in t.GetProperties(flags)) { if (!prop.CanWrite) continue; if (NormalizeName(prop.Name) normalizedTarget) { prop.SetValue( driveComponent, System.Convert.ChangeType( value, prop.PropertyType, System.Globalization.CultureInfo.InvariantCulture ) ); return; } } // 5. 最后容错尝试调用“同名方法”兼容 StopDrive 这种一次性触发逻辑 // 支持 // - void Xxx() - 当 valuetrue 时触发 // - void Xxx(bool on) - 传入布尔值 // - 标准化同名匹配忽略下划线/空格等 foreach (var method in t.GetMethods(flags)) { if (method.IsSpecialName) continue; if (NormalizeName(method.Name) ! normalizedTarget) continue; var ps method.GetParameters(); if (ps.Length 0) { bool trigger false; if (value is bool b) trigger b; else if (value ! null bool.TryParse(value.ToString(), out var parsed)) trigger parsed; // 无参方法按“脉冲触发”处理仅在 true 时调用 if (trigger) { method.Invoke(driveComponent, null); } return; } if (ps.Length 1) { var converted System.Convert.ChangeType( value, ps[0].ParameterType, System.Globalization.CultureInfo.InvariantCulture ); method.Invoke(driveComponent, new object[] { converted }); return; } } // 6. 如果所有尝试都失败记录警告 Debug.LogWarning( $DriveBridge: 在组件类型 {t.FullName} 中未找到成员 {name}。 $当前 driveComponent{driveComponent.name} ({driveComponent.GetType().Name}) ); } catch (System.Exception e) { // 捕获反射、类型转换等过程中可能出现的异常 Debug.LogError($DriveBridge 设置 {name} 时发生错误: {e.Message}); } } // 名称标准化仅保留字母数字并转小写用于宽松匹配成员名 static string NormalizeName(string input) { if (string.IsNullOrEmpty(input)) return string.Empty; var sb new System.Text.StringBuilder(input.Length); for (int i 0; i input.Length; i) { char c input[i]; if (char.IsLetterOrDigit(c)) sb.Append(char.ToLowerInvariant(c)); } return sb.ToString(); } }using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using UnityEngine; /// summary /// TCP 服务器组件用于接收外部如 WPF 客户端发送的驱动控制指令 /// 并将指令通过 DriveBridge 转发给 Unity 内部的驱动系统。 /// /summary public class TcpDriveServer : MonoBehaviour { /// summary /// TCP 监听端口可在 Inspector 中配置 /// /summary public int port 5555; /// summary /// 驱动桥接对象负责将 TCP 命令转换为实际驱动行为 /// /summary public DriveBridge bridge; /// summary /// TCP 监听器 /// /summary TcpListener listener; /// summary /// 后台监听线程避免阻塞主线程 /// /summary Thread thread; /// summary /// 服务器运行状态标志volatile 确保多线程可见性 /// /summary volatile bool running; /// summary /// 线程安全的消息队列 /// 用于把 TCP 线程接收到的命令传递到主线程Update执行 /// /summary System.Collections.Concurrent.ConcurrentQueuestring pendingMsgs new System.Collections.Concurrent.ConcurrentQueuestring(); /// summary /// Unity 生命周期启动时创建并启动 TCP 监听线程 /// /summary void Start() { running true; // 创建后台线程防止 Unity 退出时卡死 thread new Thread(ListenLoop) { IsBackground true }; thread.Start(); } /// summary /// TCP 监听与接收循环运行在线程中 /// /summary void ListenLoop() { try { // 监听所有网卡的指定端口 listener new TcpListener(IPAddress.Any, port); listener.Start(); Debug.Log($TCP server started at 0.0.0.0:{port}); } catch (System.Exception e) { Debug.LogError($TCP Start failed: {e.Message}); return; } // 持续监听直到 running false while (running) { try { // 如果没有待处理的客户端连接则短暂休眠降低 CPU 占用 if (!listener.Pending()) { Thread.Sleep(10); continue; } // 接受客户端连接 using var client listener.AcceptTcpClient(); using var stream client.GetStream(); // 设置读取超时防止阻塞线程 stream.ReadTimeout 500; byte[] buf new byte[2048]; var recvCache new StringBuilder(); // 持续读取该客户端的数据 while (running) { int len; try { len stream.Read(buf, 0, buf.Length); } catch (System.IO.IOException) { // 超时或客户端断开 if (!client.Connected) break; continue; } // 客户端正常断开 if (len 0) break; // 将收到的字节转为字符串并追加到缓存 recvCache.Append(Encoding.UTF8.GetString(buf, 0, len)); var all recvCache.ToString(); // 协议约定每条命令以 \n 结尾 // 这样可以正确处理 TCP 粘包 / 拆包 var lines all.Split(\n); // 处理完整的一行或多行命令 for (int i 0; i lines.Length - 1; i) { var line lines[i].Trim(\r, , \t); if (!string.IsNullOrEmpty(line)) { // 放入线程安全队列等待主线程处理 pendingMsgs.Enqueue(line); } } // 清空缓存并保留未读完的剩余数据 recvCache.Clear(); recvCache.Append(lines[lines.Length - 1]); } // 客户端断开后若还有残留数据也一并处理 var remain recvCache.ToString().Trim(\r, , \t); if (!string.IsNullOrEmpty(remain)) pendingMsgs.Enqueue(remain); } catch (System.Exception e) { Debug.LogWarning($TCP Client Error: {e.Message}); } } } /// summary /// Unity 主线程更新函数 /// 从队列中取出命令并交由 ProcessCommand 执行 /// /summary void Update() { // 防止空引用 if (bridge null) return; // 尽可能处理所有待执行命令 while (pendingMsgs.TryDequeue(out string msg)) { try { ProcessCommand(msg); } catch (System.Exception e) { Debug.LogError($TCP Command Error processing {msg}: {e.Message}); } } } /// summary /// 解析并执行单条 TCP 命令 /// 命令格式key:value /// 示例 /// setSpeed:50 /// jogB:1 /// startM:true /// /summary void ProcessCommand(string raw) { var m raw?.Trim(); if (string.IsNullOrEmpty(m)) return; // 查找冒号分隔符 int sep m.IndexOf(:); if (sep 0) { Debug.LogWarning($Unknown command format: {m}); return; } // 拆分命令名和参数 var cmd m.Substring(0, sep).Trim(); var arg m.Substring(sep 1).Trim(); // 特殊处理带数值的命令 if (cmd.Equals(setSpeed, System.StringComparison.OrdinalIgnoreCase)) { if (float.TryParse( arg, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v)) { bridge.SetTargetSpeed(v); } else { Debug.LogWarning($Invalid setSpeed value: {arg}); } return; } // 其余命令均为布尔类型参数 if (!TryParseBool(arg, out bool on)) { Debug.LogWarning($Invalid bool value for {cmd}: {arg}); return; } // 根据命令名称调用对应接口 if (cmd.Equals(jogF, System.StringComparison.OrdinalIgnoreCase)) bridge.JogForward(on); else if (cmd.Equals(jogB, System.StringComparison.OrdinalIgnoreCase)) bridge.JogBackward(on); else if (cmd.Equals(stop, System.StringComparison.OrdinalIgnoreCase)) bridge.StopDrive(on); else if (cmd.Equals(reset, System.StringComparison.OrdinalIgnoreCase)) bridge.ResetDrive(on); else if (cmd.Equals(startM, System.StringComparison.OrdinalIgnoreCase) || cmd.Equals(startMove, System.StringComparison.OrdinalIgnoreCase) || cmd.Equals(targetStartMove, System.StringComparison.OrdinalIgnoreCase)) { bridge.TargetStartMove(on); } else { Debug.LogWarning($Unknown command: {cmd}); } } /// summary /// 兼容多种格式的布尔值解析 /// 方便 WPF 或其他客户端快速发送简单指令 /// 支持1 / 0 / true / false / on / off /// /summary static bool TryParseBool(string text, out bool value) { if (string.IsNullOrWhiteSpace(text)) { value false; return false; } var s text.Trim(); if (s 1) { value true; return true; } if (s 0) { value false; return true; } if (s.Equals(on, System.StringComparison.OrdinalIgnoreCase)) { value true; return true; } if (s.Equals(off, System.StringComparison.OrdinalIgnoreCase)) { value false; return true; } // 标准 bool 解析 if (bool.TryParse(s, out value)) return true; return false; } /// summary /// Unity 销毁时释放资源停止 TCP 监听 /// /summary void OnDestroy() { running false; try { listener?.Stop(); } catch { // 忽略异常防止 Unity 退出时报错 } } }将上面两个脚本挂载到挂载了Drive脚本的物体上(因为我们需要控制Drive脚本的属性)如下图特别注意红色框内的配置要准确)然后将unity程序打包(路径要记得后面需要将WPF程序文件夹与该文件夹放在同一目录)

更多文章