首先简述一下实现思路,配置窗体我选择使用同样采用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


暂无关于此日志的评论。