Skip to content

Features Inspector Utility Components

github-actions[bot] edited this page Jan 26, 2026 · 5 revisions

Utility Components Guide

TL;DR — Why Use These

Drop-in MonoBehaviour components that solve common game development problems without writing custom scripts. Add them to GameObjects for instant functionality like motion animation, collision forwarding, transform following, and visual state management.


Contents


Oscillator

What it does: Automatically moves a GameObject in a circular or elliptical pattern. Think "floating pickup" or "idle hover animation" without animators.

Problem it solves: Creating simple repetitive motion (hovering, bobbing, orbiting) usually requires animation curves or custom update loops. Oscillator handles it with three parameters.

When to Use

Use for:

  • Floating/hovering UI elements
  • Pickup items that gently bob
  • Decorative objects with idle motion
  • Circular patrol paths
  • Simple pendulum motion

Don't use for:

  • Complex animation sequences (use Animator)
  • Physics-based motion (use Rigidbody)
  • Player/enemy movement (too rigid)

How to Use

  1. Add Oscillator component to any GameObject
  2. Configure three parameters:
    • speed: Rotation speed (radians per second)
    • width: Horizontal amplitude (X-axis movement range)
    • height: Vertical amplitude (Y-axis movement range)
using WallstopStudios.UnityHelpers.Utils;

// Via code
Oscillator osc = gameObject.AddComponent<Oscillator>();
osc.speed = 2f;    // Two radians/second
osc.width = 1f;    // ±1 unit horizontally
osc.height = 0.5f; // ±0.5 units vertically

Examples

Gentle hover (coin pickup):

speed = 3
width = 0
height = 0.2

Figure-8 motion:

speed = 2
width = 1
height = 1

Horizontal sway:

speed = 1
width = 0.5
height = 0

Important Notes

  • Updates transform.localPosition in Update()
  • Motion is relative to the original local position
  • Starts from current time offset (unique per instance)
  • Zero allocation per frame
  • Works in 2D and 3D (only affects X and Y)

ChildSpawner

What it does: Conditionally instantiates prefabs as children based on environment (editor/development/release) with automatic duplicate prevention.

Problem it solves: Managing debug overlays, analytics, or development tools that should only exist in certain builds. Handles deduplication across scene loads and DontDestroyOnLoad scenarios.

When to Use

Use for:

  • Debug UI overlays (FPS counters, console)
  • Analytics managers (only in release builds)
  • Development tools (cheat menus, level select)
  • Platform-specific managers
  • Scene-independent singleton spawners

Don't use for:

  • Regular gameplay objects (use Instantiate)
  • One-time spawns (just call Instantiate)
  • Objects that need complex initialization

How to Use

Add ChildSpawner to a GameObject (often on a scene manager or empty GameObject):

Inspector configuration:

  • Prefabs: Always spawned
  • Editor Only Prefabs: Only in Unity Editor
  • Development Only Prefabs: Only in Development builds
  • Spawn Method: When to spawn (Awake/OnEnable/Start)
  • Dont Destroy On Load: Persist across scenes
using WallstopStudios.UnityHelpers.Utils;

// Via code
ChildSpawner spawner = gameObject.AddComponent<ChildSpawner>();
spawner._prefabs = new[] { analyticsPrefab };
spawner._developmentOnlyPrefabs = new[] { debugMenuPrefab };
spawner._spawnMethod = ChildSpawnMethod.Awake;
spawner._dontDestroyOnLoad = true;

Deduplication Behavior

ChildSpawner prevents duplicate instantiation:

// Spawns DebugCanvas once
ChildSpawner spawner1 = obj1.AddComponent<ChildSpawner>();
spawner1._prefabs = new[] { debugCanvasPrefab };

// This will NOT spawn a second DebugCanvas (detects existing instance)
ChildSpawner spawner2 = obj2.AddComponent<ChildSpawner>();
spawner2._prefabs = new[] { debugCanvasPrefab };

Deduplication uses prefab asset path matching.

Spawn Methods

  • Awake: Spawns before anything else (use for foundational systems)
  • OnEnable: Spawns when a component is enabled (use for dynamic spawning)
  • Start: Spawns after all Awake calls (use when dependencies are needed)

DontDestroyOnLoad

When enabled:

  • Spawned objects persist across scene loads
  • Deduplication works across scene transitions
  • Objects aren't destroyed when loading new scenes

Typical use case:

Scene 1: ChildSpawner spawns AnalyticsManager with DontDestroyOnLoad
Scene 2 loads: Same ChildSpawner detects existing AnalyticsManager, doesn't spawn duplicate

CollisionProxy

What it does: Exposes Unity's 2D collision callbacks as C# events, enabling composition-based collision handling without inheriting from MonoBehaviour.

Problem it solves: To receive collision events in Unity, you traditionally override OnCollisionEnter2D etc. in a MonoBehaviour subclass. CollisionProxy lets you subscribe to events instead, supporting multiple listeners and decoupled architectures.

When to Use

Use for:

  • Composition over inheritance designs
  • Multiple systems reacting to the same collision
  • Decoupling collision logic from GameObject code
  • Testing collision responses
  • Dynamic behavior attachment/detachment

Don't use for:

  • Simple single-handler cases (override is fine)
  • 3D collisions (only supports 2D)
  • High-frequency collisions (event overhead)

How to Use

  1. Add CollisionProxy to GameObject with Collider2D
  2. Subscribe to events from other scripts
using WallstopStudios.UnityHelpers.Utils;

CollisionProxy proxy = gameObject.AddComponent<CollisionProxy>();

// Subscribe to enter event
proxy.OnCollisionEnter += HandleCollision;
proxy.OnTriggerEnter += HandleTrigger;

void HandleCollision(Collision2D collision)
{
    Debug.Log($"Hit {collision.gameObject.name}");
}

void HandleTrigger(Collider2D other)
{
    Debug.Log($"Triggered by {other.gameObject.name}");
}

// Cleanup
void OnDestroy()
{
    proxy.OnCollisionEnter -= HandleCollision;
    proxy.OnTriggerEnter -= HandleTrigger;
}

Available Events

Collision events (Collision2D parameter):

  • OnCollisionEnter
  • OnCollisionStay
  • OnCollisionExit

Trigger events (Collider2D parameter):

  • OnTriggerEnter
  • OnTriggerStay
  • OnTriggerExit

Multiple Subscribers Example

// Health system subscribes
healthSystem.OnDamageTaken += proxy.OnCollisionEnter;

// Sound system subscribes to same event
soundSystem.PlayImpactSound += proxy.OnCollisionEnter;

// Analytics subscribes
analytics.TrackCollision += proxy.OnCollisionEnter;

// All three systems react to the same collision independently

CircleLineRenderer

What it does: Visualizes CircleCollider2D with a dynamically drawn circle using LineRenderer, with randomized appearance for visual variety.

Problem it solves: Seeing collision bounds at runtime for debugging, or creating dynamic range indicators (ability ranges, explosion radii) without pre-made sprites.

When to Use

Use for:

  • Debug visualization of collision bounds
  • Dynamic range indicators (attack range, detection radius)
  • Area-of-effect visualization
  • Circular UI elements
  • Animated selection rings

Don't use for:

  • Production graphics (performance overhead)
  • Static circles (use a sprite)
  • Thousands of circles (expensive)

How to Use

  1. Add CircleLineRenderer to GameObject with CircleCollider2D
  2. Component automatically:
    • Adds LineRenderer if not present
    • Syncs circle size to collider radius
    • Randomizes line width for visual variety
using WallstopStudios.UnityHelpers.Utils;

CircleLineRenderer circleVis = gameObject.AddComponent<CircleLineRenderer>();
circleVis.color = Color.red;
circleVis.minLineWidth = 0.05f;
circleVis.maxLineWidth = 0.15f;
circleVis.updateRateSeconds = 0.5f; // Refresh twice per second

Configuration

  • minLineWidth / maxLineWidth: Random line thickness range
  • numSegments: Circle smoothness (more segments = smoother, more expensive)
  • baseSegments: Minimum segments (scaled by radius)
  • updateRateSeconds: How often to randomize appearance
  • color: Line color

Update Rate

Lower values = more frequent randomization = more visual variety but higher CPU cost

0.1f = Very active (10 updates/sec)
0.5f = Moderate (2 updates/sec)
2.0f = Subtle (0.5 updates/sec)

MatchTransform

What it does: Makes one transform follow another with configurable update timing and offset.

Problem it solves: Following transforms (UI name plates, camera targets, position constraints) usually require custom scripts. MatchTransform handles it declaratively.

When to Use

Use for:

  • UI name plates following 3D objects
  • Camera targets
  • Object attachments (weapon to hand)
  • Position constraints
  • Simple parent-child alternatives

