Created
November 21, 2018 20:25
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//---------------------------------------------------------------------------------- | |
// 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(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 ofISerializationCallbackReceiver.OnAfterDeserialize
. I thought I made this change for performance, but maybe it handles edge cases whereAwake
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:
Where
IsDummyNull
is the following extension method: (WhereUnityObject
is an alias forUnityEngine.Object
.)The other change our internal version has is we changed the loop to
foreach (FieldInfo field in GetType().GetAllInstanceFields(typeof(FixUpNullsBehaviour)))
whereGetAllInstanceFields
is the following extension method: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.)