// 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
}
}
}