はじめに
UnityでuGUIを使ってUIを組むやり方として、自分は以下の方法を取っています。
-
GameObject
を作る - 子/孫/子孫
GameObject
を作ってImage
コンポーネントとかをAddComponent
する - 必要なだけ2を行う
- 子/孫/子孫コンポーネントの管理設定用コンポーネントを作り、1で作ったルート
GameObject
にAddComponent
する - Inspectorで4の管理設定用コンポーネントに子/孫/子孫コンポーネントをセット、よしなに制御する(子/孫/子孫は直接編集しない・させない)
この 親→子 という参照方向の一方向化は
- 管理設定用コンポーネントの
GameObject
をそのままプレハブにできる - コンポーネントの組み合わせが柔軟にできる
というメリットがあり、スケールしやすいやり方です。実際Unityに最初から用意されているいくつかのコンポーネントもこのような設計になっています(ScrollRect
とか)。
ですが、何も考えずにこういう実装をすると
- 非再生中にUIを作ろうとする
- 管理設定用コンポーネントのパラメータをInspectorからいじる
- 子/孫/子孫コンポーネントに値がリアルタイムに反映されない
- いちいち再生して確認する
という手間が発生します。
本エントリではこの問題に対して「非再生中にInspectorから値をいじったとき、どうやったら処理を実行できるか」について
-
OnValidate()
を使う -
[ExecuteInEditMode]
,[ExecuteAlways]
を使う - Odin(有料アセット)の
[OnValueChanged]
を使う - Property Backing Field Drawer(無料アセット)の
[PropertyBackingField]
を使う
の4つの方法を紹介します。一番オススメなのは最後の方法なのでどうぞ最後まで読んでください。
OnValidate()
を使う
MonoBehaviour
を継承したコンポーネントにOnValidate()
メソッドを定義しておくと、Inspectorから自身の値が変更されたときに自動で呼ばれます。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 登録したグラフィックコンポーネントの色を一括で変更する
/// </summary>
public class ColorSynchronizer : MonoBehaviour
{
[SerializeField] private Color _color;
[SerializeField] private Graphic[] _graphics = {};
void OnValidate()
{
foreach (var graphic in _graphics)
{
graphic.color = _color;
}
}
}
やりたいことが単純な場合はこれで十分なのですが、細かく制御しようとするとフィールドごとに古い値を保存しておいて変更があったらif
で判定・処理する、みたいな地獄が始まります。
using UnityEngine;
using UnityEngine.UI;
public class ColorSynchronizer : MonoBehaviour
{
[SerializeField] private Color _color;
[SerializeField] private Graphic[] _graphics = {};
// 変更前の値を保持
private Color? _colorOld;
void OnValidate()
{
if (_colorOld != null) Debug.Log($"before: {_colorOld}");
_colorOld = _color;
foreach (var graphic in _graphics)
{
graphic.color = _color;
}
}
}
[ExecuteInEditMode]
,[ExecuteAlways]
を使う
これはScrollRect
などのuGUIコンポーネントで採用されているやり方です。
[ExecuteInEditMode]
と[ExecuteAlways]
はどちらも非再生中にMonoBehaviour
のコールバックを呼び出してくれるようにするのですが、
-
[ExecuteInEditMode]
:古い、今後は廃止 -
[ExecuteAlways]
:新しい、プレハブモードにも対応
という違いがあるようなので[ExecuteAlways]
が使えるならそっちを使ったほうが良さそうです。
参考:
- 【Unity】【エディタ拡張】Unity2018.3以降ExecuteInEditModeアトリビュートは廃止に向かっていく
- UnityのAttribute(属性)についてまとめてメモる。
- ExecuteInEditMode(公式ドキュメント)
- ExecuteAlways(公式ドキュメント)
using System;
using UnityEngine.Events;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[AddComponentMenu("UI/Scroll Rect", 37)]
[SelectionBase]
[ExecuteAlways]
[DisallowMultipleComponent]
[RequireComponent(typeof(RectTransform))]
/// <summary>
/// A component for making a child RectTransform scroll.
/// </summary>
/// <remarks>
/// ScrollRect will not do any clipping on its own. Combined with a Mask component, it can be turned into a scroll view.
/// </remarks>
public class ScrollRect : UIBehaviour, IInitializePotentialDragHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler, ICanvasElement, ILayoutElement, ILayoutGroup
{
...
uGUIコンポーネントはBitbucketでソースコードが公開されているので全部読みたい方はこちらからどうぞ。
このやり方は「再生時と非再生時で同じコードを使い回せる」というメリットがありますが、[ExecuteAlways]
などをつけたときにStart()
やUpdate()
がいつのタイミングで何回呼ばれるのかを知っていないと思わぬ事故の原因になります。
またOnValidate()
と同様に変更が起きたものを個別で検知できないので、基本的には「変更が必要なものは無差別にUpdate()
で全部更新」みたいな感じになると思います。
Odinの[OnValueChanged]
を使う
Odin という有料アセット($55)には[OnValueChanged]
という属性が用意されていて、これを使うとフィールドやプロパティの値の変化を個別で検知して指定したコールバックを実行できます。
コガネブログさまで紹介されているので詳細は以下のリンク先をご覧ください。
以下にコガネブログさまからコードを引用させていただきます。
[OnValueChanged( "OnChanged" )] public int a;
private void OnChanged()
{
Debug.Log( "log" );
}
"OnChanged"
とstring
で指定しているところはnameof(OnChanged)
にするとなお良いと思います。
この方法は変更を個別で検知できるのでやりたいことにかなり近いのですが、専用のコールバックメソッドを用意しだすとどんどん数が増えて管理しづらくなる欠点があります。
指定したコールバックが呼ばれるのは値の変更後なので、変更前の値が取得できないという問題もあります。
Property Backing Field Drawerの[PropertyBackingField]
を使う
一番おすすめのやり方です。
Property Backing Field Drawer(リンク先はコガネブログさまの紹介記事)という無料アセットを導入すると使えるようになる[PropertyBackingField]
を使用します。
フィールドに[PropertyBackingField]
を指定すると、Inspectorから値を変更したときにプロパティ(名前でよしなに選ばれる)を経由してくれます。
以下の2つのコードはコガネブログさまからの引用です。
using Candlelight;
using UnityEngine;
public class Example : MonoBehaviour
{
[SerializeField, PropertyBackingField]
private int m_Int = 0;
public int Int
{
get { return m_Int; }
set { m_Int = value; Debug.Log( value ); }
}
}
PropertyBackingField 属性にプロパティ名を指定することで変数を好きなプロパティと紐付けることができます
using Candlelight;
using UnityEngine;
public class Example : MonoBehaviour
{
[SerializeField, PropertyBackingField( "IntProperty" )]
private int m_intField = 0;
public int IntProperty
{
get { return m_intField; }
set { m_intField = value; Debug.Log( value ); }
}
}
プロパティを経由するので前後の値も参照できます。
注意
Property Backing Field Drawerを導入するとScripting Define SymbolsにIS_UNITYEDITOR_ANIMATIONS_AVAILABLE
というシンボルが勝手に足されます。
特に害はないと思いますが、気になる人はAssets/Plugins/Candlelight/Library/Editor/Utilities/UnityFeatureDefineSymbols.cs
の34行目[InitializeOnLoad]
を消すと足されなくなります1。
おまけ
フィールドに[Range]
を指定するとInspectorからの入力値に制限をかけてくれますが、プログラムによる値の変更については機能しないことはご存知でしょうか?
using UnityEngine;
public class RangeTest : MonoBehaviour
{
[SerializeField, Range(0, 10)] private float _range;
void Start()
{
_range = 30;
Debug.Log(_range); // -> [Range]によるバリデーションは効かず、30
}
}
「Inspectorからの入力値のみに制限をかけたい」ということは稀で、実際には「再生中のプログラムコードによる値の変更」と「Inspectorからの値の変更」両方に同じバリデーションをかけたいはずです。
using UnityEngine;
public class RangeTest : MonoBehaviour
{
// MinとMaxが2回ずつ出てくるので定数化(他にも増えるとかなり面倒...)
private const float Min = 0;
private const float Max = 10;
[SerializeField, Range(Min, Max)] private float _range;
public float range
{
get { return _range; }
set { _range = Mathf.Clamp(value, Min, Max); }
}
void Start()
{
this.range = 30;
Debug.Log(this.range); // -> 10
}
}
これは[PropertyBackingField]
を使うとシンプルに書けます。
using Candlelight;
using UnityEngine;
public class RangeTest : MonoBehaviour
{
// Inspectorから値を変更するとプロパティのsetterが呼ばれる(バリデーションが効く)
[SerializeField, PropertyBackingField] private float _range;
public float range
{
get { return _range; }
set { _range = Mathf.Clamp(value, 0, 10); }
}
void Start()
{
this.range = 30;
Debug.Log(this.range); // -> 10
}
}
元からプロパティを実装してあれば冗長にならないので、Property Backing Field Drawerを使っている人はこちらのバリデーションがおすすめです。[Range]
,[Min]
などは窓から投げ捨てましょう。
まとめ
Inspectorで値を変更したときにコールバックを実行する方法として
-
OnValidate()
を使う -
[ExecuteInEditMode]
,[ExecuteAlways]
を使う - Odin(有料アセット)の
[OnValueChanged]
を使う - Property Backing Field Drawer(無料アセット)の
[PropertyBackingField]
を使う
の4つを紹介しました。状況によりけりですが、基本的には最後のやり方で事足りるのではないかと思います。
なお Odin は神アセットなので今開催中のマッドネスセール中に半額の$27.5で購入しておくことをおすすめします2。使い方の多くはコガネブログさまで紹介されているのでそちらを読まれると良いです。
また、コガネブログさまで紹介されてる機能がOdinのすべてではないので、Unityエディタ上・エディタ拡張実装で何かしら不便に感じることがあればOdinの公式ドキュメントを漁ってみてください。