Custom Inspector/Serialization

Hi All,

I’ve built a custom MonoBehaviour and a custom Editor to go with it. Some of the data in the MonoBehaviour needs to be serialized but is not of any of the types supported by Unity’s internal serialization framework. My solution to this is to create an artificial serialization of the data in a supported type (in this case, a string). However, the editor doesn’t seem to like this. Once I stop a run, all the settings in my custom component (as seen in the editor) go back to their defaults.

Here’s a C# example of my implementation. Note: This is just an example. The actual code has complex fields of types not supported by the serialization framework (not bools).

public class SerializeTest : MonoBehaviour 
{
	///Property allowing access to the internal boolean field.
	public bool testBool
	{
		get
		{
			return m_testBool;
		}
		set
		{
			// store the new value
			m_testBool = value;

			// update the serialized value
			if(value == true)
				m_serialBool = "true";
			else
				m_serialBool = "false";
		}
	}

	///Read-only access to the serialized version of the boolean field.
	public string serialized
	{
		get
		{
			return m_serialBool;
		}
	}

	private void Awake()
	{
		// deserialize m_testBool
		if(m_serialBool == "true")
			m_testBool = true;
		else
			m_testBool = false;
	}

	/// boolean value
	private bool m_testBool;

	///Serializable version of the boolean value
	[SerializeField]
	private string m_serialBool;

}

and

[CustomEditor(typeof(SerializeTest))]
class SerializeTestEditor : Editor
{
	/**
	* Callback method, lays out the target SiblingReview component within the inspector tool in 
	*	the Unity editor. Triggered:
	*		1) Each frame, and
	*		2) Whenever the user interacts with the GUI.
	*/
	public override void OnInspectorGUI()
	{
		SerializeTest stest = (SerializeTest)target;

		// set up the gui to use default inspector styles
		EditorGUIUtility.LookLikeInspector();

		EditorGUILayout.BeginVertical();
			stest.testBool = EditorGUILayout.Toggle("testBool", stest.testBool);
			EditorGUILayout.TextField("testString", stest.serialized);
		EditorGUILayout.EndVertical();

    if(GUI.changed)
			EditorUtility.SetDirty(target);
	}
}

To see the problem,

  1. Put these scripts into the appropriate folders
  2. Add a SerializeTest component to an object
  3. In the inspector, mark “testBool” as checked (corresponds to a true value)
  4. Press “Play” in the editor
  5. Make sure the object with SerializeTest is selected in the inspector, and press “Play” again to stop the run
  6. The state of “testBool” will have reverted to its default ‘false’ state.

Does anybody have Ideas/Alternatives of how to make this state persistent across runs?

Why do you need to serialize something that Unity serializes for you, yourself? - Just let Unity handle the serialization of the boolean. Not really a big fan of what you did there, but to make it work in the way you want it, you have to parse the string to get the boolean value:

public bool testBool
{
	get
	{
		return System.Convert.ToBoolean(m_serialBool);
	}
	set
	{
		// Not even sure you look like you need this anymore since you're using strings...
		m_testBool = value;

		m_serialBool = value ? "true" : "false";
	}
}

The reason it didn’t work, because m_testBool was private, and not mocked with SerializeField so it doesn’t make it to the C++ side when assembly reload happens to be able to return to the C# managed side in the first place (For more info, see this and that)

Here’s a simple serialization solution that you could use it to serialize any type that can’t serialize assuming that type is mocked with [System.Serializable] (I used it to serialize delegates (System.Type, MethodInfo, FieldInfo, Actions, Funcs, etc)

/// <summary>
/// A wrapper to serialize classes that Unity can't
/// </summary>
public abstract class SerializedClass<T> where T : class
{
	[SerializeField]
	private string serializedData;

	protected T _class;

	public SerializedClass() { }

	public SerializedClass(T _class)
	{
		Set(_class);
	}

	public void Set(T _class)
	{
		this._class = _class;
		Serialize();
	}

	public T Get()
	{
		if (_class == null) _class = Deserialize();
		return _class;
	}

	public virtual void Serialize()
	{
		serializedData = Utils.SerializeToString<T>(_class);
	}

	protected virtual T Deserialize()
	{
		return Utils.DeserializeFromString<T>(serializedData);
	}
}

For example, now you could:

[Serializable]
public class SerializedMethodInfo : SerializedClass<MethodInfo>
{
	public SerializedMethodInfo() { }
	public SerializedMethodInfo(MethodInfo info)
		: base(info) { }

	protected override MethodInfo Deserialize()
	{
		try {
			return base.Deserialize();
		}
		catch { return null; }
	}
}

Using and old and relatively slow serializer… BinaryFormatter - but just to illustrate my point.

public static class Utils
{
   		/// <summary>
		/// Serializes 'value' to a string, using BinaryFormatter
		/// </summary>
		public static string SerializeToString<T>(T value)
		{
			using (var stream = new MemoryStream()) {
				(new BinaryFormatter()).Serialize(stream, value);
				stream.Flush();
				return Convert.ToBase64String(stream.ToArray());
			}
		}

		/// <summary>
		/// Deserializes an object of type T from the string 'data'
		/// </summary>
		public static T DeserializeFromString<T>(string data)
		{
			byte[] bytes = Convert.FromBase64String(data);
			using (var stream = new MemoryStream(bytes)) {
				return (T)(new BinaryFormatter()).Deserialize(stream);
			}
		}
}

But remember if you want to use this solution, you can’t use it to serialize any UnityEngine.Object, nor a regular class that contains any, so:

This fails:

string data = SerializeToString<Transform>(player.transform);

This fails too:

[SerializeField]
public class RegularClassThatContainsUnityEngineObject
{
    public Transform t;
}

string data = SerializeToString<RegularClassThatContainsUnityEngineObject>(myClassObject);

Because for BinaryFormatter to serialize a class, it serializes all its members except those mocked with System.NonSerialized - Since there’s a Transform reference, it stops cause it can’t serialize it (since it’s not mocked with Serializable)

The problem is that the Awake method on SerializeTest isn’t normally called from within the editor. So what happens is the editor creates an instance of the SerializeTest class and then serializes in the data for the member variables on top of that instance. So m_testBool still has it’s default value of false in the editor regardless of the value of m_serialBool. There are several ways to fix this issue but since this question is two years old I’ll just assume a specific answer isn’t needed.

Just in case anybody finds this helpful, I’ve found a fix, if not exactly a full solution. By adding the ExecuteInEditMode attribute to SerializeTest I get the desired behavior. Would still be interested in further insights anybody else might have.

Here’s something else you can consider as a solution to your issue: GitHub - LMNRY/SetProperty: A PropertyAttribute/PropertyDrawer combination that allows for properties in Unity