Unity3D应用嵌入WPF应用并实现通信

最近由于项目需要,对Unity3D应用嵌入WPF应用进行了研究,并通过Socket实现了两者的通信。由于Unity3D在5.4.x版本后不再支持WebPlayer,所以并未使用UnityWebPlayer,另外考虑到我们原有的业务系统都是基于WPF的,全部改到Unity3D里面工作量会很大,所以采用了将Unity3D生成的exe可执行程序直接嵌入到WPF中的做法。
我们的设想是WPF程序作为主程序负责业务逻辑处理和页面展示,Unity3D程序作为子程序负责模型展示和交互,二者通过Socket建立连接、相互传递和接收消息以实现操作联动。例如在主程序点击页面按钮触发模型内特定对象高亮显示或点击模型内任意对象联动主程序显示关联数据。

环境版本
操作系统Windows 10 prefessional
编译器Visual Studio 2015 update3

创建WPF应用并嵌入Unity3D应用

从本质上来讲,这是一个Win32应用程序嵌入到WPF应用程序的问题,由于两者窗口绘制原理的差异,就必需依靠Win32API,也就是大家常常会提到的user32.dll。C#中使用Win32Api与C++略有不同,需要使用DllInput,对引用user32.dll进行了简单封装,代码如下:

    public class Win32Helper
    {
        [DllImport("user32.dll")]
        static extern IntPtr SetActiveWindow(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern IntPtr GetForegroundWindow();

        [DllImport("user32.dll")]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("user32.dll", EntryPoint = "GetWindowThreadProcessId", SetLastError = true,
           CharSet = CharSet.Unicode, ExactSpelling = true,
           CallingConvention = CallingConvention.StdCall)]
        public static extern long GetWindowThreadProcessId(long hWnd, long lpdwProcessId);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern int SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

        [DllImport("user32.dll")]
        public static extern uint GetWindowLong(IntPtr hwnd, int nIndex);

        [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)]
        public static extern uint SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern long SetWindowPos(IntPtr hwnd, long hWndInsertAfter, long x, long y, long cx, long cy, long wFlags);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool MoveWindow(IntPtr hwnd, int x, int y, int cx, int cy, bool repaint);

        [DllImport("user32.dll", EntryPoint = "ostMessageA", SetLastError = true)]
        public static extern bool PostMessage(IntPtr hwnd, uint Msg, uint wParam, uint lParam);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr GetParent(IntPtr hwnd);

        [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)]
        public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        [DllImport("User32.dll", EntryPoint = "SendMessage")]
        public static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);

        public const int SWP_NOOWNERZORDER = 0x200;
        public const int SWP_NOREDRAW = 0x8;
        public const int SWP_NOZORDER = 0x4;
        public const int SWP_SHOWWINDOW = 0x0040;
        public const int WS_EX_MDICHILD = 0x40;
        public const int SWP_FRAMECHANGED = 0x20;
        public const int SWP_NOACTIVATE = 0x10;
        public const int SWP_ASYNCWINDOWPOS = 0x4000;
        public const int SWP_NOMOVE = 0x2;
        public const int SWP_NOSIZE = 0x1;
        public const int GWL_STYLE = (-16);
        public const int WS_VISIBLE = 0x10000000;
        public const int WS_MAXIMIZE = 0x01000000;
        public const int WS_BORDER = 0x00800000;
        public const int WM_CLOSE = 0x10;
        public const int WS_CHILD = 0x40000000;
        public const int WS_POPUP = -2147483648;
        public const int WS_CLIPSIBLINGS = 0x04000000;
        public const int SW_HIDE = 0; //{隐藏, 并且任务栏也没有最小化图标}
        public const int SW_SHOWNORMAL = 1; //{用最近的大小和位置显示, 激活}
        public const int SW_NORMAL = 1; //{同 SW_SHOWNORMAL}
        public const int SW_SHOWMINIMIZED = 2; //{最小化, 激活}
        public const int SW_SHOWMAXIMIZED = 3; //{最大化, 激活}
        public const int SW_MAXIMIZE = 3; //{同 SW_SHOWMAXIMIZED}
        public const int SW_SHOWNOACTIVATE = 4; //{用最近的大小和位置显示, 不激活}
        public const int SW_SHOW = 5; //{同 SW_SHOWNORMAL}
        public const int SW_MINIMIZE = 6; //{最小化, 不激活}
        public const int SW_SHOWMINNOACTIVE = 7; //{同 SW_MINIMIZE}
        public const int SW_SHOWNA = 8; //{同 SW_SHOWNOACTIVATE}
        public const int SW_RESTORE = 9; //{同 SW_SHOWNORMAL}
        public const int SW_SHOWDEFAULT = 10; //{同 SW_SHOWNORMAL}
        public const int SW_MAX = 10; //{同 SW_SHOWNORMAL}
        public const int WM_SETTEXT = 0x000C;
        public const int WM_ACTIVATE = 0x0006;
        public static readonly IntPtr WA_ACTIVE = new IntPtr(1);
        public static readonly IntPtr WA_INACTIVE = new IntPtr(0);

    }

