Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

CustomPropertyDrawer for a Class with a Generic Type

Discussion in 'Immediate Mode GUI (IMGUI)' started by SHiLLySiT, Feb 26, 2013.

  1. SHiLLySiT

    SHiLLySiT

    Joined:
    Feb 26, 2013
    Posts:
    12
    I have a class which acts like a dictionary but uses two lists instead:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5.  
    6. public class ListDictionary<T>
    7. {
    8.     private List<string> _keys;
    9.     private List<T> _values;
    10.    
    11.     public ListDictionary()
    12.     {
    13.         _keys = new List<string>();
    14.         _values = new List<T>();
    15.     }
    16.        
    17.         // ... etc
    18. }
    19.  
    I want to write a CustomPropertyDrawer for it but I'm getting the following error:
    Here's my property drawer:
    Code (csharp):
    1.  
    2. using UnityEditor;
    3. using UnityEngine;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6.  
    7. [CustomPropertyDrawer(ListDictionary<T>)]
    8. public class ListDictionaryDrawer : PropertyDrawer
    9. {
    10.     public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    11.     {
    12.         EditorGUI.BeginProperty (position, label, property);
    13.        
    14.        
    15.         EditorGUI.EndProperty ();
    16.     }
    17. }
    18.  
    I tried variations of the class name (ListDictionary, ListDictionary<>, and etc) but I haven't gotten it to work. How do I go about fixing this? On a related note, are there any tutorials on creating a custom property drawer for C#? I'm currently going off one that is for javascript.
     
  2. aalmada

    aalmada

    Joined:
    Apr 29, 2013
    Posts:
    21
    I would also like to know if this is possible.
    Thanks!
     
  3. CHaNGeTe

    CHaNGeTe

    Joined:
    Nov 16, 2012
    Posts:
    1
    You are on c#, aren't you missing the "typeof" part?

    [CustomPropertyDrawer(typeof(List<ScriptableAction>))]

    For example
     
  4. gfoot

    gfoot

    Joined:
    Jan 5, 2011
    Posts:
    550
    I tried this before and decided it didn't work. The only thing that I could do to make it work was to derive a non-generic class from the generic class, fixing its type parameters, and make the MonoBehaviours use that instead of using the generic type directly. You do need to proxy constructors, but most other things work fine. You also need the middle-man class to be [Serializable].

    It also means that your PropertyDrawer can only deal with one type - it can't be generic - though that's not quite the case either, as you can pull the same trick, defining a non-generic derived class and doing the [CustomPropertyDrawer] markup on that instead.

    So you end up with engine code:

    Code (csharp):
    1.  
    2. [Serializable]
    3. public class ListDictionary_int : ListDictionary<int>
    4. {
    5.     // ... constructors, at least ...
    6. }
    7.  
    And editor code:

    Code (csharp):
    1.  
    2. // No attributes needed
    3. public class ListDictionaryPropertyDrawer<T> : PropertyDrawer
    4. {
    5.     public override void OnGui(...)
    6.     // ... etc ...
    7. }
    8.  
    9. [CustomPropertyDrawer(typeof(ListDictionary_int))]
    10. public class ListDictionaryPropertyDrawer_int : ListDictionaryPropertyDrawer<int>
    11. {
    12.     // nothing needed here
    13. }
    14.  
     
    IsaiahKelly likes this.
  5. Xappek

    Xappek

    Joined:
    Oct 13, 2013
    Posts:
    1
    Try this [CustomPropertyDrawer(typeof(ListDictionary<>))]
     
    Last edited: Oct 13, 2013
  6. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,934
    @Xappek: I can't believe you even tested that, since it has never worked, and still (over a year later) doesn't work.
     
    EZaca likes this.
  7. BMayne

    BMayne

    Joined:
    Aug 4, 2014
    Posts:
    186
    Hey there,

    Unfortunately (from my experience) you are not able to do this. It has to do with how the inspector works and serialization works.

    Generic types are not serialized by Unity. When an inspector goes to preview an object it does not look at the class. It tells the class to serialize itself. It will then go ahead and show the results. Since Generics are not serialized there are no results to display.

    In short you can't :(
     
  8. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Too bad, I was also just trying this...

    Unity does serialize and display T[] or List<T> for example.

    Support for this would really be appreciated, because the alternative doesn't look very pretty. My goal is simply to replace the current editor for arrays with a version that is expanding automatically to reduce the need to set the size up front. This works perfectly fine for specific value types like int[], float[] and string[], but for any array of Component instances it would be nice to be able to define a generic property drawer.

    (The property drawer is not set for int[] directly, because that's also not possible. If have an ArrayInt class that can be implicitly cast to int[]. The expanding int array property drawer is placed on this class.)
     
    Last edited: Apr 18, 2017
    AdmiralThrawn likes this.
  9. Syganek

    Syganek

    Joined:
    Sep 11, 2013
    Posts:
    85
    So it is somewhat possible. Maybe not directly but there is a workaround for it. And I know I'm reviving old thread, but it was first answer in Google.

    PropertyDrawers does not support generic types. It is because Unity does not serialize generic types different than List<T>.

    So if you have:

    Code (CSharp):
    1. [Serializable]
    2. public class GenericClass<T>
    3. {
    4.     [SerializeField]
    5.     private T GenericVariable;
    6. }
    And MonoBehaviour with this class as a field:

    Code (CSharp):
    1. public class GenericPropertyTestController : MonoBehaviour
    2. {
    3.     public GenericClass<int> GenericField;
    4. }
    Unity will not serialize it. Period. Maybe someday we'll get such support but it would require changing such a base thing as Unity Serializer so we might never have it.

    But one can do a workaround.

    Basically you need to declare not generic class and your generic class will need to inherit from it. Then you can create your CustomPropertyDrawer for this class, but you need to set useForChildren as true in CustomPropertyDrawer attribute:

    Code (CSharp):
    1. [Serializable]
    2. public class GenericClassParent
    3. {
    4.  
    5. }
    6.  
    7. public class GenericClass<T> : GenericClassParent
    8. {
    9.     [SerializeField]
    10.     private T GenericVariable;
    11. }
    12.  
    13. [CustomPropertyDrawer(typeof(GenericClassParent), true)]
    14. public class GenericClassDrawer : PropertyDrawer
    15. {
    16.     //Property drawer code here.
    17. }
    But it still won't show in Inspector, because we're still trying to serialize the GenericClass<int> in our MonoBehaviour. Unity allow us to serialize types derived from generic classes though. So we can use this feature like this:

    Code (CSharp):
    1. [Serializable]
    2. public class IntGenericClass : GenericClass<int>
    3. { }
    4.  
    5. public class GenericPropertyTestController : MonoBehaviour
    6. {
    7.     public IntGenericClass GenericField;
    8. }
    This will allow Unity to serialize GenericField in GenericPropertyTestController and for drawing it'll use GenericClassDrawer.
     
    Last edited: Feb 27, 2018
  10. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Yes, that is also how I do things now. So for the automatically expanding int array, I have the following classes:
    - Array<T> (abstract, Serializable, provides implicit casting to T[])
    - ArrayInt : Array<int> (really just an empty class, but Serializable)
    - ArrayDrawer<T> : PropertyDrawer (abstract, provides a general automatically expanding array drawer)
    - ArrayIntDrawer : ArrayDrawer<int> (CustomPropertyDrawer(typeof(ArrayInt)))
     
  11. Syganek

    Syganek

    Joined:
    Sep 11, 2013
    Posts:
    85
    If you use my method and do it like this:

    Code (CSharp):
    1. class Array{}
    2.  
    3. class Array<T> : Array {}
    4.  
    5. [CustomPropertyDrawer(typeof(Array), true)]
    6.  
    7. ArrayDrawer : PropertyDrawer {}
    8.  
    Then, with a bit of help from reflections, you can create one property drawer for all types of Array.
     
    nirvanajie, phobos2077 and jvo3dc like this.
  12. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    That is an interesting approach. It won't work out for this specific Array class, because it needs type specific handling, but I'll check to see whether I can apply this in other places. I didn't know this was possible.

    Actually, somewhere further along the array line I have classes to handle standard types of Unity:
    ArrayObject<T> : Array<T> where T : Object (also implements an interface to prevent circular references.)
    ArrayComponent<T> : ArrayObject<T> where T : Component
    ArrayAudioSource : ArrayComponent<AudioSource>

    It would be nice to be able to handle ArrayAudioSource with a PropertyDrawer that is general for ArrayComponent<T>, because in this case there is nothing type specific beyond Component. That won't be possible however, because ArrayComponent<T> does have to inherit from Array<T> in some way.

    I might split up Array into type specific variants for things like Array<int> and for object references like Array<AudioSource> that can have a single PropertyDrawer.
     
    Last edited: Feb 28, 2018
  13. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    I'm actually running into a scenario where this is very useful, but it does not seem to work. The custom property drawer is simply not called for the children. It's called for the base class, but not for the child class.

    Both the base class and the generic class are serializable and the fields are too.

    Edit: If I make a non generic subclass of the base class, it does work. So this way also simply doesn't work for generic classes.
     
    Last edited: Apr 20, 2018
  14. Syganek

    Syganek

    Joined:
    Sep 11, 2013
    Posts:
    85
    Hi, sorry for late answer.

    Yes, this will only work if you have class derived from the genric one. It doesn't work for generic class probably because of the same reasons why generic classes are not serialized.

    My solution just allows to have one Property Drawer for all of these classes. But you still need to create them manually i.e. my example with IntGenericClass.

    So if you wanted to create ArrayDrawer for all Array<T> types you can do this. You'll need to use reflections to get the type of T inside your drawer. But if you'll work through it then you can have one ArrayDrawer for all classes that derive from Array<T> e.g: IntArray : Array<int> or FloatArray : Array<float>.
     
  15. Danielpunct

    Danielpunct

    Joined:
    May 9, 2013
    Posts:
    16
    I too benefited from the answers here, and I thank you.
    My follow up question is: could anyone tell how is the inspector drawer made in the case of List<> type ? I am guessing it is more low level than we would have acces from project scripts ?
     
    Whatever560 likes this.
  16. Whatever560

    Whatever560

    Joined:
    Jan 5, 2016
    Posts:
    543
  17. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    @Danielpunct and @Whatever560 I believe they hardcoded support for List<T>, and pretend it's (for the most part) an array during serialization. Despite not really knowing the full details, I know they did NOT support true generics at the time.

    However, YAY UNITY 2020.1.0a18!!!!!!!!!!!!
    Unity 2020.1 supports generic serialization, without needing to derive a concrete child type. For example, I can use
    Blah<float>
    and it serializes just fine now. :)


    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. [Serializable]
    5. public class Blah<T> {
    6.     [SerializeField] private T value;
    7. }
    8.  
    9. //Example in-use:
    10. public class ExampleMonoBehaviour : MonoBehaviour {
    11.     [SerializeField] private Blah<float> someFloat;
    12. }
    Really, thank you Unity team.. makes me on the verge of tears (due to happiness).
     
    Last edited: Jan 20, 2020
    fnnbrr, marcospgp, bsergent and 9 others like this.
  18. MialeeMialee

    MialeeMialee

    Joined:
    Feb 12, 2019
    Posts:
    8
    This is indeed worth updating for, I hope the feature will be more or less stable, 2019 and the serialize ref feature had some issues for when i first tried them
     
  19. Araj

    Araj

    Joined:
    Jan 3, 2013
    Posts:
    27
    So that means that we can create a CustomPropertyDrawer for generic types? Like a Dictionary<,> or a List<>?
     
    jeffersonrcgouveia likes this.
  20. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    If we were only as lucky. After nearly 8 years, Unity still hasn't solved this.

    Here's what I've tried:
    Code (CSharp):
    1. [System.Serializable]
    2. public class MyGenericClass<T>
    3. {
    4.     public T someField;
    5. }
    6.  
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class MyClass : MonoBehaviour
    4. {
    5.     public MyGenericClass<string> stringClass;
    6.     public MyGenericClass<int> intClass;
    7.     public MyGenericClass<float> floatClass;
    8. }
    This is how it looks in the inspector:
    upload_2020-11-4_12-6-51.png
    Now, that's what Unity 2020 enabled us to do. You don't have to create a class that derives from MyGenericClass<string> and use that as a variable type. You can use a generic class out of the box.
    That's cool, but, in our case, still useless without a property drawer.

    If we create a custom property drawer, we get an error.
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEngine;
    5.  
    6. [CustomPropertyDrawer(typeof(MyGenericClass<string>))]
    7. public class MyPropertyDrawer<TKey> : PropertyDrawer
    8. {
    9.     private MyGenericClass<TKey> reference;
    10.  
    11.     protected static readonly Dictionary<Type, Func<Rect, object, object>> fields =
    12.         new Dictionary<Type, Func<Rect, object, object>>()
    13.         {
    14.                 { typeof(int), (rect, value) => EditorGUI.IntField(rect, (int)value) },
    15.                 { typeof(float), (rect, value) => EditorGUI.FloatField(rect, (float)value) },
    16.                 { typeof(string), (rect, value) => EditorGUI.TextField(rect, (string)value) },
    17.         };
    18.  
    19.     protected static T DoField<T>(Rect rect, Type type, T value)
    20.     {
    21.         if (fields.TryGetValue(type, out Func<Rect, object, object> field))
    22.         {
    23.             return (T)field(rect, value);
    24.         }
    25.  
    26.         Debug.LogError("Type not supported: " + type);
    27.         return value;
    28.     }
    29.  
    30.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    31.     {
    32.         if(reference == null)
    33.         {
    34.             reference = (MyGenericClass<TKey>)InspectorUtil.GetTargetObjectOfProperty(property);
    35.         }
    36.  
    37.         reference.someField = DoField(position, typeof(TKey), reference.someField);
    38.     }
    39. }
    40.  
    upload_2020-11-4_12-11-10.png

    This is what works:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEngine;
    5.  
    6. [CustomPropertyDrawer(typeof(MyGenericClass<string>))]
    7. public class ThisIsStupid : MyPropertyDrawer<string> { }
    8.  
    9. public class MyPropertyDrawer<TKey> : PropertyDrawer
    10. {
    11.     private MyGenericClass<TKey> reference;
    12.     ...
    This is what it looks like in the editor:
    upload_2020-11-4_12-13-24.png

    Why this hasn't been addressed in almost a decade is beyond me.
     
  21. oparaskos

    oparaskos

    Joined:
    Jan 1, 2020
    Posts:
    3
    How are you defining `InspectorUtil.GetTargetObjectOfProperty` I don't see it anywhere provided by unity.
     
    mike_kon likes this.
  22. TomNCatz

    TomNCatz

    Joined:
    Jan 6, 2015
    Posts:
    24
    you can also do this now

    Code (CSharp):
    1.  
    2.     [CustomPropertyDrawer(typeof(CollectionItemGeneric<>))]
     
  23. c8theino

    c8theino

    Joined:
    Jun 1, 2019
    Posts:
    20
    For some reason that does not work in Unity 2020.3, but if you set "useForChildren" to true it works flawlessly.
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(CollectionItemGeneric<>), true)]
     
  24. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Sorry for necroing this thread, but I'm having some difficulty implementing this..

    I can create a [CustomPropertyDrawer(typeof(NotifyValueChanged<>), true)] but then when I try to create a PropertyField for the underlying Object, this is still failing.
    Anyone who's tried this already?

    My (stripped down) class is:

    Code (CSharp):
    1. [System.Serializable]
    2. public class NotifyValueChanged<T>
    3. {
    4.     public T Value => value;
    5.  
    6.     [SerializeField]
    7.     protected T value;
    8. }
    With the basic Drawer:

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(NotifyValueChanged<>), true)]
    2. public class NotifyValueChangedPropertyDrawer : PropertyDrawer
    3. {
    4.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    5.     {
    6.         SerializedProperty prop = property.FindPropertyRelative("value");
    7.         EditorGUILayout.PropertyField(prop, label);
    8.     }
    9. }
    I can get the SerializedProperty for value (prop is filled), but attempting to draw it throws errors about it not being able to find any valid type? (I'm using NotifyValueChanged<int> to debug)
     
  25. arsenal4567

    arsenal4567

    Joined:
    Jun 11, 2016
    Posts:
    9
    Did you try using EditorGUI.PropertyField(prop,label) instead?
     
    SF_FrankvHoof likes this.
  26. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    This does seem to have resolved it. Thanks.

    One final question that sprung up:

    Is there a way to determine the order/flow in which (custom) propertydrawers are executed?

    I make use of a custom ReadOnlyAttribute that will add a DisabledGroupScope around any object with the attribute using a custom PropertyDrawer (see below):
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
    2. public class ReadOnlyPropertyDrawer : PropertyDrawer
    3. {
    4.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    5.     {
    6.         using (var scope = new EditorGUI.DisabledGroupScope(true))
    7.              EditorGUI.PropertyField(position, property, label, true);
    8.      }
    9.      
    10.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    11.     {
    12.          return EditorGUI.GetPropertyHeight(property, label, true);
    13.     }
    14. }
    However, when I add a ReadOnlyAttribute to one of my NotifyValueChanged-Properties, the Editor ignores my custom PropertyDrawer and draws using the 'original' one (thereby adding a foldout):

    upload_2022-12-12_10-4-42.png
     
  27. MonolithBR

    MonolithBR

    Joined:
    Apr 11, 2022
    Posts:
    8
    Hello. I was looking for how to serialize generic classes too and I stumbled on a workaround However, it works only on the condition that the generic argument must derive from a base type which is serializable.

    Works for UnityEngine.Object, probably won't for System.Object though, so you can make it with any MonoBehaviour / ScriptableObject derived class and with any asset I know of, but no such luck with basic types like int, float, string, enums, etc.

    I do have a CharacterAttribute class that does work with the basic types and some structures unity uses a lot, but not with Unity Objects. Both follow similar ideas : Make a non-generic class and store the type with a string and the value using something you can serialize (This CharacterAttribute here uses a string for the value. The generic for Unity Objects workaround uses, well, UnityEngine.Object), then deserialize it in the drawer.

    I know, parsing strings in property drawers is not exactly what you'd call good coding, but it's what I've got working. Have not tried making it ReadOnly, but pic below confirms it can almost double for a value type dictionary.

    upload_2022-12-27_17-9-3.png

    Here is code for CharacterAttribute. I'm using my own utils lib to Parse the strings, but parsing int float and vector4 is nothing a quick google search won't reveal how to do.

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4.  
    5. namespace Monolith.CharacterSystem
    6. {
    7.     #region EDITOR
    8. #if UNITY_EDITOR
    9.     using UnityEditor;
    10.     using Monolith.Utils.Parse;
    11.     [CustomPropertyDrawer(typeof(CharacterAttribute))]
    12.     public sealed class CharacterAttributeDrawer : PropertyDrawer
    13.     {
    14.         Rect left;
    15.         Rect middle;
    16.         Rect right;
    17.         SerializedProperty value;
    18.         private delegate T Fielder<T>(Rect a, T value);
    19.         private void ParseField<T>(Parser<T> parser, Fielder<T> fielder, float height = 18f)
    20.         {
    21.             right.height = height;
    22.             if (parser.Invoke(value.stringValue, out T parsedValue))
    23.                 value.stringValue = fielder.Invoke(right, parsedValue).ToString();
    24.             else
    25.                 value.stringValue = fielder.Invoke(right, default).ToString();
    26.         }
    27.         public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    28.         {
    29.             var type = property.FindPropertyRelative("m_Type");
    30.             var caType = (CharacterAttribute.AttributeType)type.enumValueIndex;
    31.             return caType switch
    32.             {
    33.                 CharacterAttribute.AttributeType.Rect => 40f,
    34.                 _ => 20f
    35.             };
    36.         }
    37.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    38.         {
    39.             left = position;
    40.             middle = position;
    41.             right = position;
    42.             left.height = 18f;
    43.             middle.height = 18f;
    44.             left.width = position.width * 0.25f - 4;
    45.             middle.width = position.width * 0.25f - 4;
    46.             right.width = position.width * 0.5f - 4;
    47.             middle.x = position.x + left.width + 6;
    48.             right.x = position.x + middle.width + left.width + 12;
    49.             var name = property.FindPropertyRelative("m_Name");
    50.             name.stringValue = EditorGUI.TextField(left, name.stringValue);
    51.             var type = property.FindPropertyRelative("m_Type");
    52.             var caType = (CharacterAttribute.AttributeType)EditorGUI.EnumPopup(middle, (CharacterAttribute.AttributeType)type.enumValueIndex);
    53.             type.enumValueIndex = (int)caType;
    54.             value = property.FindPropertyRelative("m_StringValue");
    55.             switch (caType)
    56.             {
    57.                 case CharacterAttribute.AttributeType.Int:
    58.                     ParseField<int>(Parse.TryParseInt, EditorGUI.IntField);
    59.                     break;
    60.                 case CharacterAttribute.AttributeType.Long:
    61.                     ParseField<long>(Parse.TryParseLong, EditorGUI.LongField);
    62.                     break;
    63.                 case CharacterAttribute.AttributeType.Float:
    64.                     ParseField<float>(Parse.TryParseFloat, EditorGUI.FloatField);
    65.                     break;
    66.                 case CharacterAttribute.AttributeType.Double:
    67.                     ParseField<double>(Parse.TryParseDouble, EditorGUI.DoubleField);
    68.                     break;
    69.                 case CharacterAttribute.AttributeType.Rect:
    70.                     ParseField<Rect>(Parse.TryParseRect, EditorGUI.RectField, 38f);
    71.                     break;
    72.                 case CharacterAttribute.AttributeType.Vector2:
    73.                     ParseField<Vector2>(Parse.TryParseVector2, (pos, val) => EditorGUI.Vector2Field(pos, "", val));
    74.                     break;
    75.                 case CharacterAttribute.AttributeType.Vector2Int:
    76.                     ParseField<Vector2Int>(Parse.TryParseVector2Int, (pos, val) => EditorGUI.Vector2IntField(pos, "", val));
    77.                     break;
    78.                 case CharacterAttribute.AttributeType.Vector3:
    79.                     ParseField<Vector3>(Parse.TryParseVector3, (pos, val) => EditorGUI.Vector3Field(pos, "", val));
    80.                     break;
    81.                 case CharacterAttribute.AttributeType.Vector3Int:
    82.                     ParseField<Vector3Int>(Parse.TryParseVector3Int, (pos, val) => EditorGUI.Vector3IntField(pos, "", val));
    83.                     break;
    84.                 case CharacterAttribute.AttributeType.Vector4:
    85.                     ParseField<Vector4>(Parse.TryParseVector4, (pos, val) => EditorGUI.Vector4Field(pos, "", val));
    86.                     break;
    87.                 case CharacterAttribute.AttributeType.Color:
    88.                     ParseField<Color>(Parse.TryParseColor, (pos, val) => EditorGUI.ColorField(pos, "", val));
    89.                     break;
    90.                 default:
    91.                     right.height = 18f;
    92.                     value.stringValue = EditorGUI.TextField(right, value.stringValue);
    93.                     break;
    94.             }
    95.         }
    96.     }
    97. #endif
    98.     #endregion
    99.  
    100.     /// <summary>
    101.     /// This class is used to store data inherent to the individual character but not to any particular module.
    102.     /// It is optimized for data that can change, but doesn't often change. (I.e. fast get, not-so-fast set,
    103.     /// not easy on memory, having 2 <see cref="string"/>, 1 <see cref="byte"/>, 1 <see cref="long"/>, 1 <see cref="double"/>,
    104.     /// 1 <see cref="Vector4"/> and 1 <see cref="Vector3Int"/> allocated for each instance.
    105.     /// This is more of a 'just in case it's needed' thing, and may help overcome any current limitations of this system. This is
    106.     /// also a way of standardizing data to allow for things such as interchangable AI.
    107.     /// <para></para>
    108.     /// Also, I want you to understand that this thing will be a lot slower in editor than it will in runtime, because the property
    109.     /// drawer for this thing is parsing the string value every time the inspector gui updates.
    110.     /// </summary>
    111.     [Serializable]
    112.     public sealed class CharacterAttribute
    113.     {
    114.         public enum AttributeType : byte
    115.         {
    116.             Int,
    117.             Long,
    118.             Float,
    119.             Double,
    120.             Rect,
    121.             Vector2,
    122.             Vector2Int,
    123.             Vector3,
    124.             Vector3Int,
    125.             Vector4,
    126.             Color,
    127.             String
    128.         }
    129.         #region Constructors
    130.         CharacterAttribute(string name) { this.m_Name = name; }
    131.         public CharacterAttribute(string name, int value) : this(name)
    132.         {
    133.             m_StringValue = value.ToString();
    134.             m_LongValue = value;
    135.             m_Type = AttributeType.Int;
    136.         }
    137.         public CharacterAttribute(string name, long value) : this(name)
    138.         {
    139.             m_StringValue = value.ToString();
    140.             m_LongValue = value;
    141.             m_Type = AttributeType.Long;
    142.         }
    143.         public CharacterAttribute(string name, float value) : this(name)
    144.         {
    145.             m_StringValue = value.ToString();
    146.             m_DoubleValue = value;
    147.             m_Type = AttributeType.Float;
    148.         }
    149.         public CharacterAttribute(string name, double value) : this(name)
    150.         {
    151.             m_StringValue = value.ToString();
    152.             m_DoubleValue = value;
    153.             m_Type = AttributeType.Double;
    154.         }
    155.         public CharacterAttribute(string name, Rect value) : this(name)
    156.         {
    157.             m_StringValue = value.ToString();
    158.             m_RectValue = value;
    159.             m_Type = AttributeType.Rect;
    160.         }
    161.         public CharacterAttribute(string name, Vector2 value) : this(name)
    162.         {
    163.             m_StringValue = value.ToString();
    164.             m_VectorValue = value;
    165.             m_Type = AttributeType.Vector2;
    166.         }
    167.         public CharacterAttribute(string name, Vector2Int value) : this(name)
    168.         {
    169.             m_StringValue = value.ToString();
    170.             m_VectorIntValue = (Vector3Int)value;
    171.             m_Type = AttributeType.Vector2Int;
    172.         }
    173.         public CharacterAttribute(string name, Vector3 value) : this(name)
    174.         {
    175.             m_StringValue = value.ToString();
    176.             m_VectorValue = value;
    177.             m_Type = AttributeType.Vector3;
    178.         }
    179.         public CharacterAttribute(string name, Vector3Int value) : this(name)
    180.         {
    181.             m_StringValue = value.ToString();
    182.             m_VectorIntValue = value;
    183.             m_Type = AttributeType.Vector3Int;
    184.         }
    185.         public CharacterAttribute(string name, Vector4 value) : this(name)
    186.         {
    187.             m_StringValue = value.ToString();
    188.             m_VectorValue = value;
    189.             m_Type = AttributeType.Vector4;
    190.         }
    191.         public CharacterAttribute(string name, Color value) : this(name)
    192.         {
    193.             m_StringValue = value.ToString();
    194.             m_VectorValue = value;
    195.             m_Type = AttributeType.Color;
    196.         }
    197.         public CharacterAttribute(string name, string value) : this(name)
    198.         {
    199.             m_StringValue = value;
    200.             m_Type = AttributeType.String;
    201.         }
    202.         #endregion
    203.         #region Properties
    204.         public string Name { get => m_Name; }
    205.         public AttributeType Type { get => m_Type; }
    206.         public string StringValue { get => m_StringValue; set => m_StringValue = value; }
    207.         public int IntValue
    208.         {
    209.             get => (int)m_LongValue;
    210.             set
    211.             {
    212.                 m_LongValue = value;
    213.                 m_StringValue = value.ToString();
    214.             }
    215.         }
    216.         public long LongValue
    217.         {
    218.             get => m_LongValue;
    219.             set
    220.             {
    221.                 m_LongValue = value;
    222.                 m_StringValue = value.ToString();
    223.             }
    224.         }
    225.         public float FloatValue
    226.         {
    227.             get => (float)m_DoubleValue;
    228.             set
    229.             {
    230.                 m_DoubleValue = value;
    231.                 m_StringValue = value.ToString();
    232.             }
    233.         }
    234.         public double DoubleValue
    235.         {
    236.             get => m_DoubleValue;
    237.             set
    238.             {
    239.                 m_DoubleValue = value;
    240.                 m_StringValue = value.ToString();
    241.             }
    242.         }
    243.         public Rect RectValue
    244.         {
    245.             get => m_RectValue;
    246.             set
    247.             {
    248.                 m_RectValue = value;
    249.                 m_StringValue = value.ToString();
    250.             }
    251.         }
    252.         public Vector2 Vector2Value
    253.         {
    254.             get => m_VectorValue;
    255.             set
    256.             {
    257.                 m_VectorValue = value;
    258.                 m_StringValue = value.ToString();
    259.             }
    260.         }
    261.         public Vector2Int Vector2IntValue
    262.         {
    263.             get => (Vector2Int)m_VectorIntValue;
    264.             set
    265.             {
    266.                 m_VectorIntValue = (Vector3Int)value;
    267.                 m_StringValue = value.ToString();
    268.             }
    269.         }
    270.         public Vector3 Vector3Value
    271.         {
    272.             get => m_VectorValue;
    273.             set
    274.             {
    275.                 m_VectorValue = value;
    276.                 m_StringValue = value.ToString();
    277.             }
    278.         }
    279.         public Vector3Int Vector3IntValue
    280.         {
    281.             get => m_VectorIntValue;
    282.             set
    283.             {
    284.                 m_VectorIntValue = value;
    285.                 m_StringValue = value.ToString();
    286.             }
    287.         }
    288.         public Vector4 Vector4Value
    289.         {
    290.             get => m_VectorValue;
    291.             set
    292.             {
    293.                 m_VectorValue = value;
    294.                 m_StringValue = value.ToString();
    295.             }
    296.         }
    297.         public Color ColorValue
    298.         {
    299.             get => m_VectorValue;
    300.             set
    301.             {
    302.                 m_VectorValue = value;
    303.                 m_StringValue = value.ToString();
    304.             }
    305.         }
    306.         #endregion
    307.         #region Fields
    308.         [SerializeField]
    309.         string m_Name;
    310.         [SerializeField]
    311.         AttributeType m_Type;
    312.         [SerializeField]
    313.         string m_StringValue; // This is always used and updated, as it holds the unparsed value which is the only one serialized.
    314.         long m_LongValue; // Int value stored as long.
    315.         double m_DoubleValue; // Float value stored as double.
    316.         Rect m_RectValue;
    317.         Vector4 m_VectorValue; // Vector2, Vector3 and Color stored as Vector4.
    318.         Vector3Int m_VectorIntValue; // Vector2Int stored as Vector3Int.
    319.         #endregion
    320.     }
    321. }
    322.  
    Yes I put the drawer and the class in the same file, I got too lazy to move it to the editor folder after making that atrocity of a drawer.

    And here is the workaround for my Loot class which works just fine for Objects.


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [System.Serializable]
    4. public class Loot<T> : Loot where T : Object
    5. {
    6.     protected Loot() : base()
    7.     {
    8.         m_TypeString = typeof(T).AssemblyQualifiedName;
    9.     }
    10.  
    11.     public new T Collapse()
    12.     {
    13.         float choice = Random.Range(0, 1);
    14.         for (int index = 0; index < m_Odds.Length; index++)
    15.         {
    16.             choice -= m_Odds[index];
    17.             if (choice > 0) continue;
    18.             return (T)m_Loot[index];
    19.         }
    20.         return null;
    21.     }
    22. }
    23.  
    24. [System.Serializable]
    25. public class Loot
    26. {
    27.     protected Loot()
    28.     {
    29.         m_Odds = new float[0];
    30.         m_Loot = new Object[0];
    31.         m_TypeString = typeof(Object).AssemblyQualifiedName;
    32.     }
    33.     [SerializeField] protected string m_TypeString;
    34.     [SerializeField] protected float[] m_Odds;
    35.     [SerializeField] protected Object[] m_Loot;
    36.     public Object Collapse() { return null; }
    37. }
    And the drawer

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [CustomPropertyDrawer(typeof(Loot), true)]
    5. public class LootDrawer : PropertyDrawer
    6. {
    7.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    8.     {
    9.         m_OddsProperty = property.FindPropertyRelative("m_Odds");
    10.         m_LootProperty = property.FindPropertyRelative("m_Loot");
    11.         m_TypeString = property.FindPropertyRelative("m_TypeString").stringValue;
    12.         if (string.IsNullOrWhiteSpace(m_TypeString))
    13.             m_TypeString = typeof(Object).AssemblyQualifiedName;
    14.  
    15.         if (m_OddsProperty.arraySize == 0)
    16.         {
    17.             m_OddsProperty.InsertArrayElementAtIndex(0);
    18.             m_LootProperty.InsertArrayElementAtIndex(0);
    19.         }
    20.  
    21.         var odds0Property = m_OddsProperty.GetArrayElementAtIndex(0);
    22.         var loot0Property = m_LootProperty.GetArrayElementAtIndex(0);
    23.  
    24.         Rect position1 = new(position.x, position.y, position.width / 2 - 2, position.height);
    25.         Rect position2 = new(position.x + position1.width + 4, position.y, position.width / 2 - 2, position.height);
    26.  
    27.         odds0Property.floatValue = EditorGUI.FloatField(position1, odds0Property.floatValue);
    28.         loot0Property.objectReferenceValue = EditorGUI.ObjectField(position2, new GUIContent(), loot0Property.objectReferenceValue, System.Type.GetType(m_TypeString), true);
    29.     }
    30.     private void DrawLootIndex(Rect position, int index)
    31.     {
    32.  
    33.     }
    34.  
    35.     SerializedProperty m_OddsProperty;
    36.     SerializedProperty m_LootProperty;
    37.     string m_TypeString;
    38. }
    Obviously incomplete, but it gets the how-to-do across. The only problem here is that this generates an issue with this generates a bottomless rabbit hole for methods. The non-generic should only have the serialized stuff and should basically never be used. The Loot I mean, CharacterAttribute doesn't have generics and works fine as is.

    And with these two you should be good. I mean, I can't see myself ever not knowing if something I want to serialize is a base type or a whole MonoBehaviour.

    And if you do, add the Loot get-the-type-and-make-a-filtered-object-field-in-drawer logic into the CharacterAttribute and add Object to the AttributeType enum. I won't because of context (A Character should not have a whole object as an attribute, it just doesn't make sense.)

    TL;DR:

    [SerializeField] protected string m_TypeString;


    m_TypeString = typeof(Object).AssemblyQualifiedName;


    m_TypeString = property.FindPropertyRelative("m_TypeString").stringValue;
    if (string.IsNullOrWhiteSpace(m_TypeString))
    m_TypeString = typeof(Object).AssemblyQualifiedName;


    System.Type.GetType(m_TypeString)
     
    SF_FrankvHoof and ModLunar like this.
  28. Jacemon

    Jacemon

    Joined:
    Aug 2, 2021
    Posts:
    1
    Hi guys, can you tell me in a similar topic. Very similar to the SF_FrankvHoof situation.
    I have several ScriptableObject heirs classes (this is important):
    And apparently it cannot properly serialize ScriptableObject heirs. Therefore, when drawing using PropertyDrawer, when trying to find any property, it returns null.
    Below I have given a few of my classes, maybe I'm wrong after all? I would appreciate any answer.

    Code (CSharp):
    1. [Serializable]
    2.     public abstract class ValueReference<T> : ScriptableObject
    3.     {
    4.         [SerializeField]
    5.         private T value;
    6.  
    7.         public Action onValueChanged;
    8.      
    9.         public T Value
    10.         {
    11.             get => value;
    12.             set
    13.             {
    14.                 this.value = value;
    15.                 onValueChanged?.Invoke();
    16.             }
    17.         }
    18.     }
    Code (CSharp):
    1. [CreateAssetMenu(fileName = "IntReference", menuName = "Custom/Reference/Int Reference")]
    2.     [Serializable]
    3.     public class IntReference : ValueReference<int> { }
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(ValueReference<>), true)]
    2.     public class ValueReferenceDrawer : PropertyDrawer
    3.     {
    4.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    5.         {
    6.             EditorGUI.BeginProperty(position, label, property);
    7.             position = EditorGUI.PrefixLabel(position, label);
    8.          
    9.             var valueProperty = property.FindPropertyRelative("value"); // null
    10.          
    11.             EditorGUI.PropertyField(position, valueProperty, label, true); // NullReferenceException
    12.          
    13.             EditorGUI.EndProperty();
    14.         }
    15.     }
     
    Last edited: Mar 21, 2023
  29. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Actually, I got your example to work, with some tweaking!

    The main thing is that because your type inherits from UnityEngine.Object (because it inherits from ScriptableObject), it's actually not serialized inline. Instead, it's serialized as a Unity object reference, so I actually found creating a `new SerializedObject(...)` on your ValueReference object works.

    You can mostly ignore the fact that I also made a non-generic ValueReference base class, I thought I might've needed it but turns out I didn't with the code snippet given so far:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. using Object = UnityEngine.Object;
    6.  
    7. [Serializable]
    8. [CreateAssetMenu(fileName = "Int Reference", menuName = "Custom/Reference/Int Reference")]
    9. public class IntReference : ValueReference<int> { }
    10.  
    11. [Serializable]
    12. public abstract class ValueReference : ScriptableObject {
    13.     public event Action onValueChanged;
    14.  
    15.     public abstract object BaseValue { get; }
    16.     protected void RaiseEvent() {
    17.         onValueChanged?.Invoke();
    18.     }
    19. }
    20.  
    21. [Serializable]
    22. public abstract class ValueReference<T> : ValueReference {
    23.     [SerializeField] internal T value;
    24.  
    25.     public override object BaseValue => Value;
    26.     public T Value {
    27.         get => value;
    28.         set {
    29.             this.value = value;
    30.             RaiseEvent();
    31.         }
    32.     }
    33. }
    34.  
    35. [CustomPropertyDrawer(typeof(ValueReference<>), true)]
    36. public class ValueReferenceDrawer : PropertyDrawer {
    37.     private SerializedObject serializedValueRef;
    38.  
    39.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
    40.         Object valueRef = property.objectReferenceValue;
    41.  
    42.         if (serializedValueRef != null && serializedValueRef.targetObject != valueRef) {
    43.             serializedValueRef.Dispose();
    44.             serializedValueRef = null;
    45.         }
    46.         if (serializedValueRef == null)
    47.             serializedValueRef = new SerializedObject(valueRef);
    48.  
    49.         try {
    50.             serializedValueRef.Update();
    51.  
    52.             SerializedProperty valueProperty = serializedValueRef.FindProperty(nameof(ValueReference<object>.value));
    53.             Rect currentRect = EditorGUI.PrefixLabel(position, label);
    54.             EditorGUI.PropertyField(currentRect, valueProperty, label, true);
    55.  
    56.             serializedValueRef.ApplyModifiedProperties();
    57.         } catch (Exception e) {
    58.             Debug.LogException(e);
    59.             EditorGUI.PropertyField(position, property, label);
    60.         }
    61.     }
    62. }
    63.  
    I tested this on Unity 2023.1.0a7, living life on the edge haha.
    But it should work in at least 2021.3 LTS I'm guessing.

    The try-catch exception handling will let Unity draw the default GUI if something in our code fails.
     
  30. Aldeminor

    Aldeminor

    Joined:
    May 3, 2014
    Posts:
    18
    This wouldn't work because of
    Code (CSharp):
    1. private SerializedObject serializedValueRef;
    Since PropertyDrawer object is created once per type, your variable is essentially `global` and will be shared between all currently drawn properties, breaking everything if there is more than one property on screen (accounting for all Inspector windows and displayed components with the property of given type).