0
\$\begingroup\$

I'm trying to create a complex serialized object with property drawer with assignable GameObject field. I want to make something like Binding. So that you could declare a field in an X Monobehaviour with return type of e.g. float. Then in Inspector assign source object, script and path to that binding. Then in X use that binding to get value from another object. This would help decouple code.

I'm facing a problem. I either can't create UnityEngine.Object properly, or when I wrongly tried deriving binding class from ScriptableObject, it wasn't deserializing anything. Or I'll need to create GameObject field in my PropertyDrawer (I don't really like this idea, I've seen ObjectField code).

So, I can't use EditorGUILayout.ObjectField because it needs SerializedProperty of GameObject Source on binding, but I have SerializedProperty of Binding on X. I need either derive binding object from UnityEngine.Object to be able to find property on it and deal with problem of creating that binding or go hardcore recreating EditorGUILayout.ObjectField functionality without need of SerializedProperty in input.

Here is an ExampleProject with code I've thrown together. There is a problem with GUILayout, I wasn't able to use Horizontal grouping, it was throwing errors, when in my main project, exact same code was doing fine. Odin shouldn't mess with these things, I think... Anyway. I'd be incredibly happy if someone would help me with this. I'll be looking into UIElements to make this UI, so maybe I'll be fine with making an ObjectField on a poco and serialize it... And I won't need to do anything with UnityEngine.Object.

Edit: UIElements also need SerializedObject of my binding class or I need to implement IBinding interface. I wasn't able to find anything on how this is done. The only thing I've found was this topic. I dind't get anything from it. Only that it is possible and it isn't documented.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Utility;

[CustomPropertyDrawer(typeof(RelativePropertySelector<>))]
public class RelativePropertySelectorDrawer<T> : PropertyDrawer {
    bool initialized;
    private RelativePropertySelector<T> relPropSelector;
    List<int> selectIndexes = new List<int>() { 0 };
    private Component[] sourcesScripts;

    //Commented for non UnityEngine.Object
    //SerializedObject serializedObject;
    //SerializedProperty sourceProp;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        //Sadly, this is not drawing that field
        base.OnGUI(position, property, label);
        if (!initialized) Init(property);

        GUILayout.BeginHorizontal();
        //Commented since it is non UnityEngine.Object version. There is no other way to draw this without RelativePropertySelector being UnityEngine.Object
        //EditorGUILayout.ObjectField(property, typeof(GameObject), new GUIContent());

        EditorGUI.BeginChangeCheck();
        try {
            string[] options = sourcesScripts.Select(c => c.GetType().Name).ToArray();
            var newInd = EditorGUILayout.Popup(selectIndexes[0], options);
            if (newInd != selectIndexes[0]) {
                var selectedComponent = sourcesScripts[newInd];
                if (relPropSelector.Component != (object)selectedComponent) {
                    selectIndexes = new List<int>() { newInd, 0 };
                    relPropSelector.SetStep(selectedComponent);
                }
                selectIndexes[0] = newInd;
            }

            for (int i = 0; i < selectIndexes.Count - 1; i++) {
                options = new string[] { "" }.Union(relPropSelector.GetInfos(i).Select(mi => mi.Name)).ToArray();
                newInd = EditorGUILayout.Popup(selectIndexes[i + 1], options);
                if (newInd != selectIndexes[i + 1]) {
                    selectIndexes[i + 1] = newInd;
                    var info = relPropSelector.GetInfos(i).FirstOrDefault(mi => mi.Name == options[newInd]);
                    relPropSelector.SetStep(info, i);
                }
            }
        } catch (Exception) { }

