在 Winforms-Application 中嵌入的 Unity-Application 上调用函数 [重复]

Posted

技术标签:

【中文标题】在 Winforms-Application 中嵌入的 Unity-Application 上调用函数 [重复]【英文标题】:Calling Functions on Unity-Application embedded in Winforms-Application [duplicate] 【发布时间】:2018-01-15 20:02:29 【问题描述】:

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

感谢下面这篇精彩的帖子,让嵌入式 Unity 应用程序在 WinForms 应用程序中工作非常容易。

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

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

Example.zip

不幸的是,无论是示例还是我能找到的任何帖子都没有回答一个非常基本的问题:如何在嵌入式 Unity 应用程序中从 WinForms 应用程序传递数据(或调用方法)(反之亦然) ?

您的 WinForms 应用程序是否可以简单地调用 Unity 应用程序中的 MonoBehaviour 脚本或静态方法?如果是这样,怎么做?如果没有,什么是好的解决方法? Unity 到 WinForms 的通信如何作为回报?

更新:

使用程序员 (link) 提到的重复页面来实现一个解决方案,该解决方案使用命名管道在 WinForms 和 Unity 应用程序之间进行通信。

两个应用程序都使用 BackgroundWorkers,WinForms 应用程序充当服务器(因为它首先启动并且需要一个活动的连接侦听器,然后才启动客户端),而嵌入式 Unity 应用程序充当客户端。

不幸的是,Unity 应用程序在创建 NamedPipeClientStream(使用 Unity 2017.3 和 Net 2.0(不是 Net 2.0 子集)测试)时抛出 NotImplementedException,指出“ACL 在 Mono 中不受支持”。这个异常已经在the post mentioned above的一些cmets中报告了,但是不清楚,是否已经解决。已经尝试了“确保服务器在客户端尝试连接之前启动并运行”和“以管理员模式启动”的建议解决方案,但到目前为止都失败了。

解决方案:

经过更多测试,很明显,“ACL 在 Mono 中不支持”异常是由创建 NamedPipeClientStream-instance 时使用的 TokenImpersonationLevel-parameter 引起的。将其更改为 TokenImpersonationLevel.None 解决了这个问题。

这里是 WinForms 应用程序使用的代码,它充当命名管道服务器。确保在 Unity 应用程序客户端尝试连接之前执行此脚本!此外,请确保在启动服务器之前构建并发布了 Unity 应用程序。将 Unity 应用程序的 Unity 可执行文件放在 WinForms-applications 文件夹中,并将其命名为“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 中找到)的 GameObject 引用到“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://***.com/questions/43062782/send-message-from-one-program-to-another-in-unity
    /// </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;
        
    

如果你读到了这篇文章:谢谢 :) 我希望它能帮助你成为一名成功的开发者!

我的原型的最终结果:

【问题讨论】:

您可以查看进程通信。我能想到的其他方式,在计算机中创建本地网络并通过套接字传递数据。数据可以是用于更改数据值或调用方法的消息。只是内容问题。最坏的情况是,写入文件并定期轮询文件。但更多的是破解而不是解决方案。 更新了问题以记录使用命名管道的(失败的)解决方案。这应该是一个可能的解决方案,因为它已经工作了(请参阅更新问题中的链接),但现在无法让它工作。 更新了问题以记录使用命名管道的工作解决方案。 【参考方案1】:

如果不出意外,您可以依靠基本文件 I/O 在两者之间进行通信。

【讨论】:

这充其量应该是一个评论,因为它必须是最后的手段。 Websocket 是比较合适的方式。 我同意这将是最后的手段,并希望这反映在“如果没有别的”中。我没有足够的声誉来添加评论。 确实如此。即使它肯定会起作用,但如果经常使用,它的代价是性能不佳和 SSD 驱动器的静音但快速死亡。但无论哪种方式,仍然是一个有效的解决方案。谢谢你:) @P.Dörr 我不认为 SSD 有那么脆弱。现代操作系统对 SSD 的管理非常好:它们缓存了大量数据,防止不必要的使用。例如。我大约 7 年前买了我的 SSD,成功完成了几个项目,而且它看起来还不错。在最坏的情况下,您始终可以依赖 RamDisk 或类似的东西,这将保证您的文件存储在操作内存中(并偶尔同步到驱动器)。

以上是关于在 Winforms-Application 中嵌入的 Unity-Application 上调用函数 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

在 Gorilla Mux 中嵌套子路由器

ngx-bootstrap 下拉菜单在移动视图中嵌套子菜单

匹配 MongoDB 中嵌套子文档的嵌套数组

请教:wpf中嵌CefSharp, 怎么实现比例缩放功能?

mysql怎么在查询中嵌套子查询

elementui 表格中嵌套子表格