luly靓 发表于 2020-12-25 10:16

WPF 中嵌入Unity

最近,有做过一个wpf 工程中嵌入Unity 的Demo。
引用其他博主的一段话:
把Unity3D嵌入winform或者wpf程序,过去大部分使用UnityWebPlayer插件来实现,这个插件其实就是网页上播放unity页游的插件。
但是使用UnityWebPlayer嵌入桌面开发有各种问题,我认为最大的问题是效率问题(加载缓慢),毕竟是网页的加载方式,而且可以确认未来也不会得到任何优化。
由于WebGL的高速发展,unity公司认识到了webplayer十分鸡肋,毕竟WebGL不需要任何插件可以直接显示3d内容了,所以Unity3D在5.4.x版本以后明确表示不再支持webplayer了,所以桌面上UnityWebPlayer插件能不用也就别用了吧。大该意思 就是 UnityWebPlayer Unity3D在5.4.x版本以后被弃用了。所以我们使用其他方式来嵌入。


首先根据Unity官方文档,选择嵌入方式。


文档中 介绍,可以使用两种 不同方式启动在 window 程序中启动Unity
在这里 我使用的是最简单的方式:将Unity作为外部进程启动,并为其指定一个窗口,使用-parentHWND命令行参数对Unity程序进行初始化和呈现。
通信

我是使用socket进行通讯,Unity程序做客户端,使用wpf程序做服务器端。(参考文章中有更加规范的做法)
版本:

Unity 版本: Unity 2018.4.1f1
Vs 版本:vs2017
WPF端相关操作

1、新建 WPF 工程
2、新建 Winform 自定义控件
3、在用户自定控件上 新建一个 Panel 来承接 Unity.exe 画面
   打开工具箱   视图→工具箱(注:需要在设计界面 才会有控件展示)
   如下图:


Panel 拖入到 空白区域即可。然后设置Panel 自适应 如下图。
新建一个label 来显示 将错误日志可视化。
4、编写用户控件相关代码
首先 打开 用户控件代码
代码如下:
public partial class UserControl1 : UserControl
    {
      
      static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);

      internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
      
      internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);

      
      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 UserControl1()
      {
            InitializeComponent();
            this.Load += UnityControl_Load;
            panel1.Resize += panel1_Resize;
      }

      private void UnityControl_Load(object sender, EventArgs e)
      {
            try
            {
                process = new Process();
                //注意此路径 为Unity程序导出路径
                process.StartInfo.FileName = Application.StartupPath + @"\UnityApp\WPFAndU3D.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);

                label1.Text = "Unity HWND: 0x" + unityHWND.ToString("X8");
            }
            catch (Exception ex)
            {
                // label1 为 上面新建的label
                label1.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();
      }

      private void panel1_Paint(object sender, PaintEventArgs e)
      {

      }
    }5、找到 MainWindow.xaml 设置主界面 格式,将自定义控件 内置到主界面中
