GUILayoutを使ったデバッグ用GUI ~UnityによるHCI研究 その2~

この記事では,Unityで研究する際に簡単に使えるGUIを紹介します.UnityEngine.GUIクラスを活用することで,スクリプトだけでGUIを構築することができます.このGUIは,変数の状態を表示したり,関数を呼び出したりするのに使えます.また,このGUIはVRビューで表示されない特徴があり,VR HMDを使用した実験を遂行するのに特に有効です.

基本的な実装

OnGUI()の中で,GUI.Window()を呼びます.GUI.Window()の引数の1つとして,GUIの中身を記述する無名関数を登録します.OnGUI()は,GUI用に最適化されたUpdate()のようなものだと思ってください.

using UnityEngine;

public class SampleGUI : MonoBehaviour
{
    // ウィンドウの初期位置・大きさ
    // 引数は適当に決める
    private Rect m_windowRect = new Rect(0, 0, 100, 200);

    // ウィンドウ固有のID
    private int m_windowId = 0;

    // ウィンドウの題名
    private string m_windowTitle = "Window Title";

    // サンプルのグローバル変数
    private bool m_toggleFlag = false;

    public void OnGUI()
    {
        m_windowRect = GUI.Window(m_windowId, m_windowRect, (id) => 
        {
            // ここにGUIの中身を記述する

            // ただのテキスト
            GUILayout.Label("sample text");

            // クリックで反応するボタン
            if (GUILayout.Button("Press me"))
            {
                // ここにボタンを押下した時の処理を記述する
                Debug.LogWarning("Button has been pressed!");
            }

            // クリックで反応するチェックボックス
            m_toggleFlag = GUILayout.Toggle(m_toggleFlag, "sample toggle");

            // 1行に複数の要素を入れたい場合は以下のようにする
            GUILayout.BeginHorizontal();   // ここから下を1行にする
            GUILayout.Label("Left Side");
            GUILayout.FlexibleSpace();     // イイカンジに間を開ける(書かなくても良い)
            GUILayout.Label("Right Side");
            GUILayout.EndHorizontal();     // ここまでを1行にする

            // これを一番下に書くと,ドラッグでウィンドウを動かせるようになる
            GUI.DragWindow();
        }, m_windowTitle);
    }
}

サンプルコードを適当なゲームオブジェクトのコンポーネントにした後,Playモードに入ってGameビューを見ると,以下のようになります.

SampleGUIの出力結果.

GUILayoutクラスの詳細についてはリファレンスを参照してください.サンプルコードで書いたものに加え,TextAreaやHorizontalSliderなど,便利な関数が用意されています.「GUILayout」で検索すると,他にも日本語のわかりやすい記事がたくさんあると思いますので,そちらも参考にすると良いでしょう.ただし,他にも似た用語があるので,あまり検索性は高くないように思います.

GUILayoutクラスの代わりにGUIクラスで記述することも可能です.ただし,GUIクラスは要素を配置する場所をRect構造体でいちいち決める必要があるので,UIの配置に拘りたい場合に限るべきです.

ウィンドウIDの管理

複数のウィンドウを使いたい場合,それぞれのウィンドウID(m_windowId)は別々にしなければなりません.人の手で管理するとバグの元ですので,静的クラス等で変数を管理すると良いでしょう.

// 便利関数置き場
public static class Utility
{
    private static int m_windowId = 0;

    // GUIのウィンドウIDを重複なく与える
    public static int GetWindowId()
    {
        return m_windowId++;
    }
}
using UnityEngine;

public class SampleGUI : MonoBehaviour
{
    ...

    // ウィンドウ固有のID
    private int m_windowId = Utility.GetWindowId();

    ...
}

これ以降のサンプルコードでは,m_windowIdは全てUtilitiy.GetWindowId()によって与えられることとします.

サンプルコード

実験条件の切り替え

GUIで実験条件を切り替えたい場合のサンプルコードです.ここでは,Unityシーン中のゲームオブジェクトを有効化・非有効化することで実験条件を切り替えることとします.

using UnityEngine;

// 実験中に実験条件をGUIで切り替えるサンプルコード
public class ConditionController : MonoBehaviour
{
    // ウィンドウの初期位置・大きさ
    private Rect m_windowRect = new Rect(0, 0, 200, 300);
    // ウィンドウ固有のID
    private int m_windowId = Utility.GetWindowId();
    // ウィンドウの題名
    private string m_windowTitle = "Condition Controller";