有了Win32Api下步就可以着手将Unity3D应用嵌入WPF应用。大致思路就是创建一个进程用来启动Unity3D应用,同时在WPF应用中使用WindowsFormsHost划定一块区域用来放置Unity3D,并将其作为子窗口。XAML页面布局如下:

    <Grid>
        <WindowsFormsHost>
            <form:Panel x:Name="unityHost"></form:Panel>
        </WindowsFormsHost>
    </Grid>

后台代码如下:

        private Process process;
        public IntPtr childHandle;

        private void UnityInit()
        {
            string path = Environment.CurrentDirectory + @"\Unity\HelloUnity.exe";
            IntPtr hostHandle = unityHost.Handle;
            process = new Process();
            process.StartInfo.FileName = path;
            //process.StartInfo.Arguments = "-parentHWND " + panel1.Handle.ToInt32() + " " + Environment.CommandLine;
            process.StartInfo.UseShellExecute = true;
            process.StartInfo.CreateNoWindow = true;
            process.Start();
            //process.WaitForInputIdle();

            childHandle = process.MainWindowHandle;
            while (childHandle == IntPtr.Zero)
            {
                childHandle = process.MainWindowHandle;
            }
            uint oldStyle = Win32Helper.GetWindowLong(childHandle, Win32Helper.GWL_STYLE);
            //Win32Helper.SetWindowLong(childHandle, Win32Helper.GWL_STYLE, (oldStyle | WS_CHILD) & ~WS_BORDER);
            Win32Helper.SetWindowLong(childHandle, Win32Helper.GWL_STYLE, oldStyle & ~WS_BORDER);//去除边框
            Win32Helper.SetParent(childHandle, hostHandle);//设为子窗体
            Win32Helper.MoveWindow(childHandle, -2, -2, unityHost.Width+4, unityHost.Height+4, true);//移动窗口位置
        }

Unity3D应用与WPF应用通信

