与大多数UI滞后问题一样,根本原因通常是调度程序线程上发生了太多事情。但是,据我所知(source from a seperate question),进程在通往主窗口的单独线程上运行,因此这不应成为问题的起因。
如果我在这一点上错了,请纠正我。
我最初是从SO question
开始研究如何将Unity应用程序嵌入到WPF应用程序中的。这使我能够开发出一个具有嵌入式Unity窗口的应用程序。
问题是UI滞后时间很长。当我尝试在窗口上移动滑块时,要花上几秒钟才能赶上鼠标。
此UI滞后的原因是什么?如何解决?
我的MainWindow
如下所示。
隐藏代码
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.ComponentModel;
using System.Threading;
namespace CerbyEmbeddedUnity
{
/// <summary>
/// Interaction logic for 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 unityProcess;
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 isFirstTimeActivating = true;
private bool isFirstTimeDeactivating = true;
private bool processActivatedSuccessfully = false;
private const string PATH = @"..\..\..\..\Unity Side\Embedded Unity Demo\Build\Embedded Unity Demo.exe";
public MainWindow()
{
InitializeComponent();
SetUpUnityWindow();
}
private void SetUpUnityWindow()
{
bool exeExists = File.Exists(PATH);
Console.WriteLine($"{Path.GetFullPath(PATH)} {(exeExists ? "does exist" : "doesn't exist")}"); //As PATH is relative, this helps make sure it is correct
if(!exeExists)
{
//Unity .exe missing
return;
}
IntPtr handle = UnityPanel.Handle;
unityProcess = new Process();
unityProcess.StartInfo.FileName = PATH;
unityProcess.StartInfo.Arguments = "-parentHWND " + handle.ToInt32() + " " + Environment.CommandLine;
unityProcess.StartInfo.UseShellExecute = true;
unityProcess.StartInfo.CreateNoWindow = true;
try
{
unityProcess.Start();
}
catch (InvalidOperationException)
{
//Something went wrong setting up the unity process
return;
}
unityProcess.WaitForInputIdle();
processActivatedSuccessfully = true;
EnumChildWindows(handle, WindowEnum, IntPtr.Zero);
unityHWNDLabel.Content = "Unity HWND: 0x" + unityHWND.ToString("X8");
}
private void ActivateUnityWindow()
{
SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
}
private void DeactivateUnityWindow()
{
SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
}
private int WindowEnum(IntPtr hwnd, IntPtr lparam)
{
unityHWND = hwnd;
ActivateUnityWindow();
return 0;
}
private void OnUnityPanelResize(object sender, EventArgs e)
{
MoveWindow(unityHWND, 0, 0, UnityPanel.Width, UnityPanel.Height, true);
ActivateUnityWindow();
}
public void OnWindowClosing(object sender, CancelEventArgs e)
{
if(!processActivatedSuccessfully)
{
//Unity process didn't start. Nothing to close
return;
}
try
{
unityProcess.CloseMainWindow();
}
catch (InvalidOperationException)
{
//Unity process already dead
return;
}
Thread.Sleep(1000);
while (!unityProcess.HasExited)
{
unityProcess.Kill();
}
}
private void Activate(object sender, EventArgs e)
{
if(isFirstTimeActivating)
{
isFirstTimeActivating = false;
ActivateUnityWindow();
}
}
private void Deactivate(object sender, EventArgs e)
{
if(isFirstTimeDeactivating)
{
isFirstTimeDeactivating = false;
DeactivateUnityWindow();
}
}
}
}
XAML
<Window x:Class="CerbyEmbeddedUnity.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:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
mc:Ignorable="d"
Activated="Activate"
Deactivated="Deactivate"
Closing="OnWindowClosing"
WindowState="Maximized"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<WindowsFormsHost Grid.Column="1">
<wf:Panel x:Name="UnityPanel" Resize="OnUnityPanelResize"/>
</WindowsFormsHost>
<StackPanel>
<Label Name="unityHWNDLabel" HorizontalAlignment="Center"/>
<!-- #region X -->
<StackPanel Margin="4">
<Label Content="X" FontSize="25" FontWeight="Bold" HorizontalAlignment="Center"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="1"
Content="X Rotation Speed"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalContentAlignment="Center"/>
<Slider Grid.Row="1" Grid.Column="1"
Minimum="0" Maximum="100"
VerticalAlignment="Center"/>
<Label Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Center"
Content="000" ContentStringFormat="D3"
VerticalAlignment="Center"
VerticalContentAlignment="Center"/>
</Grid>
</StackPanel>
<!-- #endregion -->
</StackPanel>
</Grid>
</Window>
我在这里使用的Unity项目非常简单。它由一个摄像头和一个位于摄像头前面的立方体组成,上面带有下面的脚本。
简单的Unity旋转脚本
using UnityEngine;
public class CubeRotation : MonoBehaviour
{
[SerializeField]
[Range(0, 100)]
private float rotationSpeedX = 1;
[SerializeField]
[Range(0, 360)]
private float xAngle = 0;
[SerializeField]
[Range(0, 360)]
private float yAngle = 0;
[SerializeField]
[Range(0, 360)]
private float zAngle = 0;
[SerializeField]
private bool canXRotate = true;
private void LateUpdate()
{
if(canXRotate)
{
xAngle += rotationSpeedX;
if (xAngle > 360)
{
xAngle -= 360;
}
}
transform.localEulerAngles = new Vector3(xAngle, yAngle, zAngle);
}
}