桌面WPF程序嵌入Unity3D引擎(standalone)並實現通訊。


 

寫在前面:

 

把Unity3D嵌入winform或者wpf程序,過去大部分使用UnityWebPlayer插件來實現,這個插件其實就是網頁上播放unity頁游的插件。

但是使用UnityWebPlayer嵌入桌面開發有各種問題,我認為最大的問題是效率問題(加載緩慢),畢竟是網頁的加載方式,而且可以確認未來也不會得到任何優化。

由於WebGL的高速發展,unity公司認識到了webplayer十分雞肋,畢竟WebGL不需要任何插件可以直接顯示3d內容了,所以Unity3D在5.4.x版本以后明確表示

不再支持webplayer了,所以桌面上UnityWebPlayer插件能不用也就別用了吧。所以大家不要走彎路!!

 

主要內容:

 

將Unity嵌入桌面程序最好的方式是嵌入unity生成的exe程序,winform程序和unity之間通過socket進行通訊,我認為

這也是效率最高,效果最好和最好實現的方式。在Unity程序腳本中,嵌入socket內容,我推薦做成客戶端(client),使用wpf程序做服務器端,這是一個誰是主體的問題。

這樣wpf可以加載多個unity程序。

 

嵌入后的結果如下圖所示(請無視具體內容):

下面簡單寫了一個腳本,其中man是游戲中的一個gameobject對象。就是上圖中穿藍衣服的男人。在他身上掛着socket腳本如下:

using UnityEngine;
using System.Collections;
using System.Net.Sockets;
using System;

public class demoshows : MonoBehaviour {
    public GameObject man;



    const int portNo = 500;
    private TcpClient _client;
    byte[] data;

    string Error_Message;

    void Start () {
        try
        {
            this._client = new TcpClient();
            this._client.Connect("127.0.0.1", portNo);
            data = new byte[this._client.ReceiveBufferSize];
            //SendMessage(txtNick.Text);
            SendMessage("Unity Demo Client is Ready!");
            this._client.GetStream().BeginRead(data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
        }
        catch (Exception ex)
        {


        }
       


    }
    

    void Update () {        
        transform.Rotate(new Vector3(0, 1, 0),0.1f);
    }


    public void rotation()

    {

        transform.Rotate(new Vector3(0, 10, 0));
        //targetRotation = Quaternion.Euler(45.0f, 45.0f, 45.0f);
        //// 直接設置旋轉角度 
        //transform.rotation = targetRotation;
        ////man.transform.rotation.SetAxisAngle(new Vector3(0, 1, 0), 30);;                                                                                                                      
    }



    public void translateX(float x)

    {

        transform.Translate(new Vector3(x,0,0));
                                                                                                                      
    }

    public void translateY(float y)

    {

        transform.Translate(new Vector3(0, y, 0));
                                                                                                         
    }


    public void translateZ(float z)

    {

        transform.Translate(new Vector3(0, 0, z));
                                                                                                                   
    }



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


    public new void SendMessage(string message)
    {
        try
        {
            NetworkStream ns = this._client.GetStream();
            byte[] data = System.Text.Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            ns.Flush();
        }
        catch (Exception ex)
        {
            Error_Message = ex.Message;
            //MessageBox.Show(ex.ToString());
        }
    }
    public void ReceiveMessage(IAsyncResult ar)
    {
        try
        {
            //清空errormessage
            Error_Message = "";
            int bytesRead;
            bytesRead = this._client.GetStream().EndRead(ar);
            if (bytesRead < 1)
            {
                return;
            }
            else
            {
                Debug.Log(System.Text.Encoding.ASCII.GetString(data, 0, bytesRead));
                string message = System.Text.Encoding.ASCII.GetString(data, 0, bytesRead);
                switch (message)
                {
                    case "1":
                        translateX(1);
                    break;

                    case "2":
                        translateX(-1);
                        break;
                    case "3":
                        translateY(1);
                        break;
                    case "4":
                        translateY(-1);
                        break;
                    case "5":
                        translateZ(1);
                        break;
                    case "6":
                        translateZ(-1);
                        break;
                    default:
                        Error_Message = "unknown command";
                        break;


                }




            }
            this._client.GetStream().BeginRead(data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
        }
        catch (Exception ex)
        {
            Error_Message = ex.Message;
        }
    }


    void OnDestroy()
    {

        this._client.Close();
    }




}

 

腳本很簡單,就是通過向unity程序發送消息(1~6)實現模型的平移。

 

服務器端,在wpf程序中簡單建立一個socket類,ip和端口要和unity對應,在程序啟動時先建立服務器端。

 

using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Diagnostics;
using System.Text;
using System;


namespace Demo_Song
{
    class TcpServer
    {
        //私有成員
        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());
       
            //通過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)
                {

                }

            }
        }

