我正在开发一个简单的编辑器原型。编辑器将使用WinForms(如果可能的话,使用WPF)提供主要用户界面,并嵌入Unity 2017独立应用程序来可视化数据并提供其他控件元素(例如缩放,旋转,滚动等)。
感谢下面这篇好文章,让在WinForms应用程序中嵌入Unity应用程序变得惊人地容易。
https://forum.unity.com/threads/unity-3d-within-windows-application-enviroment.236213/
此外,还有一个简单的示例应用程序,您可以在此处访问:
不幸的是,无论是示例,还是任何我能找到的帖子都没有回答一个非常基本的问题:您如何在嵌入的Unity应用程序或您的WinForms应用程序之间传递数据(或调用方法)?
您的WinForms应用程序是否可以简单地调用Unity应用程序中的MonoBehaviour脚本或静态方法?如果可以,如何实现?如果不行,有什么好的解决方法?Unity到WinForms通信又该如何工作?
更新:
使用程序员提到的重复页面(链接)实现了一种解决方案,该解决方案使用命名管道在WinForms和Unity应用程序之间进行通信。
这两个应用程序都使用背景工作者,WinForms应用程序充当服务器(因为它首先启动并需要一个活动的连接侦听器,然后才启动客户端),而嵌入式Unity应用程序充当客户端。
不幸的是,Unity应用程序在创建NamedPipeClientStream时抛出NotImplementedException,说明“ACL在Mono中不受支持”(在Unity 2017.3和Net 2.0(而不是Net 2.0子集)下测试)。此异常已经在上面提到的帖子的一些评论中报告过,但尚不清楚是否已经解决。尝试的建议解决方法“确保在客户端尝试连接之前服务器正在运行”和“以管理员模式启动”均已失败。
解决方案:
经过更多测试,很明显,“ACL在Mono中不受支持”的异常是在创建NamedPipeClientStream实例时使用的TokenImpersonationLevel参数引起的。将其更改为TokenImpersonationLevel.None即可解决此问题。
以下是WinForms应用程序使用的代码,它充当命名管道服务器。请确保在Unity应用程序客户端尝试连接之前执行此脚本!此外,请确保在启动服务器之前已构建和发布了Unity应用程序。将Unity应用程序的可执行文件放置在WinForms应用程序文件夹中,并将其命名为“Child.exe”。
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO.Pipes;
namespace Container
{
public partial class MainForm : Form
{
[DllImport("User32.dll")]
static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);
internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
[DllImport("user32.dll")]
internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);
[DllImport("user32.dll")]
static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
/// <summary>
/// A Delegate for the Update Log Method.
/// </summary>
/// <param name="text">The Text to log.</param>
private delegate void UpdateLogCallback(string text);
/// <summary>
/// The Unity Application Process.
/// </summary>
private Process process;
/// <summary>
/// The Unity Application Window Handle.
/// </summary>
private IntPtr unityHWND = IntPtr.Zero;
private const int WM_ACTIVATE = 0x0006;
private readonly IntPtr WA_ACTIVE = new IntPtr(1);
private readonly IntPtr WA_INACTIVE = new IntPtr(0);
/// <summary>
/// The Background Worker, which will send and receive Data.
/// </summary>
private BackgroundWorker backgroundWorker;
/// <summary>
/// A Named Pipe Stream, acting as the Server for Communication between this Application and the Unity Application.
/// </summary>
private NamedPipeServerStream namedPipeServerStream;
public MainForm()
{
InitializeComponent();
try
{
//Create Server Instance
namedPipeServerStream = new NamedPipeServerStream("NamedPipeExample", PipeDirection.InOut, 1);
//Start Background Worker
backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.RunWorkerAsync();
//Start embedded Unity Application
process = new Process();
process.StartInfo.FileName = Application.StartupPath + "\\Child.exe";
process.StartInfo.Arguments = "-parentHWND " + splitContainer.Panel1.Handle.ToInt32() + " " + Environment.CommandLine;
process.StartInfo.UseShellExecute = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForInputIdle();
//Embed Unity Application into this Application
EnumChildWindows(splitContainer.Panel1.Handle, WindowEnum, IntPtr.Zero);
//Wait for a Client to connect
namedPipeServerStream.WaitForConnection();
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Activates the Unity Window.
/// </summary>
private void ActivateUnityWindow()
{
SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
}
/// <summary>
/// Deactivates the Unity Window.
/// </summary>
private void DeactivateUnityWindow()
{
SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
}
private int WindowEnum(IntPtr hwnd, IntPtr lparam)
{
unityHWND = hwnd;
ActivateUnityWindow();
return 0;
}
private void panel1_Resize(object sender, EventArgs e)
{
MoveWindow(unityHWND, 0, 0, splitContainer.Panel1.Width, splitContainer.Panel1.Height, true);
ActivateUnityWindow();
}
/// <summary>
/// Called, when this Application is closed. Tries to close the Unity Application and the Named Pipe as well.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
try
{
//Close Connection
namedPipeServerStream.Close();
//Kill the Unity Application
process.CloseMainWindow();
Thread.Sleep(1000);
while (process.HasExited == false)
{
process.Kill();
}
}
catch (Exception ex)
{
throw ex;
}
}
private void MainForm_Activated(object sender, EventArgs e)
{
ActivateUnityWindow();
}
private void MainForm_Deactivate(object sender, EventArgs e)
{
DeactivateUnityWindow();
}
/// <summary>
/// A simple Background Worker, which sends Data to the Client via a Named Pipe and receives a Response afterwards.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
//Init
UpdateLogCallback updateLog = new UpdateLogCallback(UpdateLog);
string dataFromClient = null;
try
{
//Don't pass until a Connection has been established
while (namedPipeServerStream.IsConnected == false)
{
Thread.Sleep(100);
}
//Created stream for reading and writing
StreamString serverStream = new StreamString(namedPipeServerStream);
//Send to Client and receive Response (pause using Thread.Sleep for demonstration Purposes)
Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("A Message from Server.") + " Bytes." });
Thread.Sleep(1000);
dataFromClient = serverStream.ReadString();
Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });
Thread.Sleep(1000);
Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("A small Message from Server.") + " Bytes." });
Thread.Sleep(1000);
dataFromClient = serverStream.ReadString();
Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });
Thread.Sleep(1000);
Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("Another Message from Server.") + " Bytes." });
Thread.Sleep(1000);
dataFromClient = serverStream.ReadString();
Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });
Thread.Sleep(1000);
Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("The final Message from Server.") + " Bytes." });
Thread.Sleep(1000);
dataFromClient = serverStream.ReadString();
Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });
}
catch(Exception ex)
{
//Handle usual Communication Exceptions here - just logging here for demonstration and debugging Purposes
Invoke(updateLog, new object[] { ex.Message });
}
}
/// <summary>
/// A simple Method, which writes Text into a Console / Log. Will be invoked by the Background Worker, since WinForms are not Thread-safe and will crash, if accessed directly by a non-main-Thread.
/// </summary>
/// <param name="text">The Text to log.</param>
private void UpdateLog(string text)
{
lock (richTextBox_Console)
{
Console.WriteLine(text);
richTextBox_Console.AppendText(Environment.NewLine + text);
}
}
}
}
将此代码附加到Unity应用程序中的一个游戏对象。还要确保引用具有TextMeshProUGUI组件的游戏对象(TextMeshPro-Asset,在Asset Store中可以找到)到“textObject”成员,以便应用程序不会崩溃并且您可以看到一些调试信息。 另外(如上所述),请确保构建和发布Unity应用程序,将其命名为“Child.exe”并将其放在与WinForms应用程序相同的文件夹中。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using System.IO.Pipes;
using System.Security.Principal;
using Assets;
using System.ComponentModel;
using TMPro;
/// <summary>
/// A simple Example Project, which demonstrates Communication between WinForms-Applications and embedded Unity Engine Applications via Named Pipes.
///
/// This Code (Unity) is considered as the Client, which will receive Data from the WinForms-Server and send a Response in Return.
/// </summary>
public class SendAndReceive : MonoBehaviour
{
/// <summary>
/// A GameObject with an attached Text-Component, which serves as a simple Console.
/// </summary>
public GameObject textObject;
/// <summary>
/// The Background Worker, which will send and receive Data.
/// </summary>
private BackgroundWorker backgroundWorker;
/// <summary>
/// A Buffer needed to temporarely save Text, which will be shown in the Console.
/// </summary>
private string textBuffer = "";
/// <summary>
/// Use this for initialization.
/// </summary>
void Start ()
{
//Init the Background Worker to send and receive Data
this.backgroundWorker = new BackgroundWorker();
this.backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
this.backgroundWorker.WorkerReportsProgress = true;
this.backgroundWorker.RunWorkerAsync();
}
/// <summary>
/// Update is called once per frame.
/// </summary>
void Update ()
{
//Update the Console for debugging Purposes
lock (textBuffer)
{
if (string.IsNullOrEmpty(textBuffer) == false)
{
textObject.GetComponent<TextMeshProUGUI>().text = textObject.GetComponent<TextMeshProUGUI>().text + Environment.NewLine + textBuffer;
textBuffer = "";
}
}
}
/// <summary>
/// A simple Background Worker, which receives Data from the Server via a Named Pipe and sends a Response afterwards.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
try
{
//Init
NamedPipeClientStream client = null;
string dataFromServer = null;
//Create Client Instance
client = new NamedPipeClientStream(".", "NamedPipeExample", PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.None);
updateTextBuffer("Initialized Client");
//Connect to Server
client.Connect();
updateTextBuffer("Connected to Server");
//Created stream for Reading and Writing
StreamString clientStream = new StreamString(client);
//Read from Server and send Response (flush in between to clear the Buffer and fix some strange Issues I couldn't really explain, sorry)
dataFromServer = clientStream.ReadString();
updateTextBuffer("Received Data from Server: " + dataFromServer);
client.Flush();
updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("Some data from client.") + " Bytes.");
dataFromServer = clientStream.ReadString();
updateTextBuffer("Received Data from Server: " + dataFromServer);
client.Flush();
updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("Some more data from client.") + " Bytes.");
dataFromServer = clientStream.ReadString();
updateTextBuffer("Received Data from Server: " + dataFromServer);
client.Flush();
updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("A lot of more data from client.") + " Bytes.");
dataFromServer = clientStream.ReadString();
updateTextBuffer("Received Data from Server: " + dataFromServer);
client.Flush();
updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("Clients final message.") + " Bytes.");
//Close client
client.Close();
updateTextBuffer("Done");
}
catch (Exception ex)
{
//Handle usual Communication Exceptions here - just logging here for demonstration and debugging Purposes
updateTextBuffer(ex.Message + Environment.NewLine + ex.StackTrace.ToString() + Environment.NewLine + "Last Message was: " + textBuffer);
}
}
/// <summary>
/// A Buffer, which allows the Background Worker to save Texts, which may be written into a Log or Console by the Update-Loop
/// </summary>
/// <param name="text">The Text to save.</param>
private void updateTextBuffer(string text)
{
lock (textBuffer)
{
if (string.IsNullOrEmpty(textBuffer))
{
textBuffer = text;
}
else
{
textBuffer = textBuffer + Environment.NewLine + text;
}
}
}
}
此外,这两个脚本都需要一个额外的类来封装管道流,以便发送和接收文本变得更加容易。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Assets
{
/// <summary>
/// Simple Wrapper to write / read Data to / from a Named Pipe Stream.
///
/// Code based on:
/// https://dev59.com/7KDia4cB1Zd3GeqPC2SK
/// </summary>
public class StreamString
{
private Stream ioStream;
private UnicodeEncoding streamEncoding;
public StreamString(Stream ioStream)
{
this.ioStream = ioStream;
streamEncoding = new UnicodeEncoding();
}
public string ReadString()
{
int len = 0;
len = ioStream.ReadByte() * 256;
len += ioStream.ReadByte();
byte[] inBuffer = new byte[len];
ioStream.Read(inBuffer, 0, len);
return streamEncoding.GetString(inBuffer);
}
public int WriteString(string outString)
{
byte[] outBuffer = streamEncoding.GetBytes(outString);
int len = outBuffer.Length;
if (len > UInt16.MaxValue)
{
len = (int)UInt16.MaxValue;
}
ioStream.WriteByte((byte)(len / 256));
ioStream.WriteByte((byte)(len & 255));
ioStream.Write(outBuffer, 0, len);
ioStream.Flush();
return outBuffer.Length + 2;
}
}
}
如果你已经读到这篇文章的末尾:谢谢 :) 我希望它能帮助你成为一名成功的开发者!我的原型的最终结果: