爆速開発を実現できるImmediate Mode GUI(ImGui)の魅力

に公開

はじめに

多くの方々にImmediate Mode GUI(ImGui)の魅力を知ってほしいため、
UnityでRimGuiというImGui Libraryを作成した知見を元にImGuiの魅力を解説します。

https://assetstore.unity.com/packages/tools/gui/rimgui-316805

https://gridrand.com/rimgui/webgl/core/

Immediate Mode GUI(ImGui)とは

Immediate Mode(以下、即時モード)で動くGUIライブラリです。
UnityでEditor拡張したことがある方は、Unity IMGUIという即時モードGUIを使ったことがあると思います。

uGUIのようなRetained Mode(以下、保持モード)とは根本的に考え方が異なり、「毎フレーム、コードで描画指示を出す」スタイルです。
コード駆動とも言われています。

「毎フレーム、コードで描画指示するのは重いのでは」と思うかもしれませんが、それは場合によります。
同じようなUIでもuGUIのような保持モードの方が重い場面もあればその逆もあります。
特にUIを動かす場合、uGUIなどの保持モードでは重くなることが多い一方、即時モードでは状態の再計算によるオーバーヘッドが少ないため、高速に動作することが一般的です。

迅速にUIを構築可能

保持モードのGUIでは、

ボタン作成

なんらかのUIシステムに追加

ボタンがクリックされた時の処理(ハンドラーやコールバック)を設定

という手順をとる必要があります。
一度UIに追加されたボタンはそのまま保持されるため、例えばボタンに表示されるテキストを変更したい場合には、そのボタンへの参照をどこかに保持する必要があります。

即時モードでは、ボタンに対してクリックされたときに実行されるeventを追加しなくても、クリックされた場合にButton()がtrueを返すため、if文の中に処理を書くだけで済みます。
つまり、クリック時のハンドラーは必要なく、それへの参照を格納する必要もなければ、ボタン自体の参照も保持する必要もありません。

// Unity IMGUI
if (GUILayout.Button("Button"))
{
    Debug.Log("Clicked");
}

状態を管理する必要がない

即時モードでは一般的にSliderなどの値は内部で保持はされません。

// Unity IMGUI
f = GUILayout.HorizontalSlider(f, 0f, 1f);

これによりプログラミングでよくある、以前の値を間違って使ってしまうバグを防ぐことができます。

特に顕著なのがObject Poolingの問題です。
uGUIなどで多くのUIを生成する場合、Object Poolingを使うことが多いですが、一般的な即時モードでは考慮する必要はありません。
「Poolから取得したButtonを使う際に、以前のclick eventを削除し忘れていた」といったことはありません。

ホットリロードとの相性が良い

uGUIで、ゲームを再生中に変数に対して1を加算するボタンを追加するには、

再生の終了

ボタンのPrefabを配置

AddListener()でdelegateを記述

button.onClick.AddListener(() =>
{
    vector3 += new Vector3(1f, 1f, 1f);
});

という手順を踏む必要があります。

即時モードであればゲーム再生中でも以下のコードを記述することで、即座にボタンを追加し、クリックして加算することができます。

if (GUILayout.Button("Add 1"))
{
    vector3 += Vector3.one;
}

ちなみに、Unityで使えるホットリロードは無料でも有料でもあります。

https://github.com/handzlikchris/FastScriptReload

https://assetstore.unity.com/packages/tools/utilities/hot-reload-edit-code-without-compiling-254358

低レベルな様々な描画が可能

これは即時モードGUIのライブラリにもよりますが、dear imguiなどの多くの即時モードGUIライブラリでは三角形レベルの描画をすることができます。

例えばRimGuiの場合、以下のように線を描画して、アンチエイリアス付きの柔軟なsinカーブを描画することが可能です。

Gui.CustomDelegate(static (shaper, widget) =>
{
    using var points = PooledArrayList<Vector2>.Rent((int)(2 * Mathf.PI / 0.1f));

    for (float x = 0; x <= 2 * Mathf.PI; x += 0.1f)
        points.Add(new Vector2(x * 50, Mathf.Sin(x) * 50 + 100));

    shaper.AddLineAA(points.AsSpan(), 2f,  1f,  new Color32(255, 100, 100, 255));
});

AIとの相性が良い

AIでuGUIをGameObjectとして配置し、インスペクターで設定していく作業は難しいですが、即時モードGUIであればコードを書いてもらうことでUIを構築できるので、AIとの相性が非常に良いです。

実行コスト

保持モードと比較される場合によく挙げられるのが、実行コストです。
必ずしも即時モードが保持モードよりも速い、遅い、ということはなく、どちらも実装次第だと私は考えています。
ただ、特に即時モードは最適化に気を使わないと簡単に高コストになりがちです。
そのため、ライブラリの内部ではGC Allocationをしないようにするなどの最適化が行われていることが多いです。

即時モードの方が速い1つの基本的な例は、テキストなどの要素やレイアウトなどが変更される場合です。
chromeなどのブラウザーのウィンドウのサイズをドラッグして変更してみると、レイアウト処理が遅く、CPUを大量に消費していることがわかりますが、即時モードであればウィンドウのサイズを変更しても、通常時と負荷は変わらないことが多いです。

即時モードGUIの欠点

派手な装飾や高度なアニメーション

複数の要素を組み合わせた複雑なエフェクトや、高度なアニメーションを実現するには、UIの状態を時間に沿って変化させるように管理し、それを描画に反映させる必要があるため、即時モードでは難易度が高いです。

レイアウト

例えばスクロールを描画する場合、スクロールされるUIがスクロールの領域のサイズを超えたらスクロールバーを描画する必要があり、バーの分だけスクロールの領域を小さくする必要があります。
しかし、領域を超えるかどうかは先にレイアウトを計算しなければ判別できません。

以下のコード(RimGui)で10個目のText()の実行でスクロール領域を超えることがわかり、スクロールバーを描画したい場合、Textの位置をスクロールバーだけずらす必要があります。

if (Gui.BeginScroll())
{
    for (int i = 0; i < 100; i++)
    {
        Gui.Text("A");
    }
    Gui.EndScroll();
}

このような問題に対して多くの即時モードGUIでは、前のフレームの値を用いたり、先にレイアウトを計算したり、2パスでレイアウトしたりといった対処法が行われています。

コードが書ける必要がある

uGUIなどのようにEditor上でドラッグ&ドロップで配置できるわけではないので、コードを書かなければレイアウトをすることができません。

即時モードGUIが使われているゲーム

このGithubのぺージでは、有名なImGuiライブラリであるdear imguiを使用しているゲームやアプリの一覧 (Diablo IVやLeague of Legendsやポケモンなど) を見ることができます。

最近だとマリオカートワールドでもImGui(dear imgui)が使用されているようです。

https://x.com/ocornut/status/1933528269720109363

ただこれらのタイトルでは、ツールやデバッグとして即時モードGUIが使われているものの、ユーザーに表示するUIとしては使われていないと思われます(デザイナーとの協業が難しいなどの理由が挙げられます)

ちなみに、RimWorldではユーザーが使用するUIで即時モードGUI(Unity IMGUI)が使用されているようです。

https://store.steampowered.com/app/294100/RimWorld/

最後に

最後まで読んでくださりありがとうございました。

私は即時モードGUIが好きで、簡単に視覚化ができるので楽しんで開発ができています。
ツールやデバッグだけでなく実際のゲームでも使っていきたいとも考えているので、使用する方が増えたり、議論が活発になったり、知見が広まっていくと嬉しいです。

Discussion