// MIT License - Copyright (c) 2024 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Helper { using System; using System.Collections.Generic; using Extension; using UnityEngine; using UnityEngine.SceneManagement; using WallstopStudios.UnityHelpers.Utils; using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; #endif #if UNITY_EDITOR using UnityEditor.SceneManagement; #endif /// /// Helpers for working with UnityEngine.Object, components, and GameObjects. /// public static partial class Helpers { /// /// Finds and caches an instance of on a GameObject with the given tag. /// /// Context for logging. /// Unity tag to search. /// If true, logs a warning when not found. /// The component instance if found; otherwise default. /// /// ("AudioManager"); /// ]]> /// public static T Find(this Object component, string tag, bool log = true) where T : Object { if (ObjectsByTag.TryGetValue(tag, out Object value)) { if (value != null && value is T typed) { return typed; } _ = ObjectsByTag.Remove(tag); } GameObject gameObject = GameObject.FindGameObjectWithTag(tag); if (gameObject == null) { if (log) { component.LogWarn($"Could not find {tag}."); } return default; } if (gameObject.TryGetComponent(out T instance)) { ObjectsByTag[tag] = instance; return instance; } if (log) { component.LogWarn( $"Failed to find {typeof(T).Name} on {tag} (name: {gameObject.name}), id [{gameObject.GetInstanceID()}]." ); } return default; } /// /// Finds and caches an instance of on a GameObject with the given tag. /// public static T Find(string tag, bool log = true) where T : MonoBehaviour { if (ObjectsByTag.TryGetValue(tag, out Object value)) { if (value is T typed && typed != null) { return typed; } _ = ObjectsByTag.Remove(tag); } GameObject gameObject = GameObject.FindGameObjectWithTag(tag); if (gameObject == null) { if (log) { LogObject.Log($"Could not find {tag}."); } return default; } if (gameObject.TryGetComponent(out T instance)) { ObjectsByTag[tag] = instance; return instance; } if (log) { LogObject.Log($"Failed to find {typeof(T).Name} on {tag}"); } return default; } /// /// Manually sets the cached instance for a tag. /// public static void SetInstance(string tag, T instance) where T : MonoBehaviour { ObjectsByTag[tag] = instance; } /// /// Clears the cached instance for a tag if it matches the provided instance. /// public static void ClearInstance(string tag, T instance) where T : MonoBehaviour { if (ObjectsByTag.TryGetValue(tag, out Object existing) && existing == instance) { _ = ObjectsByTag.Remove(tag); } } /// /// Returns true if the object has a component of type . /// public static bool HasComponent(this Object unityObject) where T : Object { return unityObject switch { GameObject go => go.HasComponent(), Component component => component.HasComponent(), _ => false, }; } /// /// Returns true if the component's GameObject has a component of type . /// public static bool HasComponent(this Component component) where T : Object { return component.TryGetComponent(out _); } /// /// Returns true if the GameObject has a component of type . /// public static bool HasComponent(this GameObject gameObject) where T : Object { return gameObject.TryGetComponent(out _); } /// /// Returns true if the object has a component of the specified type. /// public static bool HasComponent(this Object unityObject, Type type) { return unityObject switch { GameObject go => go.TryGetComponent(type, out _), Component component => component.TryGetComponent(type, out _), _ => false, }; } /// /// Enables or disables all components of type on this component and its child hierarchy. /// public static void EnableRecursively( this Component component, bool enabled, Func exclude = null ) where T : Behaviour { if (component == null) { return; } using PooledResource> componentBuffer = Buffers.List.Get( out List components ); component.GetComponents(components); foreach (T behaviour in components) { if (behaviour != null && !(exclude?.Invoke(behaviour) ?? false)) { behaviour.enabled = enabled; } } Transform transform = component as Transform ?? component.transform; if (transform == null) { return; } for (int i = 0; i < transform.childCount; ++i) { Transform child = transform.GetChild(i); child.EnableRecursively(enabled, exclude); } } /// /// Enables or disables all renderers of type on this component and its child hierarchy. /// public static void EnableRendererRecursively( this Component component, bool enabled, Func exclude = null ) where T : Renderer { if (component == null) { return; } T behavior = component as T ?? component.GetComponent(); if (behavior != null && !(exclude?.Invoke(behavior) ?? false)) { behavior.enabled = enabled; } Transform transform = component as Transform; if (transform == null) { transform = component.transform; if (transform == null) { return; } } for (int i = 0; i < transform.childCount; ++i) { Transform child = transform.GetChild(i); child.EnableRendererRecursively(enabled, exclude); } } /// /// Destroys all direct child GameObjects. Uses DestroyImmediate in editor (edit mode), Destroy at runtime. /// public static void DestroyAllChildrenGameObjects(this GameObject gameObject) { #if UNITY_EDITOR if (Application.isEditor) { gameObject.EditorDestroyAllChildrenGameObjects(); } else #endif { gameObject.PlayDestroyAllChildrenGameObjects(); } } /// /// Destroys all components of type on the GameObject. /// public static void DestroyAllComponentsOfType(this GameObject gameObject) where T : Component { using PooledResource> componentBuffer = Buffers.List.Get( out List components ); gameObject.GetComponents(components); foreach (T component in components) { component.SmartDestroy(); } } /// /// Destroys an object using DestroyImmediate in editor edit mode, otherwise Destroy (optionally delayed). /// Avoids deleting assets on disk; unloads asset objects instead. /// public static void SmartDestroy(this Object obj, float? afterTime = null) { if (obj == null) { return; } #if UNITY_EDITOR if (Application.isEditor && !Application.isPlaying) { // If this is an asset object, unload it so a fresh instance can be loaded next time. string assetPath = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(assetPath)) { Resources.UnloadAsset(obj); return; } Object.DestroyImmediate(obj); return; } #endif if (afterTime.HasValue) { Object.Destroy(obj, afterTime.Value); } else { Object.Destroy(obj); } } /// /// Immediately destroys all child GameObjects that match the predicate. /// public static void DestroyAllChildrenGameObjectsImmediatelyConditionally( this GameObject gameObject, Func acceptancePredicate ) { gameObject.InternalDestroyAllChildrenGameObjects(toDestroy => { if (!acceptancePredicate(toDestroy)) { return; } Object.DestroyImmediate(toDestroy); }); } /// /// Destroys all child GameObjects that match the predicate (play-mode safe). /// public static void DestroyAllChildGameObjectsConditionally( this GameObject gameObject, Func acceptancePredicate ) { gameObject.InternalDestroyAllChildrenGameObjects(toDestroy => { if (!acceptancePredicate(toDestroy)) { return; } toDestroy.Destroy(); }); } /// /// Immediately destroys all direct child GameObjects. /// public static void DestroyAllChildrenGameObjectsImmediately(this GameObject gameObject) => gameObject.InternalDestroyAllChildrenGameObjects(go => Object.DestroyImmediate(go)); /// /// Destroys all direct child GameObjects using Destroy (play mode safe). /// public static void PlayDestroyAllChildrenGameObjects(this GameObject gameObject) => gameObject.InternalDestroyAllChildrenGameObjects(go => go.Destroy()); /// /// Destroys all direct child GameObjects using Destroy (editor utility). /// public static void EditorDestroyAllChildrenGameObjects(this GameObject gameObject) => gameObject.InternalDestroyAllChildrenGameObjects(go => go.Destroy()); private static void InternalDestroyAllChildrenGameObjects( this GameObject gameObject, Action destroyFunction ) { for (int i = gameObject.transform.childCount - 1; 0 <= i; --i) { destroyFunction(gameObject.transform.GetChild(i).gameObject); } } /// /// Returns true if the GameObject represents a prefab asset or prefab stage content (Editor), or is not in a scene (Runtime). /// public static bool IsPrefab(this GameObject gameObject) { if (gameObject == null) { return false; } Scene scene = gameObject.scene; #if UNITY_EDITOR if ( scene.rootCount == 1 && string.Equals(scene.name, gameObject.name, StringComparison.Ordinal) ) { return true; } return PrefabUtility.GetPrefabAssetType(gameObject) switch { PrefabAssetType.NotAPrefab => false, PrefabAssetType.MissingAsset => scene.rootCount == 0, _ => true, }; #else return scene.rootCount == 0; #endif } /// /// Returns true if the component's GameObject is a prefab (see ). /// public static bool IsPrefab(this Component component) { if (component == null) { return false; } return component.gameObject.IsPrefab(); } /// /// Gets a component if present; otherwise adds and returns a new component of type . /// public static T GetOrAddComponent(this GameObject unityObject) where T : Component { if (!unityObject.TryGetComponent(out T instance)) { instance = unityObject.AddComponent(); } return instance; } /// /// Gets a component if present; otherwise adds and returns a new component of the specified type. /// public static Component GetOrAddComponent(this GameObject unityObject, Type componentType) { if (!unityObject.TryGetComponent(componentType, out Component instance)) { instance = unityObject.AddComponent(componentType); } return instance; } /// /// Modifies a prefab asset by opening its contents and saving the result (Editor only). If not a prefab, applies the modification to the instance. /// public static void ModifyAndSavePrefab(GameObject prefab, Action modifyAction) { if (prefab == null) { return; } #if UNITY_EDITOR if (PrefabUtility.IsPartOfPrefabAsset(prefab)) { string assetPath = AssetDatabase.GetAssetPath(prefab); GameObject content = PrefabUtility.LoadPrefabContents(assetPath); if (content == null) { Debug.LogError($"Unable to load {prefab} as a prefab", prefab); return; } modifyAction(content); _ = PrefabUtility.SaveAsPrefabAsset(content, assetPath); PrefabUtility.UnloadPrefabContents(content); } else { modifyAction(prefab); PrefabStage stage = PrefabStageUtility.GetPrefabStage(prefab); if (stage) { _ = EditorSceneManager.MarkSceneDirty(stage.scene); } } #endif } } }