在Winforms应用程序中调用嵌入的Unity应用程序的函数

13

我正在开发一个简单的编辑器原型。编辑器将使用WinForms(如果可能的话,使用WPF)提供主要用户界面,并嵌入Unity 2017独立应用程序来可视化数据并提供其他控件元素(例如缩放,旋转,滚动等)。

感谢下面这篇好文章,让在WinForms应用程序中嵌入Unity应用程序变得惊人地容易。

https://forum.unity.com/threads/unity-3d-within-windows-application-enviroment.236213/

此外,还有一个简单的示例应用程序,您可以在此处访问:

Example.zip

不幸的是,无论是示例,还是任何我能找到的帖子都没有回答一个非常基本的问题:您如何在嵌入的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;
        }
    }
}
如果你已经读到这篇文章的末尾:谢谢 :) 我希望它能帮助你成为一名成功的开发者!
我的原型的最终结果:使用命名管道成功通信的带有嵌入式Unity引擎的WinForms应用程序

你可以考虑进程通信。另一种方式是在计算机上创建本地网络,通过套接字传递数据。数据可以是用于更改数据值或调用方法的消息。只是内容的问题。最坏的情况是写入文件并定期轮询文件。但这更像是一个hack而不是解决方案。 - Everts
更新了问题,记录了使用命名管道的(失败)解决方案。这应该是一个可能的解决方案,因为它已经起作用了(请参见更新问题中的链接),但目前还无法使其工作。 - P. Dörr
更新了问题,记录了使用命名管道的可行解决方案。 - P. Dörr
1个回答

1
如果没有其他办法,您可以退而求其次,使用基本的文件I/O在两者之间进行通信。

最好只是一个注释,因为它应该是最后的手段。Websocket 是更合适的方式。 - Everts
我确实同意这将是最后的选择,并希望在“如果没有其他办法”的情况下反映出来。我没有足够的声望来添加评论。 - Lothar
没错。虽然这种方法肯定可行,但代价是性能不佳,如果经常使用会导致SSD硬盘默默地快速死亡。但无论如何,仍然是一个有效的解决方案。谢谢 :) - P. Dörr
@P.Dörr 我不认为SSD是那么脆弱的。现代操作系统对SSD的管理非常好:它们缓存大量数据,防止不必要的使用。例如,我大约7年前购买了我的SSD,成功地完成了几个项目,它仍然表现良好。在最坏的情况下,您始终可以依靠RamDisk或类似的东西,这将保证您的文件存储在操作内存中(并偶尔同步到驱动器)。 - Vitalii Vasylenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接