// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Helper { using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using DataStructure.Adapters; using Random; using UnityEngine; using Utils; using Object = UnityEngine.Object; #if !SINGLE_THREADED using System.Collections.Concurrent; #else using WallstopStudios.UnityHelpers.Core.Extension; #endif #if UNITY_EDITOR using UnityEditor; using UnityEditorInternal; #endif /// /// General-purpose utilities and Unity-centric helpers. /// /// /// Scope: Cross-cutting helpers for gameplay, math, pooling, scene, sprites, and layers. /// Threading: Unless noted otherwise, methods that touch Unity APIs must run on the main thread. /// Performance: Many methods use pooled buffers (see Buffers<T>) to avoid allocations. /// public static partial class Helpers { #if SINGLE_THREADED private static readonly Dictionary< Type, Func > AwakeMethodsByType = new(); #else private static readonly ConcurrentDictionary< Type, Func > AwakeMethodsByType = new(); #endif private static readonly Object LogObject = new(); private static readonly Dictionary ObjectsByTag = new( StringComparer.Ordinal ); internal static readonly Dictionary CachedLabels = new( StringComparer.OrdinalIgnoreCase ); private static string[] CachedLayerNames = Array.Empty(); private static bool LayerCacheInitialized; #if UNITY_EDITOR internal static Func LayerNameProvider { get => _layerNameProvider ?? DefaultLayerNameProvider; set => _layerNameProvider = value; } private static Func _layerNameProvider; private static readonly Func DefaultLayerNameProvider = () => InternalEditorUtility.layers; internal static void ResetLayerNameProvider() { _layerNameProvider = null; } #else [System.Diagnostics.CodeAnalysis.SuppressMessage("UnusedMember.Local", "")] internal static void ResetLayerNameProvider() { } #endif internal static Func JitterSampler { get => _jitterSampler ?? DefaultJitterSampler; set => _jitterSampler = value; } private static Func _jitterSampler; private static readonly Func DefaultJitterSampler = maxDelay => { if (maxDelay <= 0f) { return 0f; } return PRNG.Instance.NextFloat(0f, maxDelay); }; internal static void ResetJitterSampler() { _jitterSampler = null; } internal static void ResetLayerCache() { CachedLayerNames = Array.Empty(); LayerCacheInitialized = false; } #if UNITY_EDITOR private static readonly string[] DefaultPrefabSearchFolders = { "Assets/Prefabs", "Assets/Resources", }; private static readonly string[] DefaultScriptableObjectSearchFolders = { "Assets/Prefabs", "Assets/Resources", "Assets/TileMaps", }; [InitializeOnLoadMethod] private static void RegisterProjectChangeHandlers() { EditorApplication.projectChanged -= HandleProjectChangedForHelpers; EditorApplication.projectChanged += HandleProjectChangedForHelpers; } internal static void HandleProjectChangedForHelpers() { ResetLayerCache(); ResetSpriteLabelCache(); } #endif [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] internal static void CLearLayerNames() { ResetLayerCache(); } /// /// Indicates whether Unity is running in batch mode (no graphics device, command-line mode). /// /// /// Useful for disabling editor-only or interactive-only behavior in build scripts and CI. /// public static bool IsRunningInBatchMode => Application.isBatchMode; /// /// Environment variable names commonly set by CI systems. /// Use these constants when checking for specific CI environments. /// public static class CiEnvironmentVariables { /// Generic CI indicator, set by many CI systems. public const string Ci = "CI"; /// GitHub Actions environment indicator. public const string GitHubActions = "GITHUB_ACTIONS"; /// GitLab CI environment indicator. public const string GitLabCi = "GITLAB_CI"; /// Jenkins URL, set when running in Jenkins. public const string JenkinsUrl = "JENKINS_URL"; /// Travis CI environment indicator. public const string TravisCi = "TRAVIS"; /// CircleCI environment indicator. public const string CircleCi = "CIRCLECI"; /// Azure Pipelines environment indicator. public const string AzurePipelines = "TF_BUILD"; /// TeamCity environment indicator. public const string TeamCity = "TEAMCITY_VERSION"; /// Buildkite environment indicator. public const string Buildkite = "BUILDKITE"; /// AWS CodeBuild environment indicator. public const string AwsCodeBuild = "CODEBUILD_BUILD_ID"; /// Bitbucket Pipelines environment indicator. public const string BitbucketPipelines = "BITBUCKET_BUILD_NUMBER"; /// AppVeyor environment indicator. public const string AppVeyor = "APPVEYOR"; /// Drone CI environment indicator. public const string DroneCi = "DRONE"; /// Unity-specific CI environment indicator. public const string UnityCi = "UNITY_CI"; /// Unity test runner environment indicator. public const string UnityTests = "UNITY_TESTS"; /// /// All environment variable names checked by . /// public static readonly string[] All = { Ci, GitHubActions, GitLabCi, JenkinsUrl, TravisCi, CircleCi, AzurePipelines, TeamCity, Buildkite, AwsCodeBuild, BitbucketPipelines, AppVeyor, DroneCi, UnityCi, UnityTests, }; } /// /// Indicates whether the process appears to be running under a CI system. /// /// /// /// Checks common CI environment variables including: /// /// CI (generic, set by many CI systems) /// GITHUB_ACTIONS (GitHub Actions) /// GITLAB_CI (GitLab CI) /// JENKINS_URL (Jenkins) /// TRAVIS (Travis CI) /// CIRCLECI (CircleCI) /// TF_BUILD (Azure Pipelines) /// TEAMCITY_VERSION (TeamCity) /// BUILDKITE (Buildkite) /// CODEBUILD_BUILD_ID (AWS CodeBuild) /// BITBUCKET_BUILD_NUMBER (Bitbucket Pipelines) /// APPVEYOR (AppVeyor) /// DRONE (Drone CI) /// UNITY_CI (Unity-specific CI) /// UNITY_TESTS (Unity test runner) /// /// /// /// Environment variables are checked on each access. While this involves system calls, /// the overhead is negligible and ensures accurate detection if variables change. /// /// public static bool IsRunningInContinuousIntegration { get { foreach (string envVar in CiEnvironmentVariables.All) { if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(envVar))) { return true; } } return false; } } /// /// Checks if a specific environment variable is set to a non-empty, non-whitespace value. /// /// The name of the environment variable to check. /// True if the environment variable is set to a non-empty, non-whitespace value; otherwise, false. public static bool IsEnvironmentVariableSet(string environmentVariableName) { return !string.IsNullOrWhiteSpace( Environment.GetEnvironmentVariable(environmentVariableName) ); } internal static string[] AllSpriteLabels { get; private set; } = Array.Empty(); #if UNITY_EDITOR private static bool SpriteLabelCacheInitialized; #endif /// /// Gets all unique sprite labels in the project (Editor only). /// /// /// Returns an empty array in batch/CI or at runtime player. Results are cached and sorted. /// public static string[] GetAllSpriteLabelNames() { if (IsRunningInContinuousIntegration || IsRunningInBatchMode) { return Array.Empty(); } #if UNITY_EDITOR if (SpriteLabelCacheInitialized) { return AllSpriteLabels; } using PooledResource> labelBuffer = Buffers.List.Get( out List labels ); CollectSpriteLabels(labels); return AllSpriteLabels; #else return Array.Empty(); #endif } /// /// Copies all unique sprite labels into (Editor only). /// /// Destination list which will be cleared first. /// /// Returns without changes in batch/CI or at runtime player. Results are cached and sorted. /// public static void GetAllSpriteLabelNames(List destination) { if (destination == null) { throw new ArgumentNullException(nameof(destination)); } destination.Clear(); if (IsRunningInContinuousIntegration || IsRunningInBatchMode) { return; } #if UNITY_EDITOR string[] cached = SpriteLabelCacheInitialized ? AllSpriteLabels : GetAllSpriteLabelNames(); if (cached.Length == 0) { return; } destination.AddRange(cached); #else _ = destination; #endif } /// /// Gets all defined Unity layer names. /// /// /// Uses InternalEditorUtility.layers in Editor with caching, otherwise queries LayerMask.LayerToName. /// public static string[] GetAllLayerNames() { if (LayerCacheInitialized && CachedLayerNames != null) { return CachedLayerNames; } #if UNITY_EDITOR try { // Prefer the editor API when available string[] editorLayers = LayerNameProvider?.Invoke(); if (editorLayers is { Length: > 0 }) { LayerCacheInitialized = true; CachedLayerNames = editorLayers; return editorLayers; } } catch { // Fall through to runtime-safe fallback below } #endif if (!Application.isEditor && Application.isPlaying && LayerCacheInitialized) { return CachedLayerNames; } using PooledResource> layerBuffer = Buffers.List.Get( out List layers ); for (int i = 0; i < 32; ++i) { string name = LayerMask.LayerToName(i); if (!string.IsNullOrEmpty(name)) { layers.Add(name); } } LayerCacheInitialized = true; int layerCount = layers.Count; if (layerCount == 0) { CachedLayerNames = Array.Empty(); return CachedLayerNames; } if (CachedLayerNames == null || CachedLayerNames.Length != layerCount) { CachedLayerNames = new string[layerCount]; } for (int i = 0; i < layerCount; ++i) { CachedLayerNames[i] = layers[i]; } return CachedLayerNames; } /// /// Copies all layer names into . /// /// Destination list which will be cleared first. public static void GetAllLayerNames(List destination) { if (destination == null) { throw new ArgumentNullException(nameof(destination)); } destination.Clear(); string[] layers = GetAllLayerNames(); if (layers.Length == 0) { return; } destination.AddRange(layers); } #if UNITY_EDITOR private static void CollectSpriteLabels(List destination) { destination.Clear(); using PooledResource> labelSetResource = Buffers.HashSet.Get( out HashSet labelSet ); string[] guids = AssetDatabase.FindAssets("t:Sprite"); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); if (!path.StartsWith("Assets", StringComparison.OrdinalIgnoreCase)) { continue; } Object asset = AssetDatabase.LoadMainAssetAtPath(path); if (asset == null) { continue; } string[] labels = AssetDatabase.GetLabels(asset); if (labels.Length == 0) { continue; } CachedLabels[path] = labels; labelSet.UnionWith(labels); } if (labelSet.Count == 0) { SetSpriteLabelCache(Array.Empty(), alreadySorted: true); return; } destination.AddRange(labelSet); destination.Sort(StringComparer.Ordinal); SetSpriteLabelCache(destination, alreadySorted: true); } #endif internal static void SetSpriteLabelCache( IReadOnlyCollection labels, bool alreadySorted = false ) { if (labels == null || labels.Count == 0) { AllSpriteLabels = Array.Empty(); #if UNITY_EDITOR SpriteLabelCacheInitialized = true; #endif return; } string[] cache = AllSpriteLabels.Length == labels.Count ? AllSpriteLabels : new string[labels.Count]; if (labels is IReadOnlyList list) { for (int i = 0; i < list.Count; ++i) { cache[i] = list[i]; } } else { int index = 0; foreach (string label in labels) { cache[index++] = label; } } if (!alreadySorted) { Array.Sort(cache, StringComparer.Ordinal); } AllSpriteLabels = cache; #if UNITY_EDITOR SpriteLabelCacheInitialized = true; #endif } internal static void ResetSpriteLabelCache() { #if UNITY_EDITOR SpriteLabelCacheInitialized = false; #endif AllSpriteLabels = Array.Empty(); } // https://gamedevelopment.tutsplus.com/tutorials/unity-solution-for-hitting-moving-targets--cms-29633 /// /// Computes a lead position for a moving target given projectile speed. /// /// Target GameObject to aim at. /// Projectile launch position. /// Projectile speed (units/second). /// If false, returns current target position. /// Estimated target linear velocity. /// World position to aim at. Falls back to current target position if prediction fails. /// /// /// // Aim turret with predictive lead /// Vector2 aimPoint = target.PredictCurrentTarget(turret.position, projectileSpeed: 25f, predictiveFiring: true, targetVelocity); /// turret.transform.up = (aimPoint - (Vector2)turret.position).normalized; /// /// public static Vector2 PredictCurrentTarget( this GameObject currentTarget, Vector2 launchLocation, float projectileSpeed, bool predictiveFiring, Vector2 targetVelocity ) { Vector2 target = currentTarget.transform.position; if (!predictiveFiring) { return target; } if (projectileSpeed <= 0) { return target; } float a = targetVelocity.x * targetVelocity.x + targetVelocity.y * targetVelocity.y - projectileSpeed * projectileSpeed; float b = 2 * ( targetVelocity.x * (target.x - launchLocation.x) + targetVelocity.y * (target.y - launchLocation.y) ); float c = (target.x - launchLocation.x) * (target.x - launchLocation.x) + (target.y - launchLocation.y) * (target.y - launchLocation.y); float disc = b * b - 4 * a * c; if (disc < 0) { return target; } float t1 = (-1 * b + Mathf.Sqrt(disc)) / (2 * a); float t2 = (-1 * b - Mathf.Sqrt(disc)) / (2 * a); float t = Mathf.Max(t1, t2); // let us take the larger time value float aimX = target.x + targetVelocity.x * t; float aimY = target.y + targetVelocity.y * t; if (float.IsNaN(aimX) || float.IsNaN(aimY)) { return target; } if (float.IsInfinity(aimX) || float.IsInfinity(aimY)) { return target; } return new Vector2(aimX, aimY); } /// /// Gets a component of type from a GameObject or Component reference. /// Returns default when is neither. /// public static T GetComponent(this Object target) { return target switch { GameObject go => go != null ? go.GetComponent() : default, Component c => c != null ? c.GetComponent() : default, _ => default, }; } /// /// Gets all components of type from a GameObject or Component reference. /// Returns an empty array when no match is found. /// public static T[] GetComponents(this Object target) { return target switch { GameObject go => go != null ? go.GetComponents() : Array.Empty(), Component c => c != null ? c.GetComponents() : Array.Empty(), _ => Array.Empty(), }; } /// /// Gets all components of type into a provided buffer, avoiding allocations. /// /// Destination buffer which is cleared first. public static List GetComponents(this Object target, List buffer) { if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); } buffer.Clear(); switch (target) { case GameObject go when go != null: go.GetComponents(buffer); break; case Component component when component != null: component.GetComponents(buffer); break; } return buffer; } /// /// Extracts a GameObject from either a GameObject or Component instance; returns null otherwise. /// public static GameObject GetGameObject(this object target) { return target switch { GameObject go => go, Component c => c != null ? c.gameObject : null, _ => null, }; } /// /// Tries to get a component of type from a GameObject or Component. /// public static bool TryGetComponent(this Object target, out T component) { component = default; return target switch { GameObject go => go != null && go.TryGetComponent(out component), Component c => c != null && c.TryGetComponent(out component), _ => false, }; } /// /// Recursively searches the child hierarchy (including self) for the first GameObject with the specified tag. /// public static GameObject FindChildGameObjectWithTag(this GameObject gameObject, string tag) { using PooledResource> bufferResource = Buffers.List.Get( out List transforms ); foreach ( Transform t in gameObject.transform.IterateOverAllChildrenRecursively( transforms, includeSelf: true ) ) { GameObject go = t.gameObject; if (go.CompareTag(tag)) { return go; } } return null; } /// /// Repeatedly invokes an action at the specified update rate using a coroutine. /// /// Interval in seconds between invocations. /// If true, applies a single randomized initial delay up to . /// If true, waits one interval before the first invocation. /// The started coroutine. /// /// /// // Poll a service every 0.5s with staggered start /// this.StartFunctionAsCoroutine(CheckHealth, 0.5f, useJitter: true); /// /// public static Coroutine StartFunctionAsCoroutine( this MonoBehaviour monoBehaviour, Action action, float updateRate, bool useJitter = false, bool waitBefore = false ) { if (action == null) { throw new ArgumentNullException(nameof(action)); } return monoBehaviour.StartCoroutine( FunctionAsCoroutine(action, updateRate, useJitter, waitBefore) ); } private static IEnumerator FunctionAsCoroutine( Action action, float updateRate, bool useJitter, bool waitBefore ) { float interval = ResolveInvocationDelay(updateRate); float initialDelay = waitBefore ? interval : 0f; if (useJitter) { initialDelay += SampleInitialJitter(interval); } if (initialDelay > 0f) { yield return WaitForDelay(initialDelay); } while (true) { action(); if (interval <= 0f) { yield return null; continue; } yield return WaitForDelay(interval); } } private static float ResolveInvocationDelay(float baseDelay) { return Mathf.Max(0f, baseDelay); } private static float SampleInitialJitter(float interval) { if (interval <= 0f) { return 0f; } float jitter = JitterSampler(interval); if (float.IsNaN(jitter) || jitter <= 0f) { return 0f; } return Mathf.Min(jitter, interval); } private static IEnumerator WaitForDelay(float duration) { float clamped = Mathf.Max(0f, duration); float startTime = Time.time; do { yield return null; } while (!HasEnoughTimePassed(startTime, clamped)); } public static Coroutine ExecuteFunctionAfterDelay( this MonoBehaviour monoBehaviour, Action action, float delay ) { if (action == null) { throw new ArgumentNullException(nameof(action)); } return monoBehaviour.StartCoroutine(FunctionDelayAsCoroutine(action, delay)); } public static Coroutine ExecuteFunctionNextFrame( this MonoBehaviour monoBehaviour, Action action ) { if (action == null) { throw new ArgumentNullException(nameof(action)); } return monoBehaviour.ExecuteFunctionAfterDelay(action, 0f); } public static Coroutine ExecuteFunctionAfterFrame( this MonoBehaviour monoBehaviour, Action action ) { if (action == null) { throw new ArgumentNullException(nameof(action)); } return monoBehaviour.StartCoroutine(FunctionAfterFrame(action)); } public static IEnumerator ExecuteOverTime( Action action, int totalCount, float duration, bool delay = true ) { if (action == null) { yield break; } if (totalCount <= 0) { yield break; } int totalExecuted = 0; float startTime = Time.time; while (!HasEnoughTimePassed(startTime, duration)) { float percent = (Time.time - startTime) / duration; // optional delay execution from happening on 0, 1, 2, ... n-1 to 1, 2, ... n if ( totalExecuted < totalCount && (totalExecuted + (delay ? 1f : 0f)) / totalCount <= percent ) { action(); ++totalExecuted; } yield return null; } for (; totalExecuted < totalCount; ) { action(); ++totalExecuted; yield return null; } } private static IEnumerator FunctionDelayAsCoroutine(Action action, float delay) { float startTime = Time.time; while (!HasEnoughTimePassed(startTime, delay)) { yield return null; } action(); } private static IEnumerator FunctionAfterFrame(Action action) { yield return Buffers.WaitForEndOfFrame; action(); } public static bool HasEnoughTimePassed(float timestamp, float desiredDuration) { return timestamp + desiredDuration < Time.time; } public static Vector2 Opposite(this Vector2 vector) { return vector * -1; } public static Vector3 Opposite(this Vector3 vector) { return vector * -1; } public static IEnumerable IterateArea(this BoundsInt bounds) { foreach (Vector3Int position in bounds.allPositionsWithin) { yield return position; } } public static IEnumerable IterateBounds(this BoundsInt bounds, int padding = 1) { int xStart = bounds.xMin - padding; int xEnd = bounds.xMax + padding; int yStart = bounds.yMin - padding; int yEnd = bounds.yMax + padding; for (int x = xStart; x <= xEnd; ++x) { for (int y = yStart; y <= yEnd; ++y) { yield return new Vector3Int(x, y, 0); } } } public static Vector3Int AsVector3Int(this (int x, int y, int z) vector) { return new Vector3Int(vector.x, vector.y, vector.z); } public static Vector3Int AsVector3Int(this (uint x, uint y, uint z) vector) { return new Vector3Int((int)vector.x, (int)vector.y, (int)vector.z); } public static Vector3Int AsVector3Int(this Vector3 vector) { return new Vector3Int( (int)Math.Round(vector.x), (int)Math.Round(vector.y), (int)Math.Round(vector.z) ); } /// /// Converts a tuple to a Vector3. /// public static Vector3 AsVector3(this (uint x, uint y, uint z) vector) { return new Vector3(vector.x, vector.y, vector.z); } /// /// Converts a Vector3Int to Vector3. /// public static Vector3 AsVector3(this Vector3Int vector) { return new Vector3(vector.x, vector.y, vector.z); } /// /// Converts a Vector3Int to Vector2 by dropping Z. /// public static Vector2 AsVector2(this Vector3Int vector) { return new Vector2(vector.x, vector.y); } /// /// Converts a Vector3Int to Vector2Int by dropping Z. /// public static Vector2Int AsVector2Int(this Vector3Int vector) { return new Vector2Int(vector.x, vector.y); } /// /// Converts a Vector2Int to Vector3Int (Z = 0). /// public static Vector3Int AsVector3Int(this Vector2Int vector) { return new Vector3Int(vector.x, vector.y); } /// /// Converts a BoundsInt to a 2D Rect using X/Y and size. /// public static Rect AsRect(this BoundsInt bounds) { return new Rect(bounds.x, bounds.y, bounds.size.x, bounds.size.y); } /// /// Returns a uniformly random point inside a circle. /// /// Circle center. /// Circle radius. /// Optional RNG; defaults to PRNG.Instance. public static Vector2 GetRandomPointInCircle( Vector2 center, float radius, IRandom random = null ) { random ??= PRNG.Instance; double radiusAbs = Math.Abs(radius); if (radiusAbs <= 0f) { return center; } double radiusSample = ClampUnitInterval(random.NextDouble()); double angleSample = ClampUnitInterval(random.NextDouble()); double r = radiusAbs * Math.Sqrt(radiusSample); double theta = angleSample * 2 * Math.PI; return new Vector2( center.x + (float)(r * Math.Cos(theta)), center.y + (float)(r * Math.Sin(theta)) ); } /// /// Returns a uniformly random point inside a sphere. /// /// Sphere center. /// Sphere radius. /// Optional RNG; defaults to PRNG.Instance. public static Vector3 GetRandomPointInSphere( Vector3 center, float radius, IRandom random = null ) { random ??= PRNG.Instance; double radiusAbs = Math.Abs(radius); if (radiusAbs <= 0f) { return center; } double thetaSample = ClampUnitInterval(random.NextDouble()); double phiSample = ClampUnitInterval(random.NextDouble()); double radiusSample = ClampUnitInterval(random.NextDouble()); double theta = 2 * Math.PI * thetaSample; double phiArgument = 2 * phiSample - 1; if (phiArgument < -1) { phiArgument = -1; } else if (phiArgument > 1) { phiArgument = 1; } double phi = Math.Acos(phiArgument); double r = radiusAbs * Math.Pow(radiusSample, 1.0 / 3.0); double sinPhi = Math.Sin(phi); return new Vector3( center.x + (float)(r * sinPhi * Math.Cos(theta)), center.y + (float)(r * sinPhi * Math.Sin(theta)), center.z + (float)(r * Math.Cos(phi)) ); } /// /// Gets the first child (or self) with the Player tag. /// public static GameObject GetPlayerObjectInChildHierarchy( this GameObject gameObject, string playerTag = "Player" ) { return gameObject.GetTagObjectInChildHierarchy(playerTag); } /// /// Gets the first child (or self) with a specific tag. /// public static GameObject GetTagObjectInChildHierarchy( this GameObject gameObject, string tag ) { using PooledResource> bufferResource = Buffers.List.Get( out List transforms ); foreach ( Transform t in gameObject.transform.IterateOverAllChildrenRecursively( transforms, includeSelf: true ) ) { GameObject go = t.gameObject; if (go.CompareTag(tag)) { return go; } } return null; } //https://answers.unity.com/questions/722748/refreshing-the-polygon-collider-2d-upon-sprite-cha.html /// /// Updates a PolygonCollider2D's shape to match the current SpriteRenderer sprite. /// /// /// Useful when changing sprites at runtime and needing the collider to match the new shape. /// public static void UpdateShapeToSprite(this Component component) { if ( !component.TryGetComponent(out SpriteRenderer spriteRenderer) || !component.TryGetComponent(out PolygonCollider2D collider) ) { return; } UpdateShapeToSprite(spriteRenderer.sprite, collider); } /// /// Updates a PolygonCollider2D to match a given Sprite's physics shape. /// public static void UpdateShapeToSprite(Sprite sprite, PolygonCollider2D collider) { if (sprite == null || collider == null) { return; } int pathCount = collider.pathCount = sprite.GetPhysicsShapeCount(); using PooledResource> pathResource = Buffers.List.Get( out List path ); for (int i = 0; i < pathCount; ++i) { path.Clear(); _ = sprite.GetPhysicsShape(i, path); collider.SetPath(i, path); } } /// /// 3D cross product for Vector3Int. /// public static Vector3Int Cross(this Vector3Int vector, Vector3Int other) { int x = vector.y * other.z - other.y * vector.z; int y = (vector.x * other.z - other.x * vector.z) * -1; int z = vector.x * other.y - other.x * vector.y; return new Vector3Int(x, y, z); } /// /// Walks up the hierarchy (including self) to find the nearest GameObject with a component of type . /// public static GameObject TryGetClosestParentWithComponentIncludingSelf( this GameObject current ) where T : Component { while (current != null) { if (current.HasComponent()) { return current; } Transform parent = current.transform.parent; current = parent != null ? parent.gameObject : null; } return null; } #if UNITY_EDITOR private static string[] PrepareSearchFolders( IEnumerable assetPaths, string[] defaultFolders, out PooledResource> listResource, out PooledArray arrayResource ) { listResource = default; arrayResource = default; if (assetPaths == null) { return defaultFolders; } if (assetPaths is string[] array) { return array; } if (assetPaths is IReadOnlyList readonlyList) { arrayResource = SystemArrayPool.Get( readonlyList.Count, out string[] buffer ); for (int i = 0; i < readonlyList.Count; i++) { string path = readonlyList[i]; buffer[i] = path; } return buffer; } if (assetPaths is ICollection collection) { arrayResource = SystemArrayPool.Get(collection.Count, out string[] buffer); collection.CopyTo(buffer, 0); return buffer; } listResource = Buffers.List.Get(out List list); list.AddRange(assetPaths); arrayResource = SystemArrayPool.Get(list.Count, out string[] temp); list.CopyTo(temp); return temp; } /// /// Enumerates Prefab assets in the project (Editor only). Uses search folders when provided. /// public static IEnumerable EnumeratePrefabs( IEnumerable assetPaths = null ) { string[] searchFolders = PrepareSearchFolders( assetPaths, DefaultPrefabSearchFolders, out PooledResource> pathListResource, out PooledArray pathArrayResource ); try { foreach (string assetGuid in AssetDatabase.FindAssets("t:prefab", searchFolders)) { string path = AssetDatabase.GUIDToAssetPath(assetGuid); GameObject go = AssetDatabase.LoadAssetAtPath(path); if (go != null) { yield return go; } } } finally { pathArrayResource.Dispose(); pathListResource.Dispose(); } } /// /// Enumerates ScriptableObject assets of type in the project (Editor only). /// public static IEnumerable EnumerateScriptableObjects( IEnumerable assetPaths = null ) where T : ScriptableObject { string[] searchFolders = PrepareSearchFolders( assetPaths, DefaultScriptableObjectSearchFolders, out PooledResource> pathListResource, out PooledArray pathArrayResource ); try { foreach ( string assetGuid in AssetDatabase.FindAssets( "t:" + typeof(T).Name, searchFolders ) ) { string path = AssetDatabase.GUIDToAssetPath(assetGuid); T so = AssetDatabase.LoadAssetAtPath(path); if (so != null) { yield return so; } } } finally { pathArrayResource.Dispose(); pathListResource.Dispose(); } } #endif public static bool NameEquals(Object lhs, Object rhs) { if (lhs == rhs) { return true; } if (lhs == null || rhs == null) { return false; } if (string.Equals(lhs.name, rhs.name, StringComparison.Ordinal)) { return true; } string lhsNormalized = NormalizeCloneName(lhs.name); string rhsNormalized = NormalizeCloneName(rhs.name); return string.Equals(lhsNormalized, rhsNormalized, StringComparison.Ordinal); } private static string NormalizeCloneName(string name) { if (string.IsNullOrEmpty(name)) { return string.Empty; } string normalized = name; const string clone = "(Clone)"; while (true) { string trimmedEnd = normalized.TrimEnd(); if (!trimmedEnd.EndsWith(clone, StringComparison.Ordinal)) { normalized = trimmedEnd; break; } normalized = trimmedEnd.Substring(0, trimmedEnd.Length - clone.Length); } return normalized.Trim(); } public static Color ChangeColorBrightness(this Color color, float correctionFactor) { correctionFactor = Math.Clamp(correctionFactor, -1f, 1f); float red = color.r; float green = color.g; float blue = color.b; if (correctionFactor < 0) { correctionFactor += 1; red *= correctionFactor; green *= correctionFactor; blue *= correctionFactor; } else { red = (1f - red) * correctionFactor + red; green = (1f - green) * correctionFactor + green; blue = (1f - blue) * correctionFactor + blue; } return new Color(red, green, blue, color.a); } /// /// Invokes Awake() on all components in the GameObject's hierarchy. /// /// /// Primarily useful in tests and tooling to simulate lifecycle when instantiating objects dynamically. /// public static void AwakeObject(this GameObject gameObject) { using PooledResource> componentResource = Buffers.List.Get(out List components); gameObject.GetComponentsInChildren(false, components); foreach (MonoBehaviour script in components) { Func awakeInfo = AwakeMethodsByType.GetOrAdd( script.GetType(), type => { MethodInfo[] methods = type.GetMethods( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); foreach (MethodInfo method in methods) { if ( string.Equals(method.Name, "Awake", StringComparison.Ordinal) && method.GetParameters().Length == 0 ) { return ReflectionHelpers.GetMethodInvoker(method); } } return null; } ); _ = awakeInfo?.Invoke(script, null); } } /// /// Rotates a direction vector toward a target direction at a fixed angular speed. /// /// Desired direction (normalized or not). /// Current direction (normalized or not). /// Degrees per second. /// New normalized direction after applying rotation for the current frame. /// /// /// Vector2 facing = Vector2.right; /// facing = Helpers.GetAngleWithSpeed(target - position, facing, 180f); /// /// public static Vector2 GetAngleWithSpeed( Vector2 targetDirection, Vector2 currentDirection, float rotationSpeed ) { if (targetDirection == Vector2.zero) { return currentDirection; } float turnRatePerFrame = rotationSpeed * Time.deltaTime; float angleDiscrepancy = Vector2.SignedAngle(currentDirection, targetDirection); float turnRateThisFrame; if (Math.Sign(angleDiscrepancy) < 0) { turnRateThisFrame = -1 * Math.Min(turnRatePerFrame, -angleDiscrepancy); } else { turnRateThisFrame = Math.Min(turnRatePerFrame, angleDiscrepancy); } float currentAngle = Vector2.SignedAngle(Vector2.right, currentDirection); currentAngle += turnRateThisFrame; return (Quaternion.AngleAxis(currentAngle, Vector3.forward) * Vector3.right).normalized; } /// /// Expands a 2D BoundsInt to include the given X/Y position. /// public static void Extend2D(ref BoundsInt bounds, FastVector3Int position) { if (position.x < bounds.xMin) { bounds.xMin = position.x; } if (bounds.xMax < position.x) { bounds.xMax = position.x; } if (position.y < bounds.yMin) { bounds.yMin = position.y; } if (bounds.yMax < position.y) { bounds.yMax = position.y; } } } }