Unityで複数のカスタムPropertyAttributeを作るとPropertyDrawerが競合する件とその対応策についてまとめます。
- 問題点
- ソースコード
- EnabledIfAttributeをMultiPropertyAttributeで実装する
- NormalizedAnimationCurveAttributeをMultiPropertyAttributeで実装する
- 関連
- 参考
Unity2019.4.0f1
問題点
Unityには[Range]や[TextArea]などのアトリビュートが用意されいます。
using UnityEngine; public class Example : MonoBehaviour { [Range(0f, 5f)] public float testFloat; [TextArea] public string testString; }
これらを使うとシリアライズされたフィールドのInspectorにおける見栄えを簡単に変えたりできます。
このようなアトリビュートはカスタムPropertyAttributeを作ることで独自に定義することができます。
方法の詳細については説明しませんが以下の記事のような感じです。
さてこのカスタムPropertyAttributeは一つだけ使う分には良いのですが、
一つのフィールドに対して二つ以上設定すると、どちらか一つのPropertyDrawerしか反映されない問題があります。
この問題については以下のスレッドで議論されていて、スマートな解決策はないもののいくつかの方法が提唱されています。
本記事ではこのスレッドで紹介されている手法を元にもう少し使いやすく変えたものを掲載します。
ソースコード
ソースコードは以下の通りです。
using System.Linq; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif // GUI描画をコントロールしたいMultiPropertyAttributeにはこれを付ける public interface IAttributePropertyDrawer { #if UNITY_EDITOR void OnGUI(Rect position, SerializedProperty property, GUIContent label); float GetPropertyHeight(SerializedProperty property, GUIContent label); #endif } public abstract class MultiPropertyAttribute : PropertyAttribute { public MultiPropertyAttribute[] Attributes; public IAttributePropertyDrawer[] PropertyDrawers; #if UNITY_EDITOR public virtual void OnPreGUI(Rect position, SerializedProperty property) { } public virtual void OnPostGUI(Rect position, SerializedProperty property, bool changed) { } // アトリビュートのうち一つでもfalseだったらそのGUIは非表示になる public virtual bool IsVisible(SerializedProperty property) { return true; } #endif } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(MultiPropertyAttribute), true)] public class MultiPropertyAttributeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var attributes = GetAttributes(); var propertyDrawers = GetPropertyDrawers(); // 非表示の場合 if (attributes.Any(attr => !attr.IsVisible(property))) { return; } // 前処理 foreach (var attr in attributes) { attr.OnPreGUI(position, property); } // 描画 using (var ccs = new EditorGUI.ChangeCheckScope()) { if (propertyDrawers.Length == 0) { EditorGUI.PropertyField(position, property, label); } else { propertyDrawers.Last().OnGUI(position, property, label); } // 後処理 foreach (var attr in attributes.Reverse()) { attr.OnPostGUI(position, property, ccs.changed); } } } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { var attributes = GetAttributes(); var propertyDrawers = GetPropertyDrawers(); // 非表示の場合 if (attributes.Any(attr => !attr.IsVisible(property))) { return -EditorGUIUtility.standardVerticalSpacing; } var height = propertyDrawers.Length == 0 ? base.GetPropertyHeight(property, label) : propertyDrawers.Last().GetPropertyHeight(property, label); return height; } private MultiPropertyAttribute[] GetAttributes() { var attr = (MultiPropertyAttribute) attribute; if (attr.Attributes == null) { attr.Attributes = fieldInfo .GetCustomAttributes(typeof(MultiPropertyAttribute), false) .Cast<MultiPropertyAttribute>() .OrderBy(x => x.order) .ToArray(); } return attr.Attributes; } private IAttributePropertyDrawer[] GetPropertyDrawers() { var attr = (MultiPropertyAttribute) attribute; if (attr.PropertyDrawers == null) { attr.PropertyDrawers = fieldInfo .GetCustomAttributes(typeof(MultiPropertyAttribute), false) .OfType<IAttributePropertyDrawer>() .OrderBy(x => ((MultiPropertyAttribute) x).order) .ToArray(); } return attr.PropertyDrawers; } } #endif
説明はコメントとして記述しています。
使い方としては、カスタムPropertyAttributeを作る際にPropertyAttributeではなくMultiPropertyAttributeを継承します。
このクラスにはプロパティ描画の前後処理(OnPreGUIとOnPostGUI)を定義できます。
この前後処理についてはプロパティ描画時にすべてのアトリビュートのものが走る仕組みになっています。
加えて、プロパティの描画方法自体を変更したい場合にはIAttributePropertyDrawerを実装します。
これにはプロパティ描画用のメソッドが定義されており、アトリビュートの中で一番orderが高いものが優先的に処理されます。
EnabledIfAttributeをMultiPropertyAttributeで実装する
実際の使用例として、以前以下の記事で紹介したEnabledIfAttributeをMultiPropertyAttributeを使って実装してみます。
実装は以下の通りです。
using System; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class EnabledIfAttribute : MultiPropertyAttribute { public enum HideMode { Invisible, Disabled } private readonly int _enableIfValueIs; private readonly HideMode _hideMode; private readonly string _switcherFieldName; public EnabledIfAttribute(string switcherFieldName, bool enableIfValueIs, HideMode hideMode = HideMode.Disabled) : this(switcherFieldName, enableIfValueIs ? 1 : 0, hideMode) { } public EnabledIfAttribute(string switcherFieldName, int enableIfValueIs, HideMode hideMode = HideMode.Disabled) { _hideMode = hideMode; _switcherFieldName = switcherFieldName; _enableIfValueIs = enableIfValueIs; } #if UNITY_EDITOR public override void OnPreGUI(Rect position, SerializedProperty property) { var isEnabled = GetIsEnabled(property); if (_hideMode == HideMode.Disabled) { GUI.enabled &= isEnabled; } } public override void OnPostGUI(Rect position, SerializedProperty property, bool changed) { if (_hideMode == HideMode.Disabled) { GUI.enabled = true; } } public override bool IsVisible(SerializedProperty property) { return _hideMode != HideMode.Invisible || GetIsEnabled(property); } private bool GetIsEnabled(SerializedProperty property) { return _enableIfValueIs == GetSwitcherPropertyValue(property); } private int GetSwitcherPropertyValue(SerializedProperty property) { var propertyNameIndex = property.propertyPath.LastIndexOf(property.name, StringComparison.Ordinal); var switcherPropertyName = property.propertyPath.Substring(0, propertyNameIndex) + _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 Exception("unsupported type."); } } #endif }
NormalizedAnimationCurveAttributeをMultiPropertyAttributeで実装する
さらにもう一つの例として、以下の記事のNormalizedAnimationCurveAttributeもMultiPropertyAttributeで実装してみます。
実装は以下の通りです。
using System; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class NormalizedAnimationCurveAttribute : MultiPropertyAttribute { private readonly bool _normalizeTime; private readonly bool _normalizeValue; public NormalizedAnimationCurveAttribute(bool normalizeValue = true, bool normalizeTime = true) { _normalizeValue = normalizeValue; _normalizeTime = normalizeTime; } #if UNITY_EDITOR public override void OnPostGUI(Rect position, SerializedProperty property, bool changed) { if (changed) { if (_normalizeValue) { property.animationCurveValue = NormalizeValue(property.animationCurveValue); } if (_normalizeTime) { property.animationCurveValue = NormalizeTime(property.animationCurveValue); } } } private static AnimationCurve NormalizeValue(AnimationCurve curve) { var keys = curve.keys; if (keys.Length <= 0) { return curve; } var minVal = keys[0].value; var maxVal = minVal; foreach (var t in keys) { minVal = Mathf.Min(minVal, t.value); maxVal = Mathf.Max(maxVal, t.value); } var range = maxVal - minVal; var valScale = range < 1 ? 1 : 1 / range; var valOffset = 0f; if (range < 1) { if (minVal > 0 && minVal + range <= 1) { valOffset = minVal; } else { valOffset = 1 - range; } } for (var i = 0; i < keys.Length; ++i) { keys[i].value = (keys[i].value - minVal) * valScale + valOffset; } curve.keys = keys; return curve; } private static AnimationCurve NormalizeTime(AnimationCurve curve) { var keys = curve.keys; if (keys.Length <= 0) { return curve; } var minTime = keys[0].time; var maxTime = minTime; foreach (var t in keys) { minTime = Mathf.Min(minTime, t.time); maxTime = Mathf.Max(maxTime, t.time); } var range = maxTime - minTime; var timeScale = range < 0.0001f ? 1 : 1 / range; for (var i = 0; i < keys.Length; ++i) { keys[i].time = (keys[i].time - minTime) * timeScale; } curve.keys = keys; return curve; } #endif }