-
Notifications
You must be signed in to change notification settings - Fork 7
Features Inspector Utility Components
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.
- Oscillator — Automatic circular/elliptical motion
- ChildSpawner — Conditional prefab instantiation
- CollisionProxy — Event-based collision detection
- CircleLineRenderer — Visual circle debugging
- MatchTransform — Follow another transform
- SpriteRendererSync — Mirror sprite renderer state
- SpriteRendererMetadata — Stacked visual modifications
- CenterPointOffset — Define logical center points
- AnimatorEnumStateMachine — Type-safe animator control
- CoroutineHandler — Singleton coroutine host
- StartTracker — Lifecycle tracking
- MatchColliderToSprite — Auto-sync colliders
- PolygonCollider2DOptimizer — Simplify collider shapes
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.
✅ 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)
- Add
Oscillatorcomponent to any GameObject - 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 verticallyGentle 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
- Updates
transform.localPositionin 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)
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.
✅ 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
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;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.
- 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)
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
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.
✅ 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)
- Add
CollisionProxyto GameObject with Collider2D - 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;
}Collision events (Collision2D parameter):
OnCollisionEnterOnCollisionStayOnCollisionExit
Trigger events (Collider2D parameter):
OnTriggerEnterOnTriggerStayOnTriggerExit
// 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 independentlyWhat 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.
✅ 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)
- Add
CircleLineRendererto GameObject withCircleCollider2D - 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- 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
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)
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.
✅ 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)
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: 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
// Offset is added to target position
matcher.localOffset = new Vector3(1, 0, 0); // 1 unit to the rightIf 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 itselfWhat 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.
✅ 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)
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;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 behindSyncs in LateUpdate() to ensure source renderer has updated first.
// 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 blackWhat 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.
✅ 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)
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 colorPush/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 emptyEach 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.
Works identically for materials:
metadata.PushMaterial(this, glowMaterial);
// ... later
metadata.PopMaterial(this);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;- 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)
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.
✅ 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)
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 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.
spriteUsesOffset is a boolean flag you can check in other systems:
if (center.spriteUsesOffset)
{
// Apply sprite-specific logic
}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.
✅ 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)
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
}Setting stateMachine.Value automatically:
- Sets ALL enum-named bools to
false - Sets ONLY the matching bool to
true
This ensures exclusive state control (only one state active).
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
AnimatorEnumStateMachine<T> is serializable for debugging in Inspector.
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.
✅ 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)
using WallstopStudios.UnityHelpers.Utils;
// From anywhere
CoroutineHandler.Instance.StartCoroutine(MyCoroutine());
IEnumerator MyCoroutine()
{
yield return new WaitForSeconds(1f);
Debug.Log("Done!");
}CoroutineHandler persists across scene loads (DontDestroyOnLoad), so coroutines survive scene transitions.
Coroutine routine = CoroutineHandler.Instance.StartCoroutine(MyCoroutine());
// ... later
CoroutineHandler.Instance.StopCoroutine(routine);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.
✅ 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)
using WallstopStudios.UnityHelpers.Utils;
// Add to GameObject
StartTracker tracker = gameObject.AddComponent<StartTracker>();
// Later, check if Start has been called
if (tracker.Started)
{
// Initialization complete
}Automatically syncs PolygonCollider2D shape to sprite's physics shape.
See: Editor Tools Guide - MatchColliderToSprite
Reduces PolygonCollider2D point count using Douglas-Peucker simplification.
See: Editor Tools Guide - PolygonCollider2DOptimizer
- 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)
- 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)
- 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
- Math & Extensions - Extension methods used by utilities
- Editor Tools Guide - Editor components
- Helpers Guide - Non-component helper classes
📦 Unity Helpers | 📖 Documentation | 🐛 Issues | 📜 MIT License
- Inspector Button
- Inspector Conditional Display
- Inspector Grouping Attributes
- Inspector Inline Editor
- Inspector Overview
- Inspector Selection Attributes
- Inspector Settings
- Inspector Validation Attributes
- Utility Components
- Visual Components
- Data Structures
- Helper Utilities
- Math And Extensions
- Pooling Guide
- Random Generators
- Reflection Helpers
- Singletons