上一阶段将Unity3D应用已经嵌入到了WPF应用,但二者实际上仍没有任何联系,相互独立,并不知道对方在做什么,也就无法实现联动。为了实现联动,他们相互之间就需要传输数据,这就回归到了Windows窗口传递消息的问题上。窗口间传递消息有多种方法,比如使用Win32Api传递消息、Socket等,由于对Win32Api掌握的还不是很熟练,于是参考了网上使用Socket传递消息的示例。
Socket通信包含两部分,其中WPF应用作为服务端,Unity3D作为客户端,两者先后启动建立连接就可以相互发送和接收消息了,根据消息的内容采取相应的操作。
服务端代码如下:

    public class ConnectHelper
    {
        //私有成员
        private static byte[] result = new byte[1024];
        private int myProt = 500;   //端口  
        static Socket serverSocket;
        static Socket clientSocket;

        Thread myThread;
        static Thread receiveThread;

        //属性

        public int port { get; set; }
        //方法

        internal void StartServer()
        {
            //服务器IP地址  
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            serverSocket.Bind(new IPEndPoint(ip, myProt));  //绑定IP地址:端口  
            serverSocket.Listen(10);    //设定最多10个排队连接请求  

            //Debug.WriteLine("启动监听{0}成功", serverSocket.LocalEndPoint.ToString());
            string message = string.Format("");

            //通过Clientsoket发送数据  
            myThread = new Thread(ListenClientConnect);
            myThread.Start();

        }

        internal void QuitServer()
        {
            serverSocket.Close();
            clientSocket.Close();
            myThread.Abort();
            receiveThread.Abort();
        }

        internal void SendMessage(string msg)
        {
            clientSocket.Send(Encoding.ASCII.GetBytes(msg));
        }


        /// <summary>  
        /// 监听客户端连接  
        /// </summary>  
        private static void ListenClientConnect()
        {
            while (true)
            {
                try
                {
                    clientSocket = serverSocket.Accept();
                    clientSocket.Send(Encoding.ASCII.GetBytes("Server Say Hello"));
                    receiveThread = new Thread(ReceiveMessage);
                    receiveThread.Start(clientSocket);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                }

            }
        }

        /// <summary>  
        /// 接收消息  
        /// </summary>  
        /// <param name="clientSocket"></param>  
        private static void ReceiveMessage(object clientSocket)
        {
            Socket myClientSocket = (Socket)clientSocket;
            while (true)
            {
                try
                {
                    //通过clientSocket接收数据  
                    int receiveNumber = myClientSocket.Receive(result);
                    string message = Encoding.ASCII.GetString(result, 0, receiveNumber);
                    //Debug.WriteLine("接收客户端{0}消息{1}", myClientSocket.RemoteEndPoint.ToString(), Encoding.ASCII.GetString(result, 0, receiveNumber));
                }
                catch (Exception ex)
                {
                    try
                    {
                        Debug.WriteLine(ex.Message);
                        myClientSocket.Shutdown(SocketShutdown.Both);
                        myClientSocket.Close();
                        break;
                    }
                    catch (Exception)
                    {

                    }

                }
            }
        }
    }

WPF应用初始化完成后开启Socket服务端:

        public Helpers.ConnectHelper _connector;

        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += MainWindow_Loaded;
            this.Closed += MainWindow_Closed;

            _connector = new Helpers.ConnectHelper();
            _connector.StartServer();
        }

之后Unity3D应用初始化完成后启动Socket客户端,并与服务端建立连接

public class NewBehaviourScript : MonoBehaviour
{
    const int _port = 500;
    private TcpClient _client;
    byte[] _data;
    string _error;

    // Use this for initialization
    void Start () {
        Init();
    }

    // Update is called once per frame
    void Update () {
        //按键盘上的上下左右键可以翻看模型的各个面[模型旋转]
        // 上
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.Rotate(Vector3.right * Time.deltaTime * 10);
            SendMessage("up!");

        }
        // 下
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.Rotate(Vector3.left * Time.deltaTime * 10);
            SendMessage("down!");

        }
        // 左
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.Rotate(Vector3.up * Time.deltaTime * 10);
            SendMessage("left!");

        }
        // 右
        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.Rotate(Vector3.down * Time.deltaTime * 10);
            SendMessage("right!");

        }

    }

    void OnGUI()
    {
        GUI.Label(new Rect(50, 50, 150, 50), _error);
    }

    void OnDestory()
    {
        _client.Close();
    }

    #region 通信初始化
    private void Init()
    {
        try
        {
            _client = new TcpClient();
            _client.Connect("127.0.0.1", _port);
            _data = new byte[_client.ReceiveBufferSize];
            SendMessage("Ready!");
            _client.GetStream().BeginRead(_data, 0, _client.ReceiveBufferSize, ReceiveMessage, null);
        }
        catch (Exception ex)
        {

            _error = ex.Message;
        }

    }
    #endregion

    #region 发送消息
    public new void SendMessage(string message)
    {
        try
        {
            NetworkStream ns = _client.GetStream();
            byte[] data = System.Text.Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            ns.Flush();
        }
        catch (Exception ex)
        {
            _error = ex.Message;
        }
    }

    #endregion

    #region 接收消息
    private void ReceiveMessage(IAsyncResult ar)
    {
        try
        {
            _error = "";
            int bytesRead;
            bytesRead = _client.GetStream().EndRead(ar);
            if (bytesRead < 1)
            {
                return;
            }
            else
            {
                string message = System.Text.Encoding.ASCII.GetString(_data, 0, bytesRead);
                switch (message)
                {
                    case "up":
                        transform.Rotate(Vector3.right * 10);
                        break;
                    case "down":
                        transform.Rotate(Vector3.left * 10);
                        break;
                    case "left":
                        transform.Rotate(Vector3.up * 10);
                        break;
                    case "right":
                        transform.Rotate(Vector3.down * 10);
                        break;
                }
                _error = string.Format("{0}:{1}", DateTime.Now.ToString(), message);
                this._client.GetStream().BeginRead(_data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
            }
        }
        catch (Exception ex)
        {
            _error = ex.Message;
        }

    }
    #endregion

}

