Skip to content

Instantly share code, notes, and snippets.

@PathogenDavid
Created November 21, 2018 20:25
Show Gist options
  • Save PathogenDavid/bf5205077f1aed2e6bd3b07d9538f2d3 to your computer and use it in GitHub Desktop.
Save PathogenDavid/bf5205077f1aed2e6bd3b07d9538f2d3 to your computer and use it in GitHub Desktop.
Workaround for Unity's weird behavior of deserializing some null object references as zombies instead of null - https://github.com/dotnet/csharplang/issues/2020
//----------------------------------------------------------------------------------
// 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();
}
@PathogenDavid
Copy link
Author

PathogenDavid commented Dec 16, 2019

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment