Help us understand the problem. What is going on with this article?

JUCEのGUIにUnityのViewを埋め込んでみる

本記事はJUCE Advent Calendar 2020 の12月1日向けに投稿した記事です。

JUCEって何?

JUCE (Jules' Utility Class Extensions)は、C++言語によるマルチメディア系アプリケーションの開発を支援するフレームワークです。クロスプラットフォーム設計のライブラリと、付属されているプロジェクトジェネレータ『Projucer』から各種IDE(VisualStudio, Xcode, Makefile)向けにプロジェクトファイルを出力することで、ワンソースからWindows, macOS, Linux, iOS, Android で動作するアプリケーションを作成することができます。
公式サイト

本記事の対象環境

OS: Windows 10
IDE: Visual Studio 2019
JUCE: JUCE v6.0.4

juce::HWNDComponentを使ってみよう

JUCE 6から追加されたjuce::HWNDComponentは、任意のHWNDを任意のコンポーネント内に埋め込むことができるWindows専用のクラスです。
このクラスは、GUIコンポーネント上に配置してから setHWND() を使用して任意のHWNDを割り当てるようにして使用します。

HWNDとは?

まずハンドルとは、ファイルや画像、ウィンドウなどを操作しようとする際に、その対象を識別するためにそれぞれに割り当てられる一意の番号のことを指します。
Windowsプログラミングにおいては、ウィンドウを識別するために各ウインドウに割り当てられる一意の番号をHWND型で取り扱うことになります。

juce::HWNDComponentの注意点

juce::HWNDComponentのAPIリファレンスには以下のように記述されています。

Of course, since the window is a native object, it'll obliterate any JUCE components that may overlap this component, but that's life.

意訳

もちろん、ウィンドウはネイティブオブジェクトなので、このコンポーネントと重なる可能性のあるJUCEコンポーネントはすべて消去されますが、仕方ないよね。

この課題については、@Talokayさんの記事が参考になります。

ここで起こる問題が、Heavy-Weight(ヘビー君)とLight-Weight(ライトちゃん) Componentの問題です。
VideoComponentに限らず、OpenGL、Web系のComponentは全てヘビー君であり、ライトちゃんであるJuceのComponentと同じレベルで使用すると、ヘビー君が常に、あらゆるライトちゃんよりも前面に表示される、という問題が起こります。

juce::HWNDComponentを含む複数のコンポーネントを配置する場合にはこの点に注意しておきましょう。

UnityをJUCEのGUIに埋め込む

ゲームエンジンのUnityには、Unityをネイティブアプリのライブラリとして実行することができる、Unity As A Libraryという仕組みが用意されています。

公式サイト (https://unity.com/ja/features/unity-as-a-library) より、

Unity では、ランタイムライブラリの読み込み、アクティベーション、アンロードの方法とタイミングをネイティブアプリケーション内で管理するための制御機能を用意しています。その上、モバイルアプリの構築プロセスはほぼ同じです。Unity では iOS Xcode と Android Gradle のプロジェクトを制作できます。

Unity As A Libraryの仕組みを利用することで、ネイティブアプリケーションの一機能としてUnityの実行を制御することができます。
Unity As A Libraryを利用することができるプラットフォームは、iOS/Android/Windowsに限定されます(本記事投稿時点)。
公式ドキュメント

作業手順

  1. Unity: 組み込みたいプロジェクトをビルドしておく
  2. JUCE: プロジェクトを作成する
  3. JUCE: Unity As A Library を呼び出す処理を実装する
  4. Unityのビルド成果物をネイティブアプリケーションから参照可能なパスにコピーする
  5. Unityのビルド成果物のうちDataフォルダXxx_Data(ネイティブアプリケーション名)_Dataにリネームする

JUCEからUnity As A Libraryを呼び出すコード

■ MainComponent.h

class MainComponent  : public juce::Component, public juce::AsyncUpdater
{
public:
    //==============================================================================
    MainComponent();
    ~MainComponent() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;
    virtual void handleAsyncUpdate() override;

private:
    //==============================================================================
    // Your private member variables go here...
    std::unique_ptr<juce::DynamicLibrary> unityLibrary;
    std::unique_ptr<juce::DocumentWindow> backendWindow;
    std::unique_ptr<juce::HWNDComponent> unityViewComponent;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

■ MainComponent.cpp

//==============================================================================
MainComponent::MainComponent()
{
    backendWindow = std::make_unique<juce::DocumentWindow>("UnityWindow", Colours::black, DocumentWindow::TitleBarButtons::allButtons, true);
    backendWindow->setSize(800, 600);

    unityViewComponent = std::make_unique<juce::HWNDComponent>();
    unityViewComponent->setHWND(backendWindow->getWindowHandle());
    addAndMakeVisible(unityViewComponent.get());

    setSize (800, 600);

    triggerAsyncUpdate();
}

MainComponent::~MainComponent()
{
}

//==============================================================================
void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}

void MainComponent::resized()
{
    unityViewComponent->setBounds(20, 60, 600, 400);
}

void MainComponent::handleAsyncUpdate()
{
    juce::File dll = juce::File::getSpecialLocation(juce::File::SpecialLocationType::currentExecutableFile).getParentDirectory().getChildFile("UnityPlayer.dll");

    jassert(dll.existsAsFile());
    unityLibrary = std::make_unique<juce::DynamicLibrary>(dll.getFullPathName());
    auto um_func = (int (*)(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd))unityLibrary->getFunction("UnityMain");
    if (um_func)
    {
        auto* handle = unityViewComponent->getHWND();

        std::wstringstream stream;
        stream << L"0x" << handle;
        std::wstring hwnd = stream.str();

        std::wstringstream wss;
        wss << L"-parentHWND " << hwnd << L" delayed";
        std::wstring command = wss.str();

        auto command_utf16 = juce::CharPointer_UTF16(command.c_str());
        juce::String command_str(command_utf16);
        DBG(command_str);

        LPWSTR myWindowOutput = const_cast<LPWSTR>(command_str.toWideCharPointer());
        HINSTANCE hInstance = (HINSTANCE__*)juce::Process::getCurrentModuleInstanceHandle();

        um_func(hInstance, nullptr, myWindowOutput, 10);
    }
}

課題

手元で試してみたところ次の点で課題があったので諸々の工夫が必要そうです。

  • UnityのViewにキーボード入力のMessageを渡すことができない(マウスによる操作は可能)
  • UnityのViewにウインドウのフォーカスが占有されてしまい、親ウインドウのXボタンなどが利かなくなる

その他のフレームワークも埋め込んでみました

Sciter

Sciterとは、HTMLとCSSを使用して、クロスプラットフォームのデスクトップアプリを構築することを目的とした、アプリケーションフレームワークです。
http://sciter.com/

SciterのAPIには、ネイティブアプリケーションにEmbeddedすることを想定したAPIが用意されています。そのAPIを利用するとHWND型の値を取得できるので、Unityの場合と同じ要領でjuce::HWNDComopnentにHWNDを渡すことでSciterのViewを埋め込むことができました。

OpenSiv3D

OpenSiv3Dとは、C++ でゲームやメディアアートを作れるフレームワークです。クロスプラットフォームなライブラリ群とアプリケーションテンプレートがセットになっています。
https://siv3d.github.io/ja-jp/

OpenSiv3DのAPIには、ネイティブアプリケーションにEmbeddedすることを想定したAPIは用意されていません。それを可能にするために、ソースコードの一部を改変することで実現しました。具体的には次の手順を行っています。
1. OpenSiv3D製のexeファイルをJUCEアプリのサブプロセスとして実行する
2. サブプロセス起動の際にコマンドライン引数でHWNDの値をOpenSiv3Dに渡す
3. OpenSiv3Dのライブラリ中にあるウインドウ生成処理のコードに、引数から取得したHWNDを描画先として利用するように実装を変更する

COx2
JUCEフレームワークの日本語解説書を制作・販売しています(非公式・商標使用許諾済み)。 JUCE JAPAN の電子版はAmazon Kindleストアにてご購入いただけます。 https://www.amazon.co.jp/dp/B07HQHFKX9
http://oufac.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away