Ok we encountered one more bug with garbage collection. We figured out how to avoid the issue, but I’m not too familiar with what’s going on under the hood of unity to understand why it’s happening.
As we suspected, OnValidate seems to mess with garbage collection if you set a field on any other component during OnValidate.
For example:
[RequireComponent(typeof(Rigidbody2D))]
public class Example : MonoBehaviour
{
void OnValidate()
{
GetComponent<Rigidbody2D>().bodyType = RigidbodyType2D.Dynamic;
}
}
Attaching a MonoBehaviour like this to a GameObject will cause that GameObject and all its managed references to not be garbage collected no matter what.
This was the issue that was causing those stubborn asset references to stick around for us even after clearing out and nulling all their fields and calling EditorUtility.UnloadUnusedAssetsImmediate and GC.Collect.
Luckily we have another workaround for this. We mainly use OnValidate to validate properties whenever a dev changes those properties in the inspector. We don’t actually want this validation to occur any other time (e.g. when the asset is loaded or when entering play mode). So as a work around, we rolled our own “OnValidateProperty” method. This method is guaranteed to only execute when a property is changed in the inspector, no where else. Moving all our code from OnValidate into this method prevents all those weird issues that trip up the garbage collector.
To use this workaround, instead of inheriting from MonoBehaviour, you need to make sure all your MonoBehaviours inherit from a BaseMonoBehaviour class with the following OnValidateProperty method (though for cleanliness I am ommiting it below, remember to also include the OnDestroy code I mentioned before in this BaseMonoBehaviour class):
[CanEditMultipleObjects, CustomEditor(typeof(BaseMonoBehaviour), true)]
public class BaseMonoBehaviourEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
OnPropertyGUI(property);
}
serializedObject.ApplyModifiedProperties();
}
public void OnPropertyGUI(SerializedProperty property)
{
OnPropertyGUI(property, new GUIContent(property.displayName, property.tooltip));
}
public void OnPropertyGUI(SerializedProperty property, GUIContent label)
{
BaseMonoBehaviour t = target as BaseMonoBehaviour;
EditorGUI.BeginChangeCheck();
DrawProperty(property, label);
if (EditorGUI.EndChangeCheck())
{
property.serializedObject.ApplyModifiedProperties();
property.serializedObject.Update();
t.OnValidateProperty(property.name);
}
}
public void OnPropertyGUI(Rect position, SerializedProperty property, GUIContent label)
{
BaseMonoBehaviour t = target as BaseMonoBehaviour;
EditorGUI.BeginChangeCheck();
DrawProperty(position, property, label);
if (EditorGUI.EndChangeCheck())
{
property.serializedObject.ApplyModifiedProperties();
property.serializedObject.Update();
t.OnValidateProperty(property.name);
}
}
public virtual Rect GetPropertyControlRect(SerializedProperty property, GUIContent label, params GUILayoutOption[] options)
{
return EditorGUILayout.GetControlRect(!string.IsNullOrEmpty(label.text), EditorGUI.GetPropertyHeight(property), options);
}
protected virtual void DrawProperty(SerializedProperty property, GUIContent label, params GUILayoutOption[] options)
{
Rect position = GetPropertyControlRect(property, label, options);
DrawProperty(position, property, label);
}
protected virtual void DrawProperty(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.PropertyField(position, property, label);
}
}
This way in your class you can easily validate properties without worrying about it tripping up the garbage collector or taking up scene load time.
[RequireComponent(typeof(Rigidbody2D))]
public class Example : BaseMonoBehaviour
{
public bool m_CanMove;
public override bool OnValidateProperty(string propertyName)
{
if (propertyName == "m_CanMove")
{
if (!m_CanMove)
{
GetComponent<Rigidbody2D>().bodyType = RigidbodyType2D.Static;
return true;
}
}
return base.OnValidateProperty(propertyName);
}
}
After doing this, we have completely clean scene loads! No more ghost references hanging around and taking up valuable memory. Woo!