<Window x:Class="WpfApp.MainWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:local="clr-namespace:WpfApp"
      mc:Ignorable="d"
      
      WindowState="Maximized" WindowStyle="None"
      
      Title="MainWindow" Height="1080" Width="1920">

    <Grid>
      <Grid.RowDefinitions>
            <RowDefinition Height="50px"/>
            <RowDefinition Height="80px"/>
            <RowDefinition Height="*"/>
      </Grid.RowDefinitions>

      <!--注释绘制这个标题栏-->
      <Grid Name="toolBar" Height="50px" Width="Auto" Background="Black" MouseDown="Window_MouseDown" Margin="0" Grid.Row="0">

            <!--标题文本-->
            <StackPanel HorizontalAlignment="Left" VerticalAlignment="Center">
                <TextBlock Text="WPFTOU3D" FontSize="30" Foreground="White" ></TextBlock>
            </StackPanel>


            <!--右侧按钮-->
            <StackPanel Name="buttons" Grid.ColumnSpan="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
                <!--最小化-->
                <ButtonName="btn_min" Width="50px" Height="50px" Padding="2" Margin="5" Background="Transparent" Click="btn_min_Click">

                  <TextBlock FontFamily="Segoe MDL2 Assets" FontSize="30"Text="" Foreground="White" />
                </Button>
                <!--normal/最大化-->
                <Button Name="btn_normal" Width="50px" Height="50px" Padding="2" Margin="5" Background="Transparent" Click="btn_normal_Click">
                  <Grid>
                        <TextBlock Name="toNormalSize" FontSize="30"FontFamily="Segoe MDL2 Assets" Text="" Foreground="White" />
                        <TextBlock Name="toMaxSize" Visibility="Hidden" FontSize="30" FontWeight="Bold" FontFamily="Segoe MDL2 Assets" Text="" Foreground="White" />
                  </Grid>
                </Button>

                <!--关闭程序-->
                <Button Name="btn_close" Width="50px" Height="50px" Padding="2" Margin="5" Background="Transparent" Click="btn_close_Click">
                  <TextBlock FontFamily="Segoe MDL2 Assets" Text="" FontSize="30" FontWeight="Bold" Foreground="White" />
                </Button>
            </StackPanel>


      </Grid>

      <!--主要内容页-->

      <Grid Grid.Row="2" Background="white" Margin="16,16,16,30">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="500"/>
                <ColumnDefinition Width="233*"/>
                <ColumnDefinition Width="118*"/>
            </Grid.ColumnDefinitions>


            <ListView>
                <ListViewItem Width="500">
                  <Grid Height="100" Width="500" HorizontalAlignment="Center">
                        <StackPanel HorizontalAlignment="Left">
                            <Button Name="btn1" Click="btn1_Click">
                              <!--图片问题,可找随意图片代替,同名即可放到对应文件夹下-->
                              <Image Width="100" Height="100" Source="Assets/img/LeftArrow.png"></Image>
                            </Button>
                        </StackPanel>
                        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30">X方向移动</TextBlock>
                        <StackPanel HorizontalAlignment="Right">
                            <Button Name="btn2" Click="btn2_Click">
                              <ImageWidth="100" Height="100" Source="Assets/img/RightArrow.png" RenderTransformOrigin="3.842,0.494"></Image>
                            </Button>
                        </StackPanel>

                  </Grid>


                </ListViewItem>




                <ListViewItem Width="500">
                  <Grid Height="100" Width="500" HorizontalAlignment="Center">
                        <StackPanel HorizontalAlignment="Left">
                            <Button Name="btn3" Click="btn3_Click">
                              <Image Width="100" Height="100" Source="Assets/img/LeftArrow.png"></Image>
                            </Button>
                        </StackPanel>
                        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30">Y方向移动</TextBlock>
                        <StackPanel HorizontalAlignment="Right">
                            <Button Name="btn4" Click="btn4_Click">
                              <ImageWidth="100" Height="100" Source="Assets/img/RightArrow.png" RenderTransformOrigin="3.842,0.494"></Image>
                            </Button>
                        </StackPanel>

                  </Grid>


                </ListViewItem>


                <ListViewItem Width="500">
                  <Grid Height="100" Width="500" HorizontalAlignment="Center">
                        <StackPanel HorizontalAlignment="Left">
                            <Button Name="btn5" Click="btn5_Click">
                              <Image Width="100" Height="100" Source="Assets/img/LeftArrow.png"></Image>
                            </Button>
                        </StackPanel>
                        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30">Z方向移动</TextBlock>
                        <StackPanel HorizontalAlignment="Right">
                            <Button Name="btn6" Click="btn6_Click">
                              <ImageWidth="100" Height="100" Source="Assets/img/RightArrow.png" RenderTransformOrigin="3.842,0.494"></Image>
                            </Button>
                        </StackPanel>

                  </Grid>


                </ListViewItem>

            </ListView>


            <!--建立一个winform的容器-->
            <Grid Background="Azure"Grid.Column="1" Name="Container" >

                <WindowsFormsHost>
                  <local:UserControl1 x:Name="unityhost">

                  </local:UserControl1>
                </WindowsFormsHost>


            </Grid>


      </Grid>


    </Grid>
