Unity3D嵌入WPF

       此文章旨在记录自己做的第一个将Unity3D嵌入到WPF的工控项目,由于实际需要,也搜寻过很多博主的文章进行学习,在进行项目开发后记录如下心得以便日后参考,亦希望大家能多多指教。

       由于WPF在桌面应用程序开发且处理业务逻辑时的优点明显,但进行三维场景实时展示却捉襟见肘。相反Unity3D则具有三维场景展示与交互等优点,却在业务逻辑处理中存在一定的局限性。因此将Unity3D嵌入到WPF里并进行信息交互。

       这里先放Unity的官方链接,可以参考此文档选择嵌入方式,我这边选用的是将Unity作为外部进程启动,并放到指定窗口,使用parentHWND对Unity进行初始化和呈现。https://docs.unity3d.com/Manual/UnityasaLibrary-Windows.htmlhttps://docs.unity3d.com/Manual/UnityasaLibrary-Windows.html

       这里做一个小demo,先看看实际效果:

   一、WPF界面:

               新建WPF项目,然后在主界面拖动Border控件到窗体中,在XAML中更改到合适的位置,以此为依托来加载Unity,然后编写MainWindow.xaml的交互逻辑。

 二、MainWindow.xaml的编写:

       由于展示的是一个小demo,故拿物体简单的移动和旋转举例,故主要添加移动和旋转两个Button,再加两个TextBox作为输入。

<Window x:Class="示例1.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:示例1"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="600" Width="1000"
        Loaded="Window_Loaded"
        SizeChanged="Window_SizeChanged"
        Closed="Window_Closed"
        Deactivated="Window_Deactivated"
        Activated="Window_Activated">

    <Grid>
        <Border x:Name="Panel1" BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left" Height="516" Margin="10,30,0,0" VerticalAlignment="Top" Width="782"/>
        <Menu HorizontalAlignment="Left" Height="18
              " VerticalAlignment="Top" Width="992">
            <MenuItem Header="连接" Click="Connect" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            <MenuItem Header="加载" Name="LoadUnity3D" Click="LoadUnity_Click"/>
        </Menu>
        <Button Content="移动" HorizontalAlignment="Left" Margin="896,157,0,0" VerticalAlignment="Top" Width="75" Height="26" Click="Send"/>
        <TextBox HorizontalAlignment="Left" Height="26" Margin="807,157,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="75"/>
        <TextBox HorizontalAlignment="Left" Height="26" Margin="807,209,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="75"/>
        <Button Content="旋转" HorizontalAlignment="Left" Margin="896,209,0,0" VerticalAlignment="Top" Width="75" Height="26"/>
        <TextBox HorizontalAlignment="Left" Height="26" Margin="807,260,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="75"/>
        <Button Content="发射" HorizontalAlignment="Left" Margin="896,260,0,0" VerticalAlignment="Top" Width="75" Height="26"/>

    </Grid>
</Window>

三、MainWindow.xaml.cs的编写:

注意事项:

       1.在LoadUnity()里,这里应该在该项目的bin/debug文件夹下创造一个Unity文件夹,把Unity项目导入其中。

      process.StartInfo.FileName = appStartupPath + @"\Unity\example.exe";

     2.由于此博文主要记录Unity嵌入到WPF中,展示嵌入及运动效果,故在定时器触发事件中,我给的运动指令是一个自动指令,无需在TextBox中输入指定值。关于想要物体在自己输入的情况下进行运动,将在下一篇博文中进行记录。

        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            count += 0.05f;
            string str = string.Format("{0} , {1} ", count, -6.5f * count + 1);
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
            socketCommnication.Send(buffer);
        }

        这里放下demo的.cs整块代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Windows.Threading;
using System.Timers;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Interop;
using System.Net;

