Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 3개 있습니다.)
개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
; https://www.sysnet.pe.kr/2/0/13581

개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법
; https://www.sysnet.pe.kr/2/0/13584

닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신
; https://www.sysnet.pe.kr/2/0/13588




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);

(첨부 파일은 이 글의 예제 코드를 포함>합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 3/26/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-06-05 06시30분
안녕하세요 올려주신 글 참고하여 WPF 어플리케이션 안에 Unity 어플리케이션을 넣어보았습니다. 좋은글 감사합니다.
WPF 어플리케이션의 창 사이즈가 변경될때 Unity 화면 사이즈를 변경하는 좋은 방법이 없을까요? 올려주셨던 다른글을 참고하여 named pipe 통신을 이용하여 Unity의 Screen.SetResolution 메서드를 호출 해보았는데 단순히 렌더링 되는 해상도 변경하는 것만 됩니다. 좋은 방법이 있을까 한번 여쭤봅니다. 감사합니다.
Tom Lee
2024-06-05 07시39분
아래의 글을 보면, MoveWindow 하면 될 듯한데요. ^^

Unity window inside WPF application (XAML) not resizing
; https://forum.unity.com/threads/unity-window-inside-wpf-application-xaml-not-resizing.1239409/
정성태
2024-06-11 07시14분
답변 감사합니다. 나름의 해결책 연구해보고 여기에도 공유해봅니다.

MoveWindow 함수 호출했는데 안되서 여기에도 공유 주셨던 named pipe 프로세스간 통신으로 width, height 전달하여 Screen.SetResolution 유니티 메서드 호출 하는 것으로 해결 했습니다.

일전에 언급한 Screen.SetResolution 메서드를 호출하여 창 사이즈가 변경되려면 유니티의 빌드 옵션 "Resizable Window" 체크하여 빌드하여야 하고 development 빌드 옵션이 체크 해제되어야 합니다.

제 생각은 WPF 어플리케이션안의 쓰레드를 생성하여 UnityPlayer.dll의 UnityMain을 호출하여 실행한 것이라 MoveWindow로 전달할 윈도우 핸들이 적당한게 없는것 같습니다. 프로세스 실행방법으로 WPF에 embed를 하며 Unity 어플리케이션을 실행하면서 생성된 Handle로 MoveWindow 함수로 리사이즈가 가능한것은 확인하였습니다.
Tom Lee
2024-06-11 10시21분
공유 감사합니다. ^^

참고로, WPF에서 WindowsFormsHost를 생성해 그것의 윈도우 핸들을 넘기지만 아마도 Unity는 그 윈도우의 자식 윈도우를 하나 생성해 처리할 것이므로 그것을 MoveWindow로 하면 되지 않을까 싶은데요, ... 하지만 제가 직접 테스트하지 않고 답변하는 것이라 넘어가세요. ^^ 어쨌든 해결하셨다니 다행입니다.

-------------------------------

(2024-06-19 업데이트)

Unity Resource
; https://unitysquare.co.kr/growwith/resource
정성태
2024-11-13 10시24분
안녕하세요 올려주신 글 잘보고 있습니다.
이번에 Unity 연동하면서 따라해보고있는데

System.EntryPointNotFoundException: 'DLL 'user32.dll'에서 이름이 'GetWindowLongPtr'인 진입점을 찾을 수 없습니다.' 라는 에러가 발생하네요.

혹시 짐작가시는게 있으실까요?
정재겸
2024-11-13 10시37분
아 해결했습니다. 프로젝트 속성에 32비트 기본 사용이 체크 되어 있었네요
정재겸
2024-11-13 10시54분
@정재겸 참고로, 32비트로 해야 한다면 GetWindowLong을 호출하시면 됩니다.

using System.Runtime.InteropServices;

internal class Program
{
    [DllImport("user32.dll")]
    private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll")]
    private static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

    static void Main(string[] args)
    {
        if (IntPtr.Size == 8)
        {
            GetWindowLongPtr(IntPtr.Zero, 0);
        }
        else
        {
            GetWindowLong(IntPtr.Zero, 0);
        }
    }
}
정성태
2024-11-13 02시13분
질문이 하나 더 있습니다.

처음에 키보드 인풋은 먹다가 마우스 인풋을 하는순간 인풋이 먹히질 않네요

혹시 아시는 방법이 있을까요?
정재겸
2024-11-13 08시42분
제가 거기까지는 테스트를 안 해봤군요. ^^ 재미 삼아 저 당시 잠깐 구성해 본 것일 뿐 저도 그 이상 해본 것은 아니라서 관련한 노하우가 없습니다.
정성태

1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13612정성태4/30/20245508오류 유형: 902. Visual Studio - error MSB3021: Unable to copy file
13611정성태4/29/20245258닷넷: 2252. C# - GUID 타입 전용의 UnmanagedType.LPStruct - 두 번째 이야기파일 다운로드1
13610정성태4/28/20245359닷넷: 2251. C# - 제네릭 인자를 가진 타입을 생성하는 방법 - 두 번째 이야기
13609정성태4/27/20245482닷넷: 2250. PInvoke 호출 시 참조 타입(class)을 마샬링하는 [IN], [OUT] 특성파일 다운로드1
13608정성태4/26/20245629닷넷: 2249. C# - 부모의 필드/프로퍼티에 대해 서로 다른 자식 클래스 간에 Reflection 접근이 동작할까요?파일 다운로드1
13607정성태4/25/20245464닷넷: 2248. C# - 인터페이스 타입의 다중 포인터를 인자로 갖는 C/C++ 함수 연동
13606정성태4/24/20245447닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/20245943닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/20245009오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/20245512닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/20245367닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/20245337닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/20245607닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/20245494닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/20246477닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드2
13597정성태4/15/20245465닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20245949닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/20246122닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20245752닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20245660닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20247049C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동 [1]
13591정성태4/2/20245858닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20245456Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20246011닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20246754닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [2]파일 다운로드1
13587정성태3/27/20245933오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...