2
\$\begingroup\$

I am making a visual tool for Editor Window on Unity3D, and I am just recently using System.Reflection methods to get class's assembly data, to get information about properties, fields and methods (with their respective parameters).

I am a big fan, and quite stubborn of making generic extension methods to "agilize" coding, so I came with the idea of making a generic method that evaluates a ParameterInfo.ParameterType to determine which EditorGUI Field drawer method utilize to store (like EnumPopup, ObjectField, IntField, Popup, etc.).

My reasoning, with the information I gathered on Microsoft's API about reflection, so far to achieve my goals are:

  • Have an array of objects to store on Editor time whatever thing the EditorGUI Field drawers returns.
  • Either a GameObject, or a Monobehaviour to get methods from it.
  • An array of ParameterInfo, either defined globally on just on the local's scope, that stores all parameter's information of the current index method.


public class RootLevelNode : BaseNode
{
    [SerializeField] private GameObject nodeGameObject;     /// Container of the Methods
    [SerializeField] private object[] arguments;            /// <summary>Arguments Matrix, where the first dimension belongs to the index of the methods, and the second dimension the array of parameters of the current method.</summary>
    [SerializeField] private int _index;                    /// Current method index [selected by EditorGui Popup]
    [SerializeField] private LevelEvents levelEvent         /// Current Level Event selected.

    public enum LevelEvents                                 /// Just a enumerator for testing purposes
    {
        Unassigned,
        LevelStarts,
        LevelDevelops,
        LevelEnds
    }

    /// <summary>Draws Node's Fields.</summary>
    public override void DrawNodeFields()
    {
        /// currentLayoutY is a BaseNode property that stores the reference of the current Layout Y's value, it serves to displace each new GUI element. That's why at the start of this method I reset it.
        currentLayoutY = 0; // Reset Layout's Y reference.

        nodeGameObject = EditorGUI.ObjectField(GetNewLayoutPosition(), NO_TEXT, nodeGameObject, typeof(GameObject), true) as GameObject; /// Receive GameObject

        if(nodeGameObject != null)
        { //If there is a Monobehaviour assigned
            ///This is a extension method that filters a list of methods by type of return, it also has a overload to extend from MonoBehviour.
            List<MethodInfo> methodsInfo = nodeGameObject.GetMethods
            (
                typeof(void),
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
            );
            string[] methodsNames = methodsInfo.Select(p => p.Name).ToArray();                      /// Get methods's names.

            index = EditorGUI.Popup(GetNewLayoutPosition(),NO_TEXT, index, methodsNames);

            ParameterInfo[] methodParameters = methodsInfo[index].GetParameters();                  /// Get current method's parameters.
            arguments = new object[methodParameters.Length];                                        /// Get array of arguments.

            for(int i = 0; i < methodParameters.Length; i++)
            {
                EditorGUI.LabelField(GetNewIndentedLayoutPosition(), methodParameters[i].Name);     /// Draw the name of the parameter. Regardless of the data type.

                if(methodParameters[i].ParameterType == typeof(GameData.LevelEvents))
                { //If the methodParameter ParameterType is of the type of LevelEvents enumerator, cast object to LevelEvents.
                    arguments[i] = (LevelEvents)EditorGUI.EnumPopup(GetNewIndentedLayoutPosition(), NO_TEXT, levelEvent);
                }
                else if(methodParameters[i].ParameterType == typeof(GameObject))
                {
                    arguments[i] = (GameObject)EditorGUI.ObjectField(GetNewLayoutPosition(), NO_TEXT, (GameObject)arguments[i], typeof(GameObject), true) as GameObject;
                }
                else if(methodParameters[i].ParameterType == typeof(string))
                {
                    arguments[i] EditorGUI.StringField(GetNewLayoutPosition(), NO_TEXT, (string)arguments[i]);
                }

            }
        }

        CheckForLayoutUpdate();             ///BaseNode's method that compares this rect's haight with the currentYLayout to determine if there is resizing to be made.
    }
}

