using System; using System.Collections.Generic; using System.Linq; using UnityEngine.SceneManagement; using UnityObject = UnityEngine.Object; namespace Unity.VisualScripting { public static class UnityObjectUtility { public static bool IsDestroyed(this UnityObject target) { // Checks whether a Unity object is not actually a null reference, // but a rather destroyed native instance. return !ReferenceEquals(target, null) && target == null; } public static bool IsUnityNull(this object obj) { // Checks whether an object is null or Unity pseudo-null // without having to cast to UnityEngine.Object manually return obj == null || ((obj is UnityObject) && ((UnityObject)obj) == null); } public static string ToSafeString(this UnityObject uo) { if (ReferenceEquals(uo, null)) { return "(null)"; } if (!UnityThread.allowsAPI) { return uo.GetType().Name; } if (uo == null) { return "(Destroyed)"; } try { return uo.name; } catch (Exception ex) { return $"({ex.GetType().Name} in ToString: {ex.Message})"; } } public static string ToSafeString(this object obj) { if (obj == null) { return "(null)"; } if (obj is UnityObject uo) { return uo.ToSafeString(); } try { return obj.ToString(); } catch (Exception ex) { return $"({ex.GetType().Name} in ToString: {ex.Message})"; } } public static T AsUnityNull(this T obj) where T : UnityObject { // Converts a Unity pseudo-null to a real null, allowing for coalesce operators. // e.g.: destroyedObject.AsUnityNull() ?? otherObject if (obj == null) { return null; } return obj; } public static bool TrulyEqual(UnityObject a, UnityObject b) { // This method is required when checking two references // against one another, where one of them might have been destroyed. // It is not required when checking against null. // This is because Unity does not compare alive state // in the CompareBaseObjects method unless one of the two // operators is actually the null literal. // From the source: /* bool lhsIsNull = (object) lhs == null; bool rhsIsNull = (object) rhs == null; if (rhsIsNull && lhsIsNull) return true; if (rhsIsNull) return !Object.IsNativeObjectAlive(lhs); if (lhsIsNull) return !Object.IsNativeObjectAlive(rhs); return lhs.m_InstanceID == rhs.m_InstanceID; */ // As we can see, Object.IsNativeObjectAlive is not compared // across the two objects unless one of the operands is actually null. // But it can happen, for example when exiting play mode. // If we stored a static reference to a scene object that was destroyed, // the reference won't get cleared because assembly reloads don't happen // when exiting playmode. But the instance ID of the object will stay // the same, because it only gets reserialized. So if we compare our // stale reference that was destroyed to a new reference to the object, // it will return true, even though one reference is alive and the other isn't. if (a != b) { return false; } if ((a == null) != (b == null)) { return false; } return true; } public static IEnumerable NotUnityNull(this IEnumerable enumerable) where T : UnityObject { return enumerable.Where(i => i != null); } public static IEnumerable FindObjectsOfTypeIncludingInactive() { for (int i = 0; i < SceneManager.sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); if (scene.isLoaded) { foreach (var rootGameObject in scene.GetRootGameObjects()) { foreach (var result in rootGameObject.GetComponentsInChildren(true)) { yield return result; } } } } } } }