    // 実験条件
    public enum Condition
    {
        None,
        Triangle,
        Square,
        Pentagon,
        Hexagon,
    }

    // 実験条件
    private Condition m_condition = Condition.None;
    // 実験条件(代入時にオブジェクトが即座に有効化・非有効化される)
    public Condition CurrentCondition
    {
        get
        {
            return m_condition;
        }
        set
        {
            m_condition = value;

            OnConditionChanged(value);
        }
    }

    // 実験条件によって有効化・非有効化されるオブジェクト
    [SerializeField]
    private GameObject[] ConditionObjects;

    public void OnGUI()
    {
        m_windowRect = GUI.Window(m_windowId, m_windowRect, (id) =>
        {
            for (int i = 0; i <= (int)Condition.Hexagon; i++)
            {
                // 現在の実験条件がわかるようにボタンのテキストを強調しておく
                string text = ((int)CurrentCondition == i) ? $"<{(Condition)i}>" : $"{(Condition)i}";

                if (GUILayout.Button(text))
                {
                    CurrentCondition = (Condition)i;
                }
            }
            GUI.DragWindow();
        }, m_windowTitle);
    }

    // 実験条件に応じてオブジェクトを有効化・非有効化する
    // 実験の内容によって実装を変えること
    private void OnConditionChanged(Condition nextCondition)
    {
        for (int i=0; i<ConditionObjects.Length; i++)
        {
            ConditionObjects[i].SetActive((int)nextCondition == i);
        }
    }
}

InspectorではConditionObjectsに何個かのオブジェクトを登録していきます.ここでは,実験条件に対応した形状を持つオブジェクトを登録することにしました.

ConditionControllerのInspector.
ConditionObjectsにはNone~Hexagonの5つのオブジェクトが設定されている.

None~Hexagonの5つのオブジェクトは,(今回はサンプルなので)子オブジェクトで適当に形状を作っておきます.

Condition Controllerとその周囲のヒエラルキー.
今回はわかりやすさのためにCondition ObjectsをCondition Controllerの子オブジェクトにしたが,実際はどこに置いても良い.

すると,GUIは以下のような見た目になります.

ConditionControllerの出力結果(実験条件「Pentagon」の場合).

あるいは,以下のような実装も考えられます.

using UnityEngine;

// 実験中に実験条件をGUIで切り替えるサンプルコード
public class ConditionController : MonoBehaviour
{
    ...
    public void OnGUI()
    {
        m_windowRect = GUI.Window(m_windowId, m_windowRect, (id) =>
        {
            GUILayout.BeginHorizontal();

            // 左ボタン(押すと実験条件が1つ左隣に変わる)
            if (GUILayout.Button("<"))
            {
                CurrentCondition--;

                // 実験条件の番号が負の数になったら最大の番号にする
                if (CurrentCondition < 0)
                {
                    CurrentCondition = Condition.Hexagon;
                }
            }

            GUILayout.Label($"{CurrentCondition}");

            // 右ボタン(押すと実験条件が1つ右隣に変わる)
            if (GUILayout.Button(">"))
            {
                CurrentCondition++;

                // 実験条件が最大の番号を超えたら0にする
                if (Condition.Hexagon < CurrentCondition)
                {
                    CurrentCondition = Condition.None;
                }
            }
            GUILayout.EndHorizontal();
            GUI.DragWindow();
        }, m_windowTitle);
    }
    ...
}

この場合,GUIは以下のような見た目になります.

ConditionControllerの出力結果(実験条件「Square」の場合).
「<」を押すと実験条件が「Triangle」になり,「>」を押すと「Pentagon」になる.

いずれの書き方でも実質的に変わりはありませんが,操作しやすく,誤動作が少ないものを実験に合わせて選択すると良いでしょう.

シーンのフラグの管理

実験やデモ等で,フラグによって状態を遷移するようなコードを書くこともあると思います.この場合もGUIで表現するのが便利です.

using UnityEngine;

// シーンのフラグを管理するサンプルコード
public class FlagController : MonoBehaviour
{
    // ウィンドウの初期位置・大きさ
    private Rect m_windowRect = new Rect(0, 0, 150, 200);
    // ウィンドウ固有のID
    private int m_windowId = Utility.GetWindowId();
    // ウィンドウの題名
    private string m_windowTitle = "Flag Controller";