namespace 示例1
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        [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);

        private bool isU3DLoaded = false;
        private Point u3dLeftUpPos;

        private DispatcherTimer dispatcherTimer;
        
        System.Timers.Timer timer = new System.Timers.Timer();
        float count = 0;
        Socket socketCommnication;
        bool IsListening = true;
        Thread threadli;

        public MainWindow()
        {
            InitializeComponent();
            timer.Interval = 100;
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            timer.AutoReset = true ;
        }
        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            count += 0.05f ;
            string str = string.Format("{0} , {1}  ", count, -5f * count + 1);
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
            socketCommnication.Send(buffer);
        }

        //开始监听线程
        private void Listen(object obj)
        {
            Socket socketWatch = obj as Socket;
            while (IsListening)
            {
                socketCommnication = socketWatch.Accept();
                if (socketCommnication.Connected)
                {
                    System.Windows.MessageBox.Show(socketCommnication.RemoteEndPoint.ToString() + ":连接成功");
                    IsListening = false;
                }
            }
        }

        //窗体加载事件
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {

        }

        //窗体关闭事件
        private void Window_Closed(object sender, EventArgs e)
        {
            try
            {
                process.CloseMainWindow();

                Thread.Sleep(1000);
                while (process.HasExited == false)
                    process.Kill();

                //Sever.QuitServer();

                timer.Stop();
                socketCommnication.Close();
                IsListening = false;
                threadli.Abort();
                System.Environment.Exit(0);
            }
            catch (Exception)
            {
            }
        }

        //窗体大小改变事件
        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            ResizeU3D();
        }

        //获得焦点事件,首次打开软件、由别的软件切换到当前软件
        private void Window_Deactivated(object sender, EventArgs e)
        {
            DeactivateUnityWindow();
        }

        //失去焦点事件
        private void Window_Activated(object sender, EventArgs e)
        {
            ActivateUnityWindow();
        }

        #region Unity操作
        private void LoadUnity()
        {
            try
            {
                IntPtr hwnd = ((HwndSource)PresentationSource.FromVisual(Panel1)).Handle;
                process = new Process();

                String appStartupPath = System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
                process.StartInfo.FileName = appStartupPath + @"\Unity\example.exe";
                process.StartInfo.Arguments = "-parentHWND " + hwnd.ToInt32() + " " + Environment.CommandLine;
                process.StartInfo.UseShellExecute = true;
                process.StartInfo.CreateNoWindow = true;

                process.Start();
                process.WaitForInputIdle();
                isU3DLoaded = true;
                EnumChildWindows(hwnd, WindowEnum, IntPtr.Zero);

                dispatcherTimer = new DispatcherTimer();
                dispatcherTimer.Tick += new EventHandler(InitialResize);
                dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
                dispatcherTimer.Start();
            }
            catch (Exception ex)
            {
                string error = ex.Message;
            }
        }
        private void InitialResize(object sender, EventArgs e)
        {
            ResizeU3D();
            dispatcherTimer.Stop();
        }
        private int WindowEnum(IntPtr hwnd, IntPtr lparam)
        {
            unityHWND = hwnd;
            ActivateUnityWindow();
            return 0;
        }
        private void ActivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
        }

        private void DeactivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
        }

        private void ResizeU3D()
        {
            if (isU3DLoaded)
            {
                Window window = Window.GetWindow(this);
                u3dLeftUpPos = Panel1.TransformToAncestor(window).Transform(new Point(0, 0));
                DPIUtils.Init(this);
                u3dLeftUpPos.X *= DPIUtils.DPIX;
                u3dLeftUpPos.Y *= DPIUtils.DPIY;
                MoveWindow(unityHWND, (int)u3dLeftUpPos.X, (int)u3dLeftUpPos.Y, (int)(Panel1.ActualWidth * DPIUtils.DPIX), (int)(Panel1.ActualHeight * DPIUtils.DPIY), true);
                ActivateUnityWindow();
            }
        }
        #endregion

        private void LoadUnity_Click(object sender, RoutedEventArgs e)
        {
            LoadUnity();
        }

        private void Connect(object sender, RoutedEventArgs e)
        {
            Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            IPEndPoint endPoint = new IPEndPoint(ip, Convert.ToInt32("9000"));
            socketWatch.Bind(endPoint);
            System.Windows.Forms.MessageBox.Show("监听成功");
            socketWatch.Listen(10);
            threadli = new Thread(new ParameterizedThreadStart(Listen));  
            threadli.IsBackground = true;
            threadli.Start(socketWatch);    
        }

        private void Send(object sender, RoutedEventArgs e)
        {
            timer.Start();
        }
    }
    #region 窗体位置坐标变换
    public class DPIUtils
    {
        private static double _dpiX = 1.0;
        private static double _dpiY = 1.0;
        public static double DPIX
        {
            get
            {
                return DPIUtils._dpiX;
            }
        }
        public static double DPIY
        {
            get
            {
                return DPIUtils._dpiY;
            }
        }
        public static void Init(System.Windows.Media.Visual visual)
        {
            Matrix transformToDevice = System.Windows.PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice;
            DPIUtils._dpiX = transformToDevice.M11;
            DPIUtils._dpiY = transformToDevice.M22;
        }
        public static Point DivideByDPI(Point p)
        {
            return new Point(p.X / DPIUtils.DPIX, p.Y / DPIUtils.DPIY);
        }
        public static Rect DivideByDPI(Rect r)
        {
            return new Rect(r.Left / DPIUtils.DPIX, r.Top / DPIUtils.DPIY, r.Width, r.Height);
        }
    }
    #endregion
}

