はじめに
uGUIを使っているとインスペクタやスクリプトから値を変更するだけで、ビューも更新されて気持ちいいです。これはたぶんリアクティブです。
GUIアプリ
上記はGUIアプリケーションでは普通の挙動だと思います。プロパティを変更するとイベントが発行されてビューが更新される仕組みです。Flashで開発をおこなっていた時はこういう実装がかんたんに出来ていた記憶があります。またgetterやsetterについての面白い記事がありました。
iGUI
OnGUI
でGUILayout
などを使って作る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の実装を見ながら再実装できたのは勉強になりました。