La integración de un motor de renderizado como Unity dentro de una interfaz de Windows Presentation Foundation (WPF) permite combinar la potencia gráfica 3D con la flexibilidad de las interfaces de usuario modernas. Sin embargo, uno de los desafíos técnicos más críticos es la pérdida del enfoque (focus) del teclado cuando el usuario intercatúa entre los elementos nativos de WPF y la ventana incrustada de Unity.
Esrtuctura de la interfaz en WPF
Para alojar el proceso de Unity, es necesario definir un contenedor en el XAML que servirá como referencia visual y espacial.
<Grid>
<Border x:Name="UnityContainer"
Margin="10"
BorderBrush="Gray"
BorderThickness="2" />
</Grid>
Implementación de la lógica de incrustación mediante Win32 API
Para manipular ventanas de procesos externos, debemos recurrir a la interoperabilidad con la API de Windows. El siguiente código gestiona el ciclo de vida del proceso de Unity y su vinculación con el contenedor de WPF.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
namespace WpfUnityIntegration
{
public partial class MainWindow : Window
{
[DllImport("user32.dll")]
private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
private Process _unityProcess;
private IntPtr _unityWindowHandle = IntPtr.Zero;
private const int WM_ACTIVATE = 0x0006;
private readonly IntPtr WA_ACTIVE = new IntPtr(1);
public MainWindow()
{
InitializeComponent();
this.Loaded += OnWindowLoaded;
this.SizeChanged += (s, e) => UpdateUnityWindowPosition();
this.Closed += OnWindowClosed;
}
private void OnWindowLoaded(object sender, RoutedEventArgs e)
{
StartUnityProcess();
}
private void StartUnityProcess()
{
try
{
IntPtr hostHandle = new WindowInteropHelper(this).Handle;
_unityProcess = new Process();
_unityProcess.StartInfo.FileName = @"C:\Ruta\A\Tu\Proyecto.exe";
// El argumento -parentHWND es vital para que Unity sepa dónde incrustarse
_unityProcess.StartInfo.Arguments = $"-parentHWND {hostHandle.ToInt32()} {Environment.CommandLine}";
_unityProcess.StartInfo.UseShellExecute = true;
_unityProcess.StartInfo.CreateNoWindow = true;
_unityProcess.Start();
_unityProcess.WaitForInputIdle();
// Buscamos el handle de la ventana hija generada por Unity
EnumChildWindows(hostHandle, (handle, param) =>
{
_unityWindowHandle = handle;
ForceUnityFocus();
return false;
}, IntPtr.Zero);
// Delay para asegurar el ajuste de tamaño inicial
DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
timer.Tick += (s, e) => { UpdateUnityWindowPosition(); timer.Stop(); };
timer.Start();
}
catch (Exception ex)
{
Debug.WriteLine("Error al iniciar Unity: " + ex.Message);
}
}
private void ForceUnityFocus()
{
if (_unityWindowHandle != IntPtr.Zero)
{
SendMessage(_unityWindowHandle, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
}
}
private void UpdateUnityWindowPosition()
{
if (_unityWindowHandle != IntPtr.Zero && UnityContainer.IsLoaded)
{
Point relativePoint = UnityContainer.TransformToAncestor(this).Transform(new Point(0, 0));
// Ajuste por DPI si es necesario
var source = PresentationSource.FromVisual(this);
double dpiX = source?.CompositionTarget.TransformToDevice.M11 ?? 1.0;
double dpiY = source?.CompositionTarget.TransformToDevice.M22 ?? 1.0;
MoveWindow(
_unityWindowHandle,
(int)(relativePoint.X * dpiX),
(int)(relativePoint.Y * dpiY),
(int)(UnityContainer.ActualWidth * dpiX),
(int)(UnityContainer.ActualHeight * dpiY),
true);
}
}
private void OnWindowClosed(object sender, EventArgs e)
{
if (_unityProcess != null && !_unityProcess.HasExited)
{
_unityProcess.Kill();
}
}
}
}
Solución al problema del enfoque del teclado
Cuando Unity está incrustado, el sistema operativo a menudo mantiene el foco en el hilo principal de la aplicación WPF. Si el usuario hace clic en un elemento de WPF y luego intenta interactuar con Unity, es posible que los eventos de teclado no se redirijan correctamente. La solución más efectiva es interceptar el evento de entrada del ratón en el área de Unity para forzar la activación de su ventana mediante mensajes de Windows.
// En el XAML, añadir el evento MouseEnter al contenedor
// <Border x:Name="UnityContainer" MouseEnter="UnityContainer_MouseEnter" ... />
private void UnityContainer_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
ForceUnityFocus();
}
Comunicación bidireccional mediante Sockets
Para enviar comandos entre WPF y Unity (por ejemplo, cambiar una propiedad de un objeto 3D desde un slider de WPF), la comunicación por red local mediante Sockets es la opción más robusta debido a que ambos procesos corren de forma independiente.
Servidor en WPF (Uso de TouchSocket)
using System.Text;
using System.Threading.Tasks;
using TouchSocket.Core;
using TouchSocket.Sockets;
public class NetworkManager
{
public TcpService Service { get; private set; }
public void InitializeServer(int port)
{
Service = new TcpService();
Service.Setup(new TouchSocketConfig()
.SetListenIPHosts(new IPHost[] { new IPHost($"127.0.0.1:{port}") }))
.Start();
Service.Received = (client, e) =>
{
string data = Encoding.UTF8.GetString(e.ByteBlock.Buffer, 0, e.ByteBlock.Len);
// Procesar datos recibidos de Unity
return Task.CompletedTask;
};
}
public void SendToUnity(string message)
{
var client = Service.GetClients().FirstOrDefault();
if (client != null)
{
Service.Send(client.Id, message);
}
}
}
Cliente en Unity
En Unity, es fundamental importar las librerías necesarias en la carpeta Plugins (como TouchSocket.dll y Newtonsoft.Json.dll) para que el motor pueda reconocerlas. Dado que Unity utiliza un sistema de scripting basado en eventos, se debe asegurar que la recepción de datos se maneje de forma segura respecto al hilo principal.
using UnityEngine;
using TouchSocket.Sockets;
using TouchSocket.Core;
using System.Text;
using System;
public class UnitySocketClient : MonoBehaviour
{
private TcpClient _client;
void Start()
{
ConnectToServer();
}
private void ConnectToServer()
{
_client = new TcpClient();
_client.Setup(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:2266"));
_client.Received = (client, e) =>
{
string msg = Encoding.UTF8.GetString(e.ByteBlock.Buffer, 0, e.ByteBlock.Len);
Debug.Log("Mensaje desde WPF: " + msg);
return System.Threading.Tasks.Task.CompletedTask;
};
_client.Connect();
}
public void SendData(string data)
{
if (_client != null && _client.Online)
{
_client.Send(data);
}
}
}
Esta arquitectura garantiza que Unity funcione como un componente visual fluido dentro de WPF, manteniendo la capacidad de respuesta tanto en la interfaz de usuario como en el entorno tridimensional, resolviendo los conflictos de jerarquía de ventanas impuestos por el gestor de ventanas de Windows.