客户端的启动是在Unity3D的一个脚本里完成的,这个脚本同时包含了通过键盘控制对象旋转的代码,将脚本挂载到游戏对象里,实现操控。
同时为了实现在WPF主程序中对Unity3D对象进行控制,在WPF主程序添加了几个按钮,点击按钮发送消息给Unity3D执行操作。

        //向上旋转
        private void btnUp_Click(object sender, RoutedEventArgs e)
        {
            _connector.SendMessage("up");
        }
        //向下旋转
        private void btnDown_Click(object sender, RoutedEventArgs e)
        {
            _connector.SendMessage("down");
        }
        //向左旋转
        private void btnLeft_Click(object sender, RoutedEventArgs e)
        {
            _connector.SendMessage("left");
        }
        //向右旋转
        private void btnRight_Click(object sender, RoutedEventArgs e)
        {
            _connector.SendMessage("right");
        }

代码下载

在这个过程中也遇到了一些问题,比如Unity3D嵌入后无法单独操控(不能响应键盘/鼠标输入),试了多次,最终才有了一个相对折衷的方案。

  • 16
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 32
    评论
WPF(Windows Presentation Foundation)是微软公司推出的一种用户界面开发框架,它可以用于创建丰富的、直观的Windows应用程序。Unity3D是一种跨平台的游戏引擎,广泛应用于游戏开发、VR/AR应用程序等。将Unity3D嵌入WPF应用程序中可以实现更丰富的用户体验和交互。 在将Unity3D嵌入WPF应用程序中时,首先需要在WPF项目中添加Unity3DUnityPlayer控件。然后,可以通过调用Unity3D的接口方法来加载和显示Unity3D场景,并在WPF应用程序中设置对象之间的交互。通过这种方式,可以在WPF应用程序中直接运行Unity3D场景,使用户可以更直观地与虚拟世界进行交互。 嵌入Unity3DWPF应用程序中的好处是可以将游戏和应用程序的功能结合在一起,为用户提供更多更丰富的功能和体验。例如,可以在WPF应用程序中创建一个交互式的虚拟展示环境,用户可以在展示环境中与虚拟物体进行交互、操作。另外,嵌入Unity3D还可以增加应用程序的趣味性和视觉效果,使应用程序更加吸引人。 然而,嵌入Unity3DWPF应用程序中也存在一些挑战和限制。由于WPFUnity3D是两种不同的技术平台,需要做好平台和版本之间的兼容性工作。同时,开发人员还需要熟悉和掌握Unity3D的开发技术,以便在WPF应用程序中正确使用和操作Unity3D场景。 综上所述,将Unity3D嵌入WPF应用程序中可以提供更丰富的用户体验和功能,但也需要在技术上做好兼容性工作,并投入额外的开发资源和学习成本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值