Unity Helpers

Logo

Treasure chest of Unity developer tools. Professional inspector tooling, high-performance utilities, spatial queries, and 20+ editor tools.

Helper Utilities Guide

TL;DR — Why Use These

Static helper classes and utilities that solve common programming problems without needing components on GameObjects. Use these for predictive aiming, path utilities, threading, hashing, formatting, and more.


Contents


Coroutine Wait Pools

Unity allocates a new WaitForSeconds/WaitForSecondsRealtime every time you yield with a literal. Buffers.GetWaitForSeconds(...) and Buffers.GetWaitForSecondsRealTime(...) pool those instructions so coroutines stay allocation free, but each distinct duration used to stick around forever. Large ranges (randomized cooldowns, tweens, etc.) could leak thousands of instances.

New pooling policy knobs (Runtime 2.2.1+):

Setting Default Purpose
Buffers.WaitInstructionMaxDistinctEntries 512 Upper bound on distinct cached durations. Set to 0 to disable the cap, or tighten it for editor/dev builds. When the limit is reached the cache stops growing (or evicts, if LRU is enabled).
Buffers.WaitInstructionQuantizationStepSeconds 0 (off) Rounds requested durations to the nearest step before caching. Useful when you can tolerate millisecond snapping (e.g., .005f.01f).
Buffers.WaitInstructionUseLruEviction false When true, the cache becomes an LRU: it evicts the least recently used duration whenever it hits the max entry count instead of rejecting new ones. Diagnostics expose the eviction count.
Buffers.TryGetWaitForSecondsPooled(float seconds) / TryGetWaitForSecondsRealtimePooled n/a Returns the cached instruction or null if the request would exceed the cap. Use this when you want to detect “unsafe” usages and allocate manually instead.
Buffers.WaitForSecondsCacheDiagnostics / .WaitForSecondsRealtimeCacheDiagnostics snapshot Exposes DistinctEntries, MaxDistinctEntries, LimitRefusals, and whether quantization is active so you can surface metrics in your own tooling.

⚙️ Project-wide defaults: Open the Coroutine Wait Instruction Buffers foldout under Project Settings ▸ Wallstop Studios ▸ Unity Helpers to edit these knobs. The settings asset lives at Resources/Wallstop Studios/Unity Helpers/UnityHelpersBufferSettings.asset, ships with your build, and automatically applies on script/domain reload or when a player starts (unless your code overrides the values at runtime). Use Apply Defaults Now to push the current sliders into the active domain or Capture Current Values to snapshot whatever Buffers is using in play mode.

🔒 Persistence Behavior: When you click Apply Defaults Now, the settings are immediately:

  1. Saved to disk — The asset is marked dirty and saved via AssetDatabase.SaveAssets()
  2. Applied to the runtimeBuffers.WaitInstruction* properties are updated immediately

This ensures settings persist across:

  • Domain reloads (script recompilation, entering/exiting play mode) — Via [InitializeOnLoadMethod]
  • Editor restarts — The asset is saved to disk and reloads automatically
  • Standalone builds — The asset ships under Resources/ and auto-applies via [RuntimeInitializeOnLoadMethod]

Toggle Apply On Load to control whether the saved defaults auto-apply when the domain loads. If disabled, the asset serves as a reference and you must call asset.ApplyToBuffers() manually.

// Clamp the cache to 128 distinct waits, quantize to milliseconds, and reuse LRU entries.
Buffers.WaitInstructionMaxDistinctEntries = 128;
Buffers.WaitInstructionQuantizationStepSeconds = 0.001f;
Buffers.WaitInstructionUseLruEviction = true;

IEnumerator WeaponCooldown(Func<float> cooldownSeconds)
{
    float waitSeconds = cooldownSeconds();

    // Prefer pooled waits, but fall back to a fresh instance if the cache refuses it.
    WaitForSeconds pooled = Buffers.TryGetWaitForSecondsPooled(waitSeconds)
        ?? new WaitForSeconds(waitSeconds);

    yield return pooled;
}

void OnGUI()
{
    WaitInstructionCacheDiagnostics stats = Buffers.WaitForSecondsCacheDiagnostics;
    GUILayout.Label(
        $"Wait cache: {stats.DistinctEntries}/{stats.MaxDistinctEntries} (refusals={stats.LimitRefusals}, evictions={stats.Evictions})"
    );
}

