首先简述一下实现思路,配置窗体我选择使用同样采用C#语言的.net framework窗体来制作,窗体通过NamedPipe(命名管道)与Unity程序交流。
交流过程中Unity程序会作为Server,配置窗体作为Client。由Server打开配置窗体,并等待发送消息。在我们点击Client中的各按钮时,向Server程序建立连接发送命令。
通信具体使用的是System中NamedPipe的相关类型,如果你会其他C#语言的窗体应用开发(甚至直接用另一个Unity项目),也一样可以参考实现。
配置窗体Client实现
框架选择.net framework4.8,我这里的项目名称为DesktopHookForms,具体可以按需修改
然后从工具箱中,新拖几个Button和Label,如果你从未接触过.netfreamwork可以看看这个文章,非常简单。
大概如下图(非常简陋hh,重在功能实现):
接下来就是逻辑实现了,在窗体启动时,读取启动窗体的arg参数,从中获取NamedPipe的管道名。随后当我们点击各个命令按钮,就执行通信函数SendData:通过NamedPipeClientStream与Unity窗体建立连接,随后将序列化为json的命令数据发送过去。以下是窗体的内容代码:
using System; using System.IO; using System.IO.Pipes; using System.Security.Principal; using System.Windows.Forms; namespace DesktopHookForms { /// <summary> /// 配置窗体 /// </summary> public partial class SysForm : Form { //Program主函数的参数,在构造窗体时当作参数读取 public string[] args= null; //消息管道名,互相使用同一个管道名就能实现通信了 private string PipeName; public SysForm(string[] args) { InitializeComponent(); //获取Unity端启动窗体时传入的参数,并将其读取为管道名 this.args = args; if (args.Length>0&&!string.IsNullOrEmpty(args[0])) { this.PipeName = args[0]; } this.label_title.Text= PipeName; } /// <summary> /// 发送数据至Unity端 /// </summary> /// <param name="msg">消息内容</param> private void SendData(PipeMsg msg) { try { string str=Newtonsoft.Json.JsonConvert.SerializeObject(msg); using (NamedPipeClientStream pipeClient = new NamedPipeClientStream("localhost", PipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.None)) { try { pipeClient.Connect(3000); using (StreamWriter sw = new StreamWriter(pipeClient)) { sw.WriteLine(str); sw.Flush(); } } catch (TimeoutException ex) { //超时提示后退出窗体 MessageBox.Show("游戏程序似乎已关闭!"); Application.Exit(); } } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } //暂停按钮命令 private void button_pause_Click(object sender, EventArgs e) { SendData(new PipeMsg() { Type = PipeMsgType.GamePause, arg1 = "" }); } //开始按钮命令 private void button_start_Click(object sender, EventArgs e) { SendData(new PipeMsg() { Type = PipeMsgType.GameStart, arg1 = "" }); } //结束按钮命令 private void button_end_Click(object sender, EventArgs e) { SendData(new PipeMsg() { Type = PipeMsgType.GameEnd, arg1 = "" }); } //显示消息命令 private void button_showMsg_Click(object sender, EventArgs e) { if (!string.IsNullOrEmpty(textBox_showMsg.Text)) { SendData(new PipeMsg() { Type = PipeMsgType.ShowMsg, arg1 = textBox_showMsg.Text }); } } } /// <summary> /// 消息对象实体 /// </summary> public class PipeMsg { public PipeMsgType Type; public string arg1; } /// <summary> /// 消息类性 /// </summary> public enum PipeMsgType { GameStart = 0, GamePause, GameEnd, ShowMsg } }
我们的窗体需要一个参数来构造窗体,记得在Program.cs中加上参数:
using System; using System.Windows.Forms; namespace DesktopHookForms { internal static class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> /// <param name="args">应用程序启动时传入的参数</param> [STAThread] static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new SysForm(args)); } } }
编写完成后运行调试窗体,没问题的话就能在项目根目录\bin\Debug路径下找到窗体的可执行文件(exe)。稍后可以将其中的内容放到Unity项目的目录附近方便调用。
Unity窗体Server实现
接下来让我们实现Unity端的通信功能,这里我直接在场景中Canvas上编写了一个脚本,方便控制Canvas上的控件来显示命令效果。众所周知Unity是一个单线程程序,默认不支持异步编程。为了方便我们异步等待通信消息,我安装了UniTask组件。
以下是Unity端通信的内容,程序会一直异步执行WaitData函数等待窗体程序建立连接,在收到连接消息后,将其放入processingMsg队列,最后在Update函数中读取执行对应的消息。以下是具体的代码:
using Cysharp.Threading.Tasks; using System; using System.Collections.Concurrent; using System.IO; using System.IO.Pipes; using System.Threading; using UnityEngine; public class MyCanvas : MonoBehaviour { //时间文本,用于表示游戏是否暂停 public TMPro.TextMeshProUGUI TimeLabel; //消息文本,用于显示配置窗体发来的消息 public TMPro.TextMeshProUGUI MsgLabel; //管道名,这里随便取了一个,考虑到多开等情况,最好设置一个随机获取的逻辑 private string pipeName = "TestPipe4321"; //游戏是否暂停 bool isGamming = false; //由于Unity本身是单线程执行,异步执行的WaitData函数无法直接调用UnityEngine相关的函数,设置一个队列来存储消息,待主线程调用执行 ConcurrentQueue<PipeMsg> processingMsg = new ConcurrentQueue<PipeMsg>(); void Start() { //修改光标图案,方便区分鼠标是否被游戏窗体占用,记得在Resources中添加对应图片 Texture2D img = (Texture2D)Resources.Load("Cursor"); Cursor.SetCursor(img, Vector2.zero, CursorMode.Auto); //异步执行等待数据函数,避免阻塞主线程 WaitData(); } void Update() { //读取消息并根据消息类性,执行具体的命令逻辑 while (processingMsg.Count > 0) { if (processingMsg.TryDequeue(out var msg)) { switch (msg.Type) { case PipeMsgType.GameStart: isGamming = true; break; case PipeMsgType.GamePause: isGamming = false; break; case PipeMsgType.GameEnd: Quit(); break; case PipeMsgType.ShowMsg: MsgLabel.text = msg.arg1; break; } } } //如果游戏没暂停就持续刷新时间 if (isGamming) { TimeLabel.text = DateTime.Now.ToString(); } } //配置窗体进程 System.Diagnostics.Process process; /// <summary> /// 调用发布的Winform窗体作为配置窗口 /// </summary> public void OpenConfigForm() { if (process == null || process.HasExited) { process = new System.Diagnostics.Process(); process.StartInfo.FileName = "C:/DesktopHookForms.exe";//注意修改为窗体程序的exe名称 //将管道名作为参数传入窗体,防止管道名冲突,后续若窗体内容较复杂也可以自定义消息内容 process.StartInfo.Arguments = pipeName; process.StartInfo.UseShellExecute = true; process.Start(); } } CancellationTokenSource waitDataCancellationToken = new CancellationTokenSource(); /// <summary> /// 异步等待管道消息 /// </summary> /// <returns></returns> private async UniTask WaitData() { Debug.Log("开始等待消息"); while (true) { try { NamedPipeServerStream pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 2, PipeTransmissionMode.Message, PipeOptions.Asynchronous); try { await pipeServer.WaitForConnectionAsync(waitDataCancellationToken.Token); } catch (OperationCanceledException) { //当捕获到操作取消异常则直接跳出循环结束线程 break; } StreamReader sr = new StreamReader(pipeServer); string con = sr.ReadLine(); PipeMsg msg = (PipeMsg)JsonUtility.FromJson(con, typeof(PipeMsg)); sr.Close(); //将消息放入执行队列,由主线程的Update函数读取执行 processingMsg.Enqueue(msg); } catch (Exception ex) { Debug.Log("等待消息异常:" + ex.Message); } } Debug.Log("等待消息结束"); } /// <summary> /// 退出程序 /// </summary> public void Quit() { #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif } private void OnApplicationQuit() { //若设置窗体还未关闭,则先关闭设置窗体 if (process != null && !process.HasExited) { process.CloseMainWindow(); } //通过cancellationToken取消WaitData的等待,若不取消WaitData可能会变为阻塞线程一直存在 waitDataCancellationToken.Cancel(); } } /// <summary> /// 消息对象实体 /// </summary> public class PipeMsg { public PipeMsgType Type; public string arg1; } /// <summary> /// 消息类性 /// </summary> public enum PipeMsgType { GameStart = 0, GamePause, GameEnd, ShowMsg }
展示一下最后的效果:
到此就是本篇文章的内容,我也摩拳擦掌的准备狠狠的做一些新游戏,要是你对合作或者交流兴趣,欢迎联系我哦!
QQ:2763686216
暂无关于此日志的评论。