yotiky Tech Blog

とあるエンジニアの備忘録

Unity - Unityを外部プロセスとしてWPFでホストする(Unity as a Library)

Unity 2019.3以降、「Unity as a Library」というAndroidiOSのネイティブアプリに、Unityで作成したアプリをライブラリとして埋め込む機能が提供されています。

この中にはWindowsアプリケーションも含まれています。Windows では以下の3つの方法が紹介されています。

  • UWP
  • Unityを外部プロセスとして起動する
  • Unityをdllとしてビルドし、直接ロードする

unity.com

今回はこの中からWPFでUnityを外部プロセスとして起動する実装例を紹介します。

docs.unity3d.com

他にこんな記事も書いています。

目次

参考

以下のサイトを参考にさせていただきました。ありがとうございます。

実装例

Unity でビルドしたWindowsスタンドアロンアプリケーションを、コマンドライン引数 -parentHWND で起動すると別のアプリケーションに埋め込むことができます。

Unity - Manual: Command line arguments

記事のリサイズ可能なサンプルコードを参考に実装していきます。
まずは、HwndHost を継承した UnityHost クラスを作成します。

    class UnityHost : HwndHost
    {
        private Process _childProcess;
        private HandleRef _childHandleRef;

        private const int WM_ACTIVATE = 0x0006;
        private const int WM_CLOSE = 0x0010;
        private readonly IntPtr WA_ACTIVE = new IntPtr(1);
        private readonly IntPtr WA_INACTIVE = new IntPtr(0);

        public string AppPath { get; set; }

        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            var cmdline = $"-parentHWND {hwndParent.Handle}";
            _childProcess = Process.Start(AppPath, cmdline);

            while (true)
            {
                var hwndChild = User32.FindWindowEx(hwndParent.Handle, IntPtr.Zero, null, null);
                if (hwndChild != IntPtr.Zero)
                {
                    User32.SendMessage(hwndChild, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);

                    return _childHandleRef = new HandleRef(this, hwndChild);
                }
                Thread.Sleep(100);
            }
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
            User32.PostMessage(_childHandleRef.Handle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);

            var counter = 30;
            while (!_childProcess.HasExited)
            {
                if (--counter < 0)
                {
                    Debug.WriteLine("Process not dead yet, killing...");
                    _childProcess.Kill();
                }
                Thread.Sleep(100);
            }
            _childProcess.Dispose();
        }
    }
    static class User32
    {
        [DllImport("user32.dll")]
        public static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string pClassName, string pWindowName);

        [DllImport("user32.dll")]
        public static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern IntPtr PostMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
    }

違いはアクティベートのメッセージを送ったり、終了処理を手厚くしてるところでしょうか。アクティベートすることで、Unity側の画面で入力を受け付けるようになります。*1

続いてウィンドウ側の実装です。

        public MainWindow()
        {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
        }

        private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
            {
                grid.Children.Add(new UnityHost
                {
                    AppPath = @"生成したUnityアプリのパス.exe"
                });
            }
        }

XAMLに直接 UnityHost を当てると、デザイナーが読み込んでCPUが急上昇したのでコード側から差し込むようにしています。

これで Unity をホストすることができました。

f:id:yotiky:20200810084753g:plain

ただ WPF と Unity で何らかのデータのやり取りをしたい場合は、ここからプロセス間通信の話になります。

プロセス間通信に関しては以下の記事がありますので参考にしてみてください。

子プロセスを扱う場合、終了に関しては色々ありそうなので気をつけたいところです。

注意すべき点

Unity を追加するコントロールなどを Collapsed で非表示にすると、Unity の CPU が爆上がりするようです。 対処法としては、コントロールを非表示にするのではなくコントロールの幅や高さなどを0にします。見えなくても表示されていれば CPU を持っていかれることはないようです。