本記事は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に埋め込む
実験の続き。JUCEのGUIコンポーネントにウインドウハンドルを持たせることができたので、UnityAsALibraryをJUCEのGUIコンポーネント上で動かせることを確認した。#JUCE #Unity pic.twitter.com/DpOHNLrlDy
— COx2 ))))@ (@CO_CO_) October 31, 2020
ゲームエンジンの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に限定されます(本記事投稿時点)。
公式ドキュメント
作業手順
- Unity: 組み込みたいプロジェクトをビルドしておく
- JUCE: プロジェクトを作成する
- JUCE: Unity As A Library を呼び出す処理を実装する
- Unityのビルド成果物をネイティブアプリケーションから参照可能なパスにコピーする
- 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
深夜の実験動画。JUCE に Sciterのウインドウを埋め込んでみた。https://t.co/rlslUlIMqi#JUCE #Sciter pic.twitter.com/9bG2bxZXg5
— COx2 ))))@ (@CO_CO_) November 6, 2020
Sciterとは、HTMLとCSSを使用して、クロスプラットフォームのデスクトップアプリを構築することを目的とした、アプリケーションフレームワークです。
http://sciter.com/
SciterのAPIには、ネイティブアプリケーションにEmbeddedすることを想定したAPIが用意されています。そのAPIを利用するとHWND型の値を取得できるので、Unityの場合と同じ要領でjuce::HWNDComopnent
にHWNDを渡すことでSciterのViewを埋め込むことができました。
OpenSiv3D
朝一の実験。JUCEのUIコンポーネントにSiv3Dを埋め込んでみる。
— COx2 ))))@ (@CO_CO_) November 6, 2020
何気にキーボード入力が利くようだ。#JUCE #Siv3D pic.twitter.com/bE6dIWLgSp
OpenSiv3Dとは、C++ でゲームやメディアアートを作れるフレームワークです。クロスプラットフォームなライブラリ群とアプリケーションテンプレートがセットになっています。
https://siv3d.github.io/ja-jp/
OpenSiv3DのAPIには、ネイティブアプリケーションにEmbeddedすることを想定したAPIは用意されていません。それを可能にするために、ソースコードの一部を改変することで実現しました。具体的には次の手順を行っています。
- OpenSiv3D製のexeファイルをJUCEアプリのサブプロセスとして実行する
- サブプロセス起動の際にコマンドライン引数でHWNDの値をOpenSiv3Dに渡す
- OpenSiv3Dのライブラリ中にあるウインドウ生成処理のコードに、引数から取得したHWNDを描画先として利用するように実装を変更する