Unity3D - C# Windows Forms / WPF Application에 통합하는 방법
지난 글에서,
빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
; https://www.sysnet.pe.kr/2/0/13581
C++ Win32 응용 프로그램에서 어떻게 Unity3D 프로그램을 내장할 수 있는지 알아봤습니다. 위의 방법은, C# Windows Forms에서도 유사하게 써먹을 수 있습니다. 테스트 삼아 직접 해볼까요? ^^
이전 글에서, Unity3D 프로젝트를 Windows 대상으로 빌드하면 다음과 같은 내용을 가진 출력물이 나온다고 했는데요,
C:\temp\unity> tree /F
...[생략]...
│ My project.exe
│ UnityCrashHandler64.exe
│ UnityPlayer.dll
├───MonoBleedingEdge
│ ├───EmbedRuntime
│ └───etc
│ └───mono
│ ├───2.0
│ │ └───Browsers
│ ├───4.0
│ │ └───Browsers
│ ├───4.5
│ │ └───Browsers
│ └───mconfig
└───My project_Data
├───Managed
└───Resources
닷넷에서도 역시 저 UnityPlayer.dll의 UnityMain 함수를 P/Invoke로 호출해 활용하면 그만입니다. 이를 위해 C# Windows Forms Application을 간단하게 하나 만들고,
using System.Runtime.InteropServices;
namespace WinFormsApp1;
public partial class Form1 : Form
{
[DllImport("user32.dll")]
private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("UnityPlayer.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "UnityMain")]
public static extern int UnityMain(IntPtr hInstance, IntPtr hPrevInstance, string lpCmdLine, int nShowCmd);
const int GWLP_HINSTANCE = -6;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
IntPtr hInstFromHwnd = GetWindowLongPtr(this.Handle, GWLP_HINSTANCE); // https://www.sysnet.pe.kr/2/0/13307
string cmd = $"-parentHWND '{this.Handle}'";
UnityMain(hInstFromHwnd, IntPtr.Zero, cmd, 0); // hInstFromHwnd는 IntPtr.Zero로 줘도 무방
}
}
보는 바와 같이 UnityMain을 DllImport로 연결한 다음 -parentHWND 문자열로 적절하게 Unity가 차지할 공간을 소유한 Window Handle로 채워 전달하면 됩니다.
소스코드는 저게 끝입니다. 남은 작업은, UnityPlayer.dll을 찾을 수 있도록 OutputPath, AppendTargetFrameworkToOutputPath를 조정해야 합니다. 예를 들어, Unity 결과물이 C:\temp\unity에 있다면 다음과 같이 csproj에 설정을 추가합니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<OutputPath>c:\temp\unity</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
</Project>
그런데, 이렇게 해도 실행 시 이런 오류가 발생할 것입니다.
Data folder not found
Application folder:
c:/temp/unity
There should be 'WinFormsApp1_Data'
folder next to the executable
이름이 좀 특이하죠? 예제 윈폼 프로젝트의 이름이 "WinFormsApp1"인데 거기에 "_Data"가 붙어 있습니다. 그리고, Unity 예제 프로젝트의 이름이 "My Project.exe"인데, c:\temp\unity 하위에는 "My Project_Data"가 있습니다.
아하~~~ 그러니까, 우리가 만든 EXE 프로젝트가 아예 "My Project.exe"를 대체하는 것이군요. ^^ 따라서 출력 어셈블리 이름도 함께 바꿔야 합니다. 결국 다음과 같은 csproj 구성으로 빌드하면 됩니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- ...[생략]... --->
<OutputPath>c:\temp\unity</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AssemblyName>My Project</AssemblyName>
</PropertyGroup>
</Project>
WPF의 경우에도 Windows Forms와 방법은 같습니다. 단지 명시적인 HWND 윈도우 핸들을 주지는 않기 때문에 WindowInteropHelper를 이용해 알아낸 다음,
IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
UnityMain 호출을 거치면 됩니다.
using System;
using System.Runtime.InteropServices;
using System.Windows;
namespace WpfApp1
{
public partial class MainWindow : Window
{
[DllImport("user32.dll")]
private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("UnityPlayer.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "UnityMain")]
public static extern int UnityMain(IntPtr hInstance, IntPtr hPrevInstance, [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, int nShowCmd);
const int GWLP_HINSTANCE = -6;
const int SW_SHOWDEFAULT = 0x0a;
public MainWindow()
{
InitializeComponent();
}
void EmbedUnity()
{
IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
IntPtr hInstFromHwnd = GetWindowLongPtr(hwnd, GWLP_HINSTANCE);
string cmd = $"-parentHWND {hwnd}";
UnityMain(hInstFromHwnd, IntPtr.Zero, cmd, SW_SHOWDEFAULT); // hInstFromHwnd는 IntPtr.Zero로 줘도 무방
}
private void Button_Click(object sender, RoutedEventArgs e)
{
EmbedUnity();
}
}
}
역시 csproj에서의 경로 설정도 잊지 마시고!
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<OutputPath>c:\temp\unity</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AssemblyName>My Project</AssemblyName>
</PropertyGroup>
</Project>
참고로, Windows Forms의 경우 모든 컨트롤이 Window Handle을 갖기 때문에 메인 윈도우의 일부에서 Unity를 포함시키는 것이 어렵지 않습니다. 반면, WPF는 Element들이 모두 비-윈도우 자원이기 때문에 Main Window 내에 포함하고 싶다면
WindowsFormsHost와 같은 방법을 경유해야 합니다.
Walkthrough: Hosting a Windows Forms Control in WPF
; https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/walkthrough-hosting-a-windows-forms-control-in-wpf
Walkthrough: Hosting a Windows Forms Control in WPF by Using XAML
; https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/walkthrough-hosting-a-windows-forms-control-in-wpf-by-using-xaml
따라서 대충 WindowsFormsHost 컨트롤을 하나 올려 두고,
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d" Loaded="Window_Loaded"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Content="Button" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Click="Button_Click"
Grid.Column="0" />
<WindowsFormsHost x:Name="host" Background="Yellow" Grid.Column="1">
<wf:Panel x:Name="panel" />
</WindowsFormsHost>
</Grid>
</Window>
저 컨트롤의 Handle 값을 구해 UnityMain에 전달하면 됩니다.
IntPtr hwnd = host.Handle;
string cmd = $"-parentHWND {hwnd}";
UnityMain(IntPtr.Zero, IntPtr.Zero, cmd, SW_SHOWDEFAULT);
(
첨부 파일은 이 글의 예제 코드를 포함>합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]