yotiky Tech Blog

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

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

前回の記事で WPF で Unity をホストする実装例を紹介しました。 参考にさせて頂いた記事では、続きに Unity の起動画面がWindow全体に表示されてしまう問題を対処しています。

yotiky.hatenablog.com

手元でも同じように全体に表示されたため、続きのコードを試してみましたが、一瞬というわけでもなく再描画で計算されるまで、フルサイズから戻らない現象がどうしても解決しませんでした。

そこで今回は、stackoverflow に掲載されていた実装例を参考にします。

f:id:yotiky:20200820170441g:plain:w300

目次

実装例

UnityHwndHost

こちらのコードを使わせて頂きました。

前回の記事と重複する内容もありますが、Unity をホストするのにいくつか手を加えています。

以下コード全文です。その後に修正した部分を説明します。

public class UnityHwndHost : HwndHost
{
    internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
    [DllImport("user32.dll")]
    internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);
    [DllImport("user32.dll", SetLastError = true)]
    internal static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
    [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
    internal static extern IntPtr GetWindowLong32(IntPtr hWnd, Int32 nIndex);
    [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
    internal static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, Int32 nIndex);
    internal const Int32 GWLP_USERDATA = -21;
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern IntPtr PostMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
    internal const UInt32 WM_CLOSE = 0x0010;

    private string programName;
    private string arguments;
    private Process process = null;
    private IntPtr unityHWND = IntPtr.Zero;

    private JobObject job;

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

    public UnityHwndHost(string programName, string arguments = "")
    {
        this.programName = programName;
        this.arguments = arguments;
    }

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        Debug.WriteLine("Going to launch Unity at: " + this.programName + " " + this.arguments);
        process = new Process();
        process.StartInfo.FileName = programName;
        process.StartInfo.Arguments = arguments + (arguments.Length == 0 ? "" : " ") + "-parentHWND " + hwndParent.Handle;
        process.StartInfo.UseShellExecute = true;
        process.StartInfo.CreateNoWindow = true;

        process.Start();
        process.WaitForInputIdle();

        job = JobObject.CreateAsKillOnJobClose();
        job.AssignProcess(process);

        int repeat = 50;
        while (unityHWND == IntPtr.Zero && repeat-- > 0)
        {
            Thread.Sleep(100);
            EnumChildWindows(hwndParent.Handle, WindowEnum, IntPtr.Zero);
        }
        if (unityHWND == IntPtr.Zero)
            throw new Exception("Unable to find Unity window");
        Debug.WriteLine("Found Unity window: " + unityHWND);

        repeat += 150;
        while ((GetWindowLong(unityHWND, GWLP_USERDATA).ToInt32() & 1) == 0 && --repeat > 0)
        {
            Thread.Sleep(100);
            Debug.WriteLine("Waiting for Unity to initialize... " + repeat);
        }
        if (repeat == 0)
        {
            Debug.WriteLine("Timed out while waiting for Unity to initialize");
        }
        else
        {
            Debug.WriteLine("Unity initialized!");
        }

        ActivateUnityWindow();

        return new HandleRef(this, unityHWND);
    }

    private void ActivateUnityWindow()
    {
        SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
    }

    private void DeactivateUnityWindow()
    {
        SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
    }

    private int WindowEnum(IntPtr hwnd, IntPtr lparam)
    {
        if (unityHWND != IntPtr.Zero)
            throw new Exception("Found multiple Unity windows");
        unityHWND = hwnd;
        return 0;
    }
    private IntPtr GetWindowLong(IntPtr hWnd, int nIndex)
    {
        if (IntPtr.Size == 4)
        {
            return GetWindowLong32(hWnd, nIndex);
        }
        return GetWindowLongPtr64(hWnd, nIndex);
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        Destroy();
    }

    public void Destroy()
    {
        Debug.WriteLine("Asking Unity to exit...");
        PostMessage(unityHWND, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);

        job.Dispose();
        job = null;
        process.Dispose();
        process = null;
        unityHWND = IntPtr.Zero;
    }
}



まず、GetWindowLong が32ビットと64ビットで違うらしく、TODOついていたので出し別けるようにしました。64ビットでしかテストしてないですが。

        [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
        internal static extern IntPtr GetWindowLong32(IntPtr hWnd, Int32 nIndex);
        [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
        internal static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, Int32 nIndex);

        private IntPtr GetWindowLong(IntPtr hWnd, int nIndex)
        {
            if (IntPtr.Size == 4)
            {
                return GetWindowLong32(hWnd, nIndex);
            }
            return GetWindowLongPtr64(hWnd, nIndex);
        }



次に、BuildWindowCore の最後でアクティベートのメッセージを送っています。これを入れることでWPFで上でUnityの画面を操作できるようになります。

    ActivateUnityWindow();

    return new HandleRef(this, unityHWND);



続いて、Unity の子プロセスが確実に終了するように JobObject を使うようにします。JobObject の実装やプロセスが終了しない場合の詳細などは以下の記事を御覧ください。

yotiky.hatenablog.com

プロセス開始時にアサインして、

    job = JobObject.CreateAsKillOnJobClose();
    job.AssignProcess(process);

終了時に Dispose を呼んであげます。

    PostMessage(unityHWND, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);

    job.Dispose();
    job = null;

元のコードはプロセスが残っていたら Process を Kill する処理になっていますが、Dispose を呼んであげれば子プロセスは終了します。また、すでに終了している場合でも例外が発生しないので HasExited の判定も消しています。

使い方

任意のコンポーネントに追加してあげるだけです。

    if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
    {
        unityGrid.Children.Add(new UnityHwndHost(@"UnityApp.exe", "-possibly -other -arguments"));
    }

注意すべき点

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