</Window>设置后如下图:


注意 : 添加用户自定义空间时 , xmal可能 会报错,说Wpf 程序 不支持 WindowsFormsHost按如下操作可解决报错
在Visual Studio中,通过右键单击项目中的“引用”节点并选择“添加引用”来完成此操作:
在弹出的对话框中,选择“程序集”,然后勾选我们需要添加的两个程序集:


6、编写 MainWindow.xaml 的交互逻辑
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {

      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();


      }

      private void btn_close_Click(object sender, RoutedEventArgs e)
      {
            this.Close();

      }

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


            WpfServer.QuitServer();

      }

      void MainWindow_Deactivated(object sender, EventArgs e)
      {

            unityhost.Form1_Deactivate();
      }

      void MainWindow_Activated(object sender, EventArgs e)
      {
            unityhost.Form1_Activated();
      }

      private void btn_normal_Click(object sender, RoutedEventArgs e)
      {
            if (size_state == 0)
            {
                this.WindowState = WindowState.Normal;
                this.Width = 1280;
                this.Height = 800;
                toNormalSize.Visibility = Visibility.Hidden;
                toMaxSize.Visibility = Visibility.Visible;

                size_state = 1;
            }
            else
            {
                this.WindowState = WindowState.Maximized;
                toNormalSize.Visibility = Visibility.Visible;
                toMaxSize.Visibility = Visibility.Hidden;
                size_state = 0;
            }

      }

      private void btn_min_Click(object sender, RoutedEventArgs e)
      {
            this.WindowState = WindowState.Minimized;
      }

      private void Window_MouseDown(object sender, MouseButtonEventArgs e)
      {
            try
            {
                this.DragMove();
            }
            catch (Exception)
            {


            }

      }

      private void btn1_Click(object sender, RoutedEventArgs e)
      {
            WpfServer.SendMessage("1");
      }

      private void btn2_Click(object sender, RoutedEventArgs e)
      {
            WpfServer.SendMessage("2");
      }

      private void btn3_Click(object sender, RoutedEventArgs e)
      {
            WpfServer.SendMessage("3");
      }

      private void btn4_Click(object sender, RoutedEventArgs e)
      {
            WpfServer.SendMessage("4");
      }

      private void btn5_Click(object sender, RoutedEventArgs e)
      {
            WpfServer.SendMessage("5");
      }

      private void btn6_Click(object sender, RoutedEventArgs e)
      {
            WpfServer.SendMessage("6");
      }
    }
}

wpf 端通信类 (这个通信类,参考别人写,比较简单)
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Diagnostics;
using System.Text;
using System;



    class TcpServer
    {
      //私有成员
      private static byte[] result = new byte;
      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)
                  {
                  }

                }
            }
      }
    }

Wpf 端我们搞定了,接下来我们来配置Unity端。
Unity 部分

1、新建一个工程,在场景中 新建一个Cube
在Cube上面 添加上如下脚本。
using UnityEngine;
using System.Collections;
using System.Net.Sockets;
using System;

public class Demo : 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;
            //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)
      {


      }
    }


    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();
    }




}2、导出工程
工程设置 如下: 其中Display ResolutionDialog需要关闭,否则启动Unity导出exe文件 会有选择分辨率提示,导致wpf端 不显示。
导入上面 新建Wpf项目中文件夹的 bin/Debug/UnityApp 文件夹下。
exe 文件名称需要与wpf 端用户控件中 代码 保持一致。
如下图


至此,Unity 端设置完毕。
返回,WPF项目,点击运行,可得到下图效果:


参考文章:


git仓库地址:
页: [1]
查看完整版本: WPF 中嵌入Unity