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분
제가 거기까지는 테스트를 안 해봤군요. ^^ 재미 삼아 저 당시 잠깐 구성해 본 것일 뿐 저도 그 이상 해본 것은 아니라서 관련한 노하우가 없습니다.
정성태

... 61  62  63  64  65  66  [67]  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12258정성태7/8/202020183기타: 77. DataDog APM 간략 소개
12257정성태7/7/202016469.NET Framework: 925. C# - ETW를 이용한 Monitor Enter/Exit 감시파일 다운로드1
12256정성태7/7/202017601.NET Framework: 924. C# - Reflection으로 변경할 수 없는 readonly 정적 필드 [4]
12255정성태7/6/202018377.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시파일 다운로드1
12254정성태7/2/202016904오류 유형: 626. git - REMOTE HOST IDENTIFICATION HAS CHANGED!
12253정성태7/2/202019383.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue파일 다운로드1
12252정성태7/2/202021186.NET Framework: 921. C# - I/O 스레드를 사용한 비동기 소켓 서버/클라이언트파일 다운로드2
12251정성태7/1/202019956.NET Framework: 920. C# - 파일의 비동기 처리 유무에 따른 스레드 상황 [1]파일 다운로드2
12250정성태6/30/202022116.NET Framework: 919. C# - 닷넷에서의 진정한 비동기 호출을 가능케 하는 I/O 스레드 사용법 [1]파일 다운로드1
12249정성태6/29/202017534오류 유형: 625. Microsoft SQL Server 2019 RC1 Setup - 설치 제거 시 Warning 26003 오류 발생
12248정성태6/29/202015341오류 유형: 624. SQL 서버 오류 - service-specific error code 17051
12247정성태6/29/202017277.NET Framework: 918. C# - 불린 형 상수를 반환값으로 포함하는 3항 연산자 사용 시 단축 표현 권장(IDE0075) [2]파일 다운로드1
12246정성태6/29/202018444.NET Framework: 917. C# - USB 관련 ETW(Event Tracing for Windows)를 이용한 키보드 입력을 감지하는 방법
12245정성태6/24/202018865.NET Framework: 916. C# - Task.Yield 사용법 (2) [2]파일 다운로드1
12244정성태6/24/202018899.NET Framework: 915. ETW(Event Tracing for Windows)를 이용한 닷넷 프로그램의 내부 이벤트 활용 [1]파일 다운로드1
12243정성태6/23/202015328VS.NET IDE: 147. Visual C++ 프로젝트 - .NET Core EXE를 "Debugger Type"으로 지원하는 기능 추가
12242정성태6/23/202016903오류 유형: 623. AADSTS90072 - User account '...' from identity provider 'live.com' does not exist in tenant 'Microsoft Services'
12241정성태6/23/202020126.NET Framework: 914. C# - Task.Yield 사용법파일 다운로드1
12240정성태6/23/202021682오류 유형: 622. 소켓 바인딩 시 "System.Net.Sockets.SocketException: An attempt was made to access a socket in a way forbidden by its access permissions" 오류 발생
12239정성태6/21/202019684Linux: 30. (윈도우라면 DLL에 속하는) .so 파일이 텍스트로 구성된 사례 [1]
12238정성태6/21/202018072.NET Framework: 913. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 라이브러리
12237정성태6/20/202017582.NET Framework: 912. 리눅스 환경의 .NET Core에서 "test".IndexOf("\0")가 0을 반환
12236정성태6/19/202018309오류 유형: 621. .NET Standard 대상으로 빌드 시 dynamic 예약어에서 컴파일 오류 - error CS0656: Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create'
12235정성태6/19/202017487오류 유형: 620. Windows 10 - Inaccessible boot device 블루 스크린
12234정성태6/19/202016887개발 환경 구성: 494. NuGet - nuspec의 패키지 스키마 버전(네임스페이스) 업데이트 방법
12233정성태6/19/202017338오류 유형: 619. SQL 서버 - The transaction log for database '...' is full due to 'LOG_BACKUP'. - 두 번째 이야기
... 61  62  63  64  65  66  [67]  68  69  70  71  72  73  74  75  ...