        EditorGUI.EndChangeCheck();
        GUILayout.EndHorizontal();
    }

    private void Init(SerializedProperty property) {
        relPropSelector = property.GetValue<RelativePropertySelector<T>>();

        if (relPropSelector == null) {
            var t = GetTypeOfCustomPropertyDrawer();
            relPropSelector = (RelativePropertySelector<T>)Activator.CreateInstance(t);
            //non UnityEngine.Object
            var zzz = property.serializedObject.targetObject;
            var m = zzz.GetType().GetMembers().FirstOrDefault(mi => mi.Name == property.propertyPath);
            if (m is FieldInfo field)
                field.SetValue(zzz, relPropSelector);
            else if (m is PropertyInfo prop)
                prop.SetValue(zzz, relPropSelector);
            //UnityEngine.Object
            //property.objectReferenceValue = relPropSelector;
            //property.serializedObject.ApplyModifiedProperties();
        }
        relPropSelector.Initialize();
        if (relPropSelector.Source != null) {
            sourcesScripts = relPropSelector.Source.GetComponents<Component>();
            selectIndexes = new List<int>() { ArrayUtility.IndexOf(sourcesScripts, relPropSelector.Component) };
        } else {
            sourcesScripts = new Component[0];
            selectIndexes = new List<int>() { 0 };
        }
        for (int i = 0; i < relPropSelector.PathLength; i++) {
            selectIndexes.Add(ArrayUtility.IndexOf(relPropSelector.GetInfos(i), relPropSelector.GetInfo(i)) + 1);
        }

        //serializedObject = new SerializedObject(relPropSelector);
        //sourceProp = serializedObject.FindProperty("Source");

        initialized = true;
    }

    private Type GetTypeOfCustomPropertyDrawer() {
        var xxxx = GetType().GetCustomAttribute<CustomPropertyDrawer>();
        return (Type)xxxx.GetType().GetField("m_Type", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(xxxx);
    }

    private static MemberInfo[] GetMemberInfos(object obj) =>
        obj.GetType().GetFields().Select(f => (MemberInfo)f)
        .Union(obj.GetType().GetProperties().Select(p => (MemberInfo)p))
        .ToArray();
}

[CustomPropertyDrawer(typeof(RelativePropertySelectorGameObject))]
public class RelativePropertySelectorGameObjectDrawer : RelativePropertySelectorDrawer<GameObject> {
}

[CustomPropertyDrawer(typeof(RelativePropertySelectorString))]
public class RelativePropertySelectorStringDrawer : RelativePropertySelectorDrawer<string> {
}

[CustomPropertyDrawer(typeof(RelativePropertySelectorInt))]
public class RelativePropertySelectorIntDrawer : RelativePropertySelectorDrawer<int> {
}

[CustomPropertyDrawer(typeof(RelativePropertySelectorFloat))]
public class RelativePropertySelectorFloatDrawer : RelativePropertySelectorDrawer<float> {
}

[Serializable]
public class RelativePropertySelector<T> {

    //public static implicit operator RelativePropertySelector<T>(GameObject go) => new RelativePropertySelector<T>() { Source = go };
    public static implicit operator T(RelativePropertySelector<T> s) => s.Ready ? s.GetValue() : default;

    public GameObject Source;
    [HideInInspector]
    public Component Component;
    [HideInInspector]
    public string Path = "";
    List<MemberInfo> MIBreadCrumbs = new List<MemberInfo>();
    List<MemberInfo[]> AllInfos;

    public bool Initialized { get; private set; }
    public bool Ready { get; private set; }

    public void Initialize() {
        if (Source == null || Component == null) {
            Path = "";
            MIBreadCrumbs = new List<MemberInfo>();
            return;
        }
        object obj = Component;
        MIBreadCrumbs = new List<MemberInfo>();
        AllInfos = new List<MemberInfo[]>() { GetMemberInfos(Component) };
        if (!string.IsNullOrEmpty(Path))
            foreach (var step in Path.Split('.')) {
                var infos = GetMemberInfos(obj);
                AllInfos.Add(infos);
                var curInfo = infos.FirstOrDefault(i => i.Name == step);
                MIBreadCrumbs.Add(curInfo);
                if (curInfo is FieldInfo field)
                    obj = field.GetValue(obj);
                else if (curInfo is PropertyInfo property)
                    obj = property.GetValue(obj);
                else
                    break;
            }
        UpdatePath();
        Initialized = true;
    }

    private void UpdatePath() =>
        Path = string.Join(".", MIBreadCrumbs.Select(i => i.Name));

    public int PathLength {
        get =>
MIBreadCrumbs.Count;
    }

    private static MemberInfo[] GetMemberInfos(object obj) =>
        obj != default ? obj.GetType().GetFields().Select(f => (MemberInfo)f)
        .Union(obj.GetType().GetProperties().Select(p => (MemberInfo)p))
        .ToArray() : new MemberInfo[0];
    private static MemberInfo[] GetMemberInfos(Type t) =>
        t.GetFields().Select(f => (MemberInfo)f)
        .Union(t.GetProperties().Select(p => (MemberInfo)p))
        .ToArray();

    public MemberInfo GetInfo(int step) =>
        step < 0 || step >= MIBreadCrumbs.Count ? default : MIBreadCrumbs[step];