Don't use for:

  • Smooth following (use Vector3.Lerp in Update)
  • Physics-based following (use joints/springs)
  • Complex multi-axis constraints (use Unity Constraints)

How to Use

using WallstopStudios.UnityHelpers.Utils;

MatchTransform matcher = uiPlate.AddComponent<MatchTransform>();
matcher.toMatch = enemyTransform;
matcher.localOffset = new Vector3(0, 2, 0); // 2 units above target
matcher.mode = MatchTransform.Mode.LateUpdate; // Update after camera

Update Modes

  • Update: Standard update timing (most common)
  • FixedUpdate: For physics-synced following
  • LateUpdate: After all Updates (best for camera followers)
  • Awake: Set once at startup, then never update
  • Start: Set once after Awake, then never update

Local Offset

// Offset is added to target position
matcher.localOffset = new Vector3(1, 0, 0); // 1 unit to the right

Self-Matching

If toMatch is the same GameObject, applies offset once then disables:

matcher.toMatch = transform; // Self-reference
matcher.localOffset = new Vector3(5, 0, 0);
// GameObject moves 5 units right once, then MatchTransform disables itself

SpriteRendererSync

What it does: Mirrors one SpriteRenderer's properties (sprite, color, material, sorting) to another, with selective property matching.

Problem it solves: Creating shadow sprites, duplicate renderers for effects, or layered rendering often requires manually keeping multiple SpriteRenderers in sync.

When to Use

Use for:

  • Shadow sprites (black silhouette following character)
  • Duplicate renderers for effects (outlines, glows)
  • Mirrored sprites (reflection effects)
  • Synchronized sprite swapping
  • VFX layers

Don't use for:

  • Single sprite rendering
  • Particle effects (use ParticleSystem)
  • Complex multi-layer rendering (use LayeredImage for UI)

How to Use

using WallstopStudios.UnityHelpers.Utils;

// On the "follower" sprite renderer
SpriteRendererSync syncer = shadowRenderer.AddComponent<SpriteRendererSync>();
syncer.toMatch = characterRenderer;
syncer.matchColor = false; // Don't copy color (shadow should be black)
syncer.matchMaterial = true;
syncer.matchSortingLayer = true;
syncer.matchOrderInLayer = true;

Configuration Options

What to sync:

  • matchColor: Copy color tint
  • matchMaterial: Copy material
  • matchSortingLayer: Copy sorting layer
  • matchOrderInLayer: Copy order in layer
  • Sprite, flipX, flipY are always copied

Dynamic source:

// Change what to match at runtime
syncer.DynamicToMatch = () => GetCurrentWeaponRenderer();

Sorting override:

// Override order in layer dynamically
syncer.DynamicSortingOrderOverride = () => characterRenderer.sortingOrder - 1; // Always behind

Update Timing

Syncs in LateUpdate() to ensure source renderer has updated first.

Example: Shadow Effect

// Create shadow GameObject
GameObject shadow = new GameObject("Shadow");
shadow.transform.parent = character.transform;
shadow.transform.localPosition = new Vector3(0.2f, -0.2f, 0); // Offset

SpriteRenderer shadowRenderer = shadow.AddComponent<SpriteRenderer>();
SpriteRendererSync syncer = shadow.AddComponent<SpriteRendererSync>();

syncer.toMatch = character.GetComponent<SpriteRenderer>();
syncer.matchColor = false;
shadowRenderer.color = new Color(0, 0, 0, 0.5f); // Semi-transparent black

SpriteRendererMetadata

What it does: Stack-based color and material management for SpriteRenderers, allowing multiple systems to modify visuals with automatic priority handling and restoration.

Problem it solves: When multiple systems want to modify a sprite's color (damage flash, power-up glow, status effect) simultaneously, manually coordinating who "owns" the color is error-prone. This provides push/pop semantics with component-based ownership.

When to Use

Use for:

  • Damage flashes (red tint on hit)
  • Status effects (poison = green, frozen = blue)
  • Power-up visuals (glow effects)
  • Multiple overlapping visual modifiers
  • Temporary material swaps

Don't use for:

  • Single, exclusive color changes (just set color directly)
  • Animations (use Animator)
  • Permanent changes (just set the property)

How to Use

using WallstopStudios.UnityHelpers.Utils;

SpriteRenderer renderer = GetComponent<SpriteRenderer>();
SpriteRendererMetadata metadata = renderer.GetComponent<SpriteRendererMetadata>();
if (metadata == null)
    metadata = renderer.gameObject.AddComponent<SpriteRendererMetadata>();

// Component A pushes red color
metadata.PushColor(this, Color.red);

// Component B pushes blue color (takes precedence)
metadata.PushColor(otherComponent, Color.blue);
// Renderer is now blue

// Component B pops its color
metadata.PopColor(otherComponent);
// Renderer reverts to red (Component A's color)

// Component A pops its color
metadata.PopColor(this);
// Renderer reverts to original color

Stack Operations

Push/Pop (LIFO - Last In, First Out):

metadata.PushColor(owner, Color.red);    // Add to top of stack
metadata.PopColor(owner);                 // Remove from top (must match owner)

PushBack (add to bottom, lower priority):

metadata.PushBackColor(owner, Color.yellow);  // Added to bottom, doesn't change current color unless stack is empty

Component Ownership

Each color/material is tagged with the Component that pushed it:

public class DamageFlash : MonoBehaviour
{
    void OnDamage()
    {
        metadata.PushColor(this, Color.red);
        Invoke(nameof(RemoveFlash), 0.1f);
    }

    void RemoveFlash()
    {
        metadata.PopColor(this); // Only removes if this component owns top of stack
    }
}

This prevents Component A from accidentally removing Component B's color.

Material Stacking

Works identically for materials:

metadata.PushMaterial(this, glowMaterial);
// ... later
metadata.PopMaterial(this);

Original State

Color original = metadata.OriginalColor;     // Color before any modifications
Color current = metadata.CurrentColor;       // Current top-of-stack color

Material originalMat = metadata.OriginalMaterial;
Material currentMat = metadata.CurrentMaterial;

Important Notes

  • Automatically detects and stores original color/material in Awake()
  • Survives enable/disable cycles
  • Priority is determined by push order (last push wins)
  • Cleanup happens automatically when a component is destroyed
  • If a non-owner tries to pop, the operation is ignored (defensive)

CenterPointOffset

What it does: Defines a logical center point for a GameObject that's separate from the transform pivot, scaled by the object's local scale.

Problem it solves: Sprites with off-center pivots (for animation reasons) need a separate "logical center" for gameplay (rotation point, targeting reticle, etc.). This provides that without changing the transform pivot.

When to Use

Use for:

  • Sprites with off-center pivots that need gameplay center
  • Rotation pivots different from visual pivot
  • Targeting reticles
  • AI targeting points
  • Center-of-mass definitions

Don't use for:

  • Centered sprites (just use transform.position)
  • Complex multi-point definitions
  • Physics center of mass (use Rigidbody2D.centerOfMass)

How to Use

using WallstopStudios.UnityHelpers.Utils;

CenterPointOffset centerDef = gameObject.AddComponent<CenterPointOffset>();
centerDef.offset = new Vector2(0, 0.5f); // Center is 0.5 units above transform
centerDef.spriteUsesOffset = true; // Flag for sprite-specific logic

// Get world-space center point
Vector2 centerInWorld = centerDef.CenterPoint;

// Use for targeting
targetingSystem.AimAt(centerDef.CenterPoint);

Offset Scaling

Offset is multiplied by transform.localScale:

transform.position = (0, 0)
offset = (1, 0)
transform.localScale = (2, 2, 2)

CenterPoint = (0, 0) + (1, 0) * (2, 2) = (2, 0)

This ensures the center point scales with the object.

Sprite Flag

spriteUsesOffset is a boolean flag you can check in other systems:

if (center.spriteUsesOffset)
{
    // Apply sprite-specific logic
}

AnimatorEnumStateMachine

What it does: Type-safe, enum-based Animator state control. Maps enum values to Animator boolean parameters for exclusive state control.

Problem it solves: Setting Animator bools with magic strings (animator.SetBool("IsJumping", true)) is error-prone and hard to refactor. This provides compile-time safety and automatic cleanup of previous states.

When to Use

Use for:

  • Complex state machines (player states, enemy AI)
  • Type-safe animation control
  • State pattern implementations
  • Refactor-friendly animation code

Don't use for:

  • Simple trigger-based animations (use animator.SetTrigger)
  • Float/int parameters (only supports bools)
  • Blend trees (use animator.SetFloat)

How to Use

1. Define an enum matching your Animator parameters:

public enum PlayerState
{
    Idle,    // Maps to Animator bool "Idle"
    Running, // Maps to Animator bool "Running"
    Jumping, // Maps to Animator bool "Jumping"
    Falling  // Maps to Animator bool "Falling"
}

