许可优化
产品
解决方案
服务支持
关于
软件库
当前位置:服务支持 >  软件文章 >  Unity3D嵌入WPF

Unity3D嵌入WPF

阅读数 5
点赞 0
article_banner

       此文章旨在记录自己做的第一个将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中输入指定值。关于想要物体在自己输入的情况下进行运动,将在下一篇博文中进行记录。

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

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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Data;
  9. using System.Windows.Documents;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using System.Windows.Media.Imaging;
  13. using System.Windows.Navigation;
  14. using System.Windows.Shapes;
  15. using System.Runtime.InteropServices;
  16. using System.Diagnostics;
  17. using System.Windows.Threading;
  18. using System.Timers;
  19. using System.Net.Sockets;
  20. using System.Threading;
  21. using System.Windows.Interop;
  22. using System.Net;
  23. namespace 示例1
  24. {
  25. /// <summary>
  26. /// MainWindow.xaml 的交互逻辑
  27. /// </summary>
  28. public partial class MainWindow : Window
  29. {
  30. [DllImport("User32.dll")]
  31. static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);
  32. internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
  33. //改变指定窗口的位置和尺寸,基于左上角(屏幕/父窗口)(指定窗口的句柄,窗口左位置,窗口顶位置,窗口新宽度,窗口新高度,指定是否重画窗口)
  34. [DllImport("user32.dll")]
  35. internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);
  36. //枚举一个父窗口的所有子窗口(父窗口句柄,回调函数的地址,自定义的参数)
  37. [DllImport("user32.dll")]
  38. static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
  39. //该函数将指定的消息发送到一个或多个窗口。此函数为指定的窗口调用窗口程序,直到窗口程序处理完消息再返回。(窗口句柄。窗口可以是任何类型的屏幕对象,用于区别其他消息的常量值,通常是一个与消息有关的常量值,也可能是窗口或控件的句柄,通常是一个指向内存中数据的指针)
  40. private Process process;
  41. private IntPtr unityHWND = IntPtr.Zero;
  42. private const int WM_ACTIVATE = 0x0006;
  43. private readonly IntPtr WA_ACTIVE = new IntPtr(1);
  44. private readonly IntPtr WA_INACTIVE = new IntPtr(0);
  45. private bool isU3DLoaded = false;
  46. private Point u3dLeftUpPos;
  47. private DispatcherTimer dispatcherTimer;
  48. System.Timers.Timer timer = new System.Timers.Timer();
  49. float count = 0;
  50. Socket socketCommnication;
  51. bool IsListening = true;
  52. Thread threadli;
  53. public MainWindow()
  54. {
  55. InitializeComponent();
  56. timer.Interval = 100;
  57. timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
  58. timer.AutoReset = true ;
  59. }
  60. private void timer_Elapsed(object sender, ElapsedEventArgs e)
  61. {
  62. count += 0.05f ;
  63. string str = string.Format("{0} , {1} ", count, -5f * count + 1);
  64. byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
  65. socketCommnication.Send(buffer);
  66. }
  67. //开始监听线程
  68. private void Listen(object obj)
  69. {
  70. Socket socketWatch = obj as Socket;
  71. while (IsListening)
  72. {
  73. socketCommnication = socketWatch.Accept();
  74. if (socketCommnication.Connected)
  75. {
  76. System.Windows.MessageBox.Show(socketCommnication.RemoteEndPoint.ToString() + ":连接成功");
  77. IsListening = false;
  78. }
  79. }
  80. }
  81. //窗体加载事件
  82. private void Window_Loaded(object sender, RoutedEventArgs e)
  83. {
  84. }
  85. //窗体关闭事件
  86. private void Window_Closed(object sender, EventArgs e)
  87. {
  88. try
  89. {
  90. process.CloseMainWindow();
  91. Thread.Sleep(1000);
  92. while (process.HasExited == false)
  93. process.Kill();
  94. //Sever.QuitServer();
  95. timer.Stop();
  96. socketCommnication.Close();
  97. IsListening = false;
  98. threadli.Abort();
  99. System.Environment.Exit(0);
  100. }
  101. catch (Exception)
  102. {
  103. }
  104. }
  105. //窗体大小改变事件
  106. private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
  107. {
  108. ResizeU3D();
  109. }
  110. //获得焦点事件,首次打开软件、由别的软件切换到当前软件
  111. private void Window_Deactivated(object sender, EventArgs e)
  112. {
  113. DeactivateUnityWindow();
  114. }
  115. //失去焦点事件
  116. private void Window_Activated(object sender, EventArgs e)
  117. {
  118. ActivateUnityWindow();
  119. }
  120. #region Unity操作
  121. private void LoadUnity()
  122. {
  123. try
  124. {
  125. IntPtr hwnd = ((HwndSource)PresentationSource.FromVisual(Panel1)).Handle;
  126. process = new Process();
  127. String appStartupPath = System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
  128. process.StartInfo.FileName = appStartupPath + @"\Unity\example.exe";
  129. process.StartInfo.Arguments = "-parentHWND " + hwnd.ToInt32() + " " + Environment.CommandLine;
  130. process.StartInfo.UseShellExecute = true;
  131. process.StartInfo.CreateNoWindow = true;
  132. process.Start();
  133. process.WaitForInputIdle();
  134. isU3DLoaded = true;
  135. EnumChildWindows(hwnd, WindowEnum, IntPtr.Zero);
  136. dispatcherTimer = new DispatcherTimer();
  137. dispatcherTimer.Tick += new EventHandler(InitialResize);
  138. dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
  139. dispatcherTimer.Start();
  140. }
  141. catch (Exception ex)
  142. {
  143. string error = ex.Message;
  144. }
  145. }
  146. private void InitialResize(object sender, EventArgs e)
  147. {
  148. ResizeU3D();
  149. dispatcherTimer.Stop();
  150. }
  151. private int WindowEnum(IntPtr hwnd, IntPtr lparam)
  152. {
  153. unityHWND = hwnd;
  154. ActivateUnityWindow();
  155. return 0;
  156. }
  157. private void ActivateUnityWindow()
  158. {
  159. SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
  160. }
  161. private void DeactivateUnityWindow()
  162. {
  163. SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
  164. }
  165. private void ResizeU3D()
  166. {
  167. if (isU3DLoaded)
  168. {
  169. Window window = Window.GetWindow(this);
  170. u3dLeftUpPos = Panel1.TransformToAncestor(window).Transform(new Point(0, 0));
  171. DPIUtils.Init(this);
  172. u3dLeftUpPos.X *= DPIUtils.DPIX;
  173. u3dLeftUpPos.Y *= DPIUtils.DPIY;
  174. MoveWindow(unityHWND, (int)u3dLeftUpPos.X, (int)u3dLeftUpPos.Y, (int)(Panel1.ActualWidth * DPIUtils.DPIX), (int)(Panel1.ActualHeight * DPIUtils.DPIY), true);
  175. ActivateUnityWindow();
  176. }
  177. }
  178. #endregion
  179. private void LoadUnity_Click(object sender, RoutedEventArgs e)
  180. {
  181. LoadUnity();
  182. }
  183. private void Connect(object sender, RoutedEventArgs e)
  184. {
  185. Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  186. IPAddress ip = IPAddress.Parse("127.0.0.1");
  187. IPEndPoint endPoint = new IPEndPoint(ip, Convert.ToInt32("9000"));
  188. socketWatch.Bind(endPoint);
  189. System.Windows.Forms.MessageBox.Show("监听成功");
  190. socketWatch.Listen(10);
  191. threadli = new Thread(new ParameterizedThreadStart(Listen));
  192. threadli.IsBackground = true;
  193. threadli.Start(socketWatch);
  194. }
  195. private void Send(object sender, RoutedEventArgs e)
  196. {
  197. timer.Start();
  198. }
  199. }
  200. #region 窗体位置坐标变换
  201. public class DPIUtils
  202. {
  203. private static double _dpiX = 1.0;
  204. private static double _dpiY = 1.0;
  205. public static double DPIX
  206. {
  207. get
  208. {
  209. return DPIUtils._dpiX;
  210. }
  211. }
  212. public static double DPIY
  213. {
  214. get
  215. {
  216. return DPIUtils._dpiY;
  217. }
  218. }
  219. public static void Init(System.Windows.Media.Visual visual)
  220. {
  221. Matrix transformToDevice = System.Windows.PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice;
  222. DPIUtils._dpiX = transformToDevice.M11;
  223. DPIUtils._dpiY = transformToDevice.M22;
  224. }
  225. public static Point DivideByDPI(Point p)
  226. {
  227. return new Point(p.X / DPIUtils.DPIX, p.Y / DPIUtils.DPIY);
  228. }
  229. public static Rect DivideByDPI(Rect r)
  230. {
  231. return new Rect(r.Left / DPIUtils.DPIX, r.Top / DPIUtils.DPIY, r.Width, r.Height);
  232. }
  233. }
  234. #endregion
  235. }
