Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

How to display derived class fields if base has custom editor?

Discussion in 'Scripting' started by Desprez, Jun 20, 2022.

  1. Desprez

    Desprez

    Joined:
    Aug 31, 2012
    Posts:
    305
    Is it possible to to display derived class fields if the base class has a custom editor without having to make a custom editor for every derived class?

    The issue is that many derived classes might be relatively simple, and a custom editor is completely unnecessary, and there may be a lot of them.

    Hopefully there is a way to have the base class editor just tack on the default editor display of the derived class?
     
  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,541
    Sure, however the editor has to be written in a way so it handles all properties. A customInspector can be set to be used for derived classes (unless a derived class has its own specific one). The CustomEditor attribute has a second boolean parameter. When it is set to true the editor is also used for any derived classes. Of course that editor has to handle all SerializedProperties. So when written correctly this should not be an issue

    Though it should be said that in most of the cases you don't need a custom inspector. Some Property drawers may already be enough.

    However without knowing what your custom editor is about and what the script / fields look like we can't really offer any further help here.
     
  3. Desprez

    Desprez

    Joined:
    Aug 31, 2012
    Posts:
    305
    Yes, I've done that. The derived class uses the editor and displays the base class fields, but does not show it's own fields.

    I've attempted to grab member fields after a certain amount (20) since that's how many are in the base class, the idea being any over that amount would be from a derived class. This has failed because I can't seem to reference the derived class fields without knowing the derived class - and this won't be possible to know in advance.

    I could attempt to override a method that supplies relevant info, but this requires all derived classes have the override method and this is less than ideal.

    A property drawer is an idea. I'd have to place everything in a wrapper in the base class. But I think that approach could work.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Reflection;
    4. using System.Collections.Generic;
    5.  
    6. [CustomEditor( typeof( MF_AbstractAura ), true )]
    7.     [CanEditMultipleObjects]
    8.     public class MF_Aura_Editor : Editor {
    9.  
    10.         // declare various SerializedProperties
    11.         // [...]
    12.  
    13.         MF_AbstractAura script;
    14.         List<SerializedProperty> sp;
    15.  
    16.         void OnEnable () {
    17.  
    18.             // find various serializedProperties
    19.             // [...]
    20.  
    21.             script = (MF_AbstractAura)target; // <- this looks to be the problem: how to grab reference to an arbitrary derived class?
    22.             sp = new List<SerializedProperty>();
    23.             MemberInfo[] mi = script.GetType().GetFields(); // only finds member fields of base class
    24.             for ( int i = 20; i < mi.Length; i++ ) { // start at 20 to skip base class fields, trying to grab only derived fields
    25.                 if ( mi[i].IsDefined( typeof( HideInInspector ) ) == false ) { // skip fields with [HideInInspector]
    26.                     sp.Add( serializedObject.FindProperty( mi[i].Name ) );
    27.                 }
    28.             }
    29.         }
    30.  
    31.         public override void OnInspectorGUI () {
    32.             serializedObject.Update();
    33.  
    34.             // layout for base class fields
    35.             // [...]
    36.        
    37.             // display all found member fields
    38.             for ( int i = 0; i < sp.Count; i++ ) {
    39.                 EditorGUILayout.PropertyField( sp[i] );
    40.             }
    41.  
    42.             serializedObject.ApplyModifiedProperties();
    43.         }
    44.     }
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,541
    Well, first of all you shouldn't mix the old "target" access with the much more flexible SerializedObject / SerializedProperty approach. When using target you're loosing multi object editing and have to take care of everything yourself. The SerializedObject takes care of creating undo entries, allows editing multiple objects at the same time,allows you to actually iterate through all fields and decorators and property drawers are applied automatically when PropertyField is used. That's what the default inspector does.

    Unity has a (still) internal method that looks like this:

    Code (CSharp):
    1. protected internal static void DrawPropertiesExcluding(SerializedObject obj, params string[] propertyToExclude)
    2. {
    3.     SerializedProperty iterator = obj.GetIterator();
    4.     bool enterChildren = true;
    5.     while (iterator.NextVisible(enterChildren))
    6.     {
    7.         enterChildren = false;
    8.         if (!propertyToExclude.Contains(iterator.name))
    9.         {
    10.             EditorGUILayout.PropertyField(iterator, true);
    11.         }
    12.     }
    13. }
    It is intended to draw all remaining properties. You just provide the properties you already handled yourself. Od course since it's internal we can't use it directly. However as you can see, it's quite simple.

    You haven't really shown what you do in your actual "custom" part. Mixing target / targets access with SerializedObject / SerializedProperty is tricky and can cause all sorts of issues if not done correctly. You can't really interleave them at all. The SerializedObject, when created or Update is called on it would grab the current serialized data as a snapshot and copies that data into it's own structure (a bunch of SerializedProperties). You can not change / modify those properties and when done call ApplyModifiedProperties to actually apply those changes. However that means any direct change you made between SO.Update and SO.ApplyModifiedProperties won't have any effect. So it's best to avoid messing with the direct target / targets reference(s).

    Finally I'm not sure if you are aware of the fact that the "GetType" method is polymorphic. So
    script.GetType().GetFields()
    does not give you the fields of the base class, but the public fields of the actual class. This reflection approach has countless issues. First of all, this would iterate through all public fields, serialized or not. You're looking for HideInInspector but that's just the tip of the iceberg. What about private fields that are marked with SerializeField? At a derived class you don't get direct access via reflection to inherited private fields so you would have to walk the inheritance chain manually. If you wanted to access the System.Type object of the base class, you should have done
    typeof(MF_AbstractAura)
    .

    Anyways, we still don't know what the custom editor does or how. So it's hard to tell which solution would actually work for you.
     
    dlorre and Desprez like this.
  5. Desprez

    Desprez

    Joined:
    Aug 31, 2012
    Posts:
    305
    Yes, I discovered this on accident not too long ago. Or, rather, surmised that's what was happening behind the scenes after much hair pulling on a different problem.

    Still, for some things, using target has some uses for making comparisons within complex structures, as long as the referenced fields aren't being changed.

    This is exactly what I've been looking for. Interestingly, google searches aren't finding it.

    Except in my case, "script" was a direct reference to the base class and would not find derived fields.

    Everything works now, though I had to figure out how to exclude the script name field as well (it's "m_Script")
    Thanks for the help!
     
    dlorre likes this.
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,541
    This makes no sense. You store the value of "target" in "script". Casting a reference to a base type does not change the type of the object itself. Again, GetType is polymorphic. You could cast the type to the root base class "object" and you still get the same System.Type object. You will not get the Type object for your base class unless the object you're inspector actually is just a base class. Casting only changes the logical type of the variable on the compiler side. The actual object stays the same.
     
  7. Desprez

    Desprez

    Joined:
    Aug 31, 2012
    Posts:
    305
    So I did another test, because initial tests looked like script.GetType().GetFields() wasn't finding any of the fields in the derived class - which led me to post in the first place.

    Turns out, derived class fields were appearing at the beginning of the array, not the end, like you'd see them laid out in the inspector.
     
  8. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    700
    Thanks for this, I was having the exact same problem today, I could solve it after reading this thread:

    Code (csharp):
    1.  
    2.     [CustomEditor(typeof(MyBase), true)]
    3.     internal class MyBaseInspector : Editor
    4.     {
    5.         public override VisualElement CreateInspectorGUI()
    6.         {
    7.             var uxmlPath = "myUXMLPath";
    8.  
    9.             var inspector = new VisualElement();
    10.  
    11.             var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
    12.             visualTree.CloneTree(inspector);
    13.  
    14.             var fieldNames = new List<string>();
    15.  
    16.             fieldNames.Add("m_Script") ;
    17.  
    18.             inspector.Query<PropertyField>().ForEach(field =>
    19.             {
    20.                 fieldNames.Add(field.bindingPath);
    21.             });
    22.  
    23.             var prop = serializedObject.GetIterator();  
    24.             while (prop.NextVisible(true))
    25.             {
    26.                 if (!fieldNames.Contains(prop.name))
    27.                 {
    28.                     inspector.Add(new PropertyField(prop));
    29.                 }
    30.             }
    31.             return inspector;
    32.         }
    33.     }
    34.  
    35.