BaseNode'sinhereted methods (just Layout functionality):

    /// <summary>Gets Layout Y's position updated each time it is called.</summary>
    /// <returns>Updated value of the Layout's Y for the new GUI element.</returns>
    public virtual float DisplaceYLayout()
    {
        return (rect.y + (currentLayoutY += fieldHeight));
    }

    /// <summary>Checks Node's rect size with current content to update height.</summary>
    public virtual void CheckForLayoutUpdate()
    {
        if(rect.height <= Mathf.Abs(rect.y - currentLayoutY))
        {
            Rect newRect = new Rect(rect.x, rect.y, rect.width, Mathf.Abs((rect.y * 2) - DisplaceYLayout()));
            rect = newRect;
        }
        else if(rect.height != originalHeight)
        {
            Rect originalRect = new Rect(rect.x, rect.y, rect.width, rect.height);
            rect = originalRect;
        }
    }

    /// <summary>Generates a new Layout position according to the current Layout status.</summary>
    /// <returns>New Layout position [Rect].</returns>
    protected virtual Rect GetNewLayoutPosition()
    {
        return new Rect
        (
            (rect.x + fieldHorizontalOffset),
            DisplaceYLayout(),
            (rect.width - (fieldHorizontalOffset * 2)),
            fieldHeight

        );
    }

    /// <summary>Generates a new Layout indented position according to the current Layout status.</summary>
    /// <returns>New Layout position indented [Rect].</returns>
    protected virtual Rect GetNewIndentedLayoutPosition()
    {
        Rect indentedRect = GetNewLayoutPosition();
        indentedRect.x += fieldHorizontalOffset;
        indentedRect.width -= fieldHorizontalOffset;

        return indentedRect;
    }

enter image description here

So far it works as I would have expected, but I would like to make a more generic method for possible upcoming node classes that I may make that do not inherit directly from this implementation's class, so I thought I could play with generic type 'T' methods constrained by System.Type and extension methods. CreatePropertyField() works as the layer of abstraction, I am at the moment trying this, since I tried with the following so far:

  • Make overload methods, each one constraining T to the required EditorGUI field draw method, but T are no variables.
  • Don't make generic types, and instead pass a parsed Type (received on abstract layer's method marameter) but there are not direct conversion from System.Type to UnityEngine.Object


/// Extension class
public static class VoidlessEditorWindow
{
    /// <summary>Abstract Layer that creates EditorGUI object Field relative to the data Type needed.</summary>
    /// <param name="_object">Object variable to evaluate.</param>
    /// <param name="_type">Type of the new EditorGUI Field.</param>
    /// <param name="_layoutPosition">Field's Layout position.</param>
    /// <param name="_label">Field's Label.</param>
    /// <returns>Most proper EditorGUI's Field [null if there is none].</returns>
    public static object CreatePropertyField<T>(this object _object, Type _type, Rect _layoutPosition, string _label) where T : System.Type
    {
        if(_type == typeof(UnityEngine.Object))
        {
            //return _object.CreateObjectField<>(_type.GetType(), _layoutPosition, _label);

            ///Should run CreateObjectField.
        }
        else if(_type == typeof(System.Enum))
        {
            //return _object.CreateEnumPopUp<T>(typeof(T), _layoutPosition, _label);

            ///Should run CreateEnumPopup
        }

        return _object;
    }

    /// <summary>Creates EditorGUI object Field.</summary>
    /// <param name="_object">Object variable to evaluate.</param>
    /// <param name="_type">Type of the new EditorGUI Field.</param>
    /// <param name="_layoutPosition">Field's Layout position.</param>
    /// <param name="_label">Field's Label.</param>
    /// <returns>Most proper EditorGUI's Field [null if there is none].</returns>
    public static object CreateObjectField<T>(this object _object, T _type, Rect _layoutPosition, string _label) where T : UnityEngine.Object
    {
        return _object = EditorGUI.ObjectField(_layoutPosition, _label, (T)_object, typeof(T), true) as T;
    }

    /// <summary>Creates EditorGUI Enum Pop-Up Field.</summary>
    /// <param name="_object">Object variable to evaluate.</param>
    /// <param name="_type">Type of the new EditorGUI Field.</param>
    /// <param name="_layoutPosition">Field's Layout position.</param>
    /// <param name="_label">Field's Label.</param>
    /// <returns>Most proper EditorGUI's Field [null if there is none].</returns>
    public static object CreateEnumPopUp(this object _object, System.Enum _type, Rect _layoutPosition, string _label)
    {
        return _object = EditorGUI.EnumPopup(_layoutPosition, _label, _type);           
    }

So, my question is, it is necessary for me to always make if-else statements eahc time I want to draw EditorGUI Type fields? or maybe there is something I am missing?

Thanks in advance for your support.

\$\endgroup\$

Your Answer

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

Browse other questions tagged or ask your own question.