        /// <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);
                    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)
                    {
                    }

                }
            }
        }
    }
}  

 

使用socket一定要注意使用線程,並且退出時及時結束,我這里實現的也不是很好,誰有更好的方法可以告訴我。

下面大概就是server的啟動和釋放,效果還好,至少不卡死....

 

 

        TcpServer WpfServer;
        int size_state = 0;
        public MainWindow()
        {
            InitializeComponent();


            this.Closed += MainWindow_Closed;
            this.Activated += MainWindow_Activated;
            this.Deactivated += MainWindow_Deactivated;



            WpfServer = new TcpServer();
            WpfServer.StartServer();


        }


        void MainWindow_Closed(object sender, EventArgs e)
        {
            unityhost.Form1_FormClosed();


            WpfServer.QuitServer();

        }

 

 

 

 

關於socket通訊的問題大概就這樣,大家估計更關系如何嵌入的問題

 

如何嵌入?

 

首先在wpf程序中建立一個winform的自定義控件(不是wpf控件)usercontrol

在usercontrol內新建一個panel(或者其他帶有句柄的控件),並設置dock屬性為fill。

啟動unity.exe,通過幾個api將unity窗口附加在panel句柄上。

(說明:借鑒別人的程序)

需要把unity程序命名為child.exe,放在下面指定位置(Debug\UnityApp\Child.exe,或者release)

process.StartInfo.FileName =Application.StartupPath +@"\UnityApp\Child.exe";

詳細代碼如下:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading;

namespace Demo_Song
{
    public partial class UnityControl : UserControl
    {
        [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);

        private Process process;
        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);





        public UnityControl()
        {
            InitializeComponent();
            this.Load += UnityControl_Load;
            panel1.Resize+=panel1_Resize;
        }

        private void UnityControl_Load(object sender, EventArgs e)
        {
            try
            {
                process = new Process();
                process.StartInfo.FileName =Application.StartupPath +@"\UnityApp\Child.exe";
                process.StartInfo.Arguments = "-parentHWND " + panel1.Handle.ToInt32() + " " + Environment.CommandLine;
                process.StartInfo.UseShellExecute = true;
                process.StartInfo.CreateNoWindow = true;

                process.Start();

                process.WaitForInputIdle();
                // Doesn't work for some reason ?!
                //unityHWND = process.MainWindowHandle;
                EnumChildWindows(panel1.Handle, WindowEnum, IntPtr.Zero);

                unityHWNDLabel.Text = "Unity HWND: 0x" + unityHWND.ToString("X8");
            }
            catch (Exception ex)
            {
                unityHWNDLabel.Text = ex.Message;
                //MessageBox.Show(ex.Message);
            }
        }

        internal void ActivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
        }

        internal 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, panel1.Width, panel1.Height, true);
            ActivateUnityWindow();
        }

        // Close Unity application
        internal void Form1_FormClosed()
        {
            try
            {
                process.CloseMainWindow();

                Thread.Sleep(1000);
                while (process.HasExited == false)
                    process.Kill();
            }
            catch (Exception)
            {

            }
        }

        internal void Form1_Activated()
        {
            ActivateUnityWindow();
        }

        internal void Form1_Deactivate()
        {
            DeactivateUnityWindow();
        }
    }

}

 

 

最后附上源碼,github重置密碼死活連不上,現在就傳壓縮包到百度雲把,vs版本較高(2012)以上可以打開,注意,低版本打不開工程。

 

博客地址遷移了,請到 這里

http://www.songshizhao.com/blog/blogPage/78.html

下載。

 

貌似寫得有點長了,能看到這里的人不多吧哈哈,希望有做類似需求的人少走彎路吧,碎覺去,over~。

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM