Unity与桌面程序集成 – 使用说明
项目概述
这个解决方案演示了如何通过C#桌面程序启动Unity,并通过命名管道(Named Pipe)实现双向进程间通信,使桌面程序与Unity可以互相发送消息和调用方法。
快速开始
步骤1:准备Unity项目
打开Unity编辑器(2021.3.5f1c1或兼容版本)打开项目(当前目录)确保场景中有GameObject,并将脚本添加到该GameObject上
如果没有GameObject,在Hierarchy中右键 -> Create Empty,命名为”UnityMain”将脚本拖拽到该GameObject上将
Assets/UnityMain.cs脚本拖拽到该GameObject上(用于接收桌面消息)
Assets/PipeServer.cs脚本会在UnityMain启动时自动添加(用于向桌面发送消息) 保存场景
PipeClient
UnityMain源码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Unity主类,提供可被桌面程序通过管道调用的方法,并可以向桌面程序发送消息
/// </summary>
public class UnityMain : MonoBehaviour
{
private string currentStatus = "Unity已启动";
private PipeClient pipeClient;
// Start is called before the first frame update
void Start()
{
// 确保主线程调度器存在
UnityMainThreadDispatcher.Instance();
// 初始化管道客户端(用于向桌面程序发送消息)
pipeClient = gameObject.AddComponent<PipeClient>();
if (pipeClient == null)
{
pipeClient = FindObjectOfType<PipeClient>();
}
Debug.Log("UnityMain初始化完成 - 双向管道通信已就绪");
currentStatus = "Unity已初始化";
// 发送初始化消息到桌面程序
SendMessageToDesktop("UnityInitialized|Unity应用程序已启动并初始化完成");
}
/// <summary>
/// 按钮点击方法 - 可以被桌面程序通过管道调用
/// </summary>
public void OnClickBtn()
{
Debug.Log("OnClickBtn被调用 - hahah");
currentStatus = $"按钮已点击 - {System.DateTime.Now:HH:mm:ss}";
// 可以在这里添加更多的Unity逻辑
// 例如:播放音效、动画、改变场景等
// 示例:改变对象的颜色(如果有Renderer组件)
Renderer renderer = GetComponent<Renderer>();
if (renderer != null)
{
renderer.material.color = new Color(Random.value, Random.value, Random.value);
}
// 向桌面程序发送通知消息
SendMessageToDesktop($"ButtonClicked|OnClickBtn方法已被调用,时间: {System.DateTime.Now:HH:mm:ss}");
}
/// <summary>
/// 带消息的按钮点击方法 - 可以被桌面程序通过管道调用
/// </summary>
public void OnClickBtnWithMessage(string message)
{
Debug.Log($"OnClickBtnWithMessage被调用,消息: {message}");
currentStatus = $"收到消息: {message} - {System.DateTime.Now:HH:mm:ss}";
// 可以在这里处理传入的消息
// 向桌面程序发送响应消息
SendMessageToDesktop($"MessageReceived|Unity收到消息: {message}");
}
/// <summary>
/// 获取当前状态
/// </summary>
public string GetStatus()
{
return currentStatus;
}
/// <summary>
/// 向桌面程序发送消息
/// </summary>
/// <param name="message">消息内容,格式:类型|内容</param>
public void SendMessageToDesktop(string message)
{
if (pipeClient != null)
{
bool success = pipeClient.SendMessage(message);
if (success)
{
Debug.Log($"已发送消息到桌面程序: {message}");
}
else
{
Debug.LogWarning($"发送消息到桌面程序失败: {message}");
}
}
else
{
Debug.LogWarning("PipeClient未初始化,无法发送消息到桌面程序");
}
}
/// <summary>
/// 发送状态更新到桌面程序
/// </summary>
public void SendStatusUpdate()
{
SendMessageToDesktop($"StatusUpdate|{currentStatus}");
}
/// <summary>
/// 示例:Unity主动发送消息到桌面程序
/// 可以在Unity的Update、协程或其他事件中调用
/// </summary>
void Update()
{
// 示例:按空格键发送消息到桌面程序
if (Input.GetKeyDown(KeyCode.Space))
{
SendMessageToDesktop($"KeyPressed|Unity中按下了空格键");
}
}
void OnDestroy()
{
// 清理资源(如果需要)
}
void OnApplicationQuit()
{
// 清理资源(如果需要)
}
}
PipeServer 源码
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using UnityEngine;
/// <summary>
/// Unity中的命名管道服务器,用于接收来自桌面程序的消息
/// </summary>
public class PipeServer : MonoBehaviour
{
private const string PipeName = "UnityDesktopPipe";
private NamedPipeServerStream pipeServer;
private Thread pipeThread;
private bool isRunning = false;
private UnityMain unityMain;
void Start()
{
unityMain = FindObjectOfType<UnityMain>();
if (unityMain == null)
{
Debug.LogError("未找到UnityMain组件!");
return;
}
StartPipeServer();
}
void StartPipeServer()
{
try
{
isRunning = true;
pipeThread = new Thread(ServerThread);
pipeThread.IsBackground = true;
pipeThread.Start();
Debug.Log("管道服务器已启动,等待客户端连接...");
}
catch (Exception ex)
{
Debug.LogError($"启动管道服务器失败: {ex.Message}");
}
}
void ServerThread()
{
while (isRunning)
{
try
{
pipeServer = new NamedPipeServerStream(
PipeName,
PipeDirection.In,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
Debug.Log("等待客户端连接...");
pipeServer.WaitForConnection();
Debug.Log("客户端已连接!");
byte[] buffer = new byte[4096];
StringBuilder messageBuilder = new StringBuilder();
while (pipeServer.IsConnected && isRunning)
{
try
{
if (!pipeServer.IsConnected)
break;
int bytesRead = pipeServer.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
string received = Encoding.UTF8.GetString(buffer, 0, bytesRead);
messageBuilder.Append(received);
// 处理所有完整的消息(以换行符分隔)
string allData = messageBuilder.ToString();
// 查找所有完整的消息(以换行符结尾的)
int lastNewline = -1;
for (int i = 0; i < allData.Length; i++)
{
if (allData[i] == '
' || allData[i] == '
')
{
// 找到完整的消息
if (lastNewline + 1 < i)
{
string completeMsg = allData.Substring(lastNewline + 1, i - lastNewline - 1).Trim();
if (!string.IsNullOrEmpty(completeMsg))
{
Debug.Log($"收到消息: {completeMsg}");
UnityMainThreadDispatcher.Instance().Enqueue(() => ProcessMessage(completeMsg));
}
}
lastNewline = i;
}
}
// 保留未完成的部分(最后没有换行符的内容)
if (lastNewline >= 0)
{
messageBuilder.Clear();
if (lastNewline + 1 < allData.Length)
{
messageBuilder.Append(allData.Substring(lastNewline + 1));
}
}
}
else
{
// 没有数据,检查连接是否断开
Thread.Sleep(50);
}
}
catch (Exception readEx)
{
Debug.LogWarning($"读取管道数据时出错: {readEx.Message}");
break;
}
}
}
catch (Exception ex)
{
Debug.LogError($"管道服务器错误: {ex.Message}");
}
finally
{
if (pipeServer != null)
{
pipeServer.Close();
pipeServer.Dispose();
pipeServer = null;
}
}
// 等待一段时间后重新监听
if (isRunning)
{
Thread.Sleep(1000);
}
}
}
void ProcessMessage(string message)
{
if (string.IsNullOrEmpty(message) || unityMain == null)
{
return;
}
string[] parts = message.Split('|');
string methodName = parts[0];
string parameter = parts.Length > 1 ? parts[1] : "";
Debug.Log($"处理方法: {methodName}, 参数: {parameter}");
switch (methodName)
{
case "OnClickBtn":
unityMain.OnClickBtn();
break;
case "OnClickBtnWithMessage":
unityMain.OnClickBtnWithMessage(parameter);
break;
default:
Debug.LogWarning($"未知的方法: {methodName}");
break;
}
}
void OnDestroy()
{
isRunning = false;
if (pipeServer != null && pipeServer.IsConnected)
{
pipeServer.Close();
pipeServer.Dispose();
}
if (pipeThread != null && pipeThread.IsAlive)
{
pipeThread.Join(1000);
}
}
void OnApplicationQuit()
{
isRunning = false;
if (pipeServer != null)
{
pipeServer.Close();
pipeServer.Dispose();
}
}
}
PipeClient 源码
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using UnityEngine;
/// <summary>
/// Unity中的命名管道客户端,用于向桌面程序发送消息
/// </summary>
public class PipeClient : MonoBehaviour
{
private const string PipeName = "DesktopUnityPipe";
private NamedPipeClientStream pipeClient;
private bool isConnected = false;
private object sendLock = new object();
private Thread reconnectThread;
private bool isRunning = false;
void Start()
{
isRunning = true;
StartReconnectThread();
}
void StartReconnectThread()
{
reconnectThread = new Thread(ReconnectLoop)
{
IsBackground = true
};
reconnectThread.Start();
}
void ReconnectLoop()
{
while (isRunning)
{
if (!isConnected || pipeClient == null || !pipeClient.IsConnected)
{
TryConnect();
}
Thread.Sleep(2000); // 每2秒检查一次连接
}
}
bool TryConnect()
{
lock (sendLock)
{
if (isConnected && pipeClient != null && pipeClient.IsConnected)
{
return true;
}
try
{
if (pipeClient != null)
{
try
{
pipeClient.Close();
pipeClient.Dispose();
}
catch { }
pipeClient = null;
}
pipeClient = new NamedPipeClientStream(
".",
PipeName,
PipeDirection.Out,
PipeOptions.None);
pipeClient.Connect(1000); // 1秒超时
if (pipeClient.IsConnected)
{
isConnected = true;
Debug.Log("已连接到桌面程序管道!");
return true;
}
}
catch (TimeoutException)
{
// 桌面程序可能还没启动,继续重试
isConnected = false;
}
catch (Exception ex)
{
Debug.LogWarning($"连接桌面程序管道失败: {ex.Message}");
isConnected = false;
}
return false;
}
}
/// <summary>
/// 向桌面程序发送消息
/// </summary>
public bool SendMessage(string message)
{
lock (sendLock)
{
if (!isConnected || pipeClient == null || !pipeClient.IsConnected)
{
// 尝试连接
if (!TryConnect())
{
Debug.LogWarning("无法连接到桌面程序管道,消息发送失败");
return false;
}
}
try
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message + "
");
pipeClient.Write(messageBytes, 0, messageBytes.Length);
pipeClient.Flush();
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"发送消息到桌面程序失败: {ex.Message}");
isConnected = false;
try
{
pipeClient?.Close();
pipeClient?.Dispose();
pipeClient = null;
}
catch { }
return false;
}
}
}
/// <summary>
/// 检查是否已连接
/// </summary>
public bool IsConnected
{
get
{
lock (sendLock)
{
return isConnected && pipeClient != null && pipeClient.IsConnected;
}
}
}
void OnDestroy()
{
isRunning = false;
Disconnect();
}
void OnApplicationQuit()
{
isRunning = false;
Disconnect();
}
void Disconnect()
{
lock (sendLock)
{
isConnected = false;
if (pipeClient != null)
{
try
{
if (pipeClient.IsConnected)
{
pipeClient.Close();
}
pipeClient.Dispose();
}
catch { }
pipeClient = null;
}
}
if (reconnectThread != null && reconnectThread.IsAlive)
{
reconnectThread.Join(1000);
}
}
}
UnityMainThreadDispatcher 源码
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Unity主线程调度器,用于在其他线程中执行Unity主线程的操作
/// </summary>
public class UnityMainThreadDispatcher : MonoBehaviour
{
private static UnityMainThreadDispatcher _instance;
private Queue<Action> actions = new Queue<Action>();
public static UnityMainThreadDispatcher Instance()
{
if (_instance == null)
{
GameObject go = new GameObject("UnityMainThreadDispatcher");
_instance = go.AddComponent<UnityMainThreadDispatcher>();
DontDestroyOnLoad(go);
}
return _instance;
}
void Update()
{
lock (actions)
{
while (actions.Count > 0)
{
Action action = actions.Dequeue();
try
{
action?.Invoke();
}
catch (Exception ex)
{
Debug.LogError($"执行主线程操作失败: {ex.Message}");
}
}
}
}
public void Enqueue(Action action)
{
lock (actions)
{
actions.Enqueue(action);
}
}
}
以上便是Unity工程的主要代码
步骤2:编译Unity应用程序
在Unity编辑器中,File -> Build Settings选择平台(Windows)点击”Build”按钮,选择输出目录等待编译完成
步骤3:编译并运行桌面程序
使用Visual Studio或命令行编译:
cd DesktopApp
msbuild DesktopApp.csproj /p:Configuration=Release
运行生成的(位于
DesktopApp.exe目录)。
DesktopApp/bin/Release/
步骤5:使用流程
启动桌面程序
运行
DesktopApp.exe
配置Unity应用程序路径(首次使用)
点击”启动Unity应用程序”按钮如果找不到Unity应用程序,会弹出文件选择对话框选择你编译后的Unity应用程序exe文件(例如:)选择的路径会自动保存,下次启动时会自动使用
D:MyGamesMyUnityGame.exe
启动Unity应用程序
Unity应用程序会自动启动等待3-5秒,让Unity应用程序初始化和管道服务器启动
从桌面程序调用Unity方法
桌面程序会自动连接到Unity的管道服务器在桌面程序中,点击”调用OnClickBtn”按钮查看Unity应用程序的输出或日志,应该能看到”OnClickBtn被调用 – hahah”的日志点击”调用带消息方法”按钮,传入消息文本在Unity中按空格键,桌面程序会收到Unity发送的消息查看桌面程序的输出窗口,可以看到来自Unity的消息
工作原理
架构图(双向通信)
桌面程序 (DesktopApp)
├─ PipeClient ──→ UnityDesktopPipe ──→ PipeServer (Unity端)
│ ↓
│ UnityMain类方法
│ ↓
└─ PipeServer ←── DesktopUnityPipe ←── PipeClient (Unity端)
↑
Unity发送消息
通信方向:
桌面→Unity: 通过管道,桌面程序调用Unity方法Unity→桌面: 通过
UnityDesktopPipe管道,Unity主动发送消息到桌面程序
DesktopUnityPipe
关键组件
Unity端脚本
: 命名管道服务器,在后台线程中监听来自桌面程序的消息(接收桌面→Unity)
PipeServer.cs: 命名管道客户端,向桌面程序发送消息(发送Unity→桌面)
PipeClient.cs: 主类,提供可调用的方法(OnClickBtn、OnClickBtnWithMessage等),并提供
UnityMain.cs方法向桌面发送消息
SendMessageToDesktop(): 确保Unity API在主线程中调用PipeServer收到消息后,通过
UnityMainThreadDispatcher.cs在主线程中调用Unity方法
UnityMainThreadDispatcher
桌面程序
: 主窗体,包含启动Unity和调用方法的按钮,并接收Unity消息
MainForm.cs: 命名管道客户端,连接到Unity的管道服务器(发送桌面→Unity)
PipeClient.cs: 命名管道服务器,接收来自Unity的消息(接收Unity→桌面)通过管道发送消息(格式:
PipeServer.cs)来调用Unity方法通过事件处理接收Unity主动发送的消息
方法名|参数
通信机制
双向命名管道: 使用两个独立的Windows命名管道实现双向通信
: 桌面程序→Unity通信
UnityDesktopPipe: Unity→桌面程序通信 消息格式: 消息格式为
DesktopUnityPipe或
类型|内容,简单明了主线程执行: 所有Unity方法调用都在主线程中执行自动重连: 管道连接断开后可以自动重连实时通信: Unity和桌面程序可以随时互相发送消息
方法名|参数
自定义扩展
添加新的可调用方法
1. 在Unity中实现方法
在中添加新方法:
Assets/UnityMain.cs
public void MyNewMethod(string param1, int param2)
{
Debug.Log($"MyNewMethod被调用: {param1}, {param2}");
// 你的逻辑...
}
2. 在PipeServer中添加消息处理
在的
Assets/PipeServer.cs方法中添加对应的case分支:
ProcessMessage
case "MyNewMethod":
string[] methodParams = parameter.Split(',');
if (methodParams.Length == 2)
{
unityMain.MyNewMethod(methodParams[0], int.Parse(methodParams[1]));
}
break;
3. 在桌面程序中调用
在中添加按钮,在按钮事件中调用:
DesktopApp/MainForm.cs
private void BtnMyNewMethod_Click(object sender, EventArgs e)
{
if (pipeClient != null && pipeClient.IsConnected)
{
try
{
pipeClient.CallMethod("MyNewMethod", "Hello,123");
AppendOutput("已调用 MyNewMethod");
}
catch (Exception ex)
{
AppendOutput($"调用失败: {ex.Message}");
}
}
}
就是这么简单! 只需要在Unity中添加方法,在PipeServer中添加case分支,然后在桌面程序中通过管道发送消息。
Unity主动发送消息到桌面程序
1. 在Unity中发送消息
在中,使用
Assets/UnityMain.cs方法向桌面程序发送消息:
SendMessageToDesktop()
// 发送简单消息
SendMessageToDesktop("StatusUpdate|Unity状态已更新");
// 发送带类型和内容的消息
SendMessageToDesktop("GameEvent|玩家得分: 100");
// 在Unity的方法中自动发送消息(已在OnClickBtn和OnClickBtnWithMessage中实现)
public void OnClickBtn()
{
// Unity逻辑...
// 自动发送通知到桌面程序
SendMessageToDesktop($"ButtonClicked|OnClickBtn方法已被调用,时间: {System.DateTime.Now:HH:mm:ss}");
}
2. 桌面程序自动接收消息
桌面程序的会自动接收Unity发送的消息,并在输出窗口中显示。消息格式为:
PipeServer
类型|内容
示例输出:
[Unity消息] 类型: UnityInitialized, 内容: Unity应用程序已启动并初始化完成
[Unity消息] 类型: ButtonClicked, 内容: OnClickBtn方法已被调用,时间: 14:30:25
[Unity消息] 类型: MessageReceived, 内容: Unity收到消息: Hello from Desktop App!
3. 自定义消息处理
如果需要自定义处理Unity发送的消息,可以在的
DesktopApp/MainForm.cs方法中添加处理逻辑:
OnUnityMessageReceived
private void OnUnityMessageReceived(string message)
{
string[] parts = message.Split('|');
string messageType = parts.Length > 0 ? parts[0] : "Unknown";
string messageContent = parts.Length > 1 ? parts[1] : message;
// 根据消息类型进行不同的处理
switch (messageType)
{
case "StatusUpdate":
// 处理状态更新
break;
case "GameEvent":
// 处理游戏事件
break;
default:
// 默认处理:显示在输出窗口
AppendOutput($"[Unity消息] 类型: {messageType}, 内容: {messageContent}");
break;
}
}
修改Unity路径
桌面程序会自动保存Unity应用程序路径到配置文件中。如果需要修改:
UnityDLLAppPath.txt
方式1:使用界面按钮清除(推荐)✅
在桌面程序界面中,点击”清除保存的路径”按钮确认清除操作下次启动时会重新弹出文件选择对话框,可以重新选择路径
方式2:手动删除配置文件
找到目录(或Debug目录,取决于你的编译配置)删除
DesktopApp/bin/Release/文件下次启动时会重新弹出文件选择对话框
UnityDLLAppPath.txt
方式3:直接编辑配置文件
找到文件直接编辑文件内容,修改为新的Unity应用程序路径保存文件,下次启动时会使用新路径
DesktopApp/bin/Release/UnityDLLAppPath.txt
故障排除
问题1:管道连接失败
症状:桌面程序显示”无法连接到Unity管道”
解决方案:
确保Unity应用程序已启动等待更长时间(5-10秒)让Unity完全初始化和管道服务器启动检查Unity应用程序是否正常运行(没有崩溃)查看Unity控制台是否有管道服务器启动的日志确保Unity场景中的GameObject包含和
UnityMain组件
PipeServer
问题2:Unity应用程序未启动
症状:点击”启动Unity应用程序”按钮后没有反应
解决方案:
检查Unity应用程序exe路径是否正确确认Unity应用程序exe文件存在且没有被删除手动启动Unity应用程序exe,确认可以正常运行检查是否有权限运行该exe文件查看桌面程序的输出窗口,获取详细的错误信息
问题3:方法调用没有效果
症状:点击调用按钮后,Unity控制台没有输出
解决方案:
检查Unity场景中是否有GameObject包含和
UnityMain组件检查Unity控制台是否有收到消息的日志确认管道连接已成功(查看桌面程序状态显示”已连接到Unity管道”)检查PipeServer的ProcessMessage方法中是否有对应的方法处理重新编译Unity应用程序
PipeServer
技术细节
管道名称
项目使用两个独立的命名管道:
桌面→Unity管道:
UnityDesktopPipe
Unity端: 的
Assets/PipeServer.cs 常量桌面端:
PipeName 的
DesktopApp/PipeClient.cs 常量(必须与PipeServer一致)
PipeName
Unity→桌面管道:
DesktopUnityPipe
Unity端: 的
Assets/PipeClient.cs 常量桌面端:
PipeName 的
DesktopApp/PipeServer.cs 常量(必须与PipeClient一致)
PipeName
可以在各自的文件中修改管道名称,但必须确保两端的名称一致。
消息格式
消息格式为:
方法名|参数
示例:
– 无参数方法
OnClickBtn – 带一个字符串参数的方法
OnClickBtnWithMessage|Hello World – 多个参数用逗号分隔
MyNewMethod|param1,param2,param3
线程安全
Unity的所有API必须在主线程中调用。本项目使用以下机制确保线程安全:
在后台线程接收消息使用
PipeServer将方法调用调度到主线程所有Unity方法都在主线程中执行
UnityMainThreadDispatcher
数据传递
方法参数通过管道以字符串形式传递:
基本类型(string, int, float, bool等)通过字符串传递多个参数用逗号分隔在PipeServer的ProcessMessage中解析参数
常见问题
Q: 现在默认就是启动编译后的Unity应用程序(.exe),如何修改路径?
A: 首次运行时会弹出文件选择对话框,选择Unity应用程序exe文件后,路径会自动保存到配置文件中。如果需要修改路径,有以下几种方式:
UnityDLLAppPath.txt
使用界面按钮清除(推荐):点击桌面程序界面中的”清除保存的路径”按钮,下次启动时会重新弹出文件选择对话框手动删除配置文件:删除文件,下次启动时会重新弹出文件选择对话框直接编辑配置文件:直接编辑
DesktopApp/bin/Release/UnityDLLAppPath.txt文件,修改其中的路径即可
UnityDLLAppPath.txt
Q: 如何清除保存的Unity路径?
A: 最简单的方式是点击桌面程序界面中的”清除保存的路径”按钮。也可以手动删除文件。清除后,下次启动时会重新弹出文件选择对话框。
DesktopApp/bin/Release/UnityDLLAppPath.txt
Q: 能否同时连接多个Unity实例?
A: 当前实现使用固定的管道名称,不支持多个实例。要实现多实例支持,需要修改和
PipeServer,为每个实例使用不同的管道名称。
PipeClient
Q: 支持双向通信吗?
A: 当前实现是单向的(桌面程序 -> Unity)。要实现双向通信(Unity -> 桌面程序),可以:
在桌面程序中创建另一个管道服务器Unity端创建管道客户端连接到桌面程序Unity端通过管道发送消息给桌面程序
Q: 为什么使用命名管道而不是内存映射文件?
A: 命名管道实现更简单,无需编译DLL,所有代码都在项目中。管道通信稳定可靠,对于这种消息传递场景非常合适。
Q: 可以在Unity编辑器中使用吗?
A: 可以!在Unity编辑器中Play场景时,桌面程序也可以连接到Unity编辑器中的场景。这对于开发和调试非常有用。
许可证
本项目仅供学习和参考使用。完整示例视频如下
20251202_155633
















暂无评论内容