com.wallstop-studios.unity-helpers
Version:
Treasure chest of Unity developer tools
1,041 lines (730 loc) • 28.1 kB
Markdown
# 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
- [Gameplay Helpers](#gameplay-helpers) — Predictive aiming, spatial sampling, rotation
- [GameObject & Component Helpers](#gameobject--component-helpers) — Component discovery, hierarchy manipulation
- [Transform Helpers](#transform-helpers) — Hierarchy traversal
- [Coroutine Wait Pools](#coroutine-wait-pools) — Configure `Buffers.GetWaitForSeconds*` caching
- [Threading](#threading) — Main thread dispatcher
- [Path & File Helpers](#path--file-helpers) — Path resolution, file operations
- [Scene Helpers](#scene-helpers) — Scene queries and loading
- [Advanced Utilities](#advanced-utilities) — Null checks, hashing, formatting
- [Environment Detection](#environment-detection) — CI, batch mode, and runtime environment
## Coroutine Wait Pools
Unity allocates a new `WaitForSeconds`/`WaitForSecondsRealtime` every time you yield with a literal. `Buffers.GetWaitForSeconds(...)` and `Buffers.GetWaitForSecondsRealTime(...)` pool those instructions to reduce coroutine allocations, 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 runtime** — `Buffers.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.
```csharp
// 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.
<a id="gameplay-helpers"></a>
## 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_.
```csharp
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:**
- Turrets shooting at moving enemies
- AI aiming at moving players
- Predictive targeting systems
- Guided missiles
**When NOT to use:**
- Homing projectiles (use steering behaviors)
- Instant-hit weapons (use raycasts)
- Slow-moving or stationary targets (just aim directly)
### Spatial Sampling
**Get random points in circles/spheres:**
```csharp
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:**
- Spawn points (enemies, pickups, particles)
- Explosion damage distribution
- Random movement destinations
- Scatter patterns
### Smooth Rotation Helpers
**Get rotation speed for smooth turning:**
```csharp
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:**
- Frame-rate independence
- Shortest rotation path (doesn't spin 270° when 90° is shorter)
- Angle wrapping (0-360°)
### Delayed Execution
**Execute code after delay or next frame:**
```csharp
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:**
```csharp
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:**
- Enemy spawning with variability
- Random event triggers
- Staggered updates to spread CPU load
- Natural-feeling timing
### Layer & Label Queries
```csharp
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:**
- Populating dropdowns in editor tools
- Runtime layer/label validation
- Configuration systems
### Collider Syncing
**Update PolygonCollider2D to match sprite:**
```csharp
using WallstopStudios.UnityHelpers.Core.Helper;
SpriteRenderer renderer = GetComponent<SpriteRenderer>();
PolygonCollider2D collider = GetComponent<PolygonCollider2D>();
Helpers.UpdateShapeToSprite(renderer, collider);
// Collider now matches sprite's physics shape
```
<a id="gameobject--component-helpers"></a>
## GameObject & Component Helpers
### Cached Component Lookup
**Tag-based component finding with caching:**
```csharp
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 searches the scene using GameObject.FindWithTag; subsequent calls use a cached O(1) dictionary lookup. The cache persists until manually cleared.
### Component Existence Checks
```csharp
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
```csharp
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:**
```csharp
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:**
- Toggling collision for entire character rigs
- Hiding/showing complex prefabs
- Debug visualization toggles
### Bulk Child Destruction
```csharp
using WallstopStudios.UnityHelpers.Core.Helper;
// Destroy all children (useful for clearing containers)
Helpers.DestroyAllChildrenGameObjects(parentTransform);
```
**Use for:**
- Clearing inventory UI
- Resetting spawn containers
- Cleanup before repopulating
### Smart Destruction
**Editor/runtime aware destruction:**
```csharp
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
```csharp
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
```
<a id="transform-helpers"></a>
## Transform Helpers
### Hierarchy Traversal (Depth-First)
**Visit all children recursively:**
```csharp
using WallstopStudios.UnityHelpers.Core.Helper;
// Depth-first traversal (visits deepest children first)
Helpers.IterateOverAllChildrenRecursively<SpriteRenderer>(rootTransform, renderer =>
{
renderer.color = Color.red;
});
// Buffered version (reduces allocations)
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:**
```csharp
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:**
- Finding immediate area (not entire tree)
- Level-based operations
- Performance-sensitive searches
### Parent Traversal
**Walk up the hierarchy:**
```csharp
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:**
- Finding UI Canvas parents
- Inheritance checking (is this under X?)
- Walking to root of hierarchy
---
### Direct Children/Parents
```csharp
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
}
```
---
<a id="threading"></a>
## 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](../logging/unity-main-thread-dispatcher.md) for details about auto-creation, queue limits, the `AutoCreationScope` helper, and the `CreateTestScope(...)` convenience method that packages can use in their own test fixtures.
```csharp
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:**
```csharp
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](../logging/logging-extensions.md) for:
- Rich text tags applied directly inside interpolated strings (`$"{value:b,color=red}"`)
- Thread-aware logging helpers (`this.Log`, `this.LogWarn`, `this.LogError`, `this.LogDebug`)
- Tips for registering custom decorations and gating logs per-object or globally
These helpers rely on the same dispatcher utilities above, so logging from jobs/background threads stays safe.
**Fire-and-forget on main thread:**
```csharp
// From background thread
UnityMainThreadDispatcher.Instance.RunOnMainThread(() =>
{
Instantiate(prefab, position, rotation);
});
```
**When to use:**
- Async file loading callbacks
- Network request callbacks
- Database query results
- Background computation results that update UI
**Important:**
- Works in both edit mode and play mode
- Actions queued during edit mode execute in next editor update
- Don't block the main thread with long operations
---
<a id="path--file-helpers"></a>
## Path & File Helpers
### Path Sanitization
**Normalize path separators:**
```csharp
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:**
```csharp
using WallstopStudios.UnityHelpers.Core.Helper;
#if UNITY_EDITOR
// Creates directory and updates AssetDatabase
DirectoryHelper.EnsureDirectoryExists("Assets/Generated/Data");
#endif
```
**Find package root:**
```csharp
// Walk hierarchy to find package.json
string packageRoot = DirectoryHelper.FindPackageRootPath();
// Returns path to package containing calling script
```
**Use for:**
- Editor tools generating assets
- Finding package-relative paths
- Build scripts creating folders
### Path Conversion
**Convert between absolute and Unity-relative paths:**
```csharp
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:**
```csharp
// Uses [CallerFilePath] magic
string scriptDir = DirectoryHelper.GetCallerScriptDirectory();
// Returns directory containing the calling .cs file
```
### File Operations
**Initialize file if missing:**
```csharp
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:**
```csharp
using System.Threading;
CancellationTokenSource cts = new CancellationTokenSource();
await FileHelper.CopyFileAsync(
"source.txt",
"destination.txt",
bufferSize: 81920, // 80KB buffer
cts.Token
);
```
**Use for:**
- Large file operations without blocking
- Cancellable copy operations
- Streaming file operations
<a id="scene-helpers"></a>
## Scene Helpers
### Scene Queries
**Check if scene is loaded:**
```csharp
using WallstopStudios.UnityHelpers.Core.Helper;
bool loaded = SceneHelper.IsSceneLoaded("GameLevel");
// Checks by scene name or path
```
**Get all scene paths (editor):**
```csharp
#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:**
```csharp
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:**
- Extracting data from data-only scenes
- Editor tools reading scene contents
- Validation scripts
- Testing scene contents
<a id="advanced-utilities"></a>
## 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`.
```csharp
using WallstopStudios.UnityHelpers.Core.Helper;
GameObject obj = GetMaybeDestroyedObject();
// Proper Unity null check
bool isNull = Objects.Null(obj);
bool notNull = Objects.NotNull(obj);
```
Handles:
- Destroyed UnityEngine.Objects
- Actual null references
- Optimized checks for non-Unity types
### Deterministic Hashing
**Combine hash codes correctly:**
```csharp
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:**
```csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int hash = Objects.EnumerableHashCode(numbers);
```
**Use for:**
- Custom GetHashCode implementations
- Dictionary keys with multiple fields
- Networking determinism
- Save file hashing
### Formatting
**Human-readable byte counts:**
```csharp
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:**
- File size displays
- Memory usage UI
- Profiling output
- Download progress
### Multi-Dimensional Array Iteration
**Enumerate 2D/3D array indices:**
```csharp
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 (reduces allocations)
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
**Marshalling between int[] and byte[]:**
```csharp
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:**
- Network serialization
- Binary file formats
- Save game data
- High-performance data conversion
**Performance:** Uses native memory copy (Buffer.BlockCopy) which is faster than element-by-element loops due to optimized native implementation, though both are O(n).
### Custom Comparers
**Create IComparer from lambda:**
```csharp
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:**
```csharp
var comparer = Comparer<int>.Default;
var reversed = new ReverseComparer<int>(comparer);
// Now sorts descending
list.Sort(reversed);
```
<a id="environment-detection"></a>
## Environment Detection
### CI/CD Detection
**Detect if running in a CI environment:**
```csharp
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:**
```csharp
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:**
- Skipping interactive dialogs in CI
- Disabling expensive editor visualizations
- Conditional test behavior
- Build automation scripts
- Asset processors that shouldn't run headless
---
## Best Practices
### Performance
- **Cache lookups**: `Helpers.Find<T>()` caches, but don't call every frame anyway
- **Use buffered variants**: `IterateOverAllChildrenRecursively` with buffers for hot paths
- **Main thread dispatch**: Don't send hundreds of tiny tasks, batch work
- **Hierarchy traversal**: Use breadth-first with depth limits for large hierarchies
### Threading
- **Main thread rule**: Only Unity APIs need main thread, pure C# can stay on background threads
- **Avoid blocking**: Don't wait for main thread results in tight loops
- **CancellationToken**: Support cancellation for long operations
### Architecture
- **Component vs Helper**: Components (MonoBehaviours) for per-object state, Helpers for stateless operations
- **Static method smell**: If you need instance state, use a component instead
- **Editor/Runtime split**: Use `#if UNITY_EDITOR` guards for editor-only helpers
### Code Organization
- **Namespace imports**: Use `using WallstopStudios.UnityHelpers.Core.Helper;` at top of file
- **Don't extend helpers**: These are sealed utility classes, not inheritance hierarchies
- **Prefer composition**: Use helpers from components, don't try to combine them
---
## Related Documentation
- [Intelligent Pooling System](./pooling-guide.md) - Advanced object pooling with auto-purging
- [Math & Extensions](./math-and-extensions.md) - Extension methods on built-in types
- [Utility Components](../inspector/utility-components.md) - MonoBehaviour-based utilities
- [Reflection Helpers](./reflection-helpers.md) - High-performance reflection utilities
- [Singletons](./singletons.md) - RuntimeSingleton and ScriptableObjectSingleton
- [Data Structures](./data-structures.md) - Cache, spatial trees, and other collections