UNPKG

com.wallstop-studios.unity-helpers

Version:

Treasure chest of Unity developer tools

908 lines (618 loc) 24.6 kB
# 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](#oscillator) — Automatic circular/elliptical motion - [ChildSpawner](#childspawner) — Conditional prefab instantiation - [CollisionProxy](#collisionproxy) — Event-based collision detection - [CircleLineRenderer](#circlelinerenderer) — Visual circle debugging - [MatchTransform](#matchtransform) — Follow another transform - [SpriteRendererSync](#spriterenderersync) — Mirror sprite renderer state - [SpriteRendererMetadata](#spriterenderermetadata) — Stacked visual modifications - [CenterPointOffset](#centerpointoffset) — Define logical center points - [AnimatorEnumStateMachine](#animatorenumstatemachine) — Type-safe animator control - [CoroutineHandler](#coroutinehandler) — Singleton coroutine host - [StartTracker](#starttracker) — Lifecycle tracking - [MatchColliderToSprite](#matchcollidertosprite) — Auto-sync colliders - [PolygonCollider2DOptimizer](#polygoncollider2doptimizer) — Simplify collider shapes --- <a id="oscillator"></a> ## 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) ```csharp 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):** ```text speed = 3 width = 0 height = 0.2 ``` **Figure-8 motion:** ```text speed = 2 width = 1 height = 1 ``` **Horizontal sway:** ```text 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) --- <a id="childspawner"></a> ## 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 ```csharp 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: ```csharp // 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: ```text Scene 1: ChildSpawner spawns AnalyticsManager with DontDestroyOnLoad Scene 2 loads: Same ChildSpawner detects existing AnalyticsManager, doesn't spawn duplicate ``` --- <a id="collisionproxy"></a> ## 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 ```csharp 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 ```csharp // 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 ``` --- <a id="circlelinerenderer"></a> ## 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 ```csharp 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 ```text 0.1f = Very active (10 updates/sec) 0.5f = Moderate (2 updates/sec) 2.0f = Subtle (0.5 updates/sec) ``` --- <a id="matchtransform"></a> ## 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 ```csharp 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 ```csharp // 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: ```csharp matcher.toMatch = transform; // Self-reference matcher.localOffset = new Vector3(5, 0, 0); // GameObject moves 5 units right once, then MatchTransform disables itself ``` --- <a id="spriterenderersyncer"></a> <a id="spriterenderersync"></a> ## 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 ```csharp 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:** ```csharp // Change what to match at runtime syncer.DynamicToMatch = () => GetCurrentWeaponRenderer(); ``` **Sorting override:** ```csharp // 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 ```csharp // 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 ``` --- <a id="spriterenderermetadata"></a> ## 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 ```csharp 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):** ```csharp 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):** ```csharp 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: ```csharp 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: ```csharp metadata.PushMaterial(this, glowMaterial); // ... later metadata.PopMaterial(this); ``` ### Original State ```csharp 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) --- <a id="centerpointoffset"></a> ## 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 ```csharp 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`: ```text 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: ```csharp if (center.spriteUsesOffset) { // Apply sprite-specific logic } ``` --- <a id="animatorenumstatemachine"></a> ## 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:** ```csharp 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:** ```csharp using WallstopStudios.UnityHelpers.Utils; Animator animator = GetComponent<Animator>(); AnimatorEnumStateMachine<PlayerState> stateMachine; void Awake() { stateMachine = new AnimatorEnumStateMachine<PlayerState>(animator, PlayerState.Idle); } ``` **3. Set state:** ```csharp 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: ```text 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. --- <a id="coroutinehandler"></a> ## 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 ```csharp 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 ```csharp Coroutine routine = CoroutineHandler.Instance.StartCoroutine(MyCoroutine()); // ... later CoroutineHandler.Instance.StopCoroutine(routine); ``` --- <a id="starttracker"></a> ## 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 ```csharp using WallstopStudios.UnityHelpers.Utils; // Add to GameObject StartTracker tracker = gameObject.AddComponent<StartTracker>(); // Later, check if Start has been called if (tracker.Started) { // Initialization complete } ``` --- <a id="matchcollidertosprite"></a> ## MatchColliderToSprite Automatically syncs `PolygonCollider2D` shape to sprite's physics shape. **See:** [Editor Tools Guide - MatchColliderToSprite](../editor-tools/editor-tools-guide.md#matchcollidertosprite-editor) --- <a id="polygoncollider2doptimizer"></a> ## PolygonCollider2DOptimizer Reduces PolygonCollider2D point count using Douglas-Peucker simplification. **See:** [Editor Tools Guide - PolygonCollider2DOptimizer](../editor-tools/editor-tools-guide.md#polygoncollider2doptimizer-editor) --- ## 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 - [Math & Extensions](../utilities/math-and-extensions.md) - Extension methods used by utilities - [Editor Tools Guide](../editor-tools/editor-tools-guide.md) - Editor components - [Helpers Guide](../utilities/helper-utilities.md) - Non-component helper classes