のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)uGUIっぽいUIの書き方をしたい

はじめに

uGUIを使っているとインスペクタやスクリプトから値を変更するだけで、ビューも更新されて気持ちいいです。これはたぶんリアクティブです。

こういう動き

GUIアプリ

上記はGUIアプリケーションでは普通の挙動だと思います。プロパティを変更するとイベントが発行されてビューが更新される仕組みです。Flashで開発をおこなっていた時はこういう実装がかんたんに出来ていた記憶があります。またgetterやsetterについての面白い記事がありました。

www.eisbahn.jp

iGUI

OnGUIGUILayoutなどを使って作るuiです。こちらはスクリプトで部品を定義して毎フレーム描画する方式です(最適化されているので実際の挙動は違います)。こちらはこちらで使い勝手がいいので、僕の場合デバッグ用uiなんかはこっちを使うことが多いです。

実装する

僕が普段unityでuiコンポーネントを作る時に使っているコードを載せてみます。

UIBehaviour

ベースとなるクラスを作成します。

using UnityEngine;

public abstract class UIBehaviour : MonoBehaviour
{
    bool m_IsDirty;

    protected virtual void LateUpdate()
    {
        CheckDirty();
    }

#if UNITY_EDITOR
    // インスペクタで値を変えた時用
    protected virtual void OnValidate()
    {
        m_IsDirty = true;
        CheckDirty();
    }
#endif

    protected virtual void Render()
    {
    }

    protected void Set<T>(ref T prop, T value)
    {
        if (!value.Equals(prop))
        {
            prop = value;
            m_IsDirty = true;

#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                CheckDirty();
            }
#endif
        }
    }

    protected void CheckDirty()
    {
        if (m_IsDirty)
        {
            Render();
            m_IsDirty = false;
        }
    }
}

note!

#if UNITY_EDITOR
    // インスペクタで値を変えた時用
    protected virtual void OnValidate()
    {
        m_IsDirty = true;
        CheckDirty();
    }
#endif

エディターでインスペクタで値を変えた時のイベント発行はOnValidateがやってくれます。どのプロパティを変更したかなどは取れませんが、エディターなのであまり気にしません。

protected void Set<T>(ref T prop, T value)
    {
        if (!value.Equals(prop))
        {
            prop = value;
            m_IsDirty = true;

#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                CheckDirty();
            }
#endif
        }
    }

    protected void CheckDirty()
    {
        if (m_IsDirty)
        {
            Render();
            m_IsDirty = false;
        }
    }

値の変更を監視して変更があればフレームのおわりに再描画します。1つのプロパティが変更されるだけで全体再描画になりますが、適切にコンポーネントが部品かされていれば、そんなにオーバーヘッドは気にならないと思います。

使い方

ラベルをインスペクタで設定できるボタンを作ってみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MyButton : UIBehaviour
{
    [SerializeField]
    string m_Label;
    public string label { get => m_Label; set=>Set(ref m_Label, value); }

    [SerializeField]
    Text m_LabelText;

    protected override void Render()
    {
        m_LabelText.text = m_Label;
    }
}

これでインスペクタから値を変更しても、スクリプトから値を変更してもビューが更新されます。

動作する様子

既存コンポーネントを使った使い方

InputFieldをラップしたコンポーネントを作ってみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MyInput : UIBehaviour
{
    [SerializeField]
    string m_Message;
    public string message { get => m_Message; set => Set(ref m_Message, value); }

    [SerializeField]
    InputField m_InputField;

    private void OnEnable()
    {
        m_InputField.onValueChanged.AddListener(OnValueChange);
    }

    private void OnDisable()
    {
        m_InputField.onValueChanged.RemoveListener(OnValueChange);
    }

    void OnValueChange(string value)
    {
        message = value;
    }

    protected override void Render()
    {
        m_InputField.text = m_Message;
    }
}

こんな感じになります。

note!

OnValueChangeした結果Renderが呼ばれてm_InputField.textに値を代入しているので、無限ループが発生しそうですがこれは問題ないです。下位のコンポーネント内で「値の変更があったらイベント発火」という挙動になっているので、ループは発生しません。上記で実装したUIBehaviourもそうなっています。

コンポーネントを組み合わせた使い方

「はい」と「いいえ」のボタンを持ったコンポーネントを作ってみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MyConfirmButton : UIBehaviour
{
    [SerializeField]
    string m_YesLabel;
    public string yesLabel { get => m_YesLabel; set => Set(ref m_YesLabel, value); }

    [SerializeField]
    string m_NoLabel;
    public string noLabel { get => m_NoLabel; set => Set(ref m_NoLabel, value); }

    [SerializeField]
    MyButton m_YesButton;

    [SerializeField]
    MyButton m_NoButton;

    protected override void Render()
    {
        m_YesButton.label = m_YesLabel;
        m_NoButton.label = m_YesLabel;
    }
}

note!

protected override void Render()
{
    m_YesButton.label = m_YesLabel;
    m_NoButton.label = m_YesLabel;
}

Renderが呼ばれる度に両方のボタンを再設定していますが、上記で書いた通り「値の変更があったらイベント発火」になってるので、そんなに無駄はないと思います。

unity内部の実装

uGUIはC#で書かれているので以下のリポジトリでソースを見ることができます。これをみるとだいたい同じ様なことをしているようです。あまり統一感がなくコンポーネントによっていろいろな書き方がされているので、読んでみると面白いです。 github.com

memo!

SetPropertyUtilityを作ったりGraphicを継承したして処理を共通化しているようなので、これを使いたかったのですが、internalなクラスだったりで使えませんでした。この辺を使えるとよりunityチックに書けていいなと思いました。

イベントに関して

イベントには大きくSystem.Actionを使う方法とUnityEventを使う方法があると思います。uiに関しては後者を使うべきだと思います。uGUIはエディターを使って作るものなので、インスペクタでイベントもバインドできるUnityEventを使うのが自然だと思うからです。

おわり

この辺の実装に関してはそれぞれの流儀があると思うので、こんな流儀もあるよ、という気持ちで記事にしました。unirxなんかを使ってもスマートに書けると思います。「プロパティを変更したらビューも変更したい」みたいな原始的な仕組みを、unityの実装を見ながら再実装できたのは勉強になりました。