3
\$\begingroup\$

I want to implement a custom Unity inspector attribute, like usual ones [HideInInspector] or [Range(0, 100f)].

This attribute should hide a field from the inspector if it's inherited from a base type, rather being defined directly in the type of the object we're inspecting.

So, for example, if I have these classes...

public class Enemy : MonoBehaviour {
    [SuperField]
    public int EnemyHealth; 
}

public class GiantMonster : Enemy {
    [SuperField]
    public int Armor;
}

then the inspector for a base Enemy should show a control for the EnemyHealth variable, but when inspecting a GiantMonster we should not see a control for this variable.

So far I have my attribute:

public class SuperFieldAttribute : PropertyAttribute {}

which has its own PropertyDrawer:

[CustomPropertyDrawer(typeof(SuperFieldAttribute))]
public class SuperFieldDrawer : PropertyDrawer {

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        // ...
    }
} 

Inside OnGUI I need to access/find the type where the field we're drawing was declared with the [SuperFieldAttribute].

So if SuperFieldDrawer.OnGUI() is called for the SerializedProperty corresponding to GiantMonster.EnemyHealth, I should be able to determine that the type where EnemyHealth was declared is Enemy and not GiantMonster. (I'll use this to hide the field so it's not drawn)

And if SuperFieldDrawer.OnGUI() is called for the SerializedProperty corresponding to GiantMonster.Armor, I should be able to determine that the type where Armor was declared is GiantMonster itself. (Meaning I should proceed and draw the field)

So far I've found ways to access the type of the component we're inspecting, but not the type where the field was first declared, so that's the missing piece I need to fill in.

It's OK if I use reflection and so on; any method is good, performance isn't a concern.

\$\endgroup\$
  • \$\begingroup\$ Do I understand correctly that you're always starting with a specific property with this attribute on an instance of a (possibly derived) class, and you just need to walk up its inheritance chain until you find the class that defines this property? You don't need to list all properties with this attribute on a particular object, or identify all types in the assembly that use the attribute, correct? \$\endgroup\$ – DMGregory Oct 29 '17 at 0:47
  • \$\begingroup\$ @DMGregory yes, that is correct! I know how to list them, but since I'm doing it in PropertyDrawer and I can't use custom editor for that(to check every property if it should be hidden), well I can but custom editor would mean I had to rewrite sometimes whole inspertor every time I change or add a new class. I'm trying to make HideInDerived InspectorAttribute. For the sake of designer, and because it would allow not to rewrite much of unnecessary code parts. \$\endgroup\$ – Candid Moon _Max_ Oct 29 '17 at 0:58
  • \$\begingroup\$ I think this question might be clearer if you edit it to ask about the HideInDerived application specifically. That tells us a lot about the context, what kinds of inputs we can expect (ie. SerializedProperty rather than System.Type or FieldInfo), and might help the question catch the eye of other Unity devs who want to implement this feature. :) \$\endgroup\$ – DMGregory Oct 29 '17 at 1:30
3
\$\begingroup\$

As luck would have it, the PropertyDrawer's fieldInfo has a handy DeclaringType property that tells us exactly where the field was defined, without needing to walk the inheritance hierarchy ourselves - at least for fields directly on the object we're inspecting. As we discovered in the comments, we still need to do a bit of manual walking for fields in a child object or struct.

Here's how we can implement that [HideInDerived] attribute drawer using this:

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(HideInDerivedAttribute))]
public class HideInDerivedDrawer : PropertyDrawer {

