4
\$\begingroup\$

I have some utilities implemented as generic classes.

I would like to have fields of these types exposed to the inspector but they appear to not display.

This test code:

[Serializable]
public class TestGenericClass<T>
{
    public string testField;

    public TestGenericClass(T testValue)
    {
        testField = testValue.ToString();
    }
}

[Serializable]
public class TestNormalClass
{
    public string testField;

    public TestNormalClass(string testValue)
    {
        testField = testValue;
    }
}

public class MonoBehaviourClass : MonoBehaviour
{
    public TestGenericClass<string> testGenericClass = new TestGenericClass<string>("generic");
    public TestNormalClass testNormalClass = new TestNormalClass("normal");
}

Produces this result:

enter image description here

As you can see the field for the generic class is missing.

How can I expose a field with a generic class to the inspector?

\$\endgroup\$
3
\$\begingroup\$

I think there are several people who have come across this problem before. The short of it is that there isn't a way to directly serialise the class. However, there are a few workarounds posted in solutions in the unity answers:

here using subclasses:

 [Serializable]
 public class GenericClassOfString : GenericClass<String>
 {
 }

 [Serializable]
 public class SomeClass
 {
    public GenericClassOfString stringClassField; //serializes
 }

and here using inheritance/interfaces:

using UnityEngine;
using System;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
using UnityObject = UnityEngine.Object;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Serialization;

namespace Assets.Vexe.ShowEmAll
{
    public abstract class SerializedBehaviour : MonoBehaviour, ISerializationCallbackReceiver
    {
        [SerializeField]
        private StrObjDict serializedObjects = new StrObjDict(); // a serializable Dictionary<string, UnityObject>
        [SerializeField]
        private StrStrDict serializedStrings = new StrStrDict(); // a serializable Dictionary<string, string>

        private BinaryFormatter mSerializer;

        private BinaryFormatter serializer
        {
            get
            {
                if (mSerializer == null)
                {
                    mSerializer = new BinaryFormatter();
                    var selector = new SurrogateSelector();

                    Action<Type, ISerializationSurrogate> addSurrogate = (type, surrogate) =>
                        selector.AddSurrogate(type, new StreamingContext(), surrogate);

                    addSurrogate(typeof(Vector3), new Vector3Surrogate());
                    // add more custom surrogates here

                    // create our unity surrogate
                    var unitySurrogate = new AnotherUnityObjectSurrogate(serializedObjects);

                    // get all unity object types
                    var unityTypes = typeof(UnityObject).Assembly.GetTypes()
                                                        .Where(t => typeof(UnityObject).IsAssignableFrom(t))
                                                        .ToArray();

                    // add our surrogate to let the serializer use it to handle unity objects serialization
                    foreach (var t in unityTypes)
                        addSurrogate(t, unitySurrogate);

                    serializer.SurrogateSelector = selector;
                }
                return mSerializer;
            }
        }

        public void OnAfterDeserialize()
        {
            Deserialize();
        }

        public void OnBeforeSerialize()
        {
            Serialize();
        }

        private void Serialize()
        {
            foreach (var field in GetInterfaces())
            {
                var value = field.GetValue(this);
                if (value == null)
                    continue;

                string name = field.Name;
                var obj = value as UnityObject;
                if (obj != null) // the implementor is a UnityEngine.Object
                {
                    serializedObjects[name] = obj; // using the field's name as a key because you can't have two fields with the same name
                }
                else
                {
                    // try to serialize the interface and store the result in our other dictionary
                    using (var stream = new MemoryStream())
                    {
                        serializer.Serialize(stream, value);
                        stream.Flush();
                        serializedObjects.Remove(name); // it could happen that the field might end up in both the dictionaries, ex when you change the implementation of the interface to use a System.Object instead of a UnityObject
                        serializedStrings[name] = Convert.ToBase64String(stream.ToArray());
                    }
                }
            }
        }

        private void Deserialize()
        {
            foreach (var field in GetInterfaces())
            {
                object result = null;
                string name = field.Name;

                // Try fetch member serialized value
                UnityObject obj;
                if (serializedObjects.TryGetValue(name, out obj)) // if the implementor is a UnityObject, then we just fetch the value from our dictionary as the result
                {
                    result = obj;
                }
                else // otherwise, get it from our other dictionary
                {
                    string serializedString;
                    if (serializedStrings.TryGetValue(name, out serializedString))
                    {
                        // deserialize the string back to the original object
                        byte[] bytes = Convert.FromBase64String(serializedString);
                        using (var stream = new MemoryStream(bytes))
                            result = serializer.Deserialize(stream);
                    }
                }

                field.SetValue(this, result);
            }
        }

        private IEnumerable<FieldInfo> GetInterfaces()
        {
            return GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                            .Where(f => !f.IsDefined(typeof(HideInInspector)) && (f.IsPublic || f.IsDefined(typeof(SerializeField))))
                            .Where(f => f.FieldType.IsInterface);
        }
    }

    public interface ITestInterface
    {
        string StringValue { get; set; }
        float FloatValue { get; set; }
        Vector3 Vector3Value { get; set; }
        Transform Target { get; set; }
    }

    [Serializable]
    public class SystemImplementer : ITestInterface
    {
        public string StringValue { get; set; }
        public float FloatValue { get; set; }
        public Vector3 Vector3Value { get; set; }
        public Transform Target { get; set; }
    }

    // This should have its own file
    public class UnityImplementer : MonoBehaviour, ITestInterface
    {
        public string StringValue { get; set; }
        public float FloatValue { get; set; }
        public Vector3 Vector3Value { get; set; }
        public Transform Target { get; set; }
    }

    // Put this in a file on its own inside an "Editor" folder
    [CustomEditor(typeof(SerializationTest))]
    public class SerializationTestEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            var typedTarget = target as SerializationTest;
            if (GUILayout.Button("Set to system implementor"))
                typedTarget.test = new SystemImplementer();
            if (GUILayout.Button("Set to unity implementor"))
                typedTarget.test = UnityEngine.Object.FindObjectOfType<UnityImplementer>() ?? new GameObject().AddComponent<UnityImplementer>();
            if (GUILayout.Button("Print value"))
                Debug.Log(typedTarget.test);
        }
    }
}
\$\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.