⚠️ Limit warnings: In Editor and Development builds the first limit hit (and every 25th after) emits a warning so you can spot misuses quickly. Production builds skip the log to avoid noise.

Deterministic fallback: When the cache refuses a duration, Buffers.GetWaitForSeconds* still returns a valid instruction—it just isn’t cached, so highly variable waits no longer lead to unbounded memory growth.


Gameplay Helpers

Predictive Aiming

What it does: Calculates where to aim when shooting at a moving target, accounting for projectile travel time.

Problem it solves: Shooting a bullet at where an enemy is misses if they’re moving. You need to aim at where they will be.

using WallstopStudios.UnityHelpers.Core.Helper;

Vector2 enemyPos = enemy.transform.position;
Vector2 enemyVelocity = enemy.GetComponent<Rigidbody2D>().velocity;
Vector2 turretPos = turret.transform.position;
float bulletSpeed = 20f;

Vector2? aimPosition = Helpers.PredictCurrentTarget(
    enemyPos,
    enemyVelocity,
    turretPos,
    bulletSpeed
);

if (aimPosition.HasValue)
{
    // Aim at aimPosition to hit the moving target
    Vector2 aimDirection = (aimPosition.Value - turretPos).normalized;
    FireProjectile(aimDirection, bulletSpeed);
}
else
{
    // Target is too fast, can't hit
}

When to use:

When NOT to use:


Spatial Sampling

Get random points in circles/spheres:

using WallstopStudios.UnityHelpers.Core.Helper;

// Random point inside circle (uniform distribution)
Vector2 spawnPoint = Helpers.GetRandomPointInCircle(center, radius);

// Random point inside sphere (uniform distribution)
Vector3 explosionPoint = Helpers.GetRandomPointInSphere(center, radius);

Use for:


Smooth Rotation Helpers

Get rotation speed for smooth turning:

using WallstopStudios.UnityHelpers.Core.Helper;

// Calculate how much to rotate this frame toward target
float currentAngle = transform.eulerAngles.z;
float targetAngle = GetTargetAngle();
float maxDegreesPerSecond = 180f;

float newAngle = Helpers.GetAngleWithSpeed(
    currentAngle,
    targetAngle,
    maxDegreesPerSecond,
    Time.deltaTime
);

transform.eulerAngles = new Vector3(0, 0, newAngle);

Handles:


Delayed Execution

Execute code after delay or next frame:

using WallstopStudios.UnityHelpers.Core.Helper;

// Execute after 2 seconds
Helpers.ExecuteFunctionAfterDelay(
    monoBehaviour,
    () => Debug.Log("Delayed!"),
    delayInSeconds: 2f
);

// Execute next frame
Helpers.ExecuteFunctionNextFrame(
    monoBehaviour,
    () => Debug.Log("Next frame!")
);

Uses coroutines under the hood.


Repeating Execution with Jitter

Run function repeatedly with random timing variance:

using WallstopStudios.UnityHelpers.Core.Helper;

// Spawn enemy every 5-8 seconds
Helpers.StartFunctionAsCoroutine(
    gameManager,
    SpawnEnemy,
    baseInterval: 5f,
    intervalJitter: 3f  // Random ±3 seconds
);

void SpawnEnemy()
{
    Instantiate(enemyPrefab, spawnPoint.position, Quaternion.identity);
}

Use for:


Layer & Label Queries

using WallstopStudios.UnityHelpers.Core.Helper;

// Get all layer names (cached after first call)
string[] allLayers = Helpers.GetAllLayerNames();

// Get all sprite label names (editor only, cached)
string[] labels = Helpers.GetAllSpriteLabelNames();

Use for:


Collider Syncing

Update PolygonCollider2D to match sprite:

using WallstopStudios.UnityHelpers.Core.Helper;

SpriteRenderer renderer = GetComponent<SpriteRenderer>();
PolygonCollider2D collider = GetComponent<PolygonCollider2D>();

Helpers.UpdateShapeToSprite(renderer, collider);
// Collider now matches sprite's physics shape

GameObject & Component Helpers

Cached Component Lookup

Fast tag-based component finding with caching:

using WallstopStudios.UnityHelpers.Core.Helper;

// First call searches scene, subsequent calls use cache
Player player = Helpers.Find<Player>("Player");

// Clear cache manually if needed
Helpers.ClearInstance<Player>();

// Set cache manually (for dependency injection scenarios)
Helpers.SetInstance(playerInstance);

Performance: First call = GameObject.FindWithTag, subsequent calls = O(1) dictionary lookup.


Component Existence Checks

using WallstopStudios.UnityHelpers.Core.Helper;

// Check if component exists without allocating
bool hasRigidbody = Helpers.HasComponent<Rigidbody2D>(gameObject);

// Better than:
bool hasRigidbody = GetComponent<Rigidbody2D>() != null; // Allocates

Get-or-Add Pattern

using WallstopStudios.UnityHelpers.Core.Helper;

// Get existing component or add if missing
Rigidbody2D rb = Helpers.GetOrAddComponent<Rigidbody2D>(gameObject);

Hierarchical Enable/Disable

Recursively enable/disable components:

using WallstopStudios.UnityHelpers.Core.Helper;

// Enable all Collider2D components in children
Helpers.EnableRecursively<Collider2D>(rootObject, enable: true);

// Disable all renderers in hierarchy
Helpers.EnableRendererRecursively<SpriteRenderer>(rootObject, enable: false);

Use for:


Bulk Child Destruction

using WallstopStudios.UnityHelpers.Core.Helper;

// Destroy all children (useful for clearing containers)
Helpers.DestroyAllChildrenGameObjects(parentTransform);

Use for:


Smart Destruction

Editor/runtime aware destruction:

using WallstopStudios.UnityHelpers.Core.Helper;

// Uses DestroyImmediate in editor, Destroy in play mode
Helpers.SmartDestroy(gameObject);

// Also handles assets correctly (won't destroy project assets)

Use in editor tools to avoid “Destroying assets is not permitted” errors.


Prefab Utilities

using WallstopStudios.UnityHelpers.Core.Helper;

// Check if GameObject is a prefab asset or instance
bool isPrefab = Helpers.IsPrefab(gameObject);

// Safely modify prefab (editor only)
#if UNITY_EDITOR
Helpers.ModifyAndSavePrefab(prefabAssetPath, prefab =>
{
    // Modify prefab here
    var component = prefab.AddComponent<MyComponent>();
    component.value = 42;
    // Changes saved automatically
});
#endif

Transform Helpers

Hierarchy Traversal (Depth-First)

Visit all children recursively:

using WallstopStudios.UnityHelpers.Core.Helper;

// Depth-first traversal (visits deepest children first)
Helpers.IterateOverAllChildrenRecursively<SpriteRenderer>(rootTransform, renderer =>
{
    renderer.color = Color.red;
});

// Buffered version (zero allocation)
using (var buffer = Buffers<Transform>.List.Get())
{
    Helpers.IterateOverAllChildrenRecursively(rootTransform, buffer.Value);
    foreach (Transform child in buffer.Value)
    {
        // Process children
    }
}

Hierarchy Traversal (Breadth-First)

Visit by depth level:

using WallstopStudios.UnityHelpers.Core.Helper;

// Breadth-first traversal with depth limit
Helpers.IterateOverAllChildrenRecursivelyBreadthFirst(
    rootTransform,
    transform => Debug.Log(transform.name),
    maxDepth: 3  // Only visit 3 levels deep
);

Use for:


Parent Traversal

Walk up the hierarchy:

using WallstopStudios.UnityHelpers.Core.Helper;

// Find component in parents
Helpers.IterateOverAllParentComponentsRecursively<Canvas>(transform, canvas =>
{
    Debug.Log($"Found canvas: {canvas.name}");
});

// Get all parents (no component filter)
using (var buffer = Buffers<Transform>.List.Get())
{
    Helpers.IterateOverAllParents(transform, buffer.Value);
    // buffer contains all parent transforms up to root
}

Use for:


Direct Children/Parents

using WallstopStudios.UnityHelpers.Core.Helper;

// Get immediate children (non-recursive)
using (var buffer = Buffers<Transform>.List.Get())
{
    Helpers.IterateOverAllChildren(transform, buffer.Value);
    // Only direct children, no grandchildren
}

Threading

UnityMainThreadDispatcher

Execute code on Unity’s main thread from background threads:

Problem it solves: Unity APIs can only be called from the main thread. Background Tasks/threads can’t directly manipulate GameObjects. This marshals callbacks back to the main thread.