    bool? _chachedIsDerived;
    // Cache this so we don't have to muck with strings 
    // or walk the type hierarchy on every repaint.
    bool IsDerived(SerializedProperty property) {        
        if (_chachedIsDerived.HasValue == false) {

            string path = property.propertyPath;
            var type = property.serializedObject.targetObject.GetType();

            if (path.IndexOf('.') > 0) {
                // Field is in a nested type. Dig down to get that type.
                var fieldNames = path.Split('.');
                for(int i = 0; i < fieldNames.Length - 1; i++) {
                    var info = type.GetField(fieldNames[i]);
                    if (info == null)
                        break;
                    type = info.FieldType;
                }
            }

            _chachedIsDerived = fieldInfo.DeclaringType != type;
        }
        return _chachedIsDerived.Value; 
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        // If we're in a more derived type than where this field was declared,
        // abort and draw nothing instead.
        if (IsDerived(property))           
            return;

        EditorGUI.PropertyField(position, property, label, true);
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {

        if (IsDerived(property)) {
            // Collapse the unseen derived property.
            return -EditorGUIUtility.standardVerticalSpacing;            
        } else {
            // Provision the normal vertical spacing for this control.
            return EditorGUI.GetPropertyHeight(property, label, true);
        }
    }
}

Known issues

  • This does not stack with other property drawing attributes like [Range(min, max]. Only one attribute will take effect, depending on their order. Internally it seems Unity tracks only one inspector attribute per field.

    It may be possible with some heavy-duty reflection to identify other attributes on the field with their own CustomPropertyDrawers, and construct a corresponding PropertyDrawer to delegate the drawing to, but it would get messy fast. It might be simpler to make a combined attribute like [HideInDerivedRange(min, max)] for specific cases.

  • Types with their own custom property drawer will fall back to the default drawing style if marked [HideInDerived]. It works correctly for all of Unity's built-in property widgets though, showing colour pickers and vector/object fields as expected.

  • This does not correctly hide the label, fold-out, and size controls of an Array or List, although it does hide the collection's contents.

    As Candid Moon found, this is due to a change in Unity 4.3 to allow Attribute-targeted PropertyDrawers to apply to each element in a collection, which unfortunately makes it impossible to target the array itself in the present version.

    A workaround suggested by slippdouglas here is to define a serializable struct containing your collection, so that the [HideInDerived] attribute hides the struct as a whole, like so:

.

[System.Serializable] 
public struct HideableStringList {
     public List<string> list;

     // Optional, for convenience, so the rest of your code can
     // continue to behave like this is just a List<string>
     // without always referencing the .list field.
     public static implicit operator List<string>(HideableStringList c) {
         return c.list;
     }
     public static implicit operator HideableStringList(List<string> l) {
         return new HideableStringList(){ list = l }; 
     }
 }

Because of the way Unity draws the inspector, we can't use a single generic type to handle all these cases, so you'll have to copy this boilerplate for each array type (yuck). But at least it lets this work:

[HideInDerived]
HideableStringList myStrings = new List<string>(3);
\$\endgroup\$
  • \$\begingroup\$ Editor: PropertyDrawer attributes on members that are arrays are now applied to each element in the array rather than the array as a whole. This was always the intention since there is no other way to apply attributes to array elements, but it didn't work correctly before. Apologies for the inconvenience to anyone who relied on the unintended behavior for custom drawing of arrays. - from unity 4.3 release. \$\endgroup\$ – Candid Moon _Max_ Oct 29 '17 at 14:14
  • \$\begingroup\$ answers.unity3d.com/questions/1150679/… - looks like it's better to post an issue or feature request from unity if in 2017 it's still not viable option. \$\endgroup\$ – Candid Moon _Max_ Oct 29 '17 at 14:22
  • \$\begingroup\$ Also, my method doesn't work correctly if it's not the nested object. It hides it from inspector because the length is already 1 and it doesn't get the targetObject that you were using. That is why it might not have displayed correctly for you if you placed it inside your code. Here is my PropertyDrawer and all the other code that achieves it pastebin.com/KkUPyYu6 . Everything is working fine now I hope, except the arrays. \$\endgroup\$ – Candid Moon _Max_ Oct 29 '17 at 15:04
  • \$\begingroup\$ I have found a way to do it with multiple attributes, though they all require deriving from a specific type. It is possible though to just rewrite all the common attributes like [Range], [HideInInspector]. I will post a solution in comments today. \$\endgroup\$ – Candid Moon _Max_ Oct 30 '17 at 9:14
  • \$\begingroup\$ I recommend posting your solution as an Answer. Much easier to read that way! And you can get some upvotes. ;) \$\endgroup\$ – DMGregory Oct 30 '17 at 11:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.