-
-
Save PathogenDavid/bf5205077f1aed2e6bd3b07d9538f2d3 to your computer and use it in GitHub Desktop.
| //---------------------------------------------------------------------------------- | |
| // Copyright (c) 2018 David Maas | |
| // | |
| // Permission to use, copy, modify, and/or distribute this software for any purpose | |
| // with or without fee is hereby granted. | |
| // | |
| // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
| // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
| // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS | |
| // OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER | |
| // TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF | |
| // THIS SOFTWARE. | |
| //---------------------------------------------------------------------------------- | |
| using System.Diagnostics; | |
| using System.Reflection; | |
| using UnityEngine; | |
| using UnityObject = UnityEngine.Object; | |
| /// <summary>FixUpNullsBehaviour is a variant of MonoBehaviour for Unity scripts which want to opt-out of the fake null deserialization that Unity uses in the editor.</summary> | |
| /// <remarks> | |
| /// Details about why Unity works this way in the editor can be found in this blog post: | |
| /// https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/ | |
| /// </remarks> | |
| public abstract class FixUpNullsBehaviour : MonoBehaviour | |
| { | |
| /// <summary>Fixes references to fake null objects and zombie objects present in the fields of this script.</summary> | |
| [Conditional("UNITY_EDITOR")] | |
| protected void ClearFakeNullReferences() | |
| { | |
| // Loop through all fields on the object and fixup ones which contain references to zombie Unity objects | |
| //TODO: Skip fields which Unity would not serialize. | |
| foreach (FieldInfo field in GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) | |
| { | |
| // Skip fields which don't implement UnityObject | |
| if (!typeof(UnityObject).IsAssignableFrom(field.FieldType)) | |
| { continue; } | |
| // Don't bother with MonoBehaviour or ScriptableObject fields, they don't have this issue | |
| if (typeof(MonoBehaviour).IsAssignableFrom(field.FieldType) || typeof(ScriptableObject).IsAssignableFrom(field.FieldType)) | |
| { continue; } | |
| // Get the value of the field | |
| UnityObject unityObject = (UnityObject)field.GetValue(this); | |
| // If the field is already literally null, don't do anything | |
| if (unityObject is null) | |
| { continue; } | |
| // If it is a zombie object, replace it with null | |
| if (unityObject == null) | |
| { field.SetValue(this, null); } | |
| } | |
| } | |
| // We have to use Awake instead of the deserialization callbacks. | |
| // Deserialization happens off the main thread, and you can't reliably verify a UnityObject exists from other threads. | |
| // (You might be able to by looking at UnityObject.m_instanceID with reflection, but that's playing with fire.) | |
| protected virtual void Awake() | |
| => ClearFakeNullReferences(); | |
| } |
Hello! I have not tried it in Unity 2019.2.6f1 yet, but I have successfully used it in 2019.2.4f1 and I wouldn't expect any major changes that'd break this between those two versions.
It's been a while since I made it, but our internal version of this script has some differences. I thought the differences were primarily for compatibility with our internal inspector infrastructure and performance, but maybe they handle some edge cases this original proof-of-concept didn't.
You should try to instead call ClearFakeNullReferences from an implementation of ISerializationCallbackReceiver.OnAfterDeserialize. I thought I made this change for performance, but maybe it handles edge cases where Awake wasn't quite enough.
In order to make that change, I believe you also need to change the way the dummy null check works since deserialization happens off of the main thread, changing it to:
if (unityObject.IsDummyNull())
{ field.SetValue(this, null); }Where IsDummyNull is the following extension method: (Where UnityObject is an alias for UnityEngine.Object.)
/// <summary>Returns true if this Unity object is a dummy null.</summary>
/// <remarks>
/// Unlike the Unity equality and bool operators, this method is threadsafe.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDummyNull(this UnityObject unityObject)
{
// It has been observed that fake null objects will have an instance ID of 0
// Since the instance ID is used to test for what is effectively refernece equality:
// https://github.com/Unity-Technologies/UnityCsReference/blob/2019.2.1f1/Runtime/Export/Scripting/UnityEngineObject.bindings.cs#L108
// We can be confident this is never used as an actual instance ID.
// The instance ID is not exposed directly, but it is used directly as the hash code:
// https://github.com/Unity-Technologies/UnityCsReference/blob/2019.2.1f1/Runtime/Export/Scripting/UnityEngineObject.bindings.cs#L73-L79
// We cannot compare unityObject to null here using the == operator because that will indirectly call GetInstanceID, which must be called on the main thread:
// https://github.com/Unity-Technologies/UnityCsReference/blob/2019.2.1f1/Runtime/Export/Scripting/UnityEngineObject.bindings.cs#L69
// (This method is meant to be called off the main thread. For instance, during deserialization.)
// Dummy nulls are never present during player builds, so we can skip this check outside of the editor.
#if UNITY_EDITOR
if (unityObject is null)
{ return false; }
return unityObject.GetHashCode() == 0;
#else
return false;
#endif
}The other change our internal version has is we changed the loop to foreach (FieldInfo field in GetType().GetAllInstanceFields(typeof(FixUpNullsBehaviour))) where GetAllInstanceFields is the following extension method:
public static IEnumerable<FieldInfo> GetAllInstanceFields(this Type type, Type until)
{
while (type != until && type != null)
{
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
yield return field;
}
type = type.BaseType;
}
}This fixes some issues with non-public fields getting skipped in base classes, but I'm not actually sure if it's necessary with Unity's vanilla serializer.
I've been meaning write a blog post with an updated version of this script, but time has been hard to come by lately.
(For the lawyers: The above code snippets in this comment are governed by the same terms as the original Gist.)
Hello @PathogenDavid! I'm not sure it is working in Unity 2019.2.6f1. Can you confirm that? Thank you!