See the dedicated Unity Main Thread Dispatcher guide for details about auto-creation, queue limits, the AutoCreationScope helper, and the CreateTestScope(...) convenience method that packages can use in their own test fixtures.

using WallstopStudios.UnityHelpers.Core.Helper;
using System.Threading.Tasks;

async Task LoadDataInBackground()
{
    // Background thread work
    await Task.Run(() =>
    {
        // Expensive computation
        var data = LoadFromDatabase();

        // Need to update UI - marshal back to main thread
        UnityMainThreadDispatcher.Instance.RunOnMainThread(() =>
        {
            // Safe to call Unity APIs here
            uiText.text = data.ToString();
        });
    });
}

Async version with result:

async Task<string> GetTextFromMainThread()
{
    // Called from background thread, executes on main thread
    string text = await UnityMainThreadDispatcher.Instance.Post(() =>
    {
        return uiText.text; // Safe to access Unity objects
    });

    return text;
}

Logging

Use the Logging Extensions guide for:

These helpers rely on the same dispatcher utilities above, so logging from jobs/background threads stays safe.

Fire-and-forget on main thread:

// From background thread
UnityMainThreadDispatcher.Instance.RunOnMainThread(() =>
{
    Instantiate(prefab, position, rotation);
});

When to use:

Important:


Path & File Helpers

Path Sanitization

Normalize path separators:

using WallstopStudios.UnityHelpers.Core.Helper;

string windowsPath = @"Assets\Sprites\Player.png";
string unityPath = PathHelper.Sanitize(windowsPath);
// Result: "Assets/Sprites/Player.png"

Unity prefers forward slashes. Use this for cross-platform paths.


Directory Utilities

Create directories safely:

using WallstopStudios.UnityHelpers.Core.Helper;

#if UNITY_EDITOR
// Creates directory and updates AssetDatabase
DirectoryHelper.EnsureDirectoryExists("Assets/Generated/Data");
#endif

Find package root:

// Walk hierarchy to find package.json
string packageRoot = DirectoryHelper.FindPackageRootPath();
// Returns path to package containing calling script

Use for:


Path Conversion

Convert between absolute and Unity-relative paths:

using WallstopStudios.UnityHelpers.Core.Helper;

string absolute = "C:/Projects/MyGame/Assets/Textures/player.png";
string relative = DirectoryHelper.AbsoluteToUnityRelativePath(absolute);
// Result: "Assets/Textures/player.png"

Get calling script’s directory:

// Uses [CallerFilePath] magic
string scriptDir = DirectoryHelper.GetCallerScriptDirectory();
// Returns directory containing the calling .cs file

File Operations

Initialize file if missing:

using WallstopStudios.UnityHelpers.Core.Helper;

// Create config.json with default contents if it doesn't exist
FileHelper.InitializePath(
    "Assets/config.json",
    "{ \"version\": 1 }"
);

Async file copy:

using System.Threading;

CancellationTokenSource cts = new CancellationTokenSource();

await FileHelper.CopyFileAsync(
    "source.txt",
    "destination.txt",
    bufferSize: 81920,  // 80KB buffer
    cts.Token
);

Use for:


Scene Helpers

Scene Queries

Check if scene is loaded:

using WallstopStudios.UnityHelpers.Core.Helper;

bool loaded = SceneHelper.IsSceneLoaded("GameLevel");
// Checks by scene name or path

Get all scene paths (editor):

#if UNITY_EDITOR
string[] allScenes = SceneHelper.GetAllScenePaths();
// Returns all .unity files in project

string[] buildScenes = SceneHelper.GetScenesInBuild();
// Returns only scenes in Build Settings
#endif

Temporary Scene Loading

Load scene, extract data, auto-unload:

using WallstopStudios.UnityHelpers.Core.Helper;

// RAII pattern - scene unloaded when disposed
using (var scope = SceneHelper.GetObjectOfTypeInScene<LevelConfig>("Scenes/LevelData"))
{
    if (scope.HasObject)
    {
        LevelConfig config = scope.Object;
        // Use config data
    }
    // Scene automatically unloaded here
}

Use for:


Advanced Utilities

Unity-Aware Null Checks

The problem: Unity’s == operator overload can be slow, and destroyed UnityEngine.Objects return true for == null but false for is null.

using WallstopStudios.UnityHelpers.Core.Helper;

GameObject obj = GetMaybeDestroyedObject();