cs
运行

四、TCP类的编写:

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

1.开启服务端:

  1. public void StartServer()
  2. {
  3. IPAddress ip = IPAddress.Parse("127.0.0.1");
  4. serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  5. serverSocket.Bind(new IPEndPoint(ip, myProt));
  6. serverSocket.Listen(10);
  7. myThread = new Thread(ListenClientConnect);
  8. myThread.IsBackground = true;
  9. myThread.Start();
  10. }
cs
运行

2.监听客户端的连接:

  1. private static void ListenClientConnect()
  2. {
  3. while (true)
  4. {
  5. try
  6. {
  7. clientSocket = serverSocket.Accept();
  8. string clientInfo = clientSocket.RemoteEndPoint.ToString();
  9. receiveThread = new Thread(ReceiveMessage);
  10. receiveThread.IsBackground = true;
  11. receiveThread.Start(clientSocket);
  12. }
  13. catch (Exception)
  14. {
  15. }
  16. }
  17. }
cs
运行

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

  1. private static void ReceiveMessage()
  2. {
  3. Socket myClientSocket = (Socket)clientSocket;
  4. while (true)
  5. {
  6. try
  7. {
  8. //通过clientSocket接收数据
  9. int receiveNumber = myClientSocket.Receive(result);
  10. }
  11. catch (Exception ex)
  12. {
  13. try
  14. {
  15. myClientSocket.Shutdown(SocketShutdown.Both);
  16. myClientSocket.Close();
  17. break;
  18. }
  19. catch (Exception)
  20. {
  21. }
  22. }
  23. }
  24. }
  25. internal void SendMessage(string msg)
  26. {
  27. clientSocket.Send(Encoding.ASCII.GetBytes(msg));
  28. }
cs
运行

4.停止通信

  1. internal void QuitServer()
  2. {
  3. serverSocket.Close();
  4. clientSocket.Close();
  5. myThread.Abort();
  6. receiveThread.Abort();
  7. }
cs
运行

五、Unity的制作:

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

六、Unity中Main脚本的编写:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using System;
  7. using System.Threading;
  8. using System.Text;
  9. using System.Timers;
  10. using System.IO;
  11. public class Main : MonoBehaviour
  12. {
  13. Vector3 Foundation = new Vector3(0,0,0);
  14. Vector3 RotatePoint = new Vector3(0,0.676f,0);
  15. Vector3 Sphere = new Vector3(-0.015f,0.665f,0);
  16. public Transform foundation;
  17. public Transform sphere;
  18. Socket socketcommunication;
  19. Thread thread;
  20. Thread ConnectThread;
  21. void Start()
  22. {
  23. socketcommunication = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  24. ConnectThread = new Thread(ConnectServer);
  25. ConnectThread.IsBackground = true;
  26. ConnectThread.Start();
  27. }
  28. void Update()
  29. {
  30. foundation.transform.position = Foundation;
  31. sphere.transform.localEulerAngles = RotatePoint;
  32. }
  33. void Awake()
  34. {
  35. //设置帧率
  36. Application.targetFrameRate = 20;
  37. }
  38. private void ConnectServer(object obj)
  39. {
  40. IPAddress ip = IPAddress.Parse("127.0.0.1");
  41. IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32("9000"));
  42. while (!socketcommunication.Connected)
  43. {
  44. try
  45. {
  46. socketcommunication.Connect(endpoint);
  47. if (socketcommunication.Connected)
  48. {
  49. thread = new Thread(new ParameterizedThreadStart(Receive));
  50. thread.IsBackground = true;
  51. thread.Start(socketcommunication);
  52. ConnectThread.Join();
  53. ConnectThread.Abort();
  54. }
  55. }
  56. catch
  57. {
  58. }
  59. }
  60. }
  61. void Receive(object obj)
  62. {
  63. Socket socketCommunication = obj as Socket;
  64. byte[] buffer = new byte[1024];
  65. while (true)
  66. {
  67. int r = socketCommunication.Receive(buffer);
  68. Debug.Log(r.ToString());
  69. if (r == 0)
  70. {
  71. socketcommunication.Shutdown(SocketShutdown.Both);
  72. socketcommunication.Close();
  73. return;
  74. }
  75. else
  76. {
  77. string str = Encoding.UTF8.GetString(buffer, 0, r);
  78. String[] strs = str.Split(',');
  79. float a = float.Parse(strs[0]);
  80. float b = float.Parse(strs[1]);
  81. Foundation = new Vector3(-a,0,0);
  82. RotatePoint = new Vector3(-b, 0.676f, 0);
  83. }
  84. }
  85. }
  86. }
cs
运行

 七、导出Unity到WPF:

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

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

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


免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删
相关文章
QR Code
微信扫一扫,欢迎咨询~

联系我们
武汉格发信息技术有限公司
湖北省武汉市经开区科技园西路6号103孵化器
电话:155-2731-8020 座机:027-59821821
邮件:tanzw@gofarlic.com
Copyright © 2023 Gofarsoft Co.,Ltd. 保留所有权利
遇到许可问题?该如何解决!?
评估许可证实际采购量? 
不清楚软件许可证使用数据? 
收到软件厂商律师函!?  
想要少购买点许可证,节省费用? 
收到软件厂商侵权通告!?  
有正版license,但许可证不够用,需要新购? 
联系方式 155-2731-8020
预留信息,一起解决您的问题
* 姓名:
* 手机:

* 公司名称:

姓名不为空

手机不正确

公司不为空