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