2. Create the state machine:

using WallstopStudios.UnityHelpers.Utils;

Animator animator = GetComponent<Animator>();
AnimatorEnumStateMachine<PlayerState> stateMachine;

void Awake()
{
    stateMachine = new AnimatorEnumStateMachine<PlayerState>(animator, PlayerState.Idle);
}

3. Set state:

void Jump()
{
    stateMachine.Value = PlayerState.Jumping;
    // Automatically sets Animator bools:
    //   Idle = false
    //   Running = false
    //   Jumping = true
    //   Falling = false
}

Automatic State Management

Setting stateMachine.Value automatically:

  1. Sets ALL enum-named bools to false
  2. Sets ONLY the matching bool to true

This ensures exclusive state control (only one state active).

Animator Setup

Your Animator needs bool parameters matching enum names:

Animator parameters:
- Idle (bool)
- Running (bool)
- Jumping (bool)
- Falling (bool)

Transitions:
- Any State → Idle: Idle == true
- Any State → Running: Running == true
- Any State → Jumping: Jumping == true
- Any State → Falling: Falling == true

Serialization

AnimatorEnumStateMachine<T> is serializable for debugging in Inspector.


CoroutineHandler

What it does: Singleton MonoBehaviour that provides a global coroutine host for non-MonoBehaviour classes.

Problem it solves: Coroutines require a MonoBehaviour to start. Static classes, plain C# objects, and ScriptableObjects can't start coroutines directly.

When to Use

Use for:

  • Starting coroutines from static utility classes
  • Coroutines in plain C# objects
  • ScriptableObjects that need coroutines
  • Global/scene-independent coroutines

Don't use for:

  • MonoBehaviours (just use StartCoroutine)
  • Short-lived coroutines (might outlive the object)
  • Frame-perfect timing (singleton has overhead)

How to Use

using WallstopStudios.UnityHelpers.Utils;

// From anywhere
CoroutineHandler.Instance.StartCoroutine(MyCoroutine());

IEnumerator MyCoroutine()
{
    yield return new WaitForSeconds(1f);
    Debug.Log("Done!");
}

Lifetime

CoroutineHandler persists across scene loads (DontDestroyOnLoad), so coroutines survive scene transitions.

Stopping Coroutines

Coroutine routine = CoroutineHandler.Instance.StartCoroutine(MyCoroutine());
// ... later
CoroutineHandler.Instance.StopCoroutine(routine);

StartTracker

What it does: Simple component that tracks whether MonoBehaviour.Start() has been called.

Problem it solves: Sometimes you need to know if initialization (Start) has completed, especially in the editor or during complex initialization orders.

When to Use

Use for:

  • Initialization order checking
  • Conditional setup logic
  • Editor tools validating scene state
  • Testing initialization

Don't use for:

  • Production gameplay logic (architectural smell)
  • Most scenarios (rethink if you need this)

How to Use

using WallstopStudios.UnityHelpers.Utils;

// Add to GameObject
StartTracker tracker = gameObject.AddComponent<StartTracker>();

// Later, check if Start has been called
if (tracker.Started)
{
    // Initialization complete
}

MatchColliderToSprite

Automatically syncs PolygonCollider2D shape to sprite's physics shape.

See: Editor Tools Guide - MatchColliderToSprite


PolygonCollider2DOptimizer

Reduces PolygonCollider2D point count using Douglas-Peucker simplification.

See: Editor Tools Guide - PolygonCollider2DOptimizer


Best Practices

General

  • One utility per GameObject: Don't stack unrelated utilities on the same GameObject
  • Configure in Awake/Start: Set properties before first Update
  • Remove when done: Disable/destroy utilities that are no longer needed
  • Test in builds: Some utilities behave differently in editor vs. builds (ChildSpawner)

Performance

  • CircleLineRenderer: Use sparingly, each instance updates line vertices
  • SpriteRendererSync: Updates every LateUpdate, don't use for hundreds of sprites
  • MatchTransform: Choose an appropriate update mode (FixedUpdate for physics, LateUpdate for camera)

Architecture

  • CollisionProxy: Great for composition, but don't overuse events everywhere
  • SpriteRendererMetadata: Document ownership in team code (who can push/pop)
  • AnimatorEnumStateMachine: Keep enum names matching Animator parameters

Related Documentation

Clone this wiki locally