    public Type GetStepType(int step) {
        var info = GetInfo(step);
        if (info is FieldInfo field)
            return field.FieldType;
        else if (info is PropertyInfo prop)
            return prop.PropertyType;
        else
            return default;
    }

    public T GetValue() {
        if (!Initialized) Initialize();
        return (T)GetStep(MIBreadCrumbs.Count - 1);
    }

    internal object GetStep(int step) {
        if (!Initialized) Initialize();
        if (step >= MIBreadCrumbs.Count || step < -1) return null;
        if (step == -1) return Component;
        else if (MIBreadCrumbs[step] is FieldInfo field)
            return field.GetValue(GetStep(step - 1));
        else if (MIBreadCrumbs[step] is PropertyInfo property)
            return property.GetValue(GetStep(step - 1));
        return null;
    }

    internal void SetStep(MemberInfo info, int step) {
        if (step < 0 || step > MIBreadCrumbs.Count) return;
        if (step < MIBreadCrumbs.Count - 1) {
            MIBreadCrumbs = MIBreadCrumbs.Take(step).Union(new MemberInfo[] { info }).ToList();
            AllInfos = AllInfos.Take(step + 1).ToList();
            AllInfos.Add(GetMemberInfos(info.DeclaringType));
        } else if (step == MIBreadCrumbs.Count - 1) {
            MIBreadCrumbs[step] = info;
        } else {
            MIBreadCrumbs.Add(info);
            AllInfos.Add(GetMemberInfos(info.GetType()));
        }
        if (step == PathLength - 1 && GetStepType(step) == typeof(T))
            Ready = true;
        else
            Ready = false;
        UpdatePath();
    }
    internal void SetStep(Component comp) {
        Component = comp;
        AllInfos = new List<MemberInfo[]>() { GetMemberInfos(comp) };
        MIBreadCrumbs = new List<MemberInfo>();
        Path = "";
    }

    internal MemberInfo[] GetInfos(int step) {
        return AllInfos[step];
    }
}

[Serializable]
public class RelativePropertySelectorGameObject : RelativePropertySelector<GameObject> { }

[Serializable]
public class RelativePropertySelectorString : RelativePropertySelector<string> { }

[Serializable]
public class RelativePropertySelectorInt : RelativePropertySelector<int> { }

[Serializable]
public class RelativePropertySelectorFloat : RelativePropertySelector<float> { }
\$\endgroup\$
5
  • \$\begingroup\$ Can you show us the code you've written for this complex serialized object so far that's not working? This can help ensure we don't waste time telling you to do something you've already tried. The code should be included in the text of your question, not in an off-site zip file. \$\endgroup\$ – DMGregory Jun 18 at 21:51
  • \$\begingroup\$ I just had a brilliant Idea. What if I'll do that in a dirty way. Create a script, with those fields, like GameObject Source, path... move all mechanics there, and use a property that will implicitly be converted into that script. Script won't have anything in the editor, but that property will have a PropertyDrawer. I've found a solution! Why the hell I need to go through this torture every time? Why I do set some stupid ass goals, that nobody can help me with? And solve them myself... I hate my life. \$\endgroup\$ – zORg Alex Jun 20 at 18:07
  • \$\begingroup\$ If you've solved your problem, post your solution as an answer below. \$\endgroup\$ – DMGregory Jun 20 at 18:43
  • \$\begingroup\$ Just found out that JSONUtiliity may be able to do what I wanted... But I already wrote a bunch of code to work that around... I should give it a try. Serialize a ScriptableObject. I remember I could make it work with SO, but it wasn't being serialized. If JSONUtility will be able to do that, I might scrap this complex workaround and go back to what I had. \$\endgroup\$ – zORg Alex Jun 21 at 7:45
  • \$\begingroup\$ Scrap that JSONUtility can do nothing. I've got some results. I can post my rough version here. \$\endgroup\$ – zORg Alex Jun 21 at 9:40
0
\$\begingroup\$

I won't post the code here, it's just too long. It is mostly the same, except I've moved data I wanted to serialize into a separate script, which I've hid so nobody will be able to see it in add component menu since it is only works in tandem with RelativePropertySelector. And split it in files like it should be editor things in \Editor. MonoBeahviour into it's own, but it is nested in an empty class, so that Unity would hide it.

Asset bundle

It is not finished. There is a bug that prevents you to select anything past first property. So that if you'll want to get something deeper, you'll need to enter Path manually separated by "." e.g. on that RelativePropertySelectorScript Path:"SpawnPoint.transform.position.x" to get SpawnPoint x value.

\$\endgroup\$

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.