四、TCP类的编写:

         网上有很多资源,可以根据实际需要来选择适合的进行参考,主要有以下几点需注意。

1.开启服务端:

        public void StartServer()
        {
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            serverSocket.Bind(new IPEndPoint(ip, myProt));  
            serverSocket.Listen(10);   
            myThread = new Thread(ListenClientConnect);
            myThread.IsBackground = true;
            myThread.Start();
        }

2.监听客户端的连接:

 private static void ListenClientConnect()
        {
            while (true)
            {
                try
                {
                    clientSocket = serverSocket.Accept();
                    string clientInfo = clientSocket.RemoteEndPoint.ToString();
                    receiveThread = new Thread(ReceiveMessage);
                    receiveThread.IsBackground = true;
                    receiveThread.Start(clientSocket);
                }
                catch (Exception)
                {

                }
            }
        }

3.读取数据线程及发送数据:

        private static void ReceiveMessage()
        {
            Socket myClientSocket = (Socket)clientSocket;
            while (true)
            {
                try
                {
                    //通过clientSocket接收数据  
                    int receiveNumber = myClientSocket.Receive(result);
                }
                catch (Exception ex)
                {
                    try
                    {
                        myClientSocket.Shutdown(SocketShutdown.Both);
                        myClientSocket.Close();
                        break;
                    }
                    catch (Exception)
                    {
                    }
                }
            }
        }
        internal void SendMessage(string msg)
        {
            clientSocket.Send(Encoding.ASCII.GetBytes(msg));
        }

4.停止通信

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

五、Unity的制作:

       此 demo采用简单的基础三维体进行组合,形成一个小炮台,选用的是父子节点连接方式。下图中Rotate_Point是创建的一个空物体(仅一个点),目的是让炮管(青色圆柱体)绕该点进行旋转。

六、Unity中Main脚本的编写:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System;
using System.Threading;
using System.Text;
using System.Timers;
using System.IO;

public class Main : MonoBehaviour
{
    Vector3 Foundation = new Vector3(0,0,0);
    Vector3 RotatePoint = new Vector3(0,0.676f,0);
    Vector3 Sphere = new Vector3(-0.015f,0.665f,0);

    public Transform foundation;
    public Transform sphere;

    Socket socketcommunication;
    Thread thread;
    Thread ConnectThread;
    void Start()
    {
        socketcommunication = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        ConnectThread = new Thread(ConnectServer);
        ConnectThread.IsBackground = true;
        ConnectThread.Start();
    }
    void Update()
    {
        foundation.transform.position = Foundation;
        sphere.transform.localEulerAngles = RotatePoint;
    }
    void Awake()
    {
        //设置帧率
        Application.targetFrameRate = 20;
    }
    private void ConnectServer(object obj)
    {
        IPAddress ip = IPAddress.Parse("127.0.0.1");
        IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32("9000"));

        while (!socketcommunication.Connected)
        {
            try
            {
                socketcommunication.Connect(endpoint);
                if (socketcommunication.Connected)
                {
                    thread = new Thread(new ParameterizedThreadStart(Receive));
                    thread.IsBackground = true;
                    thread.Start(socketcommunication);
                    ConnectThread.Join();
                    ConnectThread.Abort();
                }
            }
            catch
            {
            }
        }
    }
    void Receive(object obj)
    {
        Socket socketCommunication = obj as Socket;
        byte[] buffer = new byte[1024];
        while (true)
        {
            int r = socketCommunication.Receive(buffer);
            Debug.Log(r.ToString());
            if (r == 0)
            {
                socketcommunication.Shutdown(SocketShutdown.Both);
                socketcommunication.Close();
                return;
            }
            else
            {
                string str = Encoding.UTF8.GetString(buffer, 0, r);
                String[] strs = str.Split(',');
                float a = float.Parse(strs[0]);
                float b = float.Parse(strs[1]);
                Foundation = new Vector3(-a,0,0);
                RotatePoint = new Vector3(-b, 0.676f, 0);
            }
        }
    }
}

 七、导出Unity到WPF:

      首先在Unity菜单栏Assets选项中选择Project Setting,将Display Resolution Dialog选项更改为Disabled,如下图所示:

      然后在菜单栏里File选择Build Settings,如下图所示,导出到目标文件夹下即可(此处是WPF的文件夹里bin/debug/Unity,可见注意事项1

       做到这一步,这个小demo就完成了,还有一些其他相关的细节及操作我会在有时间时记录下来,如物体结构较为复杂,实现多功能运动,运动指令的编码解码,鼠标控制相机视角的转换等等。当然作为新人博主,此demo也有很多可以改进的地方,希望各位不吝赐教,一起共同进步。

猜你喜欢

转载自blog.csdn.net/Xiaotouming0/article/details/127374970