    // 何らかのボタンが押されたかどうか
    [SerializeField]
    private bool m_flagButtonA;
    [SerializeField]
    private bool m_flagButtonB;
    [SerializeField]
    private bool m_flagButtonC;

    private void OnGUI()
    {
        m_windowRect = GUI.Window(m_windowId, m_windowRect, (id) =>
        {
            m_flagButtonA = GUILayout.Toggle(m_flagButtonA, "Button A");
            m_flagButtonB = GUILayout.Toggle(m_flagButtonB, "Button B");
            m_flagButtonC = GUILayout.Toggle(m_flagButtonC, "Button C");

            GUI.DragWindow();
        }, m_windowTitle);
    }
}

GUIの見た目は以下のようになります.ここで,ボタンはシーンのどこかに実装されているものとします.つまり,m_flagButtonA~m_flagButtonCは,シーン内のボタンの操作とこのGUIの操作の2通りの方法で変更することができます.

Flag Controllerの出力結果.

このGUIがあると,フラグの管理が簡単になる他,VRやARの体験中に何らかの原因でシーン内のボタンが押せなかった場合に,「ボタンを押したことにする」ことができます.

陥りがちな良くないコード

よく見かけるコードのうち,GUIを使用しなかったことで煩雑になった例を挙げます.

全ての状態をDebug.Logで出力する

多くの解説記事では,あるオブジェクトの状態を表示するのにとりあえずDebug.Log()で行うことが多いですが,多くの場合は代わりにGUILayout.Label()を使用した方が良いです.
Debug.Log()(LogWarning(),LogError())でConsoleに出力される文字列は検索性が悪く,毎フレーム文字列を出力しているとあっという間に情報が溢れてしまいます.先述したフラグのように,監視したいけれども毎フレームチェックする必要のない情報は,なるべくGUIに表示するのが良いでしょう.ゲームオブジェクトやコンポーネントごとにウィンドウを分ければ,表示される情報が誰のどの情報なのかを瞬時に見分けることもできます.

ある変数の状態をGUIで表示する場合は,以下のように変数名(あるいはそれに準じたラベル)と変数の中身を1行に並べて書くとわかりやすいでしょう.

// 表示したい変数
private float hoge;

void OnGUI()
{
    m_windowRect = GUI.Window(m_windowId, m_windowRect, (id) =>
    {
        ...
        GUILayout.BeginHorizontal();
        GUILayout.Label("hoge:");
        GUILayout.FlexibleSpace();
        GUILayout.Label($"{hoge:.00}"); // 小数点以下2桁までの表示
        GUILayout.EndHorizontal();
        ...
    }
}

全ての操作をキー入力で行う

ある操作を行いたい場合,よくこんなコードが見受けられます.

void Update()
{
    if (Input.GetKeyDown(KeyCode.A)
    {
        // 何らかの処理
    }
}

先ほどの実験条件の例を用いるとしたら,NキーでNone,TキーでTriangle,SキーでSquare,PでPentagon,HでHexagonに実験条件を切り替えるというものです.3,4,5,6キーで同様に切り替え処理を実装することを考えた方もいるでしょう.いずれにせよ,このような書き方ではどのキーを押すと何が起こるかが一見して理解できません.この実験を誰か他の人が行う場合や,半年後にもう一度行うのをイメージしてください.
実装する上でも,このような処理は色々なクラスのUpdate()に散りがちで,ボタンを押したときの処理をどこで書いたか忘れてしまいがちです.

このような場合は同様の処理を全てGUI.Button()で行うよう書き換えてしまうのが良いですが,以下のようにGUI.Button()のラベルにキーも付記しておくのも良いでしょう.この場合は,元々実装してあったキー入力による操作がショートカットキーとして機能します.

void Update()
{
    if (Input.GetKeyDown(KeyCode.A)
    {
        DoSomething();
    }
}

void OnGUI()
{
    m_windowRect = GUI.Window(m_windowId, m_windowRect, (id) =>
    {
        ...
        if (GUILayout.Button("Do Something (A)")
        {
            DoSomething();
        }
        ...
    }
}

// Aキー押下時とGUIのボタンの押下時に呼び出される関数
public void DoSomething()
{
    // 何らかの処理
}

この記事が気に入ったらサポートをしてみませんか?