-
-
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!