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=&#34;WpfApp.MainWindow&#34;
xmlns=&#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation&#34;
xmlns:x=&#34;http://schemas.microsoft.com/winfx/2006/xaml&#34;
xmlns:d=&#34;http://schemas.microsoft.com/expression/blend/2008&#34;
xmlns:mc=&#34;http://schemas.openxmlformats.org/markup-compatibility/2006&#34;
xmlns:local=&#34;clr-namespace:WpfApp&#34;
mc:Ignorable=&#34;d&#34;
WindowState=&#34;Maximized&#34; WindowStyle=&#34;None&#34;
Title=&#34;MainWindow&#34; Height=&#34;1080&#34; Width=&#34;1920&#34;>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=&#34;50px&#34;/>
<RowDefinition Height=&#34;80px&#34;/>
<RowDefinition Height=&#34;*&#34;/>
</Grid.RowDefinitions>
<!--注释绘制这个标题栏-->
<Grid Name=&#34;toolBar&#34; Height=&#34;50px&#34; Width=&#34;Auto&#34; Background=&#34;Black&#34; MouseDown=&#34;Window_MouseDown&#34; Margin=&#34;0&#34; Grid.Row=&#34;0&#34;>
<!--标题文本-->
<StackPanel HorizontalAlignment=&#34;Left&#34; VerticalAlignment=&#34;Center&#34;>
<TextBlock Text=&#34;WPFTOU3D&#34; FontSize=&#34;30&#34; Foreground=&#34;White&#34; ></TextBlock>
</StackPanel>
<!--右侧按钮-->
<StackPanel Name=&#34;buttons&#34; Grid.ColumnSpan=&#34;1&#34; Orientation=&#34;Horizontal&#34; HorizontalAlignment=&#34;Right&#34; VerticalAlignment=&#34;Center&#34;>
<!--最小化-->
<ButtonName=&#34;btn_min&#34; Width=&#34;50px&#34; Height=&#34;50px&#34; Padding=&#34;2&#34; Margin=&#34;5&#34; Background=&#34;Transparent&#34; Click=&#34;btn_min_Click&#34;>
<TextBlock FontFamily=&#34;Segoe MDL2 Assets&#34; FontSize=&#34;30&#34;Text=&#34;&#34; Foreground=&#34;White&#34; />
</Button>
<!--normal/最大化-->
<Button Name=&#34;btn_normal&#34; Width=&#34;50px&#34; Height=&#34;50px&#34; Padding=&#34;2&#34; Margin=&#34;5&#34; Background=&#34;Transparent&#34; Click=&#34;btn_normal_Click&#34;>
<Grid>
<TextBlock Name=&#34;toNormalSize&#34; FontSize=&#34;30&#34;FontFamily=&#34;Segoe MDL2 Assets&#34; Text=&#34;&#34; Foreground=&#34;White&#34; />
<TextBlock Name=&#34;toMaxSize&#34; Visibility=&#34;Hidden&#34; FontSize=&#34;30&#34; FontWeight=&#34;Bold&#34; FontFamily=&#34;Segoe MDL2 Assets&#34; Text=&#34;&#34; Foreground=&#34;White&#34; />
</Grid>
</Button>
<!--关闭程序-->
<Button Name=&#34;btn_close&#34; Width=&#34;50px&#34; Height=&#34;50px&#34; Padding=&#34;2&#34; Margin=&#34;5&#34; Background=&#34;Transparent&#34; Click=&#34;btn_close_Click&#34;>
<TextBlock FontFamily=&#34;Segoe MDL2 Assets&#34; Text=&#34;&#34; FontSize=&#34;30&#34; FontWeight=&#34;Bold&#34; Foreground=&#34;White&#34; />
</Button>
</StackPanel>
</Grid>
<!--主要内容页-->
<Grid Grid.Row=&#34;2&#34; Background=&#34;white&#34; Margin=&#34;16,16,16,30&#34;>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=&#34;500&#34;/>
<ColumnDefinition Width=&#34;233*&#34;/>
<ColumnDefinition Width=&#34;118*&#34;/>
</Grid.ColumnDefinitions>
<ListView>
<ListViewItem Width=&#34;500&#34;>
<Grid Height=&#34;100&#34; Width=&#34;500&#34; HorizontalAlignment=&#34;Center&#34;>
<StackPanel HorizontalAlignment=&#34;Left&#34;>
<Button Name=&#34;btn1&#34; Click=&#34;btn1_Click&#34;>
<!--图片问题,可找随意图片代替,同名即可放到对应文件夹下-->
<Image Width=&#34;100&#34; Height=&#34;100&#34; Source=&#34;Assets/img/LeftArrow.png&#34;></Image>
</Button>
</StackPanel>
<TextBlock HorizontalAlignment=&#34;Center&#34; VerticalAlignment=&#34;Center&#34; FontSize=&#34;30&#34;>X方向移动</TextBlock>
<StackPanel HorizontalAlignment=&#34;Right&#34;>
<Button Name=&#34;btn2&#34; Click=&#34;btn2_Click&#34;>
<ImageWidth=&#34;100&#34; Height=&#34;100&#34; Source=&#34;Assets/img/RightArrow.png&#34; RenderTransformOrigin=&#34;3.842,0.494&#34;></Image>
</Button>
</StackPanel>
</Grid>
</ListViewItem>
<ListViewItem Width=&#34;500&#34;>
<Grid Height=&#34;100&#34; Width=&#34;500&#34; HorizontalAlignment=&#34;Center&#34;>
<StackPanel HorizontalAlignment=&#34;Left&#34;>
<Button Name=&#34;btn3&#34; Click=&#34;btn3_Click&#34;>
<Image Width=&#34;100&#34; Height=&#34;100&#34; Source=&#34;Assets/img/LeftArrow.png&#34;></Image>
</Button>
</StackPanel>
<TextBlock HorizontalAlignment=&#34;Center&#34; VerticalAlignment=&#34;Center&#34; FontSize=&#34;30&#34;>Y方向移动</TextBlock>
<StackPanel HorizontalAlignment=&#34;Right&#34;>
<Button Name=&#34;btn4&#34; Click=&#34;btn4_Click&#34;>
<ImageWidth=&#34;100&#34; Height=&#34;100&#34; Source=&#34;Assets/img/RightArrow.png&#34; RenderTransformOrigin=&#34;3.842,0.494&#34;></Image>
</Button>
</StackPanel>
</Grid>
</ListViewItem>
<ListViewItem Width=&#34;500&#34;>
<Grid Height=&#34;100&#34; Width=&#34;500&#34; HorizontalAlignment=&#34;Center&#34;>
<StackPanel HorizontalAlignment=&#34;Left&#34;>
<Button Name=&#34;btn5&#34; Click=&#34;btn5_Click&#34;>
<Image Width=&#34;100&#34; Height=&#34;100&#34; Source=&#34;Assets/img/LeftArrow.png&#34;></Image>
</Button>
</StackPanel>
<TextBlock HorizontalAlignment=&#34;Center&#34; VerticalAlignment=&#34;Center&#34; FontSize=&#34;30&#34;>Z方向移动</TextBlock>
<StackPanel HorizontalAlignment=&#34;Right&#34;>
<Button Name=&#34;btn6&#34; Click=&#34;btn6_Click&#34;>
<ImageWidth=&#34;100&#34; Height=&#34;100&#34; Source=&#34;Assets/img/RightArrow.png&#34; RenderTransformOrigin=&#34;3.842,0.494&#34;></Image>
</Button>
</StackPanel>
</Grid>
</ListViewItem>
</ListView>
<!--建立一个winform的容器-->
<Grid Background=&#34;Azure&#34;Grid.Column=&#34;1&#34; Name=&#34;Container&#34; >
<WindowsFormsHost>
<local:UserControl1 x:Name=&#34;unityhost&#34;>
</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(&#34;1&#34;);
}
private void btn2_Click(object sender, RoutedEventArgs e)
{
WpfServer.SendMessage(&#34;2&#34;);
}
private void btn3_Click(object sender, RoutedEventArgs e)
{
WpfServer.SendMessage(&#34;3&#34;);
}
private void btn4_Click(object sender, RoutedEventArgs e)
{
WpfServer.SendMessage(&#34;4&#34;);
}
private void btn5_Click(object sender, RoutedEventArgs e)
{
WpfServer.SendMessage(&#34;5&#34;);
}
private void btn6_Click(object sender, RoutedEventArgs e)
{
WpfServer.SendMessage(&#34;6&#34;);
}
}
}
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(&#34;127.0.0.1&#34;);
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(ip, myProt));//绑定IP地址:端口
serverSocket.Listen(10); //设定最多10个排队连接请求
Debug.WriteLine(&#34;启动监听{0}成功&#34;, 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(&#34;Server Say Hello&#34;));
receiveThread = new Thread(ReceiveMessage);
receiveThread.Start(clientSocket);
}
catch (Exception)
{
}
}
}
/// <summary>
/// 接收消息
/// </summary>
/// <param name=&#34;clientSocket&#34;></param>
private static void ReceiveMessage(object clientSocket)
{
Socket myClientSocket = (Socket)clientSocket;
while (true)
{
try
{
//通过clientSocket接收数据
int receiveNumber = myClientSocket.Receive(result);
Debug.WriteLine(&#34;接收客户端{0}消息{1}&#34;, 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(&#34;127.0.0.1&#34;, portNo);
data = new byte;
//SendMessage(txtNick.Text);
SendMessage(&#34;Unity Demo Client is Ready!&#34;);
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 = &#34;&#34;;
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 &#34;1&#34;:
translateX(1);
break;
case &#34;2&#34;:
translateX(-1);
break;
case &#34;3&#34;:
translateY(1);
break;
case &#34;4&#34;:
translateY(-1);
break;
case &#34;5&#34;:
translateZ(1);
break;
case &#34;6&#34;:
translateZ(-1);
break;
default:
Error_Message = &#34;unknown command&#34;;
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]