30
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Unity】Inspectorから値を変更したときにコールバック処理を実行する簡単な方法【エディター拡張】

Posted at

はじめに

UnityでuGUIを使ってUIを組むやり方として、自分は以下の方法を取っています。

  1. GameObjectを作る
  2. 子/孫/子孫GameObjectを作ってImageコンポーネントとかをAddComponentする
  3. 必要なだけ2を行う
  4. 子/孫/子孫コンポーネントの管理設定用コンポーネントを作り、1で作ったルートGameObjectAddComponentする
  5. Inspectorで4の管理設定用コンポーネントに子/孫/子孫コンポーネントをセット、よしなに制御する(子/孫/子孫は直接編集しない・させない)

この 親→子 という参照方向の一方向化は

  • 管理設定用コンポーネントのGameObjectをそのままプレハブにできる
  • コンポーネントの組み合わせが柔軟にできる

というメリットがあり、スケールしやすいやり方です。実際Unityに最初から用意されているいくつかのコンポーネントもこのような設計になっています(ScrollRectとか)。

スクリーンショット 2019-05-11 19.13.36.png

ですが、何も考えずにこういう実装をすると

  1. 非再生中にUIを作ろうとする
  2. 管理設定用コンポーネントのパラメータをInspectorからいじる
  3. 子/孫/子孫コンポーネントに値がリアルタイムに反映されない
  4. いちいち再生して確認する

という手間が発生します。

本エントリではこの問題に対して「非再生中にInspectorから値をいじったとき、どうやったら処理を実行できるか」について

  • OnValidate()を使う
  • [ExecuteInEditMode],[ExecuteAlways]を使う
  • Odin(有料アセット)の[OnValueChanged]を使う
  • Property Backing Field Drawer(無料アセット)の[PropertyBackingField]を使う

の4つの方法を紹介します。一番オススメなのは最後の方法なのでどうぞ最後まで読んでください。

OnValidate()を使う

MonoBehaviourを継承したコンポーネントにOnValidate()メソッドを定義しておくと、Inspectorから自身の値が変更されたときに自動で呼ばれます。

ColorSynchronizer.cs
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で判定・処理する、みたいな地獄が始まります。

ColorSynchronizer.cs
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]が使えるならそっちを使ったほうが良さそうです。

参考:

ScrollRect.cs
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からの入力値に制限をかけてくれますが、プログラムによる値の変更については機能しないことはご存知でしょうか?

RangeTest.cs
using UnityEngine;

public class RangeTest : MonoBehaviour
{
    [SerializeField, Range(0, 10)] private float _range;

    void Start()
    {
        _range = 30;
        Debug.Log(_range); // -> [Range]によるバリデーションは効かず、30
    }
}

「Inspectorからの入力値のみに制限をかけたい」ということは稀で、実際には「再生中のプログラムコードによる値の変更」と「Inspectorからの値の変更」両方に同じバリデーションをかけたいはずです。

RangeTest.cs
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]を使うとシンプルに書けます。

RangeTest.cs
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の公式ドキュメントを漁ってみてください。

  1. Unity->Preferences..->Candlelight から設定できる項目ではこの挙動を止められません。

  2. リンクはアフィリエイトプログラムとかじゃないので安心してください。

30
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?