// Proper Unity null check
bool isNull = Objects.Null(obj);
bool notNull = Objects.NotNull(obj);

Handles:


Deterministic Hashing

Combine hash codes correctly:

using WallstopStudios.UnityHelpers.Core.Helper;

public class CompositeKey
{
    public string Name;
    public int Level;
    public Vector2 Position;

    public override int GetHashCode()
    {
        // FNV-1a based hash combination
        return Objects.HashCode(Name, Level, Position);
    }
}

Supports up to 11 parameters. Uses FNV-1a algorithm for good distribution.

Hash entire collections:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int hash = Objects.EnumerableHashCode(numbers);

Use for:


Formatting

Human-readable byte counts:

using WallstopStudios.UnityHelpers.Core.Helper;

long bytes = 1536000;
string formatted = FormattingHelpers.FormatBytes(bytes);
// Result: "1.46 MB"

Auto-scales to B, KB, MB, GB, TB.

Use for:


Multi-Dimensional Array Iteration

Enumerate 2D/3D array indices:

using WallstopStudios.UnityHelpers.Core.Helper;

int[,] grid = new int[10, 10];

// Get all indices as tuples
foreach (var (x, y) in IterationHelpers.IndexOver(grid))
{
    grid[x, y] = x + y;
}

// Buffered (zero allocation)
using (var buffer = Buffers<(int, int)>.List.Get())
{
    IterationHelpers.IndexOver(grid, buffer.Value);
    foreach (var (x, y) in buffer.Value)
    {
        // Process
    }
}

Also supports 3D arrays with (int, int, int) tuples.


Binary Array Conversion

Fast marshalling between int[] and byte[]:

using WallstopStudios.UnityHelpers.Core.Helper;

int[] ints = { 1, 2, 3, 4, 5 };

// Convert to bytes (uses Buffer.BlockCopy)
byte[] bytes = ArrayConverter.IntArrayToByteArrayBlockCopy(ints);

// Convert back
int[] restored = ArrayConverter.ByteArrayToIntArrayBlockCopy(bytes);

Use for:

Performance: O(n) native memory copy, much faster than element-by-element loops.


Custom Comparers

Create IComparer from lambda:

using WallstopStudios.UnityHelpers.Core.Helper;

var enemies = new List<Enemy>();

// Sort by health descending
enemies.Sort(new FuncBasedComparer<Enemy>((a, b) =>
    b.health.CompareTo(a.health) // Descending
));

Reverse any comparer:

var comparer = Comparer<int>.Default;
var reversed = new ReverseComparer<int>(comparer);

// Now sorts descending
list.Sort(reversed);

Environment Detection

CI/CD Detection

Detect if running in a CI environment:

using WallstopStudios.UnityHelpers.Core.Helper;

if (Helpers.IsRunningInContinuousIntegration)
{
    // Skip interactive dialogs, use defaults
}

if (Helpers.IsRunningInBatchMode)
{
    // Running headless (no graphics device)
}

Supported CI systems (checked via environment variables):

CI System Environment Variable
Generic CI CI
GitHub Actions GITHUB_ACTIONS
GitLab CI GITLAB_CI
Jenkins JENKINS_URL
Travis CI TRAVIS
CircleCI CIRCLECI
Azure Pipelines TF_BUILD
TeamCity TEAMCITY_VERSION
Buildkite BUILDKITE
AWS CodeBuild CODEBUILD_BUILD_ID
Bitbucket Pipelines BITBUCKET_BUILD_NUMBER
AppVeyor APPVEYOR
Drone CI DRONE
Unity CI UNITY_CI
Unity Tests UNITY_TESTS

Check specific environment variables:

using WallstopStudios.UnityHelpers.Core.Helper;

// Check if a specific environment variable is set (non-empty, non-whitespace)
bool onGitHub = Helpers.IsEnvironmentVariableSet(
    Helpers.CiEnvironmentVariables.GitHubActions
);

bool onJenkins = Helpers.IsEnvironmentVariableSet(
    Helpers.CiEnvironmentVariables.JenkinsUrl
);

// Access all known CI variable names
foreach (string varName in Helpers.CiEnvironmentVariables.All)
{
    if (Helpers.IsEnvironmentVariableSet(varName))
    {
        Debug.Log($"CI detected via: {varName}");
    }
}

Use for:


Best Practices

Performance

Threading

Architecture

Code Organization