【Unity】【エディタ拡張】あるフィールドの値に応じて別のフィールドの表示状態を切り替えるAttributeを作る

あるフィールドの値に応じて別のフィールドの表示状態を切り替えるAttributeを作る方法です。

Unity2018.3.1

作るもの

あるフィールドの値に応じて別のフィールドの表示状態を切り替えたいことがあります。

f:id:halya_11:20190116112228g:plain

上図の例では選択したenumによってその下に表示するフィールドを変更しています。
こういったものはAttributeを作るといい感じに実現できます。

ソースコード

PropertyAttributeとPropertyDrawerは次のように書きます。

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// 他のフィールドの値を条件として有効状態を切り替える
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = false)]
public class EnabledIfAttribute : PropertyAttribute 
{
    /// <summary>
    /// 非表示状態の表現方法
    /// </summary>
    public enum HideMode
    {
        Invisible, // 見えなくする
        Disabled, // 非アクティブにする
    }

    /// <summary>
    /// 条件として指定するフィールドの型
    /// </summary>
    public enum SwitcherType
    {
        Bool,
        Enum,
    }

    public SwitcherType switcherType;
    public HideMode hideMode;
    public string switcherFieldName;
    public int enableIfValueIs;
 
    public EnabledIfAttribute(string switcherFieldName, bool enableIfValueIs, HideMode hideMode = HideMode.Disabled)
        : this(SwitcherType.Bool, switcherFieldName, enableIfValueIs ? 1 : 0, hideMode)
    {
    }

    public EnabledIfAttribute(string switcherFieldName, int enableIfValueIs, HideMode hideMode = HideMode.Disabled)
        : this(SwitcherType.Enum, switcherFieldName, enableIfValueIs, hideMode)
    {
    }

    private EnabledIfAttribute(SwitcherType switcherType, string switcherFieldName, int enableIfValueIs, HideMode hideMode)
    {
        this.switcherType = switcherType;
        this.hideMode = hideMode;
        this.switcherFieldName = switcherFieldName;
        this.enableIfValueIs = enableIfValueIs;
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(EnabledIfAttribute))]
public class EnabledIfAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var attr = attribute as EnabledIfAttribute;       
        var isEnabled = GetIsEnabled(attr, property);

        // 非表示処理
        if (attr.hideMode == EnabledIfAttribute.HideMode.Disabled) {
            GUI.enabled &= isEnabled;
        }

        // プロパティを描画
        if (GetIsVisible(attr, property)) {
            EditorGUI.PropertyField(position, property, label, true);
        }

        // GUI.enabledを戻す
        if (attr.hideMode == EnabledIfAttribute.HideMode.Disabled) {
            GUI.enabled = true;
        }
    }

    public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    {
        var attr = attribute as EnabledIfAttribute;
        return GetIsVisible(attr, property) ? EditorGUI.GetPropertyHeight(property) : -2; // 表示されていない場合スペースを詰めるため-2を返す
    }

    /// <summary>
    /// 表示されるか
    /// 非アクティブでも表示されていればtrueを返す
    /// </summary>
    private bool GetIsVisible(EnabledIfAttribute attribute, SerializedProperty property)
    {
        if (GetIsEnabled(attribute, property)) {
            return true;
        }
        if (attribute.hideMode != EnabledIfAttribute.HideMode.Invisible) {
            return true;
        }
        return false;
    }

    /// <summary>
    /// フィールドが有効か
    /// </summary>
    private bool GetIsEnabled(EnabledIfAttribute attribute, SerializedProperty property)
    {
        return attribute.enableIfValueIs == GetSwitcherPropertyValue(attribute, property);
    }

    private int GetSwitcherPropertyValue(EnabledIfAttribute attribute, SerializedProperty property)
    {
        var propertyNameIndex = property.propertyPath.LastIndexOf(property.name, StringComparison.Ordinal);
        var switcherPropertyName = property.propertyPath.Substring(0, propertyNameIndex) + attribute.switcherFieldName;
        var switcherProperty = property.serializedObject.FindProperty(switcherPropertyName);
        switch (switcherProperty.propertyType) {
        case SerializedPropertyType.Boolean:
            return switcherProperty.boolValue ? 1 : 0;
        case SerializedPropertyType.Enum:
            return switcherProperty.intValue;
        default:
            throw new System.Exception("unsupported type.");
        }
    }
}
#endif

説明はコメントに書いた通りです。

CustomPropetyDrawerと組み合わせる場合

CustomPropertyDrawerと組み合わせて使う場合には、上記のままでは上手くいきません。
この時に起こる現象と原因と解決策については下記の記事で紹介しています。

light11.hatenadiary.com

この点を解決したソースコードが下記です。

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// 他のフィールドの値を条件として有効状態を切り替える
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = false)]
public class EnabledIfAttribute : PropertyAttribute 
{
    /// <summary>
    /// 非表示状態の表現方法
    /// </summary>
    public enum HideMode
    {
        Invisible, // 見えなくする
        Disabled, // 非アクティブにする
    }

    /// <summary>
    /// 条件として指定するフィールドの型
    /// </summary>
    public enum SwitcherType
    {
        Bool,
        Enum,
    }

    public SwitcherType switcherType;
    public HideMode hideMode;
    public string switcherFieldName;
    public int enableIfValueIs;
 
    public EnabledIfAttribute(string switcherFieldName, bool enableIfValueIs, HideMode hideMode = HideMode.Disabled)
        : this(SwitcherType.Bool, switcherFieldName, enableIfValueIs ? 1 : 0, hideMode)
    {
    }

    public EnabledIfAttribute(string switcherFieldName, int enableIfValueIs, HideMode hideMode = HideMode.Disabled)
        : this(SwitcherType.Enum, switcherFieldName, enableIfValueIs, hideMode)
    {
    }

    private EnabledIfAttribute(SwitcherType switcherType, string switcherFieldName, int enableIfValueIs, HideMode hideMode)
    {
        this.switcherType = switcherType;
        this.hideMode = hideMode;
        this.switcherFieldName = switcherFieldName;
        this.enableIfValueIs = enableIfValueIs;
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(EnabledIfAttribute))]
public class EnabledIfAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var attr = attribute as EnabledIfAttribute;
        var drawer = PropertyDrawerDatabase.GetDrawer(fieldInfo.FieldType);
        var isEnabled = GetIsEnabled(attr, property);

        // 非表示処理
        if (attr.hideMode == EnabledIfAttribute.HideMode.Disabled) {
            GUI.enabled &= isEnabled;
        }

        // プロパティを描画
        if (GetIsVisible(attr, property)) {
            drawer.OnGUI(position, property, label);
        }

        // GUI.enabledを戻す
        if (attr.hideMode == EnabledIfAttribute.HideMode.Disabled) {
            GUI.enabled = true;
        }
    }

    public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    {
        var attr = attribute as EnabledIfAttribute;
        var drawer = PropertyDrawerDatabase.GetDrawer(fieldInfo.FieldType);
        return GetIsVisible(attr, property) ? drawer.GetPropertyHeight(property, label) : -2; // 表示されていない場合スペースを詰めるため-2を返す
    }

    /// <summary>
    /// 表示されるか
    /// 非アクティブでも表示されていればtrueを返す
    /// </summary>
    private bool GetIsVisible(EnabledIfAttribute attribute, SerializedProperty property)
    {
        if (GetIsEnabled(attribute, property)) {
            return true;
        }
        if (attribute.hideMode != EnabledIfAttribute.HideMode.Invisible) {
            return true;
        }
        return false;
    }

    /// <summary>
    /// フィールドが有効か
    /// </summary>
    private bool GetIsEnabled(EnabledIfAttribute attribute, SerializedProperty property)
    {
        return attribute.enableIfValueIs == GetSwitcherPropertyValue(attribute, property);
    }

    private int GetSwitcherPropertyValue(EnabledIfAttribute attribute, SerializedProperty property)
    {
    var propertyNameIndex = property.propertyPath.LastIndexOf(property.name, StringComparison.Ordinal);
    var switcherPropertyName = property.propertyPath.Substring(0, propertyNameIndex) + attribute.switcherFieldName;
    var switcherProperty = property.serializedObject.FindProperty(switcherPropertyName);
        switch (switcherProperty.propertyType) {
        case SerializedPropertyType.Boolean:
            return switcherProperty.boolValue ? 1 : 0;
        case SerializedPropertyType.Enum:
            return switcherProperty.intValue;
        default:
            throw new System.Exception("unsupported type.");
        }
    }
}
#endif

EnabledIfを使いたいCustomPropertyDrawerPropertyDrawerDatabaseにあらかじめ登録しておきます。

using UnityEditor;
using System.Collections.Generic;

public static class PropertyDrawerDatabase
{
    private  static Dictionary<System.Type, PropertyDrawer> _drawers = new Dictionary<System.Type, PropertyDrawer>();

    static PropertyDrawerDatabase()
    {
        _drawers = new Dictionary<System.Type, PropertyDrawer>();
        
        // クラスと対応するPropertyDrawerを登録しておく
        _drawers.Add(typeof(SomeClass), new SomeClassDrawer());
    }

    public static PropertyDrawer GetDrawer(System.Type fieldType)
    {
        PropertyDrawer drawer;
        return _drawers.TryGetValue(fieldType, out drawer) ? drawer : null;
    }
}

これで正常に表示されるようになりました。

使う

使う側はこんな感じで書きます。

using UnityEngine;

public class EnabledIfExample : MonoBehaviour {

    public enum Example
    {
        UseExample1,
        UseExample2,
        UseExample3,
    }

    [SerializeField]
    private Example _example;
    [SerializeField, EnabledIf("_example", (int)Example.UseExample1, EnabledIfAttribute.HideMode.Invisible)]
    private float _example1;
    [SerializeField, EnabledIf("_example", (int)Example.UseExample2, EnabledIfAttribute.HideMode.Invisible)]
    private float _example2;
    [SerializeField, EnabledIf("_example", (int)Example.UseExample3, EnabledIfAttribute.HideMode.Invisible)]
    private float _example3;
}

exampleフィールドの値に応じてexample1~_example3の表示状態を切り替えています。

結果は次のようになります。

f:id:halya_11:20190116112228g:plain

関連

light11.hatenadiary.com

light11.hatenadiary.com