{"config":{"lang":["en"],"separator":"[\\s\\-,:!=\\[\\]()\"/]+|(?!\\b)(?=[A-Z][a-z])|\\.(?!\\d)|&[lg]t;","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Unity Helpers","text":"<p>Utilities tested in commercial releases</p> <p>Unity Helpers reduces repetitive work with utilities tested in commercial releases. Benchmarks demonstrate 10-15x faster random generation compared to Unity.Random and significant speedups for common reflection operations. From auto-wiring components to fast spatial queries, this toolkit provides common utilities for Unity development.</p>"},{"location":"#quick-install","title":"Quick Install","text":"OpenUPM (Recommended)Git URLNPM RegistrySource Bash<pre><code>openupm add com.wallstop-studios.unity-helpers\n</code></pre> <p>In Unity Package Manager, click Add package from git URL and enter:</p> Text Only<pre><code>https://github.com/wallstop/unity-helpers.git\n</code></pre> <p>Add scoped registry in <code>manifest.json</code>:</p> JSON<pre><code>{\n  \"scopedRegistries\": [\n    {\n      \"name\": \"npm\",\n      \"url\": \"https://registry.npmjs.org\",\n      \"scopes\": [\"com.wallstop-studios\"]\n    }\n  ],\n  \"dependencies\": {\n    \"com.wallstop-studios.unity-helpers\": \"3.0.0\"\n  }\n}\n</code></pre> <p>Download the latest release <code>.unitypackage</code> or clone the repository.</p>"},{"location":"#what-makes-this-different","title":"What Makes This Different","text":""},{"location":"#inspector-tooling","title":"Inspector Tooling","text":"<p>Grouping, buttons, conditional display, toggle grids for Unity inspectors, free and open source.</p> <p>Learn more </p>"},{"location":"#10-15x-faster-random","title":"10-15x Faster Random","text":"<p><code>PRNG.Instance</code> provides high-performance random generation with API including weighted selection, Gaussian distribution, and Perlin noise.</p> <p>Learn more </p>"},{"location":"#zero-boilerplate-component-wiring","title":"Zero-Boilerplate Component Wiring","text":"<p>Auto-wire components with attributes like <code>[SiblingComponent]</code>, <code>[ParentComponent]</code>, and <code>[ChildComponent]</code>. Works with DI containers.</p> <p>Learn more </p>"},{"location":"#data-driven-effects-system","title":"Data-Driven Effects System","text":"<p>Designer-friendly buffs and debuffs as ScriptableObjects. Add new effects via data instead of code changes.</p> <p>Learn more </p>"},{"location":"#olog-n-spatial-queries","title":"O(log n) Spatial Queries","text":"<p>QuadTree, KdTree, RTree, OctTree, and SpatialHash for 2D and 3D. Efficient spatial queries without linear iteration.</p> <p>Learn more </p>"},{"location":"#20-editor-tools","title":"20+ Editor Tools","text":"<p>Automate sprite, animation, texture, and prefab workflows. Reduces manual repetitive tasks.</p> <p>Learn more </p>"},{"location":"#first-time-here","title":"First Time Here?","text":"<p>Pick your starting point based on your biggest pain point:</p> Your Problem Your Solution Time to Value Writing custom editors Inspector Tooling - Inspector attributes, free ~2 minutes Writing <code>GetComponent</code> everywhere Relational Components - Auto-wire with attributes ~2 minutes Need buffs/debuffs system Effects System - Designer-friendly ScriptableObjects ~5 minutes Slow spatial searches Spatial Trees - O(log n) queries ~5 minutes Random is too slow/limited Random Generators - 10-15x faster with weighted selection, Gaussian, Perlin noise ~1 minute Need save/load system Serialization - Unity types supported ~10 minutes Manual sprite workflows Editor Tools - 20+ automation tools ~3 minutes <p>Not sure where to start?</p> <p>The Getting Started Guide walks through the top 3 features in 5 minutes.</p>"},{"location":"#quick-examples","title":"Quick Examples","text":""},{"location":"#auto-wire-components","title":"Auto-Wire Components","text":"Player.cs<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class Player : MonoBehaviour\n{\n    // Auto-finds on same GameObject\n    [SiblingComponent] private SpriteRenderer spriteRenderer;\n\n    // Auto-finds in parent hierarchy\n    [ParentComponent] private Rigidbody2D rigidbody;\n\n    // Auto-finds all in children\n    [ChildComponent] private Collider2D[] childColliders;\n\n    void Awake()\n    {\n        this.AssignRelationalComponents(); // One call wires all marked fields\n    }\n}\n</code></pre>"},{"location":"#fast-random-generation","title":"Fast Random Generation","text":"LootDrop.cs<pre><code>using WallstopStudios.UnityHelpers.Core.Random;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\npublic class LootDrop : MonoBehaviour\n{\n    void Start()\n    {\n        // 10-15x faster than UnityEngine.Random\n        IRandom rng = PRNG.Instance;\n\n        // Basic usage\n        int damage = rng.Next(10, 20);\n        float chance = rng.NextFloat();\n\n        // Weighted random selection\n        string[] loot = { \"Common\", \"Rare\", \"Epic\", \"Legendary\" };\n        float[] weights = { 0.6f, 0.25f, 0.10f, 0.05f };\n        int index = rng.NextWeightedIndex(weights);\n        Debug.Log($\"Dropped: {loot[index]}\");\n    }\n}\n</code></pre>"},{"location":"#inspector-attributes","title":"Inspector Attributes","text":"CharacterStats.cs<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class CharacterStats : MonoBehaviour\n{\n    [WGroup(\"combat\", \"Combat Stats\", collapsible: true)]\n    public float maxHealth = 100f;\n    [WGroupEnd(\"combat\")]\n    public float defense = 10f;\n\n    public enum WeaponType { Melee, Ranged, Magic }\n    public WeaponType weaponType;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Ranged)]\n    public int ammoCapacity = 30;\n\n    [WButton(\"Heal to Full\", groupName: \"Debug\")]\n    private void HealToFull() { maxHealth = 100f; }\n}\n</code></pre>"},{"location":"#production-ready","title":"Production Ready","text":""},{"location":"#8000-tests","title":"8,000+ Tests","text":"<p>8,000+ automated tests.</p>"},{"location":"#shipped-in-commercial-games","title":"Shipped in Commercial Games","text":"<p>Used in commercial game releases.</p>"},{"location":"#il2cpp-webgl-compatible","title":"IL2CPP &amp; WebGL Compatible","text":"<p>Compatible with IL2CPP and WebGL. Includes optimizations for AOT compilation.</p>"},{"location":"#schema-evolution","title":"Schema Evolution","text":"<p>Forward and backward compatible serialization \u2014 add new fields without breaking existing saves.</p>"},{"location":"#documentation","title":"Documentation","text":"<ul> <li>Getting Started - Quick start guide (5 minutes)</li> <li>Feature Index - Alphabetical reference of all features</li> <li>Glossary - Term definitions</li> <li>Roadmap - Upcoming features and priorities</li> </ul>"},{"location":"#features","title":"Features","text":"<ul> <li>Inspector Tooling - Attributes, buttons, validation</li> <li>Relational Components - Auto-wire with attributes</li> <li>Effects System - Data-driven buffs/debuffs</li> <li>Spatial Trees - Fast spatial queries</li> <li>Serialization - JSON and Protobuf with Unity types</li> <li>Data Structures - Heaps, tries, and more</li> <li>Random Generators - High-performance PRNGs</li> <li>Editor Tools - Sprite, animation, texture automation</li> </ul>"},{"location":"#performance","title":"Performance","text":"<ul> <li>Random Performance</li> <li>Spatial Tree 2D Performance</li> <li>Spatial Tree 3D Performance</li> <li>Relational Components Performance</li> </ul>"},{"location":"#license","title":"License","text":"<p>Unity Helpers is released under the MIT License. Use it freely in commercial and personal projects.</p> <p>Ready to get started?</p> <p>Getting Started Guide View on GitHub</p>"},{"location":"404/","title":"Page Not Found","text":"<p>The page you're looking for doesn't exist or has been moved.</p>"},{"location":"404/#what-can-you-do","title":"What can you do?","text":"<ul> <li>Use the search in the header to find what you're looking for</li> <li>Go back to the home page</li> <li>Check out the Getting Started guide</li> <li>Browse the Features documentation</li> </ul> <p>If you believe this is an error, please open an issue.</p>"},{"location":"readme/","title":"Unity Helpers","text":"<p>\ud83e\udd16 AI Assistance Disclosure:</p> <p>Recent versions of this project have utilized AI assistance for feature development, bug detection, performance optimization, and documentation.</p> <p>The original codebase was developed entirely by humans over several years.</p> <p> </p> <p>Reduces boilerplate code for common Unity patterns.</p> <p>Unity Helpers reduces repetitive work with tested utilities. Benchmarks show 10-15x faster random generation than Unity.Random and significant speedups for common reflection operations (see performance docs). From auto-wiring components to efficient spatial queries, this toolkit provides tools for Unity development.</p>"},{"location":"readme/#quick-install","title":"\ud83d\udce6 Quick Install","text":"Source Install Method OpenUPM (Recommended) <code>openupm add com.wallstop-studios.unity-helpers</code> Git URL Package Manager \u2192 Add from git URL \u2192 <code>https://github.com/wallstop/unity-helpers.git</code> NPM Add scoped registry <code>https://registry.npmjs.org</code> \u2192 search <code>com.wallstop-studios.unity-helpers</code> Source Import <code>.unitypackage</code> or clone repo <p>\ud83d\udc49 Full installation instructions with step-by-step guides for each method.</p> <p>Key Features:</p> <ul> <li>\ud83c\udfa8 Inspector tooling - Grouping, buttons, conditional display, toggle grids (free and open-source) \u2014 Migration Guide</li> <li>\u26a1 10-15x faster random generation than Unity.Random in benchmarks</li> <li>\ud83d\udd0c Reduced boilerplate component wiring with attributes</li> <li>\ud83c\udfae Designer-friendly effects system (buffs/debuffs as ScriptableObjects)</li> <li>\ud83c\udf33 O(log n) spatial queries instead of O(n) loops</li> <li>\ud83d\udee0\ufe0f 20+ editor tools that automate sprite/animation workflows</li> <li>\u2705 8,000+ tests</li> </ul> <p>\ud83d\uddfa\ufe0f Roadmap Snapshot \u2014 See the Roadmap for prioritized details.</p> <ul> <li>Inspector tooling: inline nested editors, tabbed navigation, live instrumentation, disable-if/layer attributes</li> <li>Editor automation: Animation Creator and Sprite Sheet Animation Creator enhancements, timeline-ready Event Editor upgrades, and new automation dashboards</li> <li>Random/statistics: CI statistical harness, automated quality reports, scenario samplers, job-safe stream schedulers</li> <li>Spatial trees: graduate the 3D variants, add incremental updates, physics-shape parity, and streaming builders</li> <li>UI Toolkit: control pack (dockable panes, data grids), theming samples, and performance patterns</li> <li>Utilities: cross-system bridges plus new math/combinatorics and service-pattern helpers</li> <li>Performance: automated benchmarks, Burst/Jobs rewrites of hot paths, and allocation analyzers</li> <li>Attributes &amp; tags: effect visualization tools, attribute graphs, and migration/versioning helpers</li> <li>Relational components: cached reflection, source generators, editor-time validation, and interface-based resolution</li> </ul> <p>\ud83d\udcda New to Unity Helpers? Start here: Getting Started Guide</p> <p>\ud83d\udd0d Looking for something specific? Check the Feature Index</p> <p>\u2753 Need a definition? See the Glossary</p>"},{"location":"readme/#first-time-here","title":"\ud83d\udc4b First Time Here?","text":"<p>Choose your starting point:</p> Your Problem Your Solution Time to Value \ud83c\udfa8 Writing custom editors Inspector Tooling - Odin-level features, free ~2 minutes \ud83d\udc0c Writing <code>GetComponent</code> everywhere Relational Components - Auto-wire with attributes ~2 minutes \ud83c\udfae Need buffs/debuffs system Effects System - Designer-friendly ScriptableObjects ~5 minutes \ud83d\udd0d Slow spatial searches Spatial Trees - O(log n) queries ~5 minutes \ud83c\udfb2 Random is too slow/limited PRNG.Instance - 10-15x faster in benchmarks ~1 minute \ud83d\udcbe Need save/load system Serialization - Unity types just work ~10 minutes \ud83d\udee0\ufe0f Manual sprite workflows Editor Tools - 20+ automation tools ~3 minutes <p>Not sure where to start? \u2192 Getting Started Guide walks through the top 3 features in 5 minutes.</p>"},{"location":"readme/#top-time-savers","title":"\u26a1 Top Time-Savers","text":"<p>These features reduce entire categories of repetitive work. Pick one that solves your immediate pain:</p>"},{"location":"readme/#1-inspector-tooling","title":"1. \ud83c\udfa8 Inspector Tooling","text":"<p>\u23f1\ufe0f 5-10 min/script \u00d7 200 scripts = ~20 hours saved on custom editors</p> <p>Declarative inspector attributes reduce the need for custom PropertyDrawers and EditorGUI code:</p> C#<pre><code>// \u274c OLD WAY: 100+ lines of custom editor code\n[CustomEditor(typeof(CharacterStats))]\npublic class CharacterStatsEditor : Editor {\n    // ... SerializedProperty declarations ...\n    // ... OnEnable setup ...\n    // ... OnInspectorGUI with EditorGUI.BeginFoldoutHeaderGroup ...\n    // ... Custom button rendering ...\n    // ... Conditional field display logic ...\n}\n\n// \u2705 NEW WAY: Declarative attributes, zero custom editors\npublic class CharacterStats : MonoBehaviour\n{\n    [WGroup(\"combat\", \"Combat Stats\", collapsible: true)]\n    public float maxHealth = 100f;\n    [WGroupEnd(\"combat\")]  // defense IS included, then group closes\n    public float defense = 10f;\n\n    [WGroup(\"abilities\", \"Abilities\", collapsible: true, startCollapsed: true)]\n    [System.Flags] public enum Powers { None = 0, Fly = 1, Strength = 2, Speed = 4 }\n    [WEnumToggleButtons(showSelectAll: true, buttonsPerRow: 3)]\n    [WGroupEnd(\"abilities\")]  // currentPowers IS included, then group closes\n    public Powers currentPowers;\n\n    public enum WeaponType { Melee, Ranged, Magic }\n    public WeaponType weaponType;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Ranged)]\n    public int ammoCapacity = 30;\n\n    [WButton(\"Heal to Full\", groupName: \"Debug\")]\n    private void HealToFull() { maxHealth = 100f; }\n}\n</code></pre> <p>Features:</p> <ul> <li>WGroup - Boxed sections with auto-inclusion, collapsible headers, and animations when enabled</li> <li>WButton - Method buttons with history, async support, cancellation</li> <li>WShowIf - Conditional visibility (9 comparison operators)</li> <li>WEnumToggleButtons - Flag enums as visual toggle grids</li> <li>SerializableDictionary, SerializableSet, WGuid, SerializableType - Collections Unity can't serialize</li> </ul> <p>\ud83d\udcd6 Complete Inspector Guide | \ud83d\udd04 Odin Migration Guide</p>"},{"location":"readme/#2-auto-wire-components","title":"2. \ud83d\udd0c Auto-Wire Components","text":"<p>\u23f1\ufe0f 10-20 min/script \u00d7 100 scripts = ~20 hours saved</p> <p>Reduces GetComponent boilerplate with attribute-based auto-wiring. Replace 20+ lines with 3 attributes:</p> C#<pre><code>// \u274c OLD WAY: 20+ lines per script\nvoid Awake() {\n    sprite = GetComponent&lt;SpriteRenderer&gt;();\n    if (sprite == null) Debug.LogError(\"Missing SpriteRenderer!\");\n\n    rigidbody = GetComponentInParent&lt;Rigidbody2D&gt;();\n    if (rigidbody == null) Debug.LogError(\"Missing Rigidbody2D!\");\n\n    colliders = GetComponentsInChildren&lt;Collider2D&gt;();\n    // 15 more lines...\n}\n\n// \u2705 NEW WAY: 4 lines total\n[SiblingComponent] private SpriteRenderer sprite;\n[ParentComponent] private Rigidbody2D rigidbody;\n[ChildComponent] private Collider2D[] colliders;\nvoid Awake() =&gt; this.AssignRelationalComponents();\n</code></pre> <p>Bonus: Works with VContainer/Zenject/Reflex for automatic DI + relational wiring!</p> <p>\ud83d\udcd6 Learn More | See <code>Samples~/DI - VContainer</code>, <code>Samples~/DI - Zenject</code>, and <code>Samples~/DI - Reflex</code> folders in the repository for DI examples</p>"},{"location":"readme/#3-data-driven-effects","title":"3. \ud83c\udfae Data-Driven Effects","text":"<p>\u23f1\ufe0f 2-4 hours/effect \u00d7 50 effects = ~150 hours saved</p> <p>Designers create buffs/debuffs as ScriptableObjects. Zero programmer time after 20-minute setup:</p> C#<pre><code>// Create once (ScriptableObject in editor):\n// - HasteEffect: Speed \u00d7 1.5, duration 5s, tag \"Haste\", particle effect\n\n// Use everywhere:\nplayer.ApplyEffect(hasteEffect);           // Apply buff\nif (player.HasTag(\"Stunned\")) return;      // Query state\nplayer.RemoveEffects(player.GetHandlesWithTag(\"Haste\")); // Batch removal\n</code></pre> <p>What you get:</p> <ul> <li>Automatic stacking &amp; duration management</li> <li>Reference-counted tags for gameplay queries</li> <li>Cosmetic VFX/SFX that spawn/despawn automatically</li> <li>Designer-friendly iteration without code changes</li> </ul> <p>Beyond buffs: Tags become a flexible capability system for AI decisions, permission gates, state management, and complex gameplay interactions (invulnerability, stealth, elemental systems).</p> <p>\ud83d\udcd6 Full Guide | \ud83d\ude80 5-Minute Tutorial</p>"},{"location":"readme/#4-unity-aware-serialization","title":"4. \ud83d\udcbe Unity-Aware Serialization","text":"<p>\u23f1\ufe0f 40+ hours on initial implementation + prevents player data loss</p> <p>JSON/Protobuf that understands <code>Vector3</code>, <code>GameObject</code>, <code>Color</code> - no custom converters needed:</p> C#<pre><code>// Vector3, Color, GameObject references just work:\nvar saveData = new SaveData {\n    playerPosition = new Vector3(1, 2, 3),\n    playerColor = Color.cyan,\n    inventory = new List&lt;GameObject&gt;()\n};\n\n// One line to save:\nbyte[] data = Serializer.JsonSerialize(saveData);\n\n// Schema evolution = never break old saves:\n[ProtoMember(1)] public int gold;\n[ProtoMember(2)] public Vector3 position;\n// Adding new field? Old saves still load!\n[ProtoMember(3)] public int level;  // Safe to add\n</code></pre> <p>Real-world impact: Ship updates without worrying about corrupting player saves.</p> <p>\ud83d\udcd6 Serialization Guide</p>"},{"location":"readme/#5-object-pooling","title":"5. \ud83c\udfb1 Object Pooling","text":"<p>\u23f1\ufe0f Reduces GC spikes = 5-10 FPS improvement in complex scenes</p> <p>Zero-allocation queries with automatic cleanup. Thread-safe pooling in one line:</p> C#<pre><code>// Get pooled buffer - automatically returned on scope exit\nvoid ProcessEnemies(QuadTree2D&lt;Enemy&gt; enemyTree) {\n    using var lease = Buffers&lt;Enemy&gt;.List.Get(out List&lt;Enemy&gt; buffer);\n\n    // Use it for spatial query - zero allocations!\n    enemyTree.GetElementsInRange(playerPos, 10f, buffer);\n\n    foreach (Enemy enemy in buffer) {\n        enemy.TakeDamage(5f);\n    }\n\n    // Buffer automatically returned to pool here - no cleanup needed\n}\n</code></pre> <p>Why this matters:</p> <ul> <li>Stable 60 FPS under load (no GC spikes)</li> <li>AI systems querying hundreds of neighbors per frame</li> <li>Particle systems with thousands of particles</li> <li>Works for List, HashSet, Stack, Queue, and Arrays</li> </ul> <p>\ud83d\udcd6 Buffering Pattern</p>"},{"location":"readme/#6-editor-tools-suite","title":"6. \ud83d\udee0\ufe0f Editor Tools Suite","text":"<p>\u23f1\ufe0f 1-2 hours/operation \u00d7 weekly use = ~100 hours/year</p> <p>20+ tools that automate sprite cropping, animation creation, atlas generation, prefab validation:</p> <p>Common workflows:</p> <ul> <li>Sprite Cropper: Add or remove transparent pixels from 500 sprites \u2192 1 click (was: 30 minutes in Photoshop)</li> <li>Animation Creator: Bulk-create clips from naming patterns (<code>walk_0001.png</code>) \u2192 1 minute (was: 20 minutes)</li> <li>Prefab Checker: Validate 200 prefabs for missing references \u2192 1 click (was: manual QA)</li> <li>Atlas Generator: Create sprite atlases from regex/labels \u2192 automated (was: manual setup)</li> </ul> <p>\ud83d\udcd6 Editor Tools Guide</p>"},{"location":"readme/#batteries-included-extensions","title":"\ud83c\udf81 Batteries-Included Extensions","text":"<p>Unity Helpers includes 200+ extension methods for common Unity operations:</p>"},{"location":"readme/#unity-type-extensions","title":"Unity Type Extensions","text":"C#<pre><code>// Color averaging (4 methods: LAB, HSV, Weighted, Dominant)\nColor teamColor = sprite.GetAverageColor(ColorAveragingMethod.LAB);  // Perceptually accurate\n\n// Collider auto-fitting\npolygonCollider.UpdateShapeToSprite();  // Instant sprite \u2192 collider sync\n\n// Smooth direction rotation (returns rotated direction vector)\nVector2 facing = Helpers.GetAngleWithSpeed(targetDirection, currentFacing, rotationSpeed);\n\n// Safe destruction (works in editor AND runtime)\ngameObject.SmartDestroy();  // No more #if UNITY_EDITOR everywhere\n\n// Camera world bounds\nBounds visibleArea = Camera.main.OrthographicBounds();  // For culling/spawning\n\n// Predictive targeting (intercept moving targets)\nVector2 aimPoint = target.PredictCurrentTarget(shooter.position, projectileSpeed, predictiveFiring: true, targetVelocity);\nturret.transform.up = (aimPoint - (Vector2)shooter.position).normalized;\n</code></pre>"},{"location":"readme/#math-that-should-be-built-in","title":"Math That Should Be Built-In","text":"C#<pre><code>// Positive modulo (no more negative results!)\nint index = (-1).PositiveMod(array.Length);  // 4, not -1\n\n// Wrapped add for ring buffers\nindex = index.WrappedAdd(2, capacity);  // Handles overflow correctly\n\n// Approximate equality with tolerance\nif (transform.position.x.Approximately(target.x, 0.01f)) { /* close enough */ }\n\n// Polyline simplification (Douglas\u2013Peucker)\nList&lt;Vector2&gt; simplified = LineHelper.Simplify(path, epsilon: 0.5f);  // Reduce pathfinding waypoints\n</code></pre>"},{"location":"readme/#collection-utilities","title":"Collection Utilities","text":"C#<pre><code>// Infinite iterator (no extra allocation)\nforeach (var item in itemList.Infinite()) { /* cycles infinitely */ }\n\n// Aggregate bounds from multiple renderers\nBounds? combined = renderers.Select(r =&gt; r.bounds).GetBounds();\n\n// String similarity for fuzzy search\nint distance = playerName.LevenshteinDistance(\"jon\");  // \"john\" = 1, close match!\n\n// Case conversions (6 styles: Pascal, Camel, Snake, Kebab, Title, Constant)\nstring apiKey = \"user_name\".ToPascalCase();  // \"UserName\"\n</code></pre> <p>Full list: Math &amp; Extensions Guide | Reflection Helpers</p>"},{"location":"readme/#additional-utilities","title":"\ud83d\udc8e Additional Utilities","text":"<p>These utilities solve specific problems that waste hours if you implement them yourself:</p> Feature What It Does Time Saved Predictive Targeting Accurate ballistics for turrets/missiles in one call 2-3 hours per shooting system Coroutine Jitter Prevents 100 enemies polling on same frame Reduces frame spikes IL-Emitted Reflection Up to 12x faster than System.Reflection for method invocations, IL2CPP safe Critical for serialization/modding SmartDestroy() Editor/runtime safe destruction (no scene corruption) Prevents countless debugging hours Convex/Concave Hulls Generate territory borders from point clouds 4-6 hours per hull algorithm Logging Extensions Rich tags, thread-aware logs, per-object toggles Keeps consoles readable + actionable"},{"location":"readme/#design-philosophy","title":"Design Philosophy","text":"<p>Unity Helpers reduces repetitive work by providing tested utilities for common Unity patterns, including GetComponent boilerplate, spatial query loops, and save/load systems.</p> <p>Built for Real Projects:</p> <ul> <li>\u2705 Tested in shipped commercial games</li> <li>\u2705 8,000+ automated tests catch edge cases before you hit them</li> <li>\u2705 Minimal external dependencies - depends on protobuf-net for binary serialization</li> <li>\u2705 IL2CPP/WebGL ready with optimized SINGLE_THREADED paths</li> <li>\u2705 MIT Licensed - use freely in commercial projects</li> </ul> <p>Who This Is For:</p> <ul> <li>Indie devs who need tools without enterprise overhead</li> <li>Teams who value performance and want their junior devs to use tested code</li> <li>Senior engineers who want to avoid re-implementing the same utilities every project</li> </ul>"},{"location":"readme/#installation","title":"Installation","text":"<p>Unity Helpers is available from multiple sources. Choose the one that best fits your workflow:</p> Source Best For Auto-Updates OpenUPM Most users, easy version management \u2705 Yes Git URL Latest commits, CI/CD pipelines \u2705 Yes NPM Registry Teams already using NPM \u2705 Yes Source Offline, modifications needed \u274c Manual"},{"location":"readme/#from-openupm-recommended","title":"From OpenUPM (Recommended)","text":"<p>OpenUPM is the recommended installation method for easy version management and updates.</p>"},{"location":"readme/#option-a-via-package-manager-ui","title":"Option A: Via Package Manager UI","text":"<ol> <li>Open Edit \u2192 Project Settings \u2192 Package Manager</li> <li>Under Scoped Registries, click + to add a new registry:</li> <li>Name: <code>OpenUPM</code></li> <li>URL: <code>https://package.openupm.com</code></li> <li>Scope(s): <code>com.wallstop-studios</code></li> <li>Click Save</li> <li>Open Window \u2192 Package Manager</li> <li>Change the dropdown to My Registries</li> <li>Find and install <code>Unity Helpers</code></li> </ol>"},{"location":"readme/#option-b-via-openupm-cli","title":"Option B: Via OpenUPM CLI","text":"<p>If you have the OpenUPM CLI installed:</p> Bash<pre><code>openupm add com.wallstop-studios.unity-helpers\n</code></pre>"},{"location":"readme/#option-c-manual-manifestjson","title":"Option C: Manual manifest.json","text":"<p>Add to your <code>Packages/manifest.json</code>:</p> JSON<pre><code>{\n  \"scopedRegistries\": [\n    {\n      \"name\": \"OpenUPM\",\n      \"url\": \"https://package.openupm.com\",\n      \"scopes\": [\"com.wallstop-studios\"]\n    }\n  ],\n  \"dependencies\": {\n    \"com.wallstop-studios.unity-helpers\": \"3.1.0\"\n  }\n}\n</code></pre>"},{"location":"readme/#from-git-url","title":"From Git URL","text":"<p>Install directly from GitHub for the latest version:</p> <ol> <li>Open Window \u2192 Package Manager</li> <li>Click + \u2192 Add package from git URL...</li> <li>Enter: <code>https://github.com/wallstop/unity-helpers.git</code></li> </ol> <p>OR add to your <code>Packages/manifest.json</code>:</p> JSON<pre><code>{\n  \"dependencies\": {\n    \"com.wallstop-studios.unity-helpers\": \"https://github.com/wallstop/unity-helpers.git\"\n  }\n}\n</code></pre> <p>Tip: To lock to a specific version, append <code>#3.1.0</code> to the URL.</p>"},{"location":"readme/#from-npm-registry","title":"From NPM Registry","text":"<ol> <li>Open Edit \u2192 Project Settings \u2192 Package Manager</li> <li>Under Scoped Registries, click + to add a new registry:</li> <li>Name: <code>NPM</code></li> <li>URL: <code>https://registry.npmjs.org</code></li> <li>Scope(s): <code>com.wallstop-studios</code></li> <li>Click Save</li> <li>Open Window \u2192 Package Manager</li> <li>Change the dropdown to My Registries</li> <li>Find and install <code>com.wallstop-studios.unity-helpers</code></li> </ol>"},{"location":"readme/#from-source","title":"From Source","text":""},{"location":"readme/#option-a-import-unity-package","title":"Option A: Import Unity Package","text":"<ol> <li>Download the latest <code>.unitypackage</code> from GitHub Releases</li> <li>In Unity, go to Assets \u2192 Import Package \u2192 Custom Package...</li> <li>Select the downloaded <code>.unitypackage</code> file and import</li> </ol>"},{"location":"readme/#option-b-clone-or-download-repository","title":"Option B: Clone or Download Repository","text":"<ol> <li>Clone or download the repository</li> <li>Copy the contents to your project's <code>Assets/</code> or <code>Packages/</code> folder</li> <li>Unity will automatically import the package</li> </ol>"},{"location":"readme/#compatibility","title":"Compatibility","text":"Unity Version Built-In URP HDRP 2021 Likely, but untested Likely, but untested Likely, but untested 2022 \u2705 Compatible \u2705 Compatible \u2705 Compatible 2023 \u2705 Compatible \u2705 Compatible \u2705 Compatible Unity 6 \u2705 Compatible \u2705 Compatible \u2705 Compatible"},{"location":"readme/#platform-support","title":"Platform Support","text":"<p>Unity Helpers is multiplatform compatible including:</p> <ul> <li>\u2705 WebGL - Full support with optimized SINGLE_THREADED hot paths</li> <li>\u2705 IL2CPP - Tested and compatible with ahead-of-time compilation</li> <li>\u2705 Mobile (iOS, Android) - Compatible with IL2CPP</li> <li>\u2705 Desktop (Windows, macOS, Linux) - Full threading support</li> <li>\u2705 Consoles - IL2CPP compatible</li> </ul> <p>Requirements:</p> <ul> <li>.NET Standard 2.1 - Required for core library features</li> </ul>"},{"location":"readme/#webgl-and-single-threaded-optimization","title":"WebGL and Single-Threaded Optimization","text":"<p>Unity Helpers includes a <code>SINGLE_THREADED</code> scripting define symbol for WebGL and other single-threaded environments. When enabled, the library automatically uses optimized code paths that eliminate threading overhead:</p> <p>Optimized systems with SINGLE_THREADED:</p> <ul> <li>Buffers &amp; Pooling - Uses <code>Stack&lt;T&gt;</code> and <code>Dictionary&lt;T&gt;</code> instead of <code>ConcurrentBag&lt;T&gt;</code> and <code>ConcurrentDictionary&lt;T&gt;</code></li> <li>Random Number Generation - Static instances instead of <code>ThreadLocal&lt;T&gt;</code></li> <li>Reflection Caches - Non-concurrent dictionaries for faster lookups</li> <li>Thread Pools - SingleThreadedThreadPool disabled (not needed on WebGL)</li> </ul> <p>How to enable:</p> <p>Unity automatically defines <code>UNITY_WEBGL</code> for WebGL builds. To enable SINGLE_THREADED optimization:</p> <ol> <li>Go to Project Settings &gt; Player &gt; Other Settings &gt; Scripting Define Symbols</li> <li>Add <code>SINGLE_THREADED</code> for WebGL platform</li> <li>Or use in your <code>csc.rsp</code> file: <code>-define:SINGLE_THREADED</code></li> </ol> <p>Performance impact: 10-20% faster hot path operations on single-threaded platforms by avoiding unnecessary synchronization overhead.</p>"},{"location":"readme/#il2cpp-and-code-stripping-considerations","title":"IL2CPP and Code Stripping Considerations","text":"<p>\u26a0\ufe0f Important for IL2CPP builds (WebGL, Mobile, Consoles):</p> <p>Some features in Unity Helpers use reflection internally (particularly Protobuf serialization and ReflectionHelpers). IL2CPP's managed code stripping may remove types/members that are only accessed via reflection, causing runtime errors.</p> <p>Symptoms of stripping issues:</p> <ul> <li><code>NullReferenceException</code> or <code>TypeLoadException</code> during deserialization</li> <li>Missing fields after Protobuf deserialization</li> <li>Reflection helpers failing to find types at runtime</li> </ul>"},{"location":"readme/#solution-use-linkxml-to-preserve-required-types","title":"Solution: Use link.xml to preserve required types","text":"<p>Create a <code>link.xml</code> file in your <code>Assets</code> folder to prevent stripping:</p> XML<pre><code>&lt;linker&gt;\n  &lt;!-- Preserve your serialized types --&gt;\n  &lt;assembly fullname=\"Assembly-CSharp\"&gt;\n    &lt;type fullname=\"MyNamespace.PlayerSave\" preserve=\"all\"/&gt;\n    &lt;type fullname=\"MyNamespace.InventoryData\" preserve=\"all\"/&gt;\n    &lt;!-- Add all Protobuf-serialized types here --&gt;\n  &lt;/assembly&gt;\n\n  &lt;!-- Preserve Unity Helpers if needed --&gt;\n  &lt;assembly fullname=\"WallstopStudios.UnityHelpers.Runtime\" preserve=\"all\"/&gt;\n&lt;/linker&gt;\n</code></pre> <p>Best practices:</p> <ul> <li>\u2705 Always test IL2CPP builds - Development builds don't use stripping, so bugs only appear in release builds</li> <li>\u2705 Test on target platform - WebGL stripping behaves differently than iOS/Android</li> <li>\u2705 Use link.xml for all Protobuf types - Any type with <code>[ProtoContract]</code> should be preserved</li> <li>\u2705 Verify after every schema change - Adding new serialized types requires updating link.xml</li> <li>\u2705 Check logs for stripping warnings - Unity logs which types are stripped during build</li> </ul> <p>When you don't need link.xml:</p> <ul> <li>JSON serialization (uses source-generated converters, not reflection)</li> <li>Spatial trees and data structures (no reflection used)</li> <li>Most helper methods (compiled ahead-of-time)</li> </ul> <p>Related documentation:</p> <ul> <li>Unity Manual: Managed Code Stripping</li> <li>Protobuf-net and IL2CPP</li> <li>Serialization Guide: IL2CPP Warning</li> <li>Reflection Helpers: IL2CPP Warning</li> </ul>"},{"location":"readme/#quick-start-guide","title":"Quick Start Guide","text":"<p>\ud83d\udca1 First time? Skip to section #2 (Relational Components) - it has the biggest immediate impact.</p> <p>Already read the Top 5 Time-Savers? Jump directly to the Core Features reference below, or check out the Getting Started Guide.</p>"},{"location":"readme/#core-features","title":"Core Features","text":""},{"location":"readme/#random-number-generators","title":"Random Number Generators","text":"<p>Unity Helpers includes 15 high-quality random number generators, all implementing a rich <code>IRandom</code> interface:</p>"},{"location":"readme/#available-generators","title":"Available Generators","text":"<p>The tables below are auto-generated by the performance benchmark. Run <code>RandomPerformanceTests.Benchmark</code> in the Unity Test Runner to refresh them.</p> <p>No benchmark data available yet. Run <code>RandomPerformanceTests.Benchmark</code> to populate these tables.</p>"},{"location":"readme/#rich-api","title":"Rich API","text":"<p>All generators implement <code>IRandom</code> with the following functionality:</p> C#<pre><code>IRandom random = PRNG.Instance;\n\n// Basic types\nint i = random.Next();                  // int in [0, int.MaxValue]\nint range = random.Next(10, 20);        // int in [10, 20)\nuint ui = random.NextUint();            // uint in [0, uint.MaxValue]\nfloat f = random.NextFloat();           // float in [0.0f, 1.0f]\ndouble d = random.NextDouble();         // double in [0.0d, 1.0d]\nbool b = random.NextBool();             // true or false\n\n// Unity types\nVector2 v2 = random.NextVector2();      // Random 2D vector\nVector3 v3 = random.NextVector3();      // Random 3D vector\nColor color = random.NextColor();       // Random color\nQuaternion rot = random.NextRotation(); // Random rotation\n\n// Distributions\nfloat gaussian = random.NextGaussian(mean: 0f, stdDev: 1f);\n\n// Collections\nT item = random.NextOf(collection);     // Random element\nT[] shuffled = random.Shuffle(array);   // Fisher-Yates shuffle\nint weightedIndex = random.NextWeightedIndex(weights);\n\n// Special\nGuid uuid = random.NextGuid();          // UUIDv4\nT enumValue = random.NextEnum&lt;T&gt;();     // Random enum value\nfloat[,] noise = random.NextNoiseMap(width, height); // Perlin noise\n</code></pre>"},{"location":"readme/#deterministic-gameplay","title":"Deterministic Gameplay","text":"<p>All generators are seedable for replay systems:</p> C#<pre><code>// Create seeded generator for deterministic behavior\nIRandom seededRandom = new IllusionFlow(seed: 12345);\n\n// Same seed = same sequence\nIRandom replay = new IllusionFlow(seed: 12345);\n// Both will generate identical values\n</code></pre> <p>Threading:</p> <ul> <li>Do not share a single RNG instance across threads.</li> <li>Use <code>PRNG.Instance</code> for a thread-local default, or use each generator's <code>TypeName.Instance</code> (e.g., <code>IllusionFlow.Instance</code>, <code>PcgRandom.Instance</code>).</li> <li>Alternatively, create one separate instance per thread.</li> </ul> <p>\ud83d\udcca Performance Comparison</p>"},{"location":"readme/#spatial-trees","title":"Spatial Trees","text":"<p>Efficient spatial data structures for 2D and 3D games.</p>"},{"location":"readme/#2d-spatial-trees","title":"2D Spatial Trees","text":"<ul> <li>QuadTree2D - Best general-purpose choice</li> <li>KDTree2D - Fast nearest-neighbor queries</li> <li>RTree2D - Optimized for bounding boxes</li> </ul> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Create from collection\nGameObject[] objects = FindObjectsOfType&lt;GameObject&gt;();\nQuadTree2D&lt;GameObject&gt; tree = new(objects, go =&gt; go.transform.position);\n\n// Query by radius\nList&lt;GameObject&gt; nearby = new();\ntree.GetElementsInRange(playerPos, radius: 10f, nearby);\n\n// Query by bounds\nBounds searchArea = new(center, size);\ntree.GetElementsInBounds(searchArea, nearby);\n\n// Find nearest neighbors (approximate, but fast)\ntree.GetApproximateNearestNeighbors(playerPos, count: 5, nearby);\n</code></pre>"},{"location":"readme/#3d-spatial-trees","title":"3D Spatial Trees","text":"<p>Note: KdTree3D, OctTree3D, and RTree3D are under active development. SpatialHash3D is stable and production\u2011ready.</p> <ul> <li>OctTree3D - Best general-purpose choice for 3D</li> <li>KDTree3D - Fast 3D nearest-neighbor queries</li> <li>RTree3D - Optimized for 3D bounding volumes</li> <li>SpatialHash3D - Efficient for uniformly distributed moving objects (stable)</li> </ul> C#<pre><code>// Same API as 2D, but with Vector3\nVector3[] positions = GetAllPositions();\nOctTree3D&lt;Vector3&gt; tree = new(positions, p =&gt; p);\n\nList&lt;Vector3&gt; results = new();\ntree.GetElementsInRange(center, radius: 50f, results);\n</code></pre>"},{"location":"readme/#when-to-use-spatial-trees","title":"When to Use Spatial Trees","text":"<p>\u2705 Good for:</p> <ul> <li>Many objects (100+)</li> <li>Frequent spatial queries</li> <li>Static or slowly changing data</li> <li>AI awareness systems</li> <li>Visibility culling</li> <li>Collision detection optimization</li> </ul> <p>\u274c Not ideal for:</p> <ul> <li>Few objects (&lt;50)</li> <li>Constantly moving objects</li> <li>Single queries</li> <li>Already using Unity's physics system</li> </ul> <p>\ud83d\udcca 2D Benchmarks | \ud83d\udcca 3D Benchmarks</p> <p>For behavior details and edge cases, see: Spatial Tree Semantics</p>"},{"location":"readme/#relational-components","title":"Relational Components","text":"<p>Auto-wire components using attributes to reduce GetComponent boilerplate.</p> <p>Key attributes:</p> <ul> <li><code>[SiblingComponent]</code> - Find components on same GameObject</li> <li><code>[ParentComponent]</code> - Find components in parent hierarchy</li> <li><code>[ChildComponent]</code> - Find components in children</li> <li><code>[ValidateAssignment]</code> - Validate at edit time, show errors in inspector</li> <li><code>[WNotNull]</code> - Must be assigned in inspector</li> <li><code>[WReadOnly]</code> - Read-only display in inspector</li> <li><code>[WInLineEditor]</code> - Inline inspector editing for object references</li> <li><code>[WShowIf]</code> - Conditional display based on field values</li> </ul> <p>Quick example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class Enemy : MonoBehaviour\n{\n    // Find on same GameObject\n    [SiblingComponent]\n    private Animator animator;\n\n    // Find in parent\n    [ParentComponent]\n    private EnemySpawner spawner;\n\n    // Find in children\n    [ChildComponent]\n    private List&lt;Weapon&gt; weapons;\n\n    // Optional component (no error if missing)\n    [SiblingComponent(Optional = true)]\n    private AudioSource audioSource;\n\n    // Only search direct children/parents\n    [ParentComponent(OnlyAncestors = true)]\n    private Transform[] parentHierarchy;\n\n    // Include inactive components\n    [ChildComponent(IncludeInactive = true)]\n    private ParticleSystem[] effects;\n\n    private void Awake()\n    {\n        this.AssignRelationalComponents();\n    }\n}\n</code></pre> <p>See the in-depth guide: Relational Components. Performance snapshots: Relational Component Performance Benchmarks.</p>"},{"location":"readme/#effects-attributes-and-tags","title":"Effects, Attributes, and Tags","text":"<p>Create data-driven gameplay effects that modify stats, apply tags, and drive cosmetics.</p> <p>Key pieces:</p> <ul> <li><code>AttributeEffect</code> \u2014 ScriptableObject that bundles stat changes, tags, cosmetics, and duration.</li> <li><code>EffectHandle</code> \u2014 Unique ID for one application instance; remove/refresh specific stacks.</li> <li><code>AttributesComponent</code> \u2014 Base class for components that expose modifiable <code>Attribute</code> fields.</li> <li><code>TagHandler</code> \u2014 Counts and queries string tags for gating gameplay (e.g., \"Stunned\").</li> <li><code>CosmeticEffectData</code> \u2014 Prefab-like container of behaviors shown while an effect is active.</li> </ul> <p>Quick example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Tags;\n\n// 1) Define stats on a component\npublic class CharacterStats : AttributesComponent\n{\n    public Attribute Health = 100f;\n    public Attribute Speed = 5f;\n}\n\n// 2) Author an AttributeEffect (ScriptableObject) in the editor\n//    - modifications: [ { attribute: \"Speed\", action: Multiplication, value: 1.5f } ]\n//    - durationType: Duration, duration: 5\n//    - effectTags: [ \"Haste\" ]\n//    - cosmeticEffects: [ a prefab with CosmeticEffectData + Particle/Audio components ]\n\n// 3) Apply and later remove\nGameObject player = ...;\nAttributeEffect haste = ...; // ScriptableObject reference\nEffectHandle? handle = player.ApplyEffect(haste);\nif (handle.HasValue)\n{\n    // Remove early if needed\n    player.RemoveEffect(handle.Value);\n}\n\n// Query tags anywhere\nif (player.HasTag(\"Stunned\")) { /* disable input */ }\n</code></pre> <p>Details at a glance:</p> <ul> <li><code>ModifierDurationType.Instant</code> \u2014 applies permanently; returns null handle.</li> <li><code>ModifierDurationType.Duration</code> \u2014 temporary; expires automatically; reapply can reset if enabled.</li> <li><code>ModifierDurationType.Infinite</code> \u2014 persists until <code>RemoveEffect(handle)</code> is called.</li> <li><code>AttributeModification</code> order: Addition \u2192 Multiplication \u2192 Override.</li> <li><code>CosmeticEffectData.RequiresInstancing</code> \u2014 instance per application or reuse shared presenters.</li> </ul> <p>Power Pattern: Tags aren't just for buffs\u2014use them to build capability systems for invulnerability, AI decision-making, permission gates, state management, and elemental interactions. See Advanced Scenarios for patterns.</p> <p>Further reading: see the full guide Effects System.</p>"},{"location":"readme/#serialization","title":"Serialization","text":"<p>Fast, compact serialization for save systems, config, and networking.</p> <p>This package provides three serialization technologies:</p> <ul> <li><code>Json</code> \u2014 Uses System.Text.Json with built\u2011in converters for Unity types.</li> <li><code>Protobuf</code> \u2014 Uses protobuf-net for compact, fast, schema\u2011evolvable binary.</li> <li><code>SystemBinary</code> \u2014 Uses .NET BinaryFormatter for legacy/ephemeral data only.</li> </ul> <p>All are exposed via <code>WallstopStudios.UnityHelpers.Core.Serialization.Serializer</code>.</p>"},{"location":"readme/#json-profiles","title":"JSON Profiles","text":"<ul> <li>Normal \u2014 default settings (case-insensitive, includes fields, comments/trailing commas allowed)</li> <li>Pretty \u2014 human-friendly, indented</li> <li>Fast \u2014 strict, minimal with Unity converters (case-sensitive, strict numbers, no comments/trailing commas, IncludeFields=false)</li> <li>FastPOCO \u2014 strict, minimal, no Unity converters; best for pure POCO graphs</li> </ul>"},{"location":"readme/#when-to-use-what","title":"When To Use What","text":"<ul> <li>Use Json for:</li> <li>Player or tool settings, human\u2011readable saves, serverless workflows.</li> <li>Interop with tooling, debugging, or versioning in Git.</li> <li>Use Protobuf for:</li> <li>Network payloads, large save files, bandwidth/storage\u2011sensitive data.</li> <li>Situations where you expect schema evolution across versions.</li> <li>Use SystemBinary only for:</li> <li>Transient caches in trusted environments where data and code version match.</li> <li>Never for untrusted data or long\u2011term persistence.</li> </ul>"},{"location":"readme/#json-example","title":"JSON Example","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Serialization;\n\npublic class SaveData\n{\n    public Vector3 position;\n    public Color playerColor;\n    public List&lt;GameObject&gt; inventory;\n}\n\nvar data = new SaveData\n{\n    position = new Vector3(1, 2, 3),\n    playerColor = Color.cyan,\n    inventory = new List&lt;GameObject&gt;()\n};\n\n// Serialize to UTF\u20118 JSON bytes (Unity types supported via built\u2011in converters)\nbyte[] jsonBytes = Serializer.JsonSerialize(data);\n\n// Deserialize from string\nstring jsonText = Serializer.JsonStringify(data, pretty: true);\nSaveData fromText = Serializer.JsonDeserialize&lt;SaveData&gt;(jsonText);\n\n// File helpers\nSerializer.WriteToJsonFile(data, path: \"save.json\", pretty: true);\nSaveData fromFile = Serializer.ReadFromJsonFile&lt;SaveData&gt;(\"save.json\");\n\n// Generic entry points (choose format at runtime)\nbyte[] bytes = Serializer.Serialize(data, SerializationType.Json);\nSaveData loaded = Serializer.Deserialize&lt;SaveData&gt;(bytes, SerializationType.Json);\n</code></pre>"},{"location":"readme/#protobuf-example","title":"Protobuf Example","text":"C#<pre><code>using ProtoBuf; // protobuf-net\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Serialization;\n\n[ProtoContract]\npublic class NetworkMessage\n{\n    [ProtoMember(1)] public int playerId;\n    [ProtoMember(2)] public Vector3 position; // Vector3 works in Protobuf via built-in surrogates\n}\n\nvar message = new NetworkMessage { playerId = 7, position = new Vector3(5, 0, -2) };\n\n// Protobuf bytes (small + fast)\nbyte[] bytes = Serializer.ProtoSerialize(message);\nNetworkMessage decoded = Serializer.ProtoDeserialize&lt;NetworkMessage&gt;(bytes);\n\n// Generic entry points\nbyte[] bytes2 = Serializer.Serialize(message, SerializationType.Protobuf);\nNetworkMessage decoded2 = Serializer.Deserialize&lt;NetworkMessage&gt;(bytes2, SerializationType.Protobuf);\n\n// Buffer reuse (reduce GC for hot paths)\nbyte[] buffer = null;\nint len = Serializer.Serialize(message, SerializationType.Protobuf, ref buffer);\nNetworkMessage again = Serializer.Deserialize&lt;NetworkMessage&gt;(buffer.AsSpan(0, len).ToArray(), SerializationType.Protobuf);\n</code></pre> <p>Notes:</p> <ul> <li>Protobuf\u2011net requires stable field numbers. Annotate with <code>[ProtoMember(n)]</code> and never reuse or renumber.</li> <li>Unity types supported via surrogates: Vector\u2154, Vector2Int/3Int, Quaternion, Color/Color32, Rect/RectInt, Bounds/BoundsInt, Resolution.</li> </ul> <p>Features:</p> <ul> <li>Custom converters for Unity types (Vector\u2154/4, Color, GameObject, Matrix4x4, Type)</li> <li>Protobuf (protobuf\u2011net) support for compact binary</li> <li>LZMA compression utilities (see <code>Runtime/Utils/LZMA.cs</code>)</li> <li>Type\u2011safe serialization and pooled buffers/writers to reduce GC</li> </ul> <p>Full guide: Serialization</p>"},{"location":"readme/#data-structures","title":"Data Structures","text":"<p>Additional high-performance data structures:</p> Structure Use Case CyclicBuffer Ring buffer, sliding windows BitSet Compact boolean storage ImmutableBitSet Read-only bit flags Heap Priority queue operations PriorityQueue Event scheduling Deque Double-ended queue DisjointSet Union-find operations Trie String prefix trees SparseSet Fast add/remove with iteration TimedCache Auto-expiring cache C#<pre><code>// Cyclic buffer for damage history\nCyclicBuffer&lt;float&gt; damageHistory = new(capacity: 10);\ndamageHistory.Add(25f);\ndamageHistory.Add(30f);\nfloat avgDamage = damageHistory.Average();\n\n// Priority queue for event scheduling (priority determined by comparer)\nPriorityQueue&lt;GameEvent&gt; eventQueue = new(\n    Comparer&lt;GameEvent&gt;.Create((a, b) =&gt; b.Priority.CompareTo(a.Priority)) // Higher first\n);\neventQueue.Enqueue(spawnEvent);\neventQueue.Enqueue(bossEvent);\nif (eventQueue.TryDequeue(out GameEvent next)) { /* process event */ }\n\n// Trie for autocomplete\nTrie commandTrie = new();\ncommandTrie.Insert(\"teleport\");\ncommandTrie.Insert(\"tell\");\ncommandTrie.Insert(\"terrain\");\nList&lt;string&gt; matches = commandTrie.GetWordsWithPrefix(\"tel\");\n// Returns: [\"teleport\", \"tell\"]\n</code></pre> <p>Full guide: Data Structures</p>"},{"location":"readme/#core-math-extensions","title":"Core Math &amp; Extensions","text":"<p>Numeric helpers, geometry primitives, Unity extensions, colors, collections, strings, directions.</p> <p>See the guide: Core Math &amp; Extensions.</p>"},{"location":"readme/#at-a-glance","title":"At a Glance","text":"<ul> <li><code>PositiveMod</code>, <code>WrappedAdd</code> \u2014 Safe cyclic arithmetic for indices/angles.</li> <li><code>LineHelper.Simplify</code> \u2014 Reduce polyline vertices with Douglas\u2013Peucker.</li> <li><code>Line2D.Intersects</code> \u2014 2D segment intersection and closest-point helpers.</li> <li><code>RectTransform.GetWorldRect</code> \u2014 Axis-aligned world bounds for rotated UI.</li> <li><code>Camera.OrthographicBounds</code> \u2014 Compute visible world bounds for ortho cameras.</li> <li><code>Color.GetAverageColor</code> \u2014 LAB/HSV/Weighted/Dominant color averaging.</li> <li><code>IEnumerable.Infinite</code> \u2014 Cycle sequences without extra allocations.</li> <li><code>StringExtensions.LevenshteinDistance</code> \u2014 Edit distance for fuzzy matching.</li> </ul>"},{"location":"readme/#singleton-utilities-odincompatible","title":"Singleton Utilities (ODIN\u2011compatible)","text":"<ul> <li><code>RuntimeSingleton&lt;T&gt;</code> \u2014 Global component singleton with optional cross\u2011scene persistence.</li> <li><code>ScriptableObjectSingleton&lt;T&gt;</code> \u2014 Global settings/data singleton loaded from <code>Resources/</code>, auto\u2011created by the editor tool.</li> </ul> <p>See the guide: Singleton Utilities and the tool: ScriptableObject Singleton Creator.</p>"},{"location":"readme/#editor-tools","title":"Editor Tools","text":"<p>Unity Helpers includes 20+ editor tools to streamline your workflow:</p> <ul> <li>Sprite Tools: Cropper, Atlas Generator, Animation Editor, Pivot Adjuster</li> <li>Texture Tools: Blur, Resize, Settings Applier, Fit Texture Size</li> <li>Animation Tools: Event Editor, Creator, Copier, Sheet Animation Creator</li> <li>Validation: Prefab Checker with validation rules</li> <li>Automation: ScriptableObject Singleton Creator, Attribute Cache Generator</li> <li>Compilation: Request a manual script compilation via <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Request Script Compilation</code> or use the default shortcut (Ctrl/Cmd + Alt + R) registered with Unity\u2019s Shortcut Manager (listed under Wallstop / Request Script Compilation). The shortcut now forces an <code>AssetDatabase.Refresh</code> before requesting compilation and logs whenever Unity is already compiling, so scripts added outside the editor are imported even while Unity is unfocused.</li> </ul> <p>\ud83d\udcd6 Complete Editor Tools Documentation</p> <p>Quick Access:</p> <ul> <li>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers</code></li> <li>Create Assets: <code>Assets &gt; Create &gt; Wallstop Studios &gt; Unity Helpers</code></li> </ul>"},{"location":"readme/#buffering-pattern","title":"Buffering Pattern","text":""},{"location":"readme/#object-pooling","title":"Object Pooling","text":"<p>Zero-allocation queries with automatic cleanup and thread-safe pooling.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\nusing WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Example: Use pooled buffer for spatial query\nvoid FindNearbyEnemies(QuadTree2D&lt;Enemy&gt; tree, Vector2 position)\n{\n    // Get pooled list - automatically returned when scope exits\n    using var lease = Buffers&lt;Enemy&gt;.List.Get(out List&lt;Enemy&gt; buffer);\n\n    // Use it with spatial query - combines zero-alloc query + pooled buffer!\n    tree.GetElementsInRange(position, 10f, buffer);\n\n    foreach (Enemy enemy in buffer)\n    {\n        enemy.TakeDamage(5f);\n    }\n    // buffer automatically returned to pool here\n}\n\n// Array pooling example\nvoid ProcessLargeDataset(int size)\n{\n    using var lease = WallstopArrayPool&lt;float&gt;.Get(size, out float[] buffer);\n\n    // Use buffer for temporary processing\n    for (int i = 0; i &lt; size; i++)\n    {\n        buffer[i] = ComputeValue(i);\n    }\n\n    // buffer automatically returned to pool here\n}\n</code></pre> <p>Do / Don'ts:</p> <ul> <li>Do reuse buffers per system or component.</li> <li>Do treat buffers as temporary scratch space (APIs clear them first).</li> <li>Don't keep references to pooled lists beyond their lease lifetime.</li> <li>Don't share the same buffer across overlapping async/coroutine work.</li> </ul> <p></p>"},{"location":"readme/#pooling-utilities","title":"Pooling utilities","text":"<ul> <li><code>Buffers&lt;T&gt;</code> \u2014 pooled collections (List/Stack/Queue/HashSet) with <code>PooledResource</code> leases.</li> <li>Lists: <code>using var lease = Buffers&lt;Foo&gt;.List.Get(out List&lt;Foo&gt; list);</code></li> <li>Stacks: <code>using var lease = Buffers&lt;Foo&gt;.Stack.Get(out Stack&lt;Foo&gt; stack);</code></li> <li>HashSets: <code>using var lease = Buffers&lt;Foo&gt;.HashSet.Get(out HashSet&lt;Foo&gt; set);</code></li> <li> <p>Pattern: acquire \u2192 use \u2192 Dispose (returns to pool, clears collection).</p> </li> <li> <p><code>WallstopArrayPool&lt;T&gt;</code> \u2014 rent arrays by length with automatic return on dispose.</p> </li> <li>Example: <code>using var lease = WallstopArrayPool&lt;int&gt;.Get(1024, out int[] buffer);</code></li> <li> <p>Use for temporary processing buffers, sorting, or interop with APIs that require arrays.</p> </li> <li> <p><code>WallstopFastArrayPool&lt;T&gt;</code> \u2014 fast array pool specialized for frequent short\u2011lived arrays (requires <code>T : unmanaged</code>), does not clear arrays. Returned arrays may have previous content in them.</p> </li> <li>Example: <code>using var lease = WallstopFastArrayPool&lt;int&gt;.Get(count, out int[] buffer);</code></li> <li>Used throughout Helpers for high\u2011frequency editor/runtime operations (e.g., asset searches).</li> </ul> <p>How pooling + buffering help APIs:</p> <ul> <li>Spatial queries: pass a reusable <code>List&lt;T&gt;</code> to <code>GetElementsInRange/GetElementsInBounds</code> and iterate results without allocations.</li> <li>Component queries: <code>GetComponents(buffer)</code> clears and fills your buffer instead of allocating arrays.</li> <li>Editor utilities: temporary arrays/lists from pools keep import/scan tools snappy, especially inside loops.</li> </ul>"},{"location":"readme/#dependency-injection-integrations","title":"Dependency Injection Integrations","text":"<p>Auto-detected packages:</p> <ul> <li>Zenject/Extenject: <code>com.extenject.zenject</code>, <code>com.modesttree.zenject</code>, <code>com.svermeulen.extenject</code></li> <li>VContainer: <code>jp.cysharp.vcontainer</code>, <code>jp.hadashikick.vcontainer</code></li> <li>Reflex: <code>com.gustavopsantos.reflex</code></li> </ul> <p>Manual or source imports (no UPM):</p> <ul> <li>Add scripting defines in <code>Project Settings &gt; Player &gt; Other Settings &gt; Scripting Define Symbols</code>:</li> <li><code>ZENJECT_PRESENT</code> when Zenject/Extenject is present</li> <li><code>VCONTAINER_PRESENT</code> when VContainer is present</li> <li><code>REFLEX_PRESENT</code> when Reflex is present</li> <li>Add the define per target platform (e.g., Standalone, Android, iOS).</li> </ul> <p>Notes:</p> <ul> <li>When the define is present, optional assemblies under <code>Runtime/Integrations/*</code> compile automatically and expose helpers like <code>RelationalComponentsInstaller</code> (Zenject/Reflex) and <code>RegisterRelationalComponents()</code> (VContainer).</li> <li>If you use UPM, no manual defines are required \u2014 the package IDs above trigger symbols via <code>versionDefines</code> in the asmdefs.</li> <li>For test scenarios without LifetimeScope (VContainer), SceneContext (Zenject), or SceneScope (Reflex), see DI Integrations: Testing and Edge Cases for step\u2011by\u2011step patterns.</li> </ul> <p>Quick start:</p> <ul> <li>VContainer: in your <code>LifetimeScope.Configure</code>, call <code>builder.RegisterRelationalComponents()</code>.</li> <li>Zenject/Extenject: add <code>RelationalComponentsInstaller</code> to your <code>SceneContext</code> (toggle scene scan if desired).</li> <li>Reflex: place <code>RelationalComponentsInstaller</code> on the same GameObject as your <code>SceneScope</code> to bind the assigner, run the scene scan, and (optionally) listen for additive scenes. Use <code>container.InjectWithRelations(...)</code> / <code>InstantiateGameObjectWithRelations(...)</code> for DI-friendly hydration.</li> </ul> C#<pre><code>// VContainer \u2014 LifetimeScope\nusing VContainer;\nusing VContainer.Unity;\nusing WallstopStudios.UnityHelpers.Integrations.VContainer;\n\nprotected override void Configure(IContainerBuilder builder)\n{\n    // Register assigner + one-time scene scan + additive listener (default)\n    builder.RegisterRelationalComponents(\n        RelationalSceneAssignmentOptions.Default,\n        enableAdditiveSceneListener: true\n    );\n}\n\n// Zenject \u2014 prefab instantiation with DI + relations\nusing Zenject;\nusing WallstopStudios.UnityHelpers.Integrations.Zenject;\n\nvar enemy = Container.InstantiateComponentWithRelations(enemyPrefab, parent);\n\n// Reflex \u2014 prefab instantiation with DI + relations\nusing Reflex.Core;\nusing WallstopStudios.UnityHelpers.Integrations.Reflex;\n\nvar enemy = container.InstantiateComponentWithRelations(enemyPrefab, parent);\n</code></pre> <p>See the full guide with scenarios, troubleshooting, and testing patterns: Relational Components Guide</p>"},{"location":"readme/#additional-helpers","title":"Additional Helpers","text":"<ul> <li>VContainer:</li> <li><code>resolver.InjectWithRelations(component)</code> \u2014 inject + assign a single instance</li> <li><code>resolver.InstantiateComponentWithRelations(prefab, parent)</code> \u2014 instantiate + inject + assign</li> <li><code>resolver.InjectGameObjectWithRelations(root, includeInactiveChildren)</code> \u2014 inject hierarchy + assign</li> <li> <p><code>resolver.InstantiateGameObjectWithRelations(prefab, parent)</code> \u2014 instantiate GO + inject + assign</p> </li> <li> <p>Zenject:</p> </li> <li><code>container.InjectWithRelations(component)</code> \u2014 inject + assign a single instance</li> <li><code>container.InstantiateComponentWithRelations(prefab, parent)</code> \u2014 instantiate + assign</li> <li><code>container.InjectGameObjectWithRelations(root, includeInactiveChildren)</code> \u2014 inject hierarchy + assign</li> <li> <p><code>container.InstantiateGameObjectWithRelations(prefab, parent)</code> \u2014 instantiate GO + inject + assign</p> </li> <li> <p>Reflex:</p> </li> <li><code>container.InjectWithRelations(component)</code> \u2014 inject + assign a single instance</li> <li><code>container.InstantiateComponentWithRelations(prefab, parent)</code> \u2014 instantiate + inject + assign</li> <li><code>container.InjectGameObjectWithRelations(root, includeInactiveChildren)</code> \u2014 inject hierarchy + assign</li> <li><code>container.InstantiateGameObjectWithRelations(prefab, parent)</code> \u2014 instantiate GO + inject + assign</li> </ul>"},{"location":"readme/#additive-scene-loads","title":"Additive Scene Loads","text":"<ul> <li>VContainer: <code>RegisterRelationalComponents(..., enableAdditiveSceneListener: true)</code> registers a listener that hydrates components in newly loaded scenes.</li> <li>Zenject: <code>RelationalComponentsInstaller</code> exposes a toggle \"Listen For Additive Scenes\" to register the same behavior.</li> <li>Reflex: <code>RelationalComponentsInstaller</code> exposes a toggle \"Listen For Additive Scenes\" to register the same behavior.</li> <li>Only the newly loaded scene is processed; other loaded scenes are not re\u2011scanned.</li> </ul>"},{"location":"readme/#performance-options","title":"Performance Options","text":"<ul> <li>One-time scene scan runs after container build; additive scenes are handled incrementally.</li> <li>Single-pass scan (default) reduces <code>FindObjectsOfType</code> calls by scanning once and checking type ancestry.</li> <li>VContainer: <code>new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true)</code></li> <li>Zenject: <code>new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true)</code></li> <li>Reflex: <code>new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true)</code></li> <li>Per-object paths (instantiate/inject helpers, pools) avoid global scans entirely for objects created via DI.</li> </ul>"},{"location":"readme/#performance","title":"Performance","text":"<p>Unity Helpers is built with performance as a top priority:</p> <p>Random Number Generation:</p> <ul> <li>10-15x faster than Unity.Random in benchmarks (655-885M ops/sec vs 65-85M ops/sec)</li> <li>Zero GC pressure with thread-local instances</li> <li>\ud83d\udcca Full Random Performance Benchmarks</li> </ul> <p>Spatial Queries:</p> <ul> <li>O(log n) tree queries vs O(n) linear search</li> <li>Significant speedup for large datasets (QuadTree2D example: 10,000 objects = ~13 checks vs 10,000 checks)</li> <li>See benchmarks for detailed performance comparisons</li> <li>\ud83d\udcca 2D Performance Benchmarks</li> <li>\ud83d\udcca 3D Performance Benchmarks</li> </ul> <p>Memory Management:</p> <ul> <li>Zero-allocation buffering pattern reduces GC spikes</li> <li>Object pooling for List, HashSet, Stack, Queue, Arrays</li> <li>5-10 FPS improvement in complex scenes from stable GC</li> </ul> <p>Reflection:</p> <ul> <li>Cached delegates are 10\u2013100x faster than raw <code>System.Reflection</code> (method invocations ~12x; boxed scenarios up to 100x)</li> <li>Safe for IL2CPP and AOT platforms; capability overrides (<code>ReflectionHelpers.OverrideReflectionCapabilities</code>) let tests force expression/IL fallbacks</li> <li>Run the benchmarks via ReflectionPerformanceTests.Benchmark (EditMode Test Runner) and commit the updated markdown section</li> <li>\ud83d\udcd8 Reflection Helpers Guide and \ud83d\udcca Benchmarks</li> </ul> <p>List Sorting:</p> <ul> <li>Multiple adaptive algorithms (<code>Ghost</code>, <code>Meteor</code>, <code>Power</code>, <code>Grail</code>, <code>Pattern-Defeating QuickSort</code>, <code>Insertion</code>, <code>Tim</code>, <code>Jesse</code>, <code>Green</code>, <code>Ska</code>, <code>Ipn</code>, <code>Smooth</code>, <code>Block</code>, <code>IPS4o</code>, <code>Power+</code>, <code>Glide</code>, <code>Flux</code>) tuned for <code>IList&lt;T&gt;</code></li> <li>Deterministic datasets (sorted, nearly sorted, shuffled) across sizes from 100 to 1,000,000</li> <li>\ud83d\udcca IList Sorting Performance Benchmarks</li> </ul>"},{"location":"readme/#documentation-index","title":"Documentation Index","text":"<p>Start Here:</p> <ul> <li>\ud83d\ude80 Getting Started \u2014 Getting Started Guide</li> <li>\ud83d\udd0d Feature Index \u2014 Complete A-Z Index</li> <li>\ud83d\udcd6 Glossary \u2014 Term Definitions</li> </ul> <p>Core Guides:</p> <ul> <li>Odin Migration Guide \u2014 Migrate from Odin Inspector</li> <li>Serialization Guide \u2014 Serialization</li> <li>Editor Tools Guide \u2014 Editor Tools</li> <li>Math &amp; Extensions \u2014 Core Math &amp; Extensions</li> <li>Singletons \u2014 Singleton Utilities</li> <li>Relational Components \u2014 Relational Components</li> <li>Effects System \u2014 Effects System</li> <li>Data Structures \u2014 Data Structures</li> </ul> <p>Spatial Trees:</p> <ul> <li>2D Spatial Trees Guide \u2014 2D Spatial Trees Guide</li> <li>3D Spatial Trees Guide \u2014 3D Spatial Trees Guide</li> <li>Spatial Tree Semantics \u2014 Spatial Tree Semantics</li> <li>Spatial Tree 2D Performance \u2014 Spatial Tree 2D Performance</li> <li>Spatial Tree 3D Performance \u2014 Spatial Tree 3D Performance</li> <li>Hulls (Convex vs Concave) \u2014 Hulls</li> </ul> <p>Performance &amp; Reference:</p> <ul> <li>Reflection Performance Guide \u2014 Reflection Benchmarks</li> <li>Reflection AOT/Burst Validation \u2014 IL2CPP &amp; Burst Validation</li> <li>Reflection Benchmark Workflow \u2014 Benchmarking &amp; Verification</li> <li>Random Performance \u2014 Random Performance</li> <li>Reflection Helpers \u2014 Reflection Helpers</li> <li>IList Sorting Performance \u2014 IList Sorting Performance</li> </ul> <p>Project Info:</p> <ul> <li>Changelog \u2014 Changelog</li> <li>License \u2014 License</li> <li>Third\u2011Party Notices \u2014 Third\u2011Party Notices</li> <li>Contributing \u2014 Contributing</li> <li>llms.txt \u2014 LLM-Friendly Documentation | llms.txt</li> </ul>"},{"location":"readme/#contributing","title":"Contributing","text":"<p>Contributions are welcome! Please feel free to submit a Pull Request.</p>"},{"location":"readme/#formatting-assistance","title":"Formatting Assistance","text":"<ul> <li>Dependabot PRs: Formatting fixes (CSharpier + Prettier + markdownlint) are applied automatically by CI.</li> <li>Contributor PRs: Opt-in formatting is available.</li> <li>Comment on the PR with <code>/format</code> (aliases: <code>/autofix</code>, <code>/lint-fix</code>).<ul> <li>If the PR branch is in this repo, the bot pushes a commit with fixes.</li> <li>If the PR is from a fork, the bot opens a formatting PR targeting the base branch.</li> <li>The commenter must be the PR author or a maintainer/collaborator.</li> </ul> </li> <li>Or run the Actions workflow manually: Actions \u2192 \"Opt-in Formatting\" \u2192 Run workflow \u2192 enter the PR number.</li> <li>Not everything is auto-fixable: link checks and YAML linting may still require manual changes.</li> </ul> <p>See more details in CONTRIBUTING.</p>"},{"location":"readme/#license","title":"License","text":"<p>This project is licensed under the MIT License - see the LICENSE file for details.</p>"},{"location":"readme/#20-release-notes-highlights","title":"2.0 Release Notes (Highlights)","text":"<ul> <li>BinaryFormatter deprecated, still functional for trusted/legacy data:</li> <li> <p><code>SerializationType.SystemBinary</code> is <code>[Obsolete]</code>. Use <code>SerializationType.Json</code> (System.Text.Json + Unity converters) or <code>SerializationType.Protobuf</code> (protobuf-net) for new work. Keep BinaryFormatter for trusted, non\u2011portable data only.</p> </li> <li> <p>GameObject JSON converter outputs structured JSON:</p> </li> <li> <p><code>GameObjectConverter</code> now writes a JSON object with <code>name</code>, <code>type</code> (assembly-qualified), and <code>instanceId</code> rather than a stringified placeholder.</p> </li> <li> <p>Minor robustness improvements:</p> </li> <li>Guarded stray <code>UnityEditor</code> imports in runtime files to ensure clean player builds.</li> </ul> <p>See Serialization guide for AOT/IL2CPP guidance and Unity JSON options, and Editor tools guide for Editor tool usage details.</p>"},{"location":"contributing/testing-patterns/","title":"Testing \"Impossible\" State Patterns","text":"<p>This guide documents patterns for testing states that \"should never happen\" but could occur in production. These tests catch edge cases that defensive programming must handle gracefully.</p>"},{"location":"contributing/testing-patterns/#why-test-impossible-states","title":"Why Test \"Impossible\" States","text":"<p>Production code encounters situations that seem impossible during development:</p> <ul> <li>Destroyed Unity Objects: Objects destroyed by external code, scene unloading, or domain reloads</li> <li>Null References: References that \"can't be null\" become null due to serialization issues, race conditions, or user error</li> <li>Invalid Enum Values: Casting arbitrary integers to enums produces values not defined in the enum</li> <li>Corrupted Serialization State: Save files edited by users, version mismatches, or truncated data</li> <li>Overflow Conditions: Extreme values that exceed expected ranges</li> </ul> <p>Testing these scenarios ensures code fails gracefully rather than crashing or corrupting data.</p>"},{"location":"contributing/testing-patterns/#destroyed-unity-objects","title":"Destroyed Unity Objects","text":"<p>Unity objects can be destroyed at any time by external systems. Code must handle the \"fake null\" state where an object reference is not <code>null</code> in C# terms but returns <code>true</code> for Unity's null check.</p>"},{"location":"contributing/testing-patterns/#pattern-test-behavior-after-destroyimmediate","title":"Pattern: Test Behavior After DestroyImmediate","text":"C#<pre><code>[Test]\npublic void GetGameObjectHandlesDestroyedComponent()\n{\n    GameObject go = Track(new GameObject(\"Test\", typeof(SpriteRenderer)));\n    SpriteRenderer spriteRenderer = go.GetComponent&lt;SpriteRenderer&gt;();\n\n    Object.DestroyImmediate(spriteRenderer); // UNH-SUPPRESS: Test verifies behavior after component destruction\n\n    GameObject result = spriteRenderer.GetGameObject();\n\n    Assert.IsTrue(result == null, \"Should return null for destroyed component\");\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-unityextensionsbasictestscs","title":"Real Example: UnityExtensionsBasicTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/Extensions/UnityExtensionsBasicTests.cs</code>:</p> C#<pre><code>[Test]\npublic void GetCenterUsesCenterPointOffsetWhenAvailable()\n{\n    GameObject go = Track(new GameObject(\"CenterPointTest\", typeof(CenterPointOffset)));\n\n    go.transform.position = new Vector3(5f, 5f, 0f);\n    CenterPointOffset offset = go.GetComponent&lt;CenterPointOffset&gt;();\n    offset.offset = new Vector2(3f, 4f);\n\n    Assert.AreEqual(offset.CenterPoint, go.GetCenter());\n\n    Object.DestroyImmediate(offset); // UNH-SUPPRESS: Test verifies behavior after component destruction\n    Assert.AreEqual((Vector2)go.transform.position, go.GetCenter());\n}\n</code></pre> <p>This test verifies that <code>GetCenter()</code> falls back to the GameObject's transform position when the <code>CenterPointOffset</code> component is destroyed.</p>"},{"location":"contributing/testing-patterns/#real-example-objecthelpertestscs","title":"Real Example: ObjectHelperTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/Helper/ObjectHelperTests.cs</code>:</p> C#<pre><code>[UnityTest]\npublic IEnumerator GetGameObject()\n{\n    GameObject go = Track(new GameObject(\"Test\", typeof(SpriteRenderer)));\n    SpriteRenderer spriteRenderer = go.GetComponent&lt;SpriteRenderer&gt;();\n\n    GameObject result = go.GetGameObject();\n    Assert.AreEqual(result, go);\n    result = spriteRenderer.GetGameObject();\n    Assert.AreEqual(result, go);\n\n    Object.DestroyImmediate(spriteRenderer); // UNH-SUPPRESS: Test verifies behavior after component destruction\n    result = spriteRenderer.GetGameObject();\n    Assert.IsTrue(result == null);\n    result = go.GetGameObject();\n    Assert.AreEqual(result, go);\n\n    Object.DestroyImmediate(go); // UNH-SUPPRESS: Test verifies behavior after GameObject destruction\n    result = spriteRenderer.GetGameObject();\n    Assert.IsTrue(result == null);\n    result = go.GetGameObject();\n    Assert.IsTrue(result == null);\n\n    result = ((GameObject)null).GetGameObject();\n    Assert.IsTrue(result == null);\n\n    result = ((SpriteRenderer)null).GetGameObject();\n    Assert.IsTrue(result == null);\n    yield break;\n}\n</code></pre> <p>This test verifies:</p> <ol> <li>Normal operation with valid objects</li> <li>Behavior after component destruction (object still valid)</li> <li>Behavior after GameObject destruction (both references invalid)</li> <li>Explicit null input handling</li> </ol>"},{"location":"contributing/testing-patterns/#pattern-serializedobject-with-destroyed-target","title":"Pattern: SerializedObject with Destroyed Target","text":"<p>Editor code often works with <code>SerializedObject</code> and <code>SerializedProperty</code>. When the target object is destroyed, these become invalid but may not be null.</p> C#<pre><code>[Test]\npublic void DrawerHandlesDestroyedSerializedObjectTarget()\n{\n    MyScriptableObject target = CreateScriptableObject&lt;MyScriptableObject&gt;();\n    SerializedObject serializedObject = new SerializedObject(target);\n    SerializedProperty property = serializedObject.FindProperty(\"myField\");\n\n    Object.DestroyImmediate(target); // UNH-SUPPRESS: Test verifies behavior after target destroyed\n\n    // SerializedObject.targetObject is now null\n    Assert.DoesNotThrow(() =&gt; drawer.OnGUI(rect, property, label));\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-scriptablesingletonserializationtestscs","title":"Real Example: ScriptableSingletonSerializationTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Editor/CustomDrawers/ScriptableSingletonSerializationTests.cs</code>:</p> C#<pre><code>[Test]\npublic void IsScriptableSingletonTypeWithDestroyedObjectReturnsFalse()\n{\n    RegularScriptableObject target = CreateScriptableObject&lt;RegularScriptableObject&gt;();\n    Object.DestroyImmediate(target); // UNH-SUPPRESS: Testing destroyed object handling\n\n    // Unity's null check should handle destroyed objects\n    bool result = SerializableDictionaryPropertyDrawer.IsScriptableSingletonType(target);\n    Assert.IsFalse(result, \"Destroyed object should return false (Unity null check).\");\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-wbuttonrenderingtestscs","title":"Real Example: WButtonRenderingTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Editor/Utils/WButton/WButtonRenderingTests.cs</code>:</p> C#<pre><code>[Test]\npublic void NullEditorTargetHandledGracefully()\n{\n    RenderingTargetSingleButton asset = Track(\n        ScriptableObject.CreateInstance&lt;RenderingTargetSingleButton&gt;()\n    );\n    UnityEditor.Editor editor = Track(UnityEditor.Editor.CreateEditor(asset));\n    Dictionary&lt;WButtonGroupKey, WButtonPaginationState&gt; paginationStates = new();\n    Dictionary&lt;WButtonGroupKey, bool&gt; foldoutStates = new();\n\n    Object.DestroyImmediate(asset); // UNH-SUPPRESS: Test verifies behavior when target is destroyed\n    _trackedObjects.Remove(asset);\n\n    bool drawn = WButtonGUI.DrawButtons(\n        editor,\n        WButtonPlacement.Top,\n        paginationStates,\n        foldoutStates,\n        UnityHelpersSettings.WButtonFoldoutBehavior.AlwaysOpen,\n        triggeredContexts: null,\n        globalPlacementIsTop: true\n    );\n\n    Assert.That(drawn, Is.False, \"Should return false when target is destroyed\");\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#null-references-where-shouldnt-happen","title":"Null References Where \"Shouldn't Happen\"","text":"<p>References that \"can't be null\" sometimes become null due to serialization issues, race conditions, improper initialization, or user error. Robust code must handle these cases gracefully.</p>"},{"location":"contributing/testing-patterns/#pattern-explicit-null-input-handling","title":"Pattern: Explicit Null Input Handling","text":"<p>Test that methods handle null inputs gracefully, even when callers are \"supposed to\" provide non-null values.</p> C#<pre><code>[Test]\npublic void ProcessNullInputDoesNotThrow()\n{\n    Assert.DoesNotThrow(() =&gt; Processor.Process(null));\n}\n\n[Test]\npublic void ProcessNullInputReturnsDefault()\n{\n    var result = Processor.Process(null);\n    Assert.AreEqual(default(MyType), result);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-objecthelpertestscs_1","title":"Real Example: ObjectHelperTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/Helper/ObjectHelperTests.cs</code>:</p> C#<pre><code>[UnityTest]\npublic IEnumerator GetGameObject()\n{\n    GameObject go = Track(new GameObject(\"Test\", typeof(SpriteRenderer)));\n    SpriteRenderer spriteRenderer = go.GetComponent&lt;SpriteRenderer&gt;();\n\n    // ... (normal operation tests) ...\n\n    // Test explicit null input handling\n    result = ((GameObject)null).GetGameObject();\n    Assert.IsTrue(result == null);\n\n    result = ((SpriteRenderer)null).GetGameObject();\n    Assert.IsTrue(result == null);\n    yield break;\n}\n</code></pre> <p>This test verifies that extension methods handle explicit null inputs gracefully, returning null rather than throwing NullReferenceException.</p>"},{"location":"contributing/testing-patterns/#pattern-null-serialized-property-handling","title":"Pattern: Null Serialized Property Handling","text":"<p>Editor code may receive null SerializedProperty references due to timing issues or invalid property paths.</p> C#<pre><code>[Test]\npublic void DrawPropertyHandlesNullProperty()\n{\n    Assert.DoesNotThrow(() =&gt; CustomDrawer.DrawProperty(null, GUIContent.none));\n}\n\n[Test]\npublic void GetValueFromNullPropertyReturnsDefault()\n{\n    object result = PropertyHelper.GetValue(null);\n    Assert.IsNull(result);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-null-collection-elements","title":"Pattern: Null Collection Elements","text":"<p>Collections may contain null elements even when the code assumes they won't.</p> C#<pre><code>[Test]\npublic void ProcessCollectionWithNullElementsSucceeds()\n{\n    List&lt;string&gt; items = new() { \"A\", null, \"B\", null, \"C\" };\n\n    Assert.DoesNotThrow(() =&gt; Processor.ProcessAll(items));\n}\n\n[Test]\npublic void FilterHandlesNullElements()\n{\n    List&lt;Component&gt; components = new() { validComponent, null, anotherValid };\n\n    List&lt;Component&gt; filtered = ComponentFilter.FilterValid(components);\n\n    Assert.That(filtered, Has.None.Null);\n    Assert.AreEqual(2, filtered.Count);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#invalid-enum-values","title":"Invalid Enum Values","text":"<p>Enums can hold any integer value their underlying type supports, not just defined members. This occurs when:</p> <ul> <li>Deserializing data from older/newer versions</li> <li>Casting user input or external data</li> <li>Data corruption</li> </ul>"},{"location":"contributing/testing-patterns/#pattern-cast-invalid-integer-to-enum","title":"Pattern: Cast Invalid Integer to Enum","text":"C#<pre><code>[Test]\npublic void DisplayNameWithInvalidEnumValue()\n{\n    TestEnum invalidValue = (TestEnum)999;\n\n    string displayName = invalidValue.ToDisplayName();\n\n    Assert.IsNotEmpty(displayName, \"Should return some string, not crash\");\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-test-all-enum-operations-with-invalid-values","title":"Pattern: Test All Enum Operations with Invalid Values","text":"C#<pre><code>[Test]\npublic void CachedNameWithInvalidEnumValue()\n{\n    TestEnum invalidValue = (TestEnum)999;\n\n    string cachedName = invalidValue.ToCachedName();\n\n    Assert.IsNotEmpty(cachedName);\n}\n\n[Test]\npublic void HasFlagNoAllocWithInvalidEnumValue()\n{\n    TestEnum invalidValue = (TestEnum)999;\n\n    Assert.IsTrue(invalidValue.HasFlagNoAlloc(invalidValue));\n    Assert.IsFalse(TestEnum.First.HasFlagNoAlloc(invalidValue));\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-enumextensiontestscs","title":"Real Example: EnumExtensionTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/Extensions/EnumExtensionTests.cs</code>:</p> C#<pre><code>[Test]\npublic void DisplayNameWithInvalidEnumValue()\n{\n    TestEnum invalidValue = (TestEnum)999;\n    string displayName = invalidValue.ToDisplayName();\n    Assert.IsNotEmpty(displayName);\n}\n\n[Test]\npublic void CachedNameWithInvalidEnumValue()\n{\n    TestEnum invalidValue = (TestEnum)999;\n    string cachedName = invalidValue.ToCachedName();\n    Assert.IsNotEmpty(cachedName);\n}\n\n[Test]\npublic void HasFlagNoAllocWithInvalidEnumValue()\n{\n    TestEnum invalidValue = (TestEnum)999;\n    Assert.IsTrue(invalidValue.HasFlagNoAlloc(invalidValue));\n    Assert.IsFalse(TestEnum.First.HasFlagNoAlloc(invalidValue));\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-invalid-serializationtype","title":"Pattern: Invalid SerializationType","text":"C#<pre><code>[Test]\npublic void GenericSerializeInvalidSerializationTypeThrowsException()\n{\n    TestMessage msg = new() { Id = 1, Name = \"Test\" };\n\n    Assert.Throws&lt;InvalidEnumArgumentException&gt;(() =&gt;\n        Serializer.Serialize(msg, (SerializationType)999)\n    );\n}\n\n[Test]\npublic void GenericDeserializeInvalidSerializationTypeThrowsException()\n{\n    byte[] data = { 1, 2, 3 };\n\n    Assert.Throws&lt;InvalidEnumArgumentException&gt;(() =&gt;\n        Serializer.Deserialize&lt;TestMessage&gt;(data, (SerializationType)999)\n    );\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-flags-enum-with-all-bits-set","title":"Pattern: Flags Enum with All Bits Set","text":"C#<pre><code>[Test]\npublic void FlagsEnumShowsWhenAllFlagsSetAndExpectedIsSubset()\n{\n    OdinShowIfFlagsTarget target = CreateScriptableObject&lt;OdinShowIfFlagsTarget&gt;();\n    target.flags = (TestFlagsEnum)(-1); // All bits set\n\n    (bool success, bool shouldShow) = EvaluateCondition(\n        target,\n        nameof(OdinShowIfFlagsTarget.flags),\n        new WShowIfAttribute(\n            nameof(OdinShowIfFlagsTarget.flags),\n            expectedValues: new object[] { TestFlagsEnum.FlagA | TestFlagsEnum.FlagB }\n        )\n    );\n\n    Assert.That(success, Is.True);\n    Assert.That(shouldShow, Is.True, \"Field should show when all flags set and expected is subset\");\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#overflow-conditions","title":"Overflow Conditions","text":"<p>Test behavior at the boundaries of numeric types to catch overflow, underflow, and precision issues.</p>"},{"location":"contributing/testing-patterns/#pattern-extreme-numeric-values","title":"Pattern: Extreme Numeric Values","text":"C#<pre><code>[Test]\npublic void BinaryRoundTripComplexObjectAllFieldsCorrect()\n{\n    ComplexMessage msg = new()\n    {\n        Integer = int.MaxValue,\n        Double = Math.PI,\n        Text = \"Complex test with unicode\",\n        Data = new byte[] { 1, 2, 3, 255, 0, 128 },\n    };\n\n    byte[] serialized = Serializer.BinarySerialize(msg);\n    ComplexMessage deserialized = Serializer.BinaryDeserialize&lt;ComplexMessage&gt;(serialized);\n\n    Assert.AreEqual(msg.Integer, deserialized.Integer);\n    Assert.AreEqual(msg.Double, deserialized.Double);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-edge-case-values-via-testcasesource","title":"Pattern: Edge Case Values via TestCaseSource","text":"C#<pre><code>private static IEnumerable&lt;TestCaseData&gt; EdgeCaseTestData()\n{\n    yield return new TestCaseData(new[] { int.MaxValue }, int.MaxValue)\n        .SetName(\"Input.MaxValue.HandlesCorrectly\");\n    yield return new TestCaseData(new[] { int.MinValue }, int.MinValue)\n        .SetName(\"Input.MinValue.HandlesCorrectly\");\n    yield return new TestCaseData(new[] { 0 }, 0)\n        .SetName(\"Input.Zero.ReturnsZero\");\n    yield return new TestCaseData(new[] { -1 }, -1)\n        .SetName(\"Input.Negative.HandlesCorrectly\");\n}\n\n[Test]\n[TestCaseSource(nameof(EdgeCaseTestData))]\npublic void ProcessHandlesEdgeCases(int[] input, int expected)\n{\n    int result = MyProcessor.Process(input);\n\n    Assert.AreEqual(expected, result);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-serializeradditionaltestscs","title":"Real Example: SerializerAdditionalTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/Serialization/SerializerAdditionalTests.cs</code>:</p> C#<pre><code>[Test]\npublic void GenericSerializeWithAllTypesEdgeCaseData()\n{\n    ComplexMessage msg = new()\n    {\n        Integer = int.MinValue,\n        Double = double.MaxValue,\n        Text = string.Empty,\n        Data = new byte[] { 0, 255 },\n        StringList = new List&lt;string&gt; { \"\", \"test\" },\n        Dictionary = new Dictionary&lt;string, int&gt; { [\"\"] = 0, [\"test\"] = -1 },\n    };\n\n    foreach (SerializationType type in new[]\n    {\n        SerializationType.SystemBinary,\n        SerializationType.Protobuf,\n    })\n    {\n        byte[] serialized = Serializer.Serialize(msg, type);\n        ComplexMessage deserialized = Serializer.Deserialize&lt;ComplexMessage&gt;(serialized, type);\n\n        Assert.AreEqual(msg.Integer, deserialized.Integer, $\"Failed for {type}\");\n        Assert.AreEqual(msg.Double, deserialized.Double, $\"Failed for {type}\");\n    }\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#corrupted-serialization-state","title":"Corrupted Serialization State","text":"<p>Test handling of malformed, truncated, or invalid serialized data.</p>"},{"location":"contributing/testing-patterns/#pattern-empty-data","title":"Pattern: Empty Data","text":"C#<pre><code>[Test]\npublic void BinaryDeserializeEmptyArrayThrowsException()\n{\n    byte[] emptyData = Array.Empty&lt;byte&gt;();\n\n    Assert.Throws&lt;SerializationException&gt;(() =&gt;\n        Serializer.BinaryDeserialize&lt;TestMessage&gt;(emptyData)\n    );\n}\n\n[Test]\npublic void ProtoDeserializeEmptyArrayReturnsDefaultInstance()\n{\n    byte[] emptyData = Array.Empty&lt;byte&gt;();\n\n    TestMessage result = Serializer.ProtoDeserialize&lt;TestMessage&gt;(emptyData);\n\n    Assert.NotNull(result);\n    Assert.AreEqual(0, result.Id);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-corrupted-data","title":"Pattern: Corrupted Data","text":"C#<pre><code>[Test]\npublic void BinaryDeserializeCorruptedDataThrowsException()\n{\n    byte[] corruptedData = { 0xFF, 0xFF, 0xFF, 0xFF };\n\n    Assert.Throws&lt;SerializationException&gt;(() =&gt;\n        Serializer.BinaryDeserialize&lt;TestMessage&gt;(corruptedData)\n    );\n}\n\n[Test]\npublic void FileIOReadFromInvalidJsonThrowsException()\n{\n    string filePath = Path.Combine(_tempDirectory, \"invalid.json\");\n    File.WriteAllText(filePath, \"{ invalid json content }\");\n\n    Assert.Throws&lt;JsonException&gt;(() =&gt;\n        Serializer.ReadFromJsonFile&lt;TestMessage&gt;(filePath)\n    );\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-null-data","title":"Pattern: Null Data","text":"C#<pre><code>[Test]\npublic void ProtoDeserializeNullDataThrowsException()\n{\n    Assert.Throws&lt;ProtoException&gt;(() =&gt;\n        Serializer.ProtoDeserialize&lt;TestMessage&gt;(null)\n    );\n}\n\n[Test]\npublic void ProtoDeserializeWithTypeNullDataThrowsException()\n{\n    Assert.Throws&lt;ArgumentException&gt;(() =&gt;\n        Serializer.ProtoDeserialize&lt;object&gt;(null, typeof(TestMessage))\n    );\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#concurrent-access-edge-cases","title":"Concurrent Access Edge Cases","text":"<p>Multi-threaded code can encounter states that are impossible in single-threaded execution. Unity Helpers uses <code>#if !SINGLE_THREADED</code> conditionals to wrap concurrent tests.</p>"},{"location":"contributing/testing-patterns/#pattern-concurrent-operations-do-not-corrupt-state","title":"Pattern: Concurrent Operations Do Not Corrupt State","text":"C#<pre><code>#if !SINGLE_THREADED\n[Test]\npublic void ConcurrentSetsDoNotCorruptCache()\n{\n    using Cache&lt;int, int&gt; cache = CacheBuilder&lt;int, int&gt;\n        .NewBuilder()\n        .MaximumSize(1000)\n        .Build();\n\n    int threadCount = 4;\n    int operationsPerThread = 250;\n    CountdownEvent countdownEvent = new(threadCount);\n    Exception capturedException = null;\n\n    for (int t = 0; t &lt; threadCount; t++)\n    {\n        int threadIndex = t;\n        ThreadPool.QueueUserWorkItem(_ =&gt;\n        {\n            try\n            {\n                for (int i = 0; i &lt; operationsPerThread; i++)\n                {\n                    int key = threadIndex * operationsPerThread + i;\n                    cache.Set(key, key);\n                }\n            }\n            catch (Exception ex)\n            {\n                capturedException = ex;\n            }\n            finally\n            {\n                countdownEvent.Signal();\n            }\n        });\n    }\n\n    countdownEvent.Wait(TimeSpan.FromSeconds(10));\n\n    Assert.IsTrue(capturedException == null, $\"Exception during concurrent sets: {capturedException}\");\n    Assert.AreEqual(threadCount * operationsPerThread, cache.Count);\n}\n#endif\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-mixed-readwrite-operations","title":"Pattern: Mixed Read/Write Operations","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/DataStructures/CacheTests.cs</code>:</p> C#<pre><code>#if !SINGLE_THREADED\n[Test]\npublic void ConcurrentSetsAndGetsDoNotCorruptCache()\n{\n    using Cache&lt;int, int&gt; cache = CacheBuilder&lt;int, int&gt;\n        .NewBuilder()\n        .MaximumSize(500)\n        .Build();\n\n    int threadCount = 4;\n    int operationsPerThread = 500;\n    CountdownEvent countdownEvent = new(threadCount);\n    Exception capturedException = null;\n\n    for (int t = 0; t &lt; threadCount; t++)\n    {\n        int threadIndex = t;\n        ThreadPool.QueueUserWorkItem(_ =&gt;\n        {\n            try\n            {\n                for (int i = 0; i &lt; operationsPerThread; i++)\n                {\n                    if (i % 2 == 0)\n                    {\n                        int key = threadIndex * 100 + (i % 100);\n                        cache.Set(key, key);\n                    }\n                    else\n                    {\n                        int key = (threadIndex + 1) % threadCount * 100 + (i % 100);\n                        cache.TryGet(key, out _);\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                capturedException = ex;\n            }\n            finally\n            {\n                countdownEvent.Signal();\n            }\n        });\n    }\n\n    countdownEvent.Wait(TimeSpan.FromSeconds(10));\n\n    Assert.IsNull(capturedException, $\"Exception during concurrent operations: {capturedException}\");\n}\n#endif\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-rapid-allocationdeallocation","title":"Pattern: Rapid Allocation/Deallocation","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/Utils/BuffersTests.cs</code>:</p> C#<pre><code>#if !SINGLE_THREADED\n[Test]\npublic void WallstopFastArrayPoolConcurrentAccessRapidAllocationDeallocation()\n{\n    const int iterations = 1000;\n    const int threadCount = 4;\n\n    CountdownEvent countdownEvent = new(threadCount);\n    Exception capturedException = null;\n\n    for (int t = 0; t &lt; threadCount; t++)\n    {\n        ThreadPool.QueueUserWorkItem(_ =&gt;\n        {\n            try\n            {\n                for (int i = 0; i &lt; iterations; i++)\n                {\n                    using (PooledArray&lt;int&gt; pooled = WallstopFastArrayPool&lt;int&gt;.Get(64, out _))\n                    {\n                        // Rapid acquire/release cycle\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                capturedException = ex;\n            }\n            finally\n            {\n                countdownEvent.Signal();\n            }\n        });\n    }\n\n    countdownEvent.Wait(TimeSpan.FromSeconds(30));\n    Assert.IsNull(capturedException, $\"Exception during rapid allocation: {capturedException}\");\n}\n#endif\n</code></pre>"},{"location":"contributing/testing-patterns/#key-practices-for-concurrent-tests","title":"Key Practices for Concurrent Tests","text":"<ol> <li>Use <code>CountdownEvent</code> to synchronize thread completion</li> <li>Capture exceptions in threads since NUnit cannot catch them directly</li> <li>Use reasonable timeouts (10-30 seconds) to prevent test hangs</li> <li>Wrap in <code>#if !SINGLE_THREADED</code> for WebGL/IL2CPP compatibility</li> <li>Test both success and exception cases for thread safety</li> </ol>"},{"location":"contributing/testing-patterns/#invalid-state-combinations","title":"Invalid State Combinations","text":"<p>Some states are logically impossible during normal execution but can occur due to reflection, serialization bugs, or corrupted data.</p>"},{"location":"contributing/testing-patterns/#pattern-empty-collections-where-non-empty-expected","title":"Pattern: Empty Collections Where Non-Empty Expected","text":"C#<pre><code>[Test]\npublic void ProcessEmptyArrayGracefully()\n{\n    int[] emptyArray = Array.Empty&lt;int&gt;();\n\n    // Methods that \"shouldn't\" receive empty arrays should handle them\n    int result = collection.Min(emptyArray);\n\n    Assert.AreEqual(default(int), result);\n}\n\n[Test]\npublic void SortEmptyCollection()\n{\n    List&lt;int&gt; emptyList = new();\n\n    Assert.DoesNotThrow(() =&gt; emptyList.Sort(SortAlgorithm.Tim));\n    Assert.AreEqual(0, emptyList.Count);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-spatial-tree-with-zero-elements","title":"Real Example: Spatial Tree with Zero Elements","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/DataStructures/QuadTree2DTests.cs</code>:</p> C#<pre><code>[Test]\npublic void ConstructorWithEmptyCollectionSucceeds()\n{\n    List&lt;Vector2&gt; points = new();\n    QuadTree2D&lt;Vector2&gt; tree = CreateTree(points);\n    Assert.IsNotNull(tree);\n\n    List&lt;Vector2&gt; results = new();\n    tree.GetElementsInRange(Vector2.zero, 10000f, results);\n    Assert.AreEqual(0, results.Count);\n}\n\n[Test]\npublic void GetApproximateNearestNeighborsWithEmptyTreeReturnsEmpty()\n{\n    List&lt;Vector2&gt; points = new();\n    QuadTree2D&lt;Vector2&gt; tree = CreateTree(points);\n    List&lt;Vector2&gt; results = new();\n\n    tree.GetApproximateNearestNeighbors(Vector2.zero, 5, results);\n    Assert.AreEqual(0, results.Count);\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-invalid-indexkey-access","title":"Pattern: Invalid Index/Key Access","text":"C#<pre><code>[Test]\npublic void IndexerThrowsOnInvalidIndex()\n{\n    CyclicBuffer&lt;int&gt; buffer = new(5) { 1, 2 };\n\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt; { _ = buffer[-1]; });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt; { _ = buffer[2]; });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt; { _ = buffer[int.MaxValue]; });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt; { _ = buffer[int.MinValue]; });\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#real-example-cyclicbuffertestscs","title":"Real Example: CyclicBufferTests.cs","text":"<p>From <code>/workspaces/com.wallstop-studios.unity-helpers/Tests/Runtime/DataStructures/CyclicBufferTests.cs</code>:</p> C#<pre><code>[Test]\npublic void IndexerGetOutOfBounds()\n{\n    CyclicBuffer&lt;int&gt; buffer = new(5) { 1, 2 };\n\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt;\n    {\n        _ = buffer[-1];\n    });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt;\n    {\n        _ = buffer[2];\n    });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt;\n    {\n        _ = buffer[5];\n    });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt;\n    {\n        _ = buffer[int.MaxValue];\n    });\n    Assert.Throws&lt;IndexOutOfRangeException&gt;(() =&gt;\n    {\n        _ = buffer[int.MinValue];\n    });\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-disposed-object-access","title":"Pattern: Disposed Object Access","text":"C#<pre><code>[Test]\npublic void AccessAfterDisposeThrows()\n{\n    Cache&lt;int, int&gt; cache = CacheBuilder&lt;int, int&gt;\n        .NewBuilder()\n        .MaximumSize(100)\n        .Build();\n\n    cache.Dispose();\n\n    Assert.Throws&lt;ObjectDisposedException&gt;(() =&gt; cache.Set(1, 1));\n    Assert.Throws&lt;ObjectDisposedException&gt;(() =&gt; cache.TryGet(1, out _));\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#pattern-extreme-capacity-values","title":"Pattern: Extreme Capacity Values","text":"C#<pre><code>[Test]\npublic void IntMaxCapacityOk()\n{\n    CyclicBuffer&lt;int&gt; buffer = new(int.MaxValue);\n    CollectionAssert.AreEquivalent(Array.Empty&lt;int&gt;(), buffer);\n\n    const int tries = 50;\n    List&lt;int&gt; expected = new(tries);\n    for (int i = 0; i &lt; tries; ++i)\n    {\n        buffer.Add(i);\n        expected.Add(i);\n        CollectionAssert.AreEquivalent(expected, buffer);\n    }\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#best-practices","title":"Best Practices","text":""},{"location":"contributing/testing-patterns/#identifying-impossible-states-to-test","title":"Identifying \"Impossible\" States to Test","text":"<ol> <li>Review defensive code paths: Any <code>if (x == null)</code> or <code>try-catch</code> suggests a potential \"impossible\" state</li> <li>Examine switch statements: Missing <code>default</code> cases indicate unhandled enum values</li> <li>Check serialization boundaries: Data crossing process/version boundaries can be corrupted</li> <li>Consider Unity lifecycle: Objects can be destroyed at any frame</li> <li>Look for race conditions: Multi-threaded code has timing-dependent states</li> </ol>"},{"location":"contributing/testing-patterns/#test-structure","title":"Test Structure","text":"<p>Always include these categories in your tests:</p> Category Examples Normal cases Typical usage, common inputs Edge cases Empty, single element, boundary values Negative cases Invalid inputs, error conditions Extreme cases Maximum values, large collections \"The Impossible\" Destroyed objects, invalid enums, corrupted data"},{"location":"contributing/testing-patterns/#unh-suppress-usage","title":"UNH-SUPPRESS Usage","text":"<p>When testing destroyed object behavior, use the <code>// UNH-SUPPRESS</code> comment:</p> C#<pre><code>// UNH-SUPPRESS tells the test linter this DestroyImmediate is intentional\nObject.DestroyImmediate(target); // UNH-SUPPRESS: Test verifies behavior after destruction\n</code></pre> <p>Only use this for intentional destruction testing, not for cleanup. Use <code>Track()</code> for normal test cleanup.</p>"},{"location":"contributing/testing-patterns/#assertions-for-impossible-states","title":"Assertions for \"Impossible\" States","text":"<p>Choose assertions based on expected behavior:</p> C#<pre><code>// When graceful handling is expected\nAssert.DoesNotThrow(() =&gt; Process(invalidInput));\nAssert.IsNotEmpty(invalidValue.ToDisplayName());\nAssert.IsTrue(result == null);\n\n// When exceptions are expected\nAssert.Throws&lt;InvalidEnumArgumentException&gt;(() =&gt; Serialize(msg, (SerializationType)999));\nAssert.Throws&lt;SerializationException&gt;(() =&gt; Deserialize(corruptedData));\n\n// When default values are expected\nAssert.AreEqual(default(T), result);\nAssert.AreEqual(0, deserializedFromEmpty.Id);\n</code></pre>"},{"location":"contributing/testing-patterns/#data-driven-testing-for-edge-cases","title":"Data-Driven Testing for Edge Cases","text":"<p>Use <code>[TestCaseSource]</code> to systematically cover impossible states:</p> C#<pre><code>private static IEnumerable&lt;TestCaseData&gt; ImpossibleStateTestCases()\n{\n    // Destroyed references\n    yield return new TestCaseData(CreateDestroyedObject())\n        .SetName(\"State.DestroyedObject.HandledGracefully\");\n\n    // Invalid enums\n    yield return new TestCaseData((MyEnum)(-1))\n        .SetName(\"State.NegativeEnumValue.HandledGracefully\");\n    yield return new TestCaseData((MyEnum)999)\n        .SetName(\"State.LargeEnumValue.HandledGracefully\");\n    yield return new TestCaseData((MyEnum)int.MaxValue)\n        .SetName(\"State.MaxIntEnumValue.HandledGracefully\");\n\n    // Overflow values\n    yield return new TestCaseData(int.MaxValue)\n        .SetName(\"State.IntMaxValue.HandledGracefully\");\n    yield return new TestCaseData(int.MinValue)\n        .SetName(\"State.IntMinValue.HandledGracefully\");\n\n    // Corrupted strings\n    yield return new TestCaseData(\"\\0\\0\\0\")\n        .SetName(\"State.NullChars.HandledGracefully\");\n    yield return new TestCaseData(new string('\\uD800', 1000))\n        .SetName(\"State.InvalidSurrogates.HandledGracefully\");\n}\n\n[Test]\n[TestCaseSource(nameof(ImpossibleStateTestCases))]\npublic void ProcessHandlesImpossibleStates(object input)\n{\n    Assert.DoesNotThrow(() =&gt; Process(input));\n}\n</code></pre>"},{"location":"contributing/testing-patterns/#summary","title":"Summary","text":"<p>Testing \"impossible\" states is essential for robust production code. These tests:</p> <ol> <li>Catch silent failures before they reach users</li> <li>Document expected behavior for edge cases</li> <li>Prevent regressions when code is refactored</li> <li>Build confidence that defensive code works</li> </ol> <p>When adding new features, always ask: \"What happens if this input is destroyed, null, invalid, or corrupted?\" Then write tests to answer that question.</p> <p>For more information on contributing to Unity Helpers, see the Contributing guide.</p>"},{"location":"features/editor-tools/asset-change-detection/","title":"Asset Change Detection","text":"<p>Automatically respond to asset creation and deletion events.</p> <p>The <code>[DetectAssetChanged]</code> attribute allows you to annotate methods that should execute automatically when specific asset types are created or deleted in the Unity Editor. Perfect for cache invalidation, autoconfiguration, validation, and maintaining derived data.</p>"},{"location":"features/editor-tools/asset-change-detection/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Basic Usage</li> <li>Attribute Parameters</li> <li>Method Signatures</li> <li>Inheritance Support</li> <li>Asset Change Context</li> <li>Best Practices</li> <li>Examples</li> </ul>"},{"location":"features/editor-tools/asset-change-detection/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\npublic class SpriteCache : ScriptableObject\n{\n    private static readonly HashSet&lt;string&gt; TrackedSpritePaths = new();\n\n    [DetectAssetChanged(\n        typeof(Sprite),\n        AssetChangeFlags.Created | AssetChangeFlags.Deleted\n    )]\n    private static void OnSpriteChanged(AssetChangeContext context)\n    {\n        foreach (string path in context.CreatedAssetPaths)\n        {\n            TrackedSpritePaths.Add(path);\n            Debug.Log($\"New sprite added: {path}\");\n        }\n\n        foreach (string path in context.DeletedAssetPaths)\n        {\n            TrackedSpritePaths.Remove(path);\n            Debug.Log($\"Sprite removed: {path}\");\n        }\n    }\n}\n</code></pre> <p>Visual Reference</p> <p> Automatic method invocation when assets are created or deleted</p>"},{"location":"features/editor-tools/asset-change-detection/#attribute-parameters","title":"Attribute Parameters","text":"C#<pre><code>[DetectAssetChanged(\n    Type assetType,                          // Type of asset to monitor (required)\n    AssetChangeFlags flags,                  // Created, Deleted, or both (required)\n    DetectAssetChangedOptions options = None // IncludeAssignableTypes for inheritance\n)]\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#assetchangeflags","title":"AssetChangeFlags","text":"C#<pre><code>[Flags]\npublic enum AssetChangeFlags\n{\n    None = 0,\n    Created = 1 &lt;&lt; 0,     // Trigger on asset creation\n    Deleted = 1 &lt;&lt; 1,     // Trigger on asset deletion\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#detectassetchangedoptions","title":"DetectAssetChangedOptions","text":"C#<pre><code>[Flags]\npublic enum DetectAssetChangedOptions\n{\n    None = 0,\n    IncludeAssignableTypes = 1 &lt;&lt; 0,  // Also trigger for derived types\n    SearchPrefabs = 1 &lt;&lt; 1,           // Search prefabs for MonoBehaviour handlers\n    SearchSceneObjects = 1 &lt;&lt; 2,      // Search open scenes for MonoBehaviour handlers\n}\n</code></pre> <p>Important: <code>SearchPrefabs</code> and <code>SearchSceneObjects</code> are only applicable to instance methods on MonoBehaviour classes. Static methods work without these options.</p>"},{"location":"features/editor-tools/asset-change-detection/#method-signatures","title":"Method Signatures","text":"<p>The attribute supports three method signatures:</p>"},{"location":"features/editor-tools/asset-change-detection/#1-no-parameters-fire-and-forget","title":"1. No Parameters (Fire-and-Forget)","text":"C#<pre><code>[DetectAssetChanged(typeof(ScriptableObject), AssetChangeFlags.Created)]\nprivate static void OnScriptableObjectCreated()\n{\n    Debug.Log(\"A ScriptableObject was created - invalidate cache\");\n}\n</code></pre> <p>When to use: Simple cache invalidation that doesn't need asset details</p>"},{"location":"features/editor-tools/asset-change-detection/#2-full-context-recommended","title":"2. Full Context (Recommended)","text":"C#<pre><code>[DetectAssetChanged(typeof(AudioClip), AssetChangeFlags.Created | AssetChangeFlags.Deleted)]\nprivate static void OnAudioClipChanged(AssetChangeContext context)\n{\n    Debug.Log($\"AudioClip change: {context.Flags}\");\n\n    foreach (string path in context.CreatedAssetPaths)\n    {\n        AudioClip clip = AssetDatabase.LoadAssetAtPath&lt;AudioClip&gt;(path);\n        ProcessAudioClip(clip);\n    }\n\n    foreach (string path in context.DeletedAssetPaths)\n    {\n        Debug.Log($\"AudioClip deleted: {path}\");\n    }\n}\n</code></pre> <p>When to use: Need to handle both creation and deletion, or need access to all changed paths</p>"},{"location":"features/editor-tools/asset-change-detection/#3-typed-arrays-advanced","title":"3. Typed Arrays (Advanced)","text":"C#<pre><code>[DetectAssetChanged(typeof(Material), AssetChangeFlags.Created | AssetChangeFlags.Deleted)]\nprivate static void OnMaterialChanged(Material[] createdMaterials, string[] deletedPaths)\n{\n    foreach (Material mat in createdMaterials)\n    {\n        Debug.Log($\"Material created: {mat.name}\");\n        ValidateMaterial(mat);\n    }\n\n    foreach (string path in deletedPaths)\n    {\n        Debug.Log($\"Material deleted: {path}\");\n    }\n}\n</code></pre> <p>When to use: Need strongly-typed access to created assets; deleted assets are always paths since the asset no longer exists</p>"},{"location":"features/editor-tools/asset-change-detection/#inheritance-support","title":"Inheritance Support","text":"<p>By default, the attribute only triggers for exact type matches. Use <code>IncludeAssignableTypes</code> to include derived types:</p> C#<pre><code>// Triggers for ScriptableObject and ALL derived types\n[DetectAssetChanged(\n    typeof(ScriptableObject),\n    AssetChangeFlags.Created,\n    DetectAssetChangedOptions.IncludeAssignableTypes\n)]\nprivate static void OnAnyScriptableObjectCreated(ScriptableObject obj)\n{\n    Debug.Log($\"ScriptableObject created: {obj.GetType().Name}\");\n}\n\n// Only triggers for exact Material type (not derived classes)\n[DetectAssetChanged(typeof(Material), AssetChangeFlags.Created)]\nprivate static void OnExactMaterialCreated(Material mat)\n{\n    Debug.Log(\"Material (exact type) created\");\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#asset-change-context","title":"Asset Change Context","text":"<p>The <code>AssetChangeContext</code> class provides complete information about the change:</p> C#<pre><code>public sealed class AssetChangeContext\n{\n    public Type AssetType { get; }                      // The type being watched\n    public AssetChangeFlags Flags { get; }              // Created, Deleted, or both\n    public IReadOnlyList&lt;string&gt; CreatedAssetPaths { get; }  // Paths of created assets\n    public IReadOnlyList&lt;string&gt; DeletedAssetPaths { get; }  // Paths of deleted assets\n    public bool HasCreatedAssets { get; }               // True if any created\n    public bool HasDeletedAssets { get; }               // True if any deleted\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#best-practices","title":"Best Practices","text":""},{"location":"features/editor-tools/asset-change-detection/#performance-considerations","title":"Performance Considerations","text":"<ol> <li>Keep methods fast - They run synchronously during asset import</li> <li>Avoid heavy operations - Consider deferring work with <code>EditorApplication.delayCall</code></li> <li>Use static methods when possible - Faster invocation, no instance required</li> </ol>"},{"location":"features/editor-tools/asset-change-detection/#design-patterns","title":"Design Patterns","text":"C#<pre><code>// \u2705 GOOD: Static method for global cache\n[DetectAssetChanged(typeof(Sprite), AssetChangeFlags.Created | AssetChangeFlags.Deleted)]\nprivate static void OnSpriteChanged()\n{\n    SpriteManager.InvalidateCache();\n}\n\n// \u2705 GOOD: Instance method for component-specific logic\n[DetectAssetChanged(typeof(AudioClip), AssetChangeFlags.Created)]\nprivate void OnAudioClipCreated(AudioClip clip)\n{\n    if (clip.name.StartsWith(audioPrefix))\n    {\n        RegisterClip(clip);\n    }\n}\n\n// \u26a0\ufe0f CAUTION: Expensive operation during import\n[DetectAssetChanged(typeof(Texture2D), AssetChangeFlags.Created)]\nprivate static void OnTextureCreated(Texture2D texture)\n{\n    // Heavy processing - consider deferring\n    ProcessTexture(texture);\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#avoiding-reentrant-issues","title":"Avoiding Reentrant Issues","text":"C#<pre><code>[DetectAssetChanged(typeof(Material), AssetChangeFlags.Created)]\nprivate static void OnMaterialCreated(string assetPath)\n{\n    // \u274c BAD: Creating assets during asset processing can cause loops\n    // AssetDatabase.CreateAsset(newMaterial, \"Assets/Generated.mat\");\n\n    // \u2705 GOOD: Defer asset creation\n    EditorApplication.delayCall += () =&gt;\n    {\n        AssetDatabase.CreateAsset(newMaterial, \"Assets/Generated.mat\");\n    };\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#examples","title":"Examples","text":""},{"location":"features/editor-tools/asset-change-detection/#cache-invalidation","title":"Cache Invalidation","text":"C#<pre><code>public class TextureAtlas : ScriptableObject\n{\n    private static List&lt;Texture2D&gt; _cachedTextures;\n\n    [DetectAssetChanged(typeof(Texture2D), AssetChangeFlags.Created | AssetChangeFlags.Deleted)]\n    private static void OnTextureChanged()\n    {\n        _cachedTextures = null; // Invalidate cache\n    }\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#auto-configuration","title":"Auto-Configuration","text":"C#<pre><code>public class MaterialValidator : ScriptableObject\n{\n    [DetectAssetChanged(typeof(Material), AssetChangeFlags.Created)]\n    private static void ValidateNewMaterials(Material[] createdMaterials, string[] deletedPaths)\n    {\n        foreach (Material material in createdMaterials)\n        {\n            if (material.shader.name == \"Standard\")\n            {\n                // Apply project-wide defaults\n                material.SetFloat(\"_Metallic\", 0.0f);\n                material.SetFloat(\"_Glossiness\", 0.5f);\n                EditorUtility.SetDirty(material);\n            }\n        }\n    }\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#derived-type-monitoring","title":"Derived Type Monitoring","text":"C#<pre><code>public abstract class GameData : ScriptableObject { }\n\npublic class DataRegistry : ScriptableObject\n{\n    private static readonly HashSet&lt;string&gt; RegisteredPaths = new();\n\n    [DetectAssetChanged(\n        typeof(GameData),\n        AssetChangeFlags.Created | AssetChangeFlags.Deleted,\n        DetectAssetChangedOptions.IncludeAssignableTypes\n    )]\n    private static void OnGameDataChanged(GameData[] created, string[] deletedPaths)\n    {\n        foreach (GameData data in created)\n        {\n            string path = AssetDatabase.GetAssetPath(data);\n            RegisteredPaths.Add(path);\n            Debug.Log($\"Registered: {data.GetType().Name} at {path}\");\n        }\n\n        foreach (string path in deletedPaths)\n        {\n            RegisteredPaths.Remove(path);\n            Debug.Log($\"Unregistered: {path}\");\n        }\n    }\n}\n</code></pre>"},{"location":"features/editor-tools/asset-change-detection/#prefab-based-instance-methods","title":"Prefab-Based Instance Methods","text":"<p>Use <code>SearchPrefabs</code> to invoke instance methods on MonoBehaviours attached to prefabs:</p> C#<pre><code>public class SpriteCache : MonoBehaviour\n{\n    [SerializeField] private List&lt;Sprite&gt; _cachedSprites = new();\n\n    [DetectAssetChanged(\n        typeof(Sprite),\n        AssetChangeFlags.Created | AssetChangeFlags.Deleted,\n        DetectAssetChangedOptions.SearchPrefabs\n    )]\n    private void OnSpriteChanged(AssetChangeContext context)\n    {\n        // This instance method is called on the prefab asset\n        Debug.Log($\"SpriteCache on prefab received sprite change: {context.Flags}\");\n        RefreshCache();\n    }\n\n    private void RefreshCache()\n    {\n        _cachedSprites.Clear();\n        // Rebuild cache...\n    }\n}\n</code></pre> <p>When to use: When your MonoBehaviour needs instance-specific state or serialized fields</p>"},{"location":"features/editor-tools/asset-change-detection/#scene-object-instance-methods","title":"Scene Object Instance Methods","text":"<p>Use <code>SearchSceneObjects</code> to invoke instance methods on MonoBehaviours in open scenes:</p> C#<pre><code>public class LiveAssetWatcher : MonoBehaviour\n{\n    [SerializeField] private string _watchedFolder;\n\n    [DetectAssetChanged(\n        typeof(Texture2D),\n        AssetChangeFlags.Created,\n        DetectAssetChangedOptions.SearchSceneObjects\n    )]\n    private void OnTextureCreated(AssetChangeContext context)\n    {\n        // Called on every LiveAssetWatcher instance in all open scenes\n        foreach (string path in context.CreatedAssetPaths)\n        {\n            if (path.StartsWith(_watchedFolder))\n            {\n                Debug.Log($\"{name} detected new texture: {path}\");\n                HandleNewTexture(path);\n            }\n        }\n    }\n\n    private void HandleNewTexture(string path) { /* ... */ }\n}\n</code></pre> <p>When to use: For editor tools that need to react to changes based on scene-specific configuration</p>"},{"location":"features/editor-tools/asset-change-detection/#combined-prefab-and-scene-search","title":"Combined Prefab and Scene Search","text":"<p>Use both options together to find handlers in both prefabs and open scenes:</p> C#<pre><code>public class UniversalAssetHandler : MonoBehaviour\n{\n    [DetectAssetChanged(\n        typeof(AudioClip),\n        AssetChangeFlags.Created | AssetChangeFlags.Deleted,\n        DetectAssetChangedOptions.SearchPrefabs | DetectAssetChangedOptions.SearchSceneObjects\n    )]\n    private void OnAudioClipChanged(AssetChangeContext context)\n    {\n        // Called on instances in both prefabs AND scene objects\n        Debug.Log($\"{name} (on {gameObject.name}) received audio change\");\n    }\n}\n</code></pre> <p>Performance Note: Searching prefabs and scenes has overhead. Use these options only when you need instance-specific behavior. For simple notifications, prefer static methods.</p>"},{"location":"features/editor-tools/asset-change-detection/#implementation-details","title":"Implementation Details","text":"<p>The <code>DetectAssetChangeProcessor</code> (Editor assembly) automatically:</p> <ol> <li>Scans for methods decorated with <code>[DetectAssetChanged]</code></li> <li>Registers callbacks with Unity's <code>AssetPostprocessor</code></li> <li>Invokes methods when matching assets change</li> <li>Handles null checks and error cases</li> <li>Supports both Edit Mode and Play Mode</li> </ol> <p>Threading: All callbacks execute on the main thread during asset processing</p> <p>Timing: Methods are called after Unity completes asset import/deletion</p>"},{"location":"features/editor-tools/asset-change-detection/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/editor-tools/asset-change-detection/#method-is-not-called","title":"Method Is Not Called","text":"<ul> <li>Ensure the method is in a type that Unity can discover (not in a generic class)</li> <li>Check that the asset type matches exactly (unless using <code>IncludeAssignableTypes</code>)</li> <li>Verify the asset change flags match the operation (Created vs. Deleted)</li> <li>For MonoBehaviour instance methods:</li> <li>Use <code>SearchPrefabs</code> if the handler is on a prefab asset</li> <li>Use <code>SearchSceneObjects</code> if the handler is on a GameObject in a scene</li> <li>Instance methods without these options only work for ScriptableObjects saved as assets</li> </ul>"},{"location":"features/editor-tools/asset-change-detection/#monobehaviour-instance-methods-not-working","title":"MonoBehaviour Instance Methods Not Working","text":"<p>If your instance method on a MonoBehaviour isn't being called:</p> <ol> <li>On a prefab? Add <code>DetectAssetChangedOptions.SearchPrefabs</code></li> <li>In a scene? Add <code>DetectAssetChangedOptions.SearchSceneObjects</code></li> <li>Need both? Combine: <code>SearchPrefabs | SearchSceneObjects</code></li> <li>Don't need instance state? Use a <code>static</code> method instead (most efficient)</li> </ol>"},{"location":"features/editor-tools/asset-change-detection/#performance-issues","title":"Performance Issues","text":"<ul> <li>Profile with Unity Profiler during asset import</li> <li>Consider deferring work with <code>EditorApplication.delayCall</code></li> <li>Use <code>static</code> methods to avoid unnecessary instance lookups</li> <li>Avoid <code>SearchPrefabs</code> in large projects - it loads all prefabs to check for components</li> <li>Avoid <code>SearchSceneObjects</code> with many open scenes - searches all loaded scenes</li> </ul>"},{"location":"features/editor-tools/asset-change-detection/#null-reference-exceptions","title":"Null Reference Exceptions","text":"<ul> <li>Remember: asset parameter is <code>null</code> for deletion events</li> <li>Always null-check when handling <code>AssetChangeFlags.Deleted</code></li> </ul>"},{"location":"features/editor-tools/asset-change-detection/#related-features","title":"Related Features","text":"<ul> <li>Attribute Metadata Cache Generator - Caches attribute metadata for fast lookup</li> <li>ScriptableObject Singleton Creator - Auto-creates singleton assets</li> <li>Inspector Attributes - Other custom inspector features</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/","title":"Wallstop Studios Unity Helpers - Editor Tools Guide","text":""},{"location":"features/editor-tools/editor-tools-guide/#tldr-what-you-get","title":"TL;DR \u2014 What You Get","text":"<ul> <li>One\u2011click utilities for sprites, textures, validation, and automation.</li> <li>Clear menus, step\u2011by\u2011step workflows, and safe previews before destructive actions.</li> <li>Start with: Sprite Cropper, Image Blur, Animation Creator, Prefab Checker, and the ScriptableObject Singleton Creator.</li> </ul> <p>Comprehensive documentation for all editor wizards, windows, and automation tools.</p>"},{"location":"features/editor-tools/editor-tools-guide/#what-do-you-want-to-do-task-based-index","title":"What Do You Want To Do? (Task-Based Index)","text":""},{"location":"features/editor-tools/editor-tools-guide/#optimize-sprite-memory-performance","title":"Optimize Sprite Memory &amp; Performance","text":"<ul> <li>Remove transparent padding \u2192 Sprite Cropper</li> <li>Adjust texture size automatically \u2192 Fit Texture Size</li> <li>Batch apply import settings \u2192 Texture Settings Applier</li> <li>Standardize sprite settings \u2192 Sprite Settings Applier</li> <li>Adjust sprite pivots \u2192 Sprite Pivot Adjuster</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#create-edit-animations","title":"Create &amp; Edit Animations","text":"<ul> <li>Edit animation timing/frames visually \u2192 Sprite Animation Editor</li> <li>Bulk-create animations from sprites \u2192 Animation Creator</li> <li>Convert sprite sheets to clips \u2192 Sprite Sheet Animation Creator</li> <li>Add/edit animation events \u2192 Animation Event Editor</li> <li>Copy/sync animations between folders \u2192 Animation Copier</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#build-sprite-atlases","title":"Build Sprite Atlases","text":"<ul> <li>Create atlases with regex/labels \u2192 Sprite Atlas Generator</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#validate-fix-prefabs","title":"Validate &amp; Fix Prefabs","text":"<ul> <li>Check prefabs for errors \u2192 Prefab Checker</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#analyze-code-quality","title":"Analyze Code Quality","text":"<ul> <li>Detect inheritance and Unity method issues \u2192 Unity Method Analyzer</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#apply-visual-effects","title":"Apply Visual Effects","text":"<ul> <li>Blur textures (backgrounds, DOF) \u2192 Image Blur Tool</li> <li>Resize textures with filtering \u2192 Texture Resizer</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#automate-setup-maintenance","title":"Automate Setup &amp; Maintenance","text":"<ul> <li>Auto-create singleton assets \u2192 ScriptableObject Singleton Creator</li> <li>Respond to asset changes \u2192 Asset Change Detection</li> <li>Cache attribute metadata \u2192 Attribute Metadata Cache Generator</li> <li>Track sprite labels \u2192 Sprite Label Processor</li> <li>Capture and export test failures \u2192 Failed Tests Exporter</li> <li>Manually trigger script recompilation \u2192 Request Script Compilation</li> <li>Configure buffer settings \u2192 Project Settings: Unity Helpers</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#enhance-inspector-workflows","title":"Enhance Inspector Workflows","text":"<p>See the Inspector Attributes documentation for:</p> <ul> <li><code>[WInLineEditor]</code> \u2014 Embed nested object inspectors inline</li> <li><code>[WShowIf]</code> \u2014 Conditional field visibility</li> <li><code>[WEnumToggleButtons]</code> \u2014 Visual toggle buttons for enums</li> <li><code>[WValueDropDown]</code>, <code>[IntDropDown]</code>, <code>[StringInList]</code> \u2014 Selection dropdowns</li> <li><code>[WReadOnly]</code>, <code>[WNotNull]</code> \u2014 Validation attributes</li> <li><code>[WGroup]</code>, <code>[WButton]</code> \u2014 Layout and method invocation</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#table-of-contents","title":"Table of Contents","text":"<ol> <li>Texture &amp; Sprite Tools</li> <li>Animation Tools</li> <li>Sprite Atlas Tools</li> <li>Validation &amp; Quality Tools</li> <li>Prefab Checker</li> <li>Unity Method Analyzer</li> <li>Custom Component Editors</li> <li>Property Drawers &amp; Attributes</li> <li>Automation &amp; Utilities</li> <li>ScriptableObject Singleton Creator</li> <li>Asset Change Detection</li> <li>Quick Reference</li> </ol>"},{"location":"features/editor-tools/editor-tools-guide/#texture-sprite-tools","title":"Texture &amp; Sprite Tools","text":""},{"location":"features/editor-tools/editor-tools-guide/#image-blur-tool","title":"Image Blur Tool","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Image Blur</code></p> <p>Purpose: Apply Gaussian blur effects to textures in batch for backgrounds, depth-of-field, or softened sprites.</p> <p>Key Features:</p> <ul> <li>Configurable blur radius (1-200 pixels)</li> <li>Batch processing support</li> <li>Drag-and-drop folders/files</li> <li>Preserves original files</li> <li>Parallel processing for speed</li> </ul> <p>Common Workflow:</p> Text Only<pre><code>1. Open Image Blur Tool\n2. Drag sprite folder into designated area\n3. Set blur radius (e.g., 10 for subtle, 50 for heavy)\n4. Click \"Apply Blur\"\n5. Find blurred versions with \"_blurred_[radius]\" suffix\n</code></pre> <p>Best For:</p> <ul> <li>UI background blur effects</li> <li>Depth-of-field texture generation</li> <li>Post-processing texture preparation</li> </ul> <p>Visual Demo</p> <p></p> <p>Adjusting blur radius from 0 to 200 pixels on a UI background texture</p>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-cropper","title":"Sprite Cropper","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Cropper</code></p> <p>Purpose: Automatically remove transparent padding from sprites to optimize memory and atlas packing.</p> <p>Key Features:</p> <ul> <li>Alpha threshold detection (0.01)</li> <li>Configurable padding preservation</li> <li>Batch directory processing</li> <li>\"Only Necessary\" mode to skip optimal sprites</li> <li>Pivot point preservation in normalized coordinates</li> </ul> <p>Common Workflow:</p> Text Only<pre><code>1. Open Sprite Cropper\n2. Add sprite directories to \"Input Directories\"\n3. Set padding (e.g., 2px on all sides for outlines)\n4. Enable \"Only Necessary\" to skip already-cropped sprites\n5. Click \"Find Sprites To Process\" to preview\n6. Click \"Process X Sprites\"\n7. Replace originals with \"Cropped_*\" versions\n\n**Danger Zone \u2014 Reference Replacement:**\n- After cropping into `Cropped_*` outputs, you can optionally replace references to original sprites across assets with their cropped counterparts. This is powerful but destructive; review the preview output and ensure you have version control backups before applying.\n</code></pre> <p>Best For:</p> <ul> <li>Sprites exported with excessive padding</li> <li>Character animation optimization</li> <li>Sprite atlas memory reduction</li> <li>Preparing assets for efficient packing</li> </ul> <p>Performance Impact: Can reduce texture memory by 30-70% on padded sprites.</p> <p>Visual Demo</p> <p></p> <p>Before and after: transparent padding removed while preserving sprite content and pivot</p> <p>Related Tools:</p> <ul> <li>After cropping, use Texture Settings Applier to batch apply import settings</li> <li>Before creating atlases, run Sprite Cropper \u2192 Sprite Atlas Generator</li> <li>Use Sprite Pivot Adjuster after cropping to fix pivot points</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#texture-settings-applier","title":"Texture Settings Applier","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Texture Settings Applier</code></p> <p>Purpose: Batch apply standardized texture import settings across multiple assets.</p> <p>Configurable Settings:</p> <ul> <li>Read/Write enabled</li> <li>Mipmap generation</li> <li>Wrap Mode (Clamp/Repeat/Mirror)</li> <li>Filter Mode (Point/Bilinear/Trilinear)</li> <li>Compression (CompressedHQ/LQ/Uncompressed)</li> <li>Crunch compression</li> <li>Max texture size (32-8192)</li> <li>Texture format</li> </ul> <p>Common Configurations:</p> <p>UI Sprites (Pixel-Perfect):</p> Text Only<pre><code>Filter Mode: Point\nWrap Mode: Clamp\nCompression: CompressedHQ or None\nGenerate Mip Maps: false\nMax Size: 2048 or match source\n</code></pre> <p>Environment Textures:</p> Text Only<pre><code>Filter Mode: Trilinear\nWrap Mode: Repeat\nCompression: CompressedHQ\nGenerate Mip Maps: true\nCrunch Compression: true\n</code></pre> <p>Character Sprites:</p> Text Only<pre><code>Filter Mode: Bilinear\nWrap Mode: Clamp\nCompression: CompressedHQ\nGenerate Mip Maps: false\n</code></pre> <p>Workflow:</p> Text Only<pre><code>1. Open Texture Settings Applier\n2. Configure desired settings with checkboxes\n3. Add textures individually OR add directories\n4. Click \"Set\" to apply\n5. Unity reimports affected textures\n</code></pre> <p>Best For:</p> <ul> <li>Standardizing settings after art imports</li> <li>Fixing texture quality issues across directories</li> <li>Maintaining performance standards</li> <li>Team consistency enforcement</li> </ul> <p>Visual Reference</p> <p></p> <p>Texture Settings Applier with filter mode, wrap mode, and compression options</p> <p>Related Tools:</p> <ul> <li>After setting texture settings, use Sprite Settings Applier for sprite-specific options</li> <li>Use Sprite Cropper first to optimize memory before applying settings</li> <li>Combine with Fit Texture Size to auto-adjust max texture sizes</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-pivot-adjuster","title":"Sprite Pivot Adjuster","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Pivot Adjuster</code></p> <p>Purpose: Compute and apply alpha\u2011weighted center\u2011of\u2011mass pivots in bulk. Produces perceptually centered pivots (ignoring near\u2011transparent pixels) and speeds re\u2011imports by skipping unchanged results.</p> <p>Key Features:</p> <ul> <li>Alpha\u2011weighted center\u2011of\u2011mass pivot (configurable cutoff)</li> <li>Optional sprite name regex filter</li> <li>Skip unchanged (fuzzy threshold) and Force Reimport</li> <li>Directory picker with recursive processing</li> </ul> <p>Workflow:</p> Text Only<pre><code>1) Open Sprite Pivot Adjuster\n2) Add one or more directories\n3) (Optional) Set Sprite Name Regex to filter\n4) Adjust Alpha Cutoff (e.g., 0.01 to ignore fringe pixels)\n5) Enable \u201cSkip Unchanged\u201d to reimport only when pivot changes\n6) (Optional) Enable \u201cForce Reimport\u201d to override skip\n7) Run the adjuster to write importer pivot values\n</code></pre> <p>Best For:</p> <ul> <li>Ground\u2011aligning characters while keeping lateral centering</li> <li>Consistent pivots across varied silhouettes</li> <li>Normalizing pivots before animation creation</li> </ul> <p>Visual Reference</p> <p></p> <p>Sprite Pivot Adjuster with alpha cutoff slider and directory selection</p>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-settings-applier","title":"Sprite Settings Applier","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Settings Applier</code></p> <p>Purpose: Apply sprite\u2011specific importer settings in bulk, driven by matchable \u201cprofiles\u201d (Any/NameContains/PathContains/Regex/Extension) with priorities. Great for standardizing PPU, pivots, modes, and compression rules across large folders.</p> <p>Profiles &amp; Matching:</p> <ul> <li>Create a <code>SpriteSettingsProfileCollection</code> ScriptableObject</li> <li>Add one or more profiles (with priority) and choose a match mode:</li> <li>Any, NameContains, PathContains, Extension, Regex</li> <li>Higher priority wins when multiple profiles match</li> </ul> <p>Key Settings (per profile):</p> <ul> <li>Pixels Per Unit, Pivot, Sprite Mode</li> <li>Generate Mip Maps, Read/Write, Alpha is Transparency</li> <li>Extrude Edges, Wrap Mode, Filter Mode</li> <li>Compression Level and Crunch Compression</li> <li>Texture Type override (ensure Sprite)</li> </ul> <p>Workflow:</p> Text Only<pre><code>1) Create a SpriteSettingsProfileCollection (Assets &gt; Create &gt; \u2026 if available) or configure profiles in the window\n2) Open Sprite Settings Applier\n3) Add directories and/or explicit sprites\n4) Choose which profile(s) to apply and click Set\n5) Unity reimports affected sprites\n</code></pre> <p>Best For:</p> <ul> <li>Enforcing project\u2011wide sprite import standards</li> <li>Fixing inconsistent PPU/pivots automatically</li> <li>Applying different settings per folder/pattern (via Regex/Path)</li> </ul> <p>Visual Reference</p> <p></p> <p>Sprite Settings Applier with profile matching modes and import settings</p>"},{"location":"features/editor-tools/editor-tools-guide/#texture-resizer","title":"Texture Resizer","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Texture Resizer</code></p> <p>Purpose: Batch resize textures using bilinear or point filtering algorithms with configurable scaling multipliers.</p> <p>Configuration Options:</p> <ul> <li>textures: Manually selected textures to resize</li> <li>textureSourcePaths: Drag folders to process all textures within</li> <li>numResizes: Number of resize iterations to apply</li> <li>scalingResizeAlgorithm: Bilinear (smooth) or Point (pixel-perfect)</li> <li>pixelsPerUnit: Base PPU for scaling calculations</li> <li>widthMultiplier: Width scaling factor (default: 0.54)</li> <li>heightMultiplier: Height scaling factor (default: 0.245)</li> </ul> <p>How It Works:</p> <ol> <li>For each texture, calculates: <code>extraWidth = width / (PPU * widthMultiplier)</code></li> <li>Resizes to <code>newSize = (width + extraWidth, height + extraHeight)</code></li> <li>Repeats for <code>numResizes</code> iterations</li> <li>Overwrites original PNG files</li> </ol> <p>Workflow:</p> Text Only<pre><code>1. Open Texture Resizer wizard\n2. Add textures manually OR drag texture folders\n3. Set algorithm (Bilinear for smooth, Point for pixel art)\n4. Configure PPU and multipliers\n5. Set number of resize passes\n6. Click \"Resize\" to apply\n</code></pre> <p>Resize Algorithms:</p> <p>Bilinear:</p> <ul> <li>Smooth interpolation</li> <li>Good for photographic/realistic textures</li> <li>Prevents harsh edges</li> <li>Slight blur on upscaling</li> </ul> <p>Point:</p> <ul> <li>Nearest-neighbor sampling</li> <li>Perfect for pixel art</li> <li>Maintains sharp edges</li> <li>No interpolation blur</li> </ul> <p>Best For:</p> <ul> <li>Batch upscaling sprites</li> <li>Standardizing texture dimensions</li> <li>Preparing assets for specific PPU</li> <li>Pixel art scaling (use Point)</li> <li>Multiple resize passes for gradual scaling</li> </ul> <p>Important Notes:</p> <ul> <li>Destructive operation: Overwrites original files</li> <li>Textures are made readable automatically</li> <li>Changes are permanent (backup originals!)</li> <li>AssetDatabase refreshes after completion</li> </ul> <p>Visual Reference</p> <p></p> <p>Texture Resizer with bilinear/point algorithm selection and multiplier settings</p>"},{"location":"features/editor-tools/editor-tools-guide/#fit-texture-size","title":"Fit Texture Size","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Fit Texture Size</code></p> <p>Purpose: Automatically adjust texture max size import settings to match actual source dimensions (power-of-two).</p> <p>Key Features:</p> <ul> <li>Grow and Shrink: Adjust to perfect fit (default)</li> <li>Grow Only: Only increase max size if too small</li> <li>Shrink Only: Only decrease max size if too large</li> <li>Preview mode: Calculate changes before applying</li> <li>Batch processing: Process entire directories at once</li> </ul> <p>Fit Modes:</p> <p>GrowAndShrink:</p> <ul> <li>Sets max texture size to the nearest power-of-2 that fits the source</li> <li>Example: 1500x800 source \u2192 2048 max size</li> <li>Prevents both over-allocation and quality loss</li> </ul> <p>GrowOnly:</p> <ul> <li>Increases max size if the source is larger</li> <li>Never decreases size</li> <li>Useful for preventing quality loss on imports</li> </ul> <p>ShrinkOnly:</p> <ul> <li>Decreases max size if the source is smaller</li> <li>Never increases size</li> <li>Useful for reducing memory usage</li> </ul> <p>Workflow:</p> Text Only<pre><code>1. Open Fit Texture Size\n2. Select Fit Mode (GrowAndShrink/GrowOnly/ShrinkOnly)\n3. Add texture folders to process\n4. Click \"Calculate Potential Changes\" to preview\n5. Review how many textures will be modified\n6. Click \"Run Fit Texture Size\" to apply\n</code></pre> <p>Example:</p> Text Only<pre><code>Source Texture: 1920x1080 pixels\nCurrent Max Size: 512\nFit Mode: GrowAndShrink\nResult: Max Size \u2192 2048 (fits source dimensions)\n\nSource Texture: 64x64 pixels\nCurrent Max Size: 2048\nFit Mode: ShrinkOnly\nResult: Max Size \u2192 64 (matches source)\n</code></pre> <p>Algorithm:</p> <ul> <li>Reads actual source width/height (not imported size)</li> <li>Calculates required power-of-2: <code>size = max(width, height)</code></li> <li>Rounds up to the next power-of-2 (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)</li> <li>Applies based on fit mode constraints</li> </ul> <p>Best For:</p> <ul> <li>Fixing texture import settings after bulk imports</li> <li>Optimizing memory usage automatically</li> <li>Ensuring quality matches source resolution</li> <li>Standardizing texture settings across the project</li> <li>Build size optimization</li> </ul> <p>Performance:</p> <ul> <li>Non-destructive (only changes import settings)</li> <li>Uses AssetDatabase batch editing for speed</li> <li>Progress bar for large operations</li> <li>Cancellable during processing</li> </ul> <p>Visual Reference</p> <p></p> <p>Fit Texture Size with GrowAndShrink/GrowOnly/ShrinkOnly mode selection</p>"},{"location":"features/editor-tools/editor-tools-guide/#animation-tools","title":"Animation Tools","text":""},{"location":"features/editor-tools/editor-tools-guide/#sprite-animation-editor-animation-viewer-window","title":"Sprite Animation Editor (Animation Viewer Window)","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Animation Editor</code></p> <p>Purpose: Visual editor for 2D sprite animations with real-time preview and frame manipulation.</p> <p>Key Features:</p> <ul> <li>Real-time preview: See animations as you edit</li> <li>Multi-layer support: Preview multiple clips simultaneously</li> <li>Drag-and-drop reordering: Intuitive frame organization</li> <li>FPS control: Adjust playback speed independently</li> <li>Frame management: Add/remove/reorder/duplicate frames</li> <li>Multi-file browser: Quick batch loading</li> <li>Binding preservation: Maintains SpriteRenderer paths</li> </ul> <p>Typical Workflow:</p> Text Only<pre><code>1. Open Sprite Animation Editor\n2. Click \"Browse Clips (Multi)...\" to select animations\n3. Click a loaded clip in left panel to edit\n4. Drag frames in \"Frames\" panel to reorder\n5. Adjust FPS field and click \"Apply FPS\"\n6. Preview updates in real-time\n7. Click \"Save Clip\" to write changes\n</code></pre> <p>Example Session:</p> Text Only<pre><code>// Edit walk cycle animation:\n1. Load \"PlayerWalk.anim\"\n2. Preview plays at original 12 FPS\n3. Drag frame 3 to position 1 (change starting pose)\n4. Change FPS to 10 for slower walk\n5. Click \"Apply FPS\" to preview\n6. Click \"Save Clip\" to finalize\n</code></pre> <p>Best For:</p> <ul> <li>Tweaking animation timing without re-export</li> <li>Creating variations by reordering frames</li> <li>Previewing character animation sets</li> <li>Testing different FPS values</li> <li>Quick prototyping from sprite sheets</li> <li>Fixing frame order mistakes</li> </ul> <p>Tips:</p> <ul> <li>Use the multi-file browser for entire animation sets</li> <li>Preview updates automatically while dragging</li> <li>FPS changes only affect the preview until saved</li> <li>Type frame numbers for precise positioning</li> <li>Press Enter to apply frame changes</li> </ul> <p>Visual Demo</p> <p></p> <p>Drag-and-drop frame reordering with real-time preview updates</p> <p></p> <p>Adjusting FPS slider and seeing immediate preview speed change</p>"},{"location":"features/editor-tools/editor-tools-guide/#animation-creator","title":"Animation Creator","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Animation Creator</code></p> <p>Purpose: Bulk\u2011create AnimationClips from sprite naming patterns \u2014 one\u2011click generation from folders of sprites. Eliminates manual clip setup and ensures consistent naming, ordering, and FPS/loop settings.</p> <p>Problems Solved:</p> <ul> <li>Manual and error\u2011prone clip creation from many sprites</li> <li>Inconsistent frame ordering (lexicographic vs. numeric)</li> <li>Collisions/duplicates when generating many clips at once</li> <li>Repeating busywork when adding suffixes/prefixes across sets</li> </ul> <p>Key Features:</p> <ul> <li>Folder sources with regex sprite filtering (<code>spriteNameRegex</code>)</li> <li>Auto\u2011parse into clips using naming patterns (one click)</li> <li>Custom group regex with named groups <code>(?&lt;base&gt;)(?&lt;index&gt;)</code></li> <li>Case\u2011insensitive grouping and numeric sorting toggle</li> <li>Prefix clip names with a leaf folder or full folder path</li> <li>Auto\u2011parse name prefix/suffix and duplicate\u2011name resolution</li> <li>Dry\u2011run and preview (see groups and final asset paths)</li> <li>Per\u2011clip FPS and loop flag; bulk name append/remove</li> <li>\"Populate First Slot with X Matched Sprites\" helper</li> <li>Live preview: Real-time animation playback in the editor before committing</li> <li>Variable framerate: Constant FPS or curve-based frame timing per clip</li> </ul> <p>Framerate Modes:</p> Mode Description <code>Constant</code> Fixed frames per second (default) <code>Curve</code> AnimationCurve-based timing for per-frame speed variation <p>Common Naming Patterns (auto\u2011detected):</p> Text Only<pre><code>Player_Idle_0.png, Player_Idle_1.png, ...       // base: Player_Idle, index: 0..N\nslime-walk-01.png, slime-walk-02.png            // base: slime-walk, index: 1..N\nMage/Attack (0).png, Mage/Attack (1).png        // base: Mage_Attack, index: 0..N (folder prefix optional)\n</code></pre> <p>Custom Group Regex Examples:</p> Text Only<pre><code>// Named groups are optional but powerful when needed\n^(?&lt;base&gt;.*?)(?:_|\\s|-)?(?&lt;index&gt;\\d+)\\.[Pp][Nn][Gg]$   // base + trailing digits\n^Enemy_(?&lt;base&gt;Walk)_(?&lt;index&gt;\\d+)$                      // narrow to specific clip type\n</code></pre> <p>How To Use (one\u2011click flow):</p> Text Only<pre><code>1) Open Animation Creator\n2) Add one or more source folders\n3) (Optional) Set sprite filter regex to narrow matches\n4) Click \u201cAuto\u2011Parse Matched Sprites into Animations\u201d\n5) Review generated Animation Data (set FPS/loop per clip)\n6) Click \u201cCreate\u201d (Action button) to write .anim assets\n</code></pre> <p>Preview &amp; Safety:</p> <ul> <li>Use \u201cGenerate Auto\u2011Parse Preview\u201d to see detected groups</li> <li>Use \u201cGenerate Dry\u2011Run Apply\u201d to see final clip names/paths</li> <li>Toggle \u201cStrict Numeric Ordering\u201d to avoid <code>1,10,11,2,\u2026</code> issues</li> <li>Enable \u201cResolve Duplicate Animation Names\u201d to auto\u2011rename</li> </ul> <p>Tips:</p> <ul> <li>Keep sprite names consistent (e.g., <code>Name_Action_###</code>)</li> <li>Use the built\u2011in Regex Tester before applying</li> <li>Use folder name/path prefixing to avoid collisions across sets</li> <li>Batch rename tokens with the \u201cBulk Naming Operations\u201d section</li> </ul> <p>Best For:</p> <ul> <li>One\u2011click bulk clip creation from sprite folders</li> <li>Converting exported frame sequences into clips</li> <li>Large projects standardizing animation naming and FPS/loop</li> </ul> <p>Visual Demo</p> <p></p> <p>One-click auto-parse: sprites grouped by naming pattern, clips generated instantly</p> <p>Related Tools:</p> <ul> <li>After creating animations, edit timing with Sprite Animation Editor</li> <li>Add events to created animations with Animation Event Editor</li> <li>Organize animations between folders with Animation Copier</li> <li>For sprite sheets (not sequences), use Sprite Sheet Animation Creator</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#animation-copier","title":"Animation Copier","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Animation Copier</code></p> <p>Purpose: Analyze, duplicate, and synchronize AnimationClips between source and destination folders with previews, dry\u2011runs, and cleanup actions.</p> <p>What It Analyzes:</p> <ul> <li>New: exist in the source but not the destination</li> <li>Changed: exist in both but differ (hash mismatch)</li> <li>Unchanged: identical in both (duplicates)</li> <li>Destination Orphans: only in destination</li> </ul> <p>Workflow:</p> Text Only<pre><code>1) Open Animation Copier\n2) Select Source Path (e.g., Assets/Sprites/Animations)\n3) Select Destination Path (e.g., Assets/Animations)\n4) Click \u201cAnalyze Source &amp; Destination\u201d\n5) Review New/Changed/Unchanged/Orphans tabs (filter/sort)\n6) Choose a copy mode:\n   - Copy New / Copy Changed / Copy All (optional force replace)\n7) (Optional) Dry Run to preview without writing\n8) Use Cleanup:\n   - Delete Unchanged Source Duplicates\n   - Delete Destination Orphans\n</code></pre> <p>Safety &amp; Options:</p> <ul> <li>Dry Run (no changes) for all copy/cleanup operations</li> <li>\u201cInclude Unchanged in Copy All\u201d to force overwrite duplicates</li> <li>Open Source/Destination folder buttons for quick navigation</li> </ul> <p>Best For:</p> <ul> <li>Creating animation variants and organizing libraries</li> <li>Syncing generated clips into your canonical destination</li> <li>Keeping animation folders tidy with cleanup actions</li> </ul> <p>Visual Reference</p> <p></p> <p>Animation Copier with new/changed/unchanged/orphan tabs and copy actions</p>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-sheet-animation-creator","title":"Sprite Sheet Animation Creator","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Sheet Animation Creator</code></p> <p>Purpose: Turn a sliced sprite sheet into one or more AnimationClips with live preview, drag\u2011to\u2011select sprite ranges, and per\u2011clip FPS/loop/cycle offset.</p> <p>Key Features:</p> <ul> <li>Load a multi\u2011sprite Texture2D (sliced in the Sprite Editor)</li> <li>Drag\u2011select sprite ranges to define clips visually</li> <li>Constant FPS or curve\u2011based frame rate per clip</li> <li>Live preview/playback controls and scrubbing</li> <li>Loop toggle and cycle offset per clip</li> <li>Safe asset creation with unique file names</li> </ul> <p>Usage:</p> Text Only<pre><code>1) Open Sprite Sheet Animation Creator\n2) Drag a sliced Texture2D (or use the object field)\n3) Select frames (drag across thumbnails) to define a clip\n4) Name the clip, set FPS/curve, loop, cycle offset\n5) Repeat to add multiple definitions\n6) Click \u201cGenerate Animations\u201d and choose output folder\n</code></pre> <p>Best For:</p> <ul> <li>Converting sprite sheets to animation clips with fine control</li> <li>Mixed timings using AnimationCurves for frame pacing</li> <li>Fast iteration via visual selection and preview</li> </ul> <p>Visual Demo</p> <p></p> <p>Drag-select frame ranges on sprite sheet thumbnails with instant preview playback</p>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-sheet-extractor","title":"Sprite Sheet Extractor","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Sheet Extractor</code></p> <p>Purpose: Extract individual sprites from sprite sheet textures and save them as separate PNG files with batch processing, preview, and optional reference replacement.</p> <p>Key Features:</p> <ul> <li>Multiple extraction modes: From existing metadata, grid-based, alpha detection, or padded grid</li> <li>Auto-detection algorithms: Automatically detect optimal grid dimensions from transparency patterns</li> <li>Pivot preservation: Maintain original pivot points or set custom pivots per sprite</li> <li>Preview interface: Visual preview of all detected sprites before extraction</li> <li>Batch processing: Process multiple sprite sheets at once</li> <li>Reference replacement: Optionally update prefab/scene references to new sprites</li> <li>Per-sheet configuration: Override global settings for individual sprite sheets</li> <li>Config persistence: Save and load extraction settings per sprite sheet</li> </ul> <p>Extraction Modes:</p> <p>FromMetadata:</p> <ul> <li>Uses existing Unity sprite metadata (SpriteImportMode.Multiple)</li> <li>Preserves original names, rects, pivots, and borders</li> <li>Best for already-sliced sprite sheets</li> </ul> <p>GridBased:</p> <ul> <li>Divides texture into uniform grid cells</li> <li>Auto or manual grid dimensions</li> <li>Good for evenly-spaced sprite sheets</li> </ul> <p>AlphaDetection:</p> <ul> <li>Analyzes transparency to find sprite boundaries</li> <li>Works with irregular sprite layouts</li> <li>Configurable alpha threshold</li> </ul> <p>PaddedGrid:</p> <ul> <li>Grid-based with configurable padding/gutters</li> <li>Handles sprite sheets with spacing between cells</li> </ul> <p>Pivot Modes:</p> <ul> <li>Center: Pivot at sprite center (0.5, 0.5)</li> <li>BottomCenter: Pivot at bottom center (0.5, 0)</li> <li>TopCenter: Pivot at top center (0.5, 1)</li> <li>LeftCenter: Pivot at left center (0, 0.5)</li> <li>RightCenter: Pivot at right center (1, 0.5)</li> <li>BottomLeft: Pivot at bottom left (0, 0)</li> <li>BottomRight: Pivot at bottom right (1, 0)</li> <li>TopLeft: Pivot at top left (0, 1)</li> <li>TopRight: Pivot at top right (1, 1)</li> <li>Custom: User-specified pivot coordinates</li> </ul> <p>Workflow:</p> Text Only<pre><code>1. Open Sprite Sheet Extractor\n2. Add input directories containing sprite sheets\n3. (Optional) Set sprite name regex filter\n4. Configure extraction mode and settings\n5. Click \"Discover Sprite Sheets\" to scan\n6. Review detected sheets and sprites in preview\n7. Adjust per-sheet settings if needed\n8. Set output directory\n9. Click \"Extract\" to generate individual sprites\n10. (Optional) Use reference replacement for prefab updates\n</code></pre> <p>Best For:</p> <ul> <li>Converting packed sprite sheets to individual assets</li> <li>Preparing sprites for animation systems that expect separate files</li> <li>Creating backup copies of individual sprites</li> <li>Reorganizing sprite assets</li> <li>Migrating from third-party sprite sheet formats</li> </ul> <p>Tips:</p> <ul> <li>Use \"Snap to Texture Divisor\" for cleaner grid alignment</li> <li>Preview sprites before extraction to verify detection</li> <li>Save per-sheet configs for consistent re-extraction</li> <li>Enable reference replacement only with VCS backup</li> <li>Use alpha detection for irregularly-spaced sprite sheets</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#animation-event-editor","title":"Animation Event Editor","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; AnimationEvent Editor</code></p> <p>Purpose: Advanced visual editor for creating and managing animation events with sprite preview, method auto-discovery, and parameter editing.</p> <p>Key Features:</p> <ul> <li>Sprite preview: See the sprite at each event time</li> <li>Method auto-discovery: Automatically finds valid animation event methods</li> <li>Explicit mode: Restrict to methods marked with <code>[AnimationEvent]</code> attribute</li> <li>Parameter editing: Visual editors for int, float, string, object, and enum parameters</li> <li>Frame-based editing: Work with frame numbers instead of time values</li> <li>Search filtering: Filter types and methods by search terms</li> <li>Real-time validation: Shows invalid texture rects and read/write issues</li> </ul> <p>Workflow:</p> Text Only<pre><code>1. Open Animation Event Editor\n2. Drag Animator component into \"Animator Object\" field\n3. Select animation from dropdown (or use Animation Search)\n4. Set \"FrameIndex\" for new event\n5. Click \"Add Event\" to create event at that frame\n6. Configure event:\n   a. Select TypeName (MonoBehaviour with event methods)\n   b. Select MethodName from available methods\n   c. Set parameters (int, float, string, object, enum)\n7. Reorder events if needed (Move Up/Down buttons)\n8. Click \"Save\" to write changes to animation clip\n</code></pre> <p>Modes:</p> <p>Explicit Mode (Default):</p> <ul> <li>Only shows methods marked with <code>[AnimationEvent]</code> attribute</li> <li>Cleaner, curated list of event methods</li> <li>Recommended for large projects</li> </ul> <p>Non-Explicit Mode:</p> <ul> <li>Shows all public methods with valid signatures</li> <li>Use the \"Search\" field to filter by type/method name</li> <li>Good for discovery and prototyping</li> </ul> <p>Control Frame Time:</p> <ul> <li>Disabled: Work with frame indices (snaps to frames)</li> <li>Enabled: Edit precise time values (floating point)</li> </ul> <p>Sprite Preview:</p> <ul> <li>Automatically shows sprite at event time</li> <li>Requires texture Read/Write enabled</li> <li>\"Fix\" button to enable Read/Write if needed</li> <li>Warns if a sprite is packed too tightly</li> </ul> <p>Event Management:</p> <p>Adding Events:</p> <ol> <li>Set \"FrameIndex\" to desired frame</li> <li>Click \"Add Event\"</li> <li>Event created at frame time</li> </ol> <p>Editing Events:</p> <ul> <li>Change frame/time directly</li> <li>Select type and method from dropdowns</li> <li>Edit parameters based on method signature</li> <li>Override enum values if needed</li> </ul> <p>Reordering:</p> <ul> <li>\"Move Up\"/\"Move Down\" for events at the same time</li> <li>\"Re-Order\" button sorts all events by time</li> <li>Maintains proper event order for playback</li> </ul> <p>Resetting:</p> <ul> <li>Per-event \"Reset\" button (reverts to saved state)</li> <li>Global \"Reset\" button (discards all changes)</li> </ul> <p>Parameter Types Supported:</p> <ul> <li><code>int</code> - IntField editor</li> <li><code>float</code> - FloatField editor</li> <li><code>string</code> - TextField editor</li> <li><code>UnityEngine.Object</code> - ObjectField editor</li> <li><code>Enum</code> - Dropdown with an override option</li> </ul> <p>Best For:</p> <ul> <li>Complex animation event setup</li> <li>Character combat systems</li> <li>Footstep/sound effect events</li> <li>Particle effect triggers</li> <li>Animation state notifications</li> <li>Visual debugging of event timing</li> </ul> <p>Tips:</p> <ul> <li>Enable \"Explicit Mode\" to reduce clutter</li> <li>Use \"Animation Search\" for quick filtering</li> <li>Frame numbers are more intuitive than time values</li> <li>Sprite preview helps verify timing</li> <li>Multiple events can exist at the same frame</li> <li>Use \"Re-Order\" before saving for consistency</li> </ul> <p>Common Method Signatures:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class CharacterAnimationEvents : MonoBehaviour\n{\n    [AnimationEvent]  // Shows in Explicit Mode\n    public void PlayFootstep() { }\n\n    [AnimationEvent]\n    public void SpawnEffect(string effectName) { }\n\n    [AnimationEvent]\n    public void ApplyDamage(int damage) { }\n\n    [AnimationEvent]\n    public void SetAnimationState(CharacterState state) { }  // Enum parameter\n}\n</code></pre>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-atlas-tools","title":"Sprite Atlas Tools","text":""},{"location":"features/editor-tools/editor-tools-guide/#sprite-atlas-generator","title":"Sprite Atlas Generator","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Sprite Atlas Generator</code></p> <p>Purpose: Comprehensive tool for creating and managing Unity Sprite Atlases with regex-based sprite selection, label filtering, and automated packing.</p> <p>Key Features:</p> <ul> <li>Regex-based sprite selection: Use regular expressions to automatically find sprites</li> <li>Label filtering: Select sprites based on Unity asset labels</li> <li>Multiple source folders: Configure different folders with different selection criteria</li> <li>Batch atlas generation: Create/update multiple atlases at once</li> <li>Advanced packing settings: Control texture size, compression, padding, rotation</li> <li>Source sprite utilities: Force uncompressed settings for source sprites</li> <li>Scan and preview: See what sprites will be added/removed before applying changes</li> </ul> <p>Configuration Asset: Create configurations via <code>Assets &gt; Create &gt; Wallstop Studios &gt; Unity Helpers &gt; Scriptable Sprite Atlas Config</code></p> <p>ScriptableSpriteAtlas Configuration:</p> Text Only<pre><code>Sprite Sources:\n- spritesToPack: Manually added sprites (always included)\n- sourceFolderEntries: Define folders with regex/label filters\n\nSource Folder Entry Options:\n- folderPath: Folder to scan (relative to Assets/)\n- selectionMode: Regex | Labels | Both\n- regexes: List of regex patterns (all must match - AND logic)\n- regexAndTagLogic: How to combine regex and labels (And/Or)\n- labelSelectionMode: All | AnyOf\n- labels: Asset labels to filter by\n\nOutput Atlas Settings:\n- outputSpriteAtlasDirectory: Where to save .spriteatlas\n- outputSpriteAtlasName: Name of atlas file\n\nPacking Settings:\n- maxTextureSize: 32-16384 (power of 2)\n- enableRotation: Allow sprite rotation for better packing\n- padding: Pixels between sprites (0-32)\n- enableTightPacking: Optimize packing density\n- enableAlphaDilation: Dilate alpha edges\n- readWriteEnabled: Enable Read/Write on atlas texture\n\nCompression Settings:\n- useCrunchCompression: Enable crunch compression\n- crunchCompressionLevel: 0-100 quality\n- compression: Compressed/CompressedHQ/Uncompressed\n</code></pre> <p>Typical Workflow:</p> Text Only<pre><code>1. Open Sprite Atlas Generator\n2. Click \"Create New Config in 'Assets/Data'\"\n3. Configure the new atlas:\n   a. Set output name and directory\n   b. Click \"Add New Source Folder Entry\"\n   c. Select folder containing sprites\n   d. Add regex patterns (e.g., \"^character_.*\\\\.png$\")\n   e. Or/and add labels for filtering\n   f. Configure packing settings (texture size, padding, etc.)\n4. Click \"Scan Folders for '[config name]'\"\n5. Review sprites to add/remove\n6. Click \"Add X Sprites\" to populate the list\n7. Click \"Generate/Update '[atlas name].spriteatlas' ONLY\"\n8. Click \"Pack All Generated Sprite Atlases\" to pack textures\n</code></pre> <p>Example Configurations:</p> <p>Character Sprites Atlas:</p> Text Only<pre><code>folderPath: Assets/Sprites/Characters\nselectionMode: Regex\nregexes: [\"^player_.*\\\\.png$\", \".*_idle_.*\"]\nmaxTextureSize: 2048\npadding: 4\ncompression: CompressedHQ\n</code></pre> <p>UI Icons by Label:</p> Text Only<pre><code>folderPath: Assets/Sprites/UI\nselectionMode: Labels\nlabelSelectionMode: AnyOf\nlabels: [\"icon\", \"ui\"]\nmaxTextureSize: 1024\npadding: 2\n</code></pre> <p>Combined Regex + Labels:</p> Text Only<pre><code>folderPath: Assets/Sprites/Effects\nselectionMode: Regex | Labels\nregexes: [\"^vfx_.*\"]\nlabels: [\"particle\"]\nregexAndTagLogic: And\nmaxTextureSize: 2048\n</code></pre> <p>Advanced Features:</p> <p>Scan and Preview:</p> <ul> <li>Shows the exact sprite count that will be added/removed</li> <li>Prevents accidental overwrites</li> <li>Displays current vs. scanned sprite lists</li> </ul> <p>Source Sprite Utilities:</p> <ul> <li>\"Force Uncompressed for X Source Sprites\" button</li> <li>Sets source sprites to uncompressed (RGBA32/RGB24)</li> <li>Disables crunch compression on sources</li> <li>Ensures maximum quality before atlas packing</li> </ul> <p>Batch Operations:</p> <ul> <li>\"Generate/Update All .spriteatlas Assets\" - processes all configs</li> <li>\"Pack All Generated Sprite Atlases\" - packs all atlases in the project</li> <li>Progress bars for long operations</li> </ul> <p>Best For:</p> <ul> <li>Managing large sprite collections</li> <li>Automating sprite atlas creation</li> <li>Consistent atlas configuration across the team</li> <li>Dynamic sprite selection based on naming conventions</li> <li>Organizing sprites by labels/tags</li> <li>Build pipeline atlas generation</li> </ul> <p>Tips:</p> <ul> <li>Use regex for consistent naming patterns</li> <li>Combine multiple source folders for complex selections</li> <li>Test regex patterns with \"Scan Folders\" before generating</li> <li>Keep source sprites uncompressed for the best atlas quality</li> <li>Use labels for cross-folder sprite grouping</li> <li>Regular expressions use case-insensitive matching</li> </ul> <p>Common Issues:</p> <ul> <li>No sprites found: Check regex patterns and folder paths</li> <li>Sprites not packing: Run \"Pack All Generated Sprite Atlases\"</li> <li>Quality issues: Use \"Force Uncompressed\" on source sprites</li> <li>Regex errors: Validate patterns (will log specific errors)</li> </ul> <p>Visual Reference</p> <p></p> <p>Sprite Atlas Generator with regex-based sprite selection and packing settings</p> <p></p> <p>Scanning folders and previewing which sprites will be added to the atlas</p> <p> </p>"},{"location":"features/editor-tools/editor-tools-guide/#validation-quality-tools","title":"Validation &amp; Quality Tools","text":""},{"location":"features/editor-tools/editor-tools-guide/#prefab-checker","title":"Prefab Checker","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Prefab Checker</code></p> <p>Purpose: Comprehensive prefab validation to detect configuration issues before runtime.</p> <p>Validation Checks:</p> Check Description Severity Missing Scripts Detects broken MonoBehaviour references Critical Nulls in Lists/Arrays Finds null elements in serialized collections High Missing Required Components Validates [RequireComponent] dependencies Critical Empty String Fields Identifies unset string fields Medium Null Object References Finds unassigned UnityEngine.Object fields High Only if [ValidateAssignment] Restricts null checks to annotated fields Configurable Disabled Root GameObject Flags inactive prefab roots Medium Disabled Components Reports disabled Behaviour components Low <p>Typical Validation Workflow:</p> Text Only<pre><code>// Before committing prefab changes:\n1. Open Prefab Checker\n2. Enable relevant checks (especially Missing Scripts, Required Components)\n3. Add \"Assets/Prefabs\" folder\n4. Click \"Run Checks\"\n5. Review console output (click to select problematic prefabs)\n6. Fix reported issues\n7. Re-run checks to verify\n8. Commit changes\n</code></pre> <p>CI/CD Integration:</p> Text Only<pre><code>// Can be scripted for automated builds\n- Run validation on changed prefab folders\n- Parse console output for errors\n- Fail build if critical issues found\n</code></pre> <p>Best Practices:</p> <ul> <li>Use <code>[ValidateAssignment]</code> attribute on critical fields</li> <li>Run checks before committing prefab changes</li> <li>Enable \"Only if [ValidateAssignment]\" to reduce noise</li> <li>Fix \"Missing Required Components\" errors immediately</li> <li>Schedule regular validation runs</li> </ul> <p>Performance: Uses cached reflection for fast repeated checks.</p> <p>Best For:</p> <ul> <li>Pre-build validation</li> <li>Code review assistance</li> <li>Team onboarding with prefab standards</li> <li>Migration validation after Unity upgrades</li> <li>Continuous integration health checks</li> </ul> <p>Visual Reference</p> <p></p> <p>Prefab Checker with configurable validation checks and folder selection</p> <p></p> <p>Console output showing detected prefab issues with clickable links</p>"},{"location":"features/editor-tools/editor-tools-guide/#unity-method-analyzer","title":"Unity Method Analyzer","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Unity Method Analyzer</code></p> <p>Purpose: Detect inheritance issues and Unity lifecycle method errors across your entire C# codebase before they cause runtime bugs.</p> <p>\ud83d\udcd6 Full Documentation: Unity Method Analyzer Guide</p> <p>Key Features:</p> <ul> <li>Static analysis without external dependencies (no Roslyn required)</li> <li>Parallel scanning of thousands of files</li> <li>Multiple issue categories: Unity Lifecycle, Unity Inheritance, General Inheritance</li> <li>Five severity levels: Critical, High, Medium, Low, Info</li> <li>Export options: JSON and Markdown for CI/CD integration</li> <li>Flexible filtering: By severity, category, or free-text search</li> </ul> <p>What It Detects:</p> Issue Type Example Missing <code>override</code> keyword Hiding base method instead of overriding Wrong Unity method signature <code>OnCollisionEnter(Collider c)</code> instead of <code>OnCollisionEnter(Collision c)</code> Shadowed lifecycle methods Both base and derived class have <code>private void Start()</code> Static lifecycle methods <code>static void Awake()</code> won't be called by Unity <p>Quick Start:</p> Text Only<pre><code>1. Open Unity Method Analyzer\n2. Add source directories (e.g., Assets/Scripts)\n3. Click \"Analyze Code\"\n4. Review issues grouped by file, severity, or category\n5. Double-click to navigate to problematic code\n6. Export report for team review or CI/CD\n</code></pre> <p>Visual Demo</p> <p></p> <p>The analyzer scanning a project and displaying categorized issues</p> <p>Suppressing Warnings:</p> <p>For test code or intentional patterns, use <code>[SuppressAnalyzer]</code>:</p> C#<pre><code>[SuppressAnalyzer(\"Test fixture for analyzer validation\")]\npublic class TestClassWithIntentionalIssues : BaseClass\n{\n    public void HiddenMethod() { }  // Won't trigger warning\n}\n</code></pre> <p>Best For:</p> <ul> <li>Pre-commit code validation</li> <li>Code review assistance</li> <li>CI/CD quality gates</li> <li>Team onboarding</li> <li>Post-refactoring verification</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#custom-component-editors","title":"Custom Component Editors","text":"<p>These custom inspectors enhance Unity components with additional functionality and convenience features.</p>"},{"location":"features/editor-tools/editor-tools-guide/#matchcollidertosprite-editor","title":"MatchColliderToSprite Editor","text":"<p>Component: <code>MatchColliderToSprite</code></p> <p>Purpose: Provides a button in the inspector to manually trigger collider-to-sprite matching.</p> <p>Features:</p> <ul> <li>\"MatchColliderToSprite\" button in inspector</li> <li>Manually invoke <code>OnValidate()</code> to update collider</li> <li>Useful when automatic updates don't trigger</li> </ul> <p>When to Use:</p> <ul> <li>After changing sprite at runtime</li> <li>When collider doesn't match sprite automatically</li> <li>Manual override of collider shape</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#polygoncollider2doptimizer-editor","title":"PolygonCollider2DOptimizer Editor","text":"<p>Component: <code>PolygonCollider2DOptimizer</code></p> <p>Purpose: Custom inspector for optimizing PolygonCollider2D point counts with configurable tolerance.</p> <p>Features:</p> <ul> <li>tolerance: Adjustable simplification tolerance</li> <li>Optimize button: Manually trigger polygon simplification</li> <li>Reduces collider complexity while maintaining shape</li> </ul> <p>How It Works:</p> <ol> <li>Adjust tolerance slider (lower = more accurate, higher = simpler)</li> <li>Click \"Optimize\" to simplify polygon points</li> <li>Collider updates with a reduced point count</li> </ol> <p>Best For:</p> <ul> <li>Reducing physics performance overhead</li> <li>Simplifying complex sprite colliders</li> <li>Balancing accuracy vs. performance</li> <li>Editor-time optimization of imported sprites</li> </ul> <p>Tolerance Guide:</p> <ul> <li>0.0 - 0.1: High accuracy, minimal simplification</li> <li>0.1 - 0.5: Balanced (recommended)</li> <li>0.5 - 2.0: Aggressive simplification</li> <li>2.0+: Maximum simplification (may lose detail)</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#enhancedimage-editor","title":"EnhancedImage Editor","text":"<p>Component: <code>EnhancedImage</code> (extends Unity's Image)</p> <p>Purpose: Extended inspector for EnhancedImage with HDR color support and shape mask configuration.</p> <p>Additional Properties:</p> <ul> <li>HDR Color: High dynamic range color multiplication</li> <li>Shape Mask: Texture2D for masking/shaping the image</li> <li>Material Auto-Fix: Detects and fixes incorrect material assignment</li> </ul> <p>Features:</p> <p>Material Detection:</p> <ul> <li>Warns if using \"Default UI Material\"</li> <li>\"Incorrect Material Detected - Try Fix?\" button (yellow)</li> <li>Automatically finds and applies the correct BackgroundMask material</li> <li>Material path: <code>Shaders/Materials/BackgroundMask-Material.mat</code></li> </ul> <p>Shape Mask:</p> <ul> <li>Requires shader with <code>_ShapeMask</code> texture2D property</li> <li>Allows complex masking effects</li> <li>Integrates with a custom shader system</li> </ul> <p>HDR Color:</p> <ul> <li>Color picker with HDR support</li> <li>Intensity values &gt; 1.0 for bloom/glow effects</li> <li>Works with post-processing</li> </ul> <p>Best For:</p> <ul> <li>UI elements requiring HDR effects</li> <li>Masked UI images</li> <li>Custom-shaped UI elements</li> <li>Material-based UI effects</li> </ul> <p>Workflow:</p> Text Only<pre><code>1. Add EnhancedImage component to UI GameObject\n2. If yellow \"Fix Material\" button appears, click it\n3. Configure HDR Color for glow/intensity\n4. Assign Shape Mask texture if needed\n5. Shader must expose _ShapeMask property\n</code></pre> <p>Icon Customization:</p> <ul> <li>Automatically uses Image icon in project/hierarchy</li> <li>Seamless integration with standard Unity UI</li> </ul> <p> </p>"},{"location":"features/editor-tools/editor-tools-guide/#property-drawers-attributes","title":"Property Drawers &amp; Attributes","text":"<p>Custom property drawers enhance the inspector with conditional display, validation, and specialized input fields.</p> <p></p>"},{"location":"features/editor-tools/editor-tools-guide/#winlineeditor-property-drawer","title":"WInLineEditor Property Drawer","text":"<p>Attribute: <code>[WInLineEditor]</code></p> <p>Purpose: Embed the inspector for object references (ScriptableObjects, Materials, Components, Textures, etc.) directly below the field so you can edit configuration without losing context.</p> <p>Modes (<code>WInLineEditorMode</code>):</p> <ul> <li><code>AlwaysExpanded</code> \u2014 always draws the inline inspector.</li> <li><code>FoldoutExpanded</code> \u2014 shows a foldout that starts expanded.</li> <li><code>FoldoutCollapsed</code> \u2014 shows a foldout that starts collapsed.</li> </ul> <p>Options: tune the presentation with constructor parameters:</p> <ul> <li><code>inspectorHeight</code> (default 200, min 160) \u2014 vertical space reserved for the inspector body.</li> <li><code>drawObjectField</code> \u2014 hide or show the object picker next to the label.</li> <li><code>drawHeader</code> \u2014 draw a bold header with a ping button (ping is shown only while a Project window tab is visible).</li> <li><code>drawPreview</code> &amp; <code>previewHeight</code> \u2014 render the preview area when the target editor exposes one.</li> <li><code>enableScrolling</code> \u2014 wrap the inspector body in a scroll view for long inspectors.</li> <li><code>minInspectorWidth</code> (default 520) \u2014 when the content area is narrower than this width, a horizontal scrollbar appears; set to <code>0</code> to disable the safeguard.</li> </ul> <p>WInLineEditor respects the Inline Editors defaults inside Unity Helpers Settings. Leave the <code>mode</code> argument unset to inherit the global foldout behaviour, or supply a <code>WInLineEditorMode</code> per field to override it. By default, Inline Editors start collapsed to mirror collapsible WGroups, so <code>[WInLineEditor]</code> without a mode expands only when you opt into a different setting.</p> <p>Examples:</p> C#<pre><code>public class AbilityConfig : ScriptableObject\n{\n    public string displayName;\n    public float cooldown;\n}\n\npublic class AbilityDatabase : ScriptableObject\n{\n    [WInLineEditor] public AbilityConfig defaultConfig;\n\n    [WInLineEditor(\n        WInLineEditorMode.FoldoutCollapsed,\n        inspectorHeight: 180f,\n        drawPreview: true,\n        previewHeight: 64f)]\n    public Texture2D abilityIcon;\n\n    [WInLineEditor(\n        WInLineEditorMode.AlwaysExpanded,\n        inspectorHeight: 220f,\n        drawObjectField: false,\n        enableScrolling: false)]\n    public AbilityConfig sharedTemplate;\n}\n</code></pre> <p>Features:</p> <ul> <li>Bespoke implementation (no Odin dependency) tailored to the most common inline editing workflows.</li> <li>Reuses Unity\u2019s native editors, respecting custom inspectors, validation, and undo.</li> <li>Optional scroll view that keeps large inspectors usable without stealing space from the parent inspector.</li> <li>Preview support for assets that implement <code>HasPreviewGUI</code>.</li> <li>Header includes a quick \u201cPing\u201d button so you can jump to the asset whenever the Project window is visible.</li> <li>Smooth expand/collapse animations with configurable speed.</li> </ul> <p>Animation Settings:</p> <p>Foldout animations are controlled via Unity Helpers Settings (<code>Edit &gt; Project Settings &gt; Unity Helpers</code>):</p> <ul> <li><code>InlineEditorFoldoutTweenEnabled</code> \u2014 Enable/disable smooth expand/collapse animations (default: <code>true</code>)</li> <li><code>InlineEditorFoldoutSpeed</code> \u2014 Animation speed from 2.0 to 12.0 (default: <code>2.0</code>)</li> </ul> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class AbilityConfig : ScriptableObject\n{\n    public string abilityName;\n    public Sprite icon;\n    public float cooldown;\n}\n\npublic class WInLineEditorExample : MonoBehaviour\n{\n    // Animation applies to foldout modes only\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)] // Animated\n    public AbilityConfig animatedFoldout;\n\n    [WInLineEditor(WInLineEditorMode.AlwaysExpanded)] // No animation (always visible)\n    public AbilityConfig alwaysVisible;\n}\n</code></pre> <p>See also: Inspector Settings Reference for complete settings documentation.</p> <p>Visual Reference</p> <p></p> <p>WInLineEditor with embedded inspector for a ScriptableObject reference with foldout and collapse transitions</p>"},{"location":"features/editor-tools/editor-tools-guide/#wshowif-property-drawer","title":"WShowIf Property Drawer","text":"<p>Attribute: <code>[WShowIf]</code></p> <p>Purpose: Conditionally show/hide fields in inspector based on boolean fields or enum values.</p> <p>Syntax:</p> C#<pre><code>[WShowIf(nameof(fieldName))]\n[WShowIf(nameof(fieldName), inverse = true)]\n[WShowIf(nameof(fieldName), expectedValues: new object[] { value1, value2 })]\n[WShowIf(nameof(numericField), WShowIfComparison.GreaterThan, 10)]\n[WShowIf(nameof(stringField), WShowIfComparison.IsNotNullOrEmpty)]\n[WShowIf(nameof(parentField) + \".\" + nameof(ChildType.someFlag))]\n</code></pre> <p><code>WShowIfComparison.Unknown</code> exists only for backward compatibility and is marked obsolete; always choose an explicit comparison mode.</p> <p>Examples:</p> <p>Boolean Condition:</p> C#<pre><code>public bool enableFeature;\n\n[WShowIf(nameof(enableFeature))]\npublic float featureIntensity;\n\n[WShowIf(nameof(enableFeature), inverse = true)]\npublic string disabledMessage;\n</code></pre> <p>Enum Condition:</p> C#<pre><code>public enum Mode { Simple, Advanced, Expert }\npublic Mode currentMode;\n\n[WShowIf(nameof(currentMode), expectedValues = new object[] { Mode.Advanced, Mode.Expert })]\npublic int advancedSetting;\n</code></pre> <p>Multiple Values:</p> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing UnityEngine.Serialization;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\nusing WallstopStudios.UnityHelpers.Editor.Sprites;\n\npublic class SelectionModeExample : MonoBehaviour\n{\n    public SpriteSelectionMode selectionMode;\n\n    [WShowIf(\n        nameof(selectionMode),\n        expectedValues = new object[]\n        {\n            SpriteSelectionMode.Regex,\n            SpriteSelectionMode.Regex | SpriteSelectionMode.Labels,\n        }\n    )]\n    public List&lt;string&gt; regexPatterns;\n\n    [FormerlySerializedAs(\"regexPatterns\")]\n    [WShowIf(\n        nameof(selectionMode),\n        expectedValues = new object[]\n        {\n            SpriteSelectionMode.Regex,\n            SpriteSelectionMode.Regex | SpriteSelectionMode.Labels,\n        }\n    )]\n    public SerializableHashSet&lt;string&gt; regexPatterns1;\n\n    [WShowIf(\n        nameof(selectionMode),\n        expectedValues = new object[]\n        {\n            SpriteSelectionMode.Regex,\n            SpriteSelectionMode.Regex | SpriteSelectionMode.Labels,\n        }\n    )]\n    public SerializableDictionary&lt;int, string&gt; regexPatterns2;\n\n    public bool someOtherField;\n}\n</code></pre> <p>Features:</p> <ul> <li>Hides field when condition false (0 height)</li> <li>Supports boolean, enum, numeric, any <code>IComparable</code>/<code>IComparable&lt;T&gt;</code> type, null, and string/collection comparisons</li> <li>Allows referencing nested paths, properties, and parameterless methods</li> <li><code>inverse</code> parameter inverts any comparison result</li> <li><code>expectedValues</code> (or params arguments) for equality checks without manual arrays</li> <li>Falls back to reflection for non-SerializedProperty fields</li> <li>Cached reflection for performance</li> </ul> <p>Best For:</p> <ul> <li>Conditional inspector fields</li> <li>Reducing inspector clutter</li> <li>Mode-based configuration UI</li> <li>Complex nested settings</li> </ul> <p>Visual Reference</p> <p></p> <p>Field appears/disappears based on enum toggle state</p>"},{"location":"features/editor-tools/editor-tools-guide/#stringinlist-property-drawer","title":"StringInList Property Drawer","text":"<p>Attribute: <code>[StringInList]</code></p> <p>Purpose: Display string or int fields as dropdown with predefined options.</p> <p>Syntax:</p> C#<pre><code>// Static array\n[StringInList(\"Option1\", \"Option2\", \"Option3\")]\npublic string selectedOption;\n\n// Method reference\n[StringInList(typeof(MyClass), nameof(MyClass.GetOptions))]\npublic string dynamicOption;\n\n// With int field\n[StringInList(\"Low\", \"Medium\", \"High\")]\npublic int priorityIndex;\n</code></pre> <p>Dynamic Options Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\npublic class MySettings\n{\n    [StringInList(typeof(Helpers), nameof(Helpers.GetAllSpriteLabelNames))]\n    public List&lt;string&gt; selectedLabels;\n}\n</code></pre> <p>Features:</p> <ul> <li>String fields: Dropdown with string values</li> <li>Int fields: Dropdown with indices</li> <li>Arrays/Lists: UI Toolkit list view with per-element dropdowns, add/remove, and drag-to-reorder</li> <li>Dynamic lists via static method reference</li> <li>Search/filter with lightweight autocomplete (Tab auto-completes and assigns, Enter just fills the search)</li> <li>Inline hint beneath the search box shows the current best match for quick Tab acceptance</li> <li>Pagination automatically appears when the option count exceeds the configured page size</li> <li>Page size configurable via Project Settings \u25b8 Wallstop Studios \u25b8 Unity Helpers \u25b8 StringInList Page Size</li> <li>Auto-finds current value in list</li> <li>The same search + pagination experience is reused by <code>SerializableType</code>, so adjusting the page size updates both drawers</li> </ul> <p>Best For:</p> <ul> <li>Predefined option selection</li> <li>Tag/label selection</li> <li>Enum-like string fields</li> <li>Dynamic option lists</li> <li>User-friendly enumerations</li> </ul> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class StringInListExample : MonoBehaviour\n{\n    [StringInList(typeof(StringInListExample), nameof(GetValues))]\n    public string value;\n\n    private IEnumerable&lt;string&gt; GetValues()\n    {\n        yield return \"A\";\n        yield return \"B\";\n        yield return \"C\";\n        yield return \"D\";\n    }\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>StringInList dropdown showing search filtering and pagination</p> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class StringInListExample : MonoBehaviour\n{\n    [StringInList(typeof(StringInListExample), nameof(GetValues))]\n    public string value;\n\n    private IEnumerable&lt;string&gt; GetValues()\n    {\n        yield return \"A\";\n        yield return \"B\";\n        yield return \"C\";\n        yield return \"D\";\n    }\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>StringInList on a List field with per-element dropdowns and drag reordering</p>"},{"location":"features/editor-tools/editor-tools-guide/#intdropdown-property-drawer","title":"IntDropdown Property Drawer","text":"<p>Attribute: <code>[IntDropdown]</code></p> <p>Purpose: Display int fields as dropdown with specific integer options.</p> <p>Syntax:</p> C#<pre><code>[IntDropdown(32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)]\npublic int textureSize;\n\n[IntDropdown(0, 2, 4, 8, 16, 32)]\npublic int padding;\n</code></pre> <p>Features:</p> <ul> <li>Restricts int values to specific options</li> <li>Dropdown shows integer values as strings</li> <li>Prevents invalid values</li> <li>Visual clarity for constrained integers</li> </ul> <p>Best For:</p> <ul> <li>Power-of-two values (texture sizes)</li> <li>Discrete numeric options</li> <li>Preventing invalid integer input</li> <li>Configuration with specific valid values</li> </ul> <p>Common Use Cases:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class IntDropDownExample : MonoBehaviour\n{\n    // Texture sizes (power of 2)\n    [IntDropDown(32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384)]\n    public int maxTextureSize = 2048;\n\n    // Padding options\n    [IntDropDown(0, 2, 4, 8, 16, 32)]\n    public int spritePadding = 4;\n\n    // Quality levels\n    [IntDropDown(0, 1, 2, 3, 4, 5)]\n    public int qualityLevel = 3;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>IntDropDown for texture sizes showing power-of-two options</p>"},{"location":"features/editor-tools/editor-tools-guide/#wvaluedropdown-property-drawer","title":"WValueDropDown Property Drawer","text":"<p>Attribute: <code>[WValueDropDown]</code></p> <p>Purpose: Generic dropdown for any type with fixed values or provider methods. More flexible than <code>StringInList</code> or <code>IntDropdown</code> \u2014 supports all primitive types, custom classes, and dynamic providers.</p> <p>Syntax:</p> C#<pre><code>// Fixed primitive values\n[WValueDropDown(0, 25, 50, 100)]\npublic int staminaThreshold = 50;\n\n[WValueDropDown(0.5f, 1.0f, 1.5f, 2.0f)]\npublic float damageMultiplier = 1.0f;\n\n[WValueDropDown(\"Easy\", \"Normal\", \"Hard\", \"Insane\")]\npublic string difficulty = \"Normal\";\n\n// Static provider method\n[WValueDropDown(typeof(PowerUpLibrary), nameof(PowerUpLibrary.GetAvailablePowerUps))]\npublic PowerUpDefinition selectedPowerUp;\n\n// Instance provider method (context-aware)\n[WValueDropDown(nameof(GetAvailableOptions), typeof(string))]\npublic string selectedOption;\n</code></pre> <p>Features:</p> <ul> <li>All primitive types: <code>bool</code>, <code>char</code>, <code>byte</code>, <code>sbyte</code>, <code>short</code>, <code>ushort</code>, <code>int</code>, <code>uint</code>, <code>long</code>, <code>ulong</code>, <code>float</code>, <code>double</code>, <code>string</code></li> <li>Custom types with <code>ToString()</code> for labels</li> <li>Static provider methods from any type</li> <li>Instance provider methods for context-aware options</li> <li>Type-safe value selection</li> </ul> <p>Best For:</p> <ul> <li>Type-safe options beyond strings/ints</li> <li>Custom class/struct selection</li> <li>Dynamic options from runtime data</li> <li>Designer-friendly preset selection</li> </ul> <p>Custom Types Example:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing System.Collections.Generic;\n\n[System.Serializable]\npublic class Preset\n{\n    public string name;\n    public float value;\n\n    public override string ToString() =&gt; name;  // Used for dropdown label\n}\n\npublic class Config : MonoBehaviour\n{\n    [WValueDropDown(typeof(Config), nameof(GetPresets))]\n    public Preset selectedPreset;\n\n    public static IEnumerable&lt;Preset&gt; GetPresets()\n    {\n        return new[]\n        {\n            new Preset { name = \"Low\", value = 0.5f },\n            new Preset { name = \"Medium\", value = 1.0f },\n            new Preset { name = \"High\", value = 2.0f },\n        };\n    }\n}\n</code></pre> <p>Instance Provider Example:</p> C#<pre><code>public class DynamicOptions : MonoBehaviour\n{\n    public string prefix = \"Option\";\n    public int optionCount = 5;\n\n    [WValueDropDown(nameof(GetAvailableOptions), typeof(string))]\n    public string selectedOption;\n\n    private IEnumerable&lt;string&gt; GetAvailableOptions()\n    {\n        for (int i = 1; i &lt;= optionCount; i++)\n        {\n            yield return $\"{prefix}_{i}\";\n        }\n    }\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>WValueDropDown showing predefined integer, float, and string values</p> <p>For more detailed documentation, including all constructor forms, see Inspector Selection Attributes.</p>"},{"location":"features/editor-tools/editor-tools-guide/#wreadonly-property-drawer","title":"WReadOnly Property Drawer","text":"<p>Attribute: <code>[WReadOnly]</code></p> <p>Purpose: Display fields as read-only in the inspector (grayed out, non-editable).</p> <p>Syntax:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Attributes;\n\n[WReadOnly]\npublic int calculatedValue;\n\n[WReadOnly]\npublic string currentState;\n</code></pre> <p>Features:</p> <ul> <li>Disables GUI for field</li> <li>Shows value but prevents editing</li> <li>Maintains proper height and layout</li> <li>Works with all property types</li> </ul> <p>Best For:</p> <ul> <li>Displaying runtime state</li> <li>Showing calculated/derived values</li> <li>Debug information in inspector</li> <li>Values set by code only</li> </ul> <p>Example:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WReadOnlyExample : MonoBehaviour\n{\n    public int baseHealth = 100;\n    public int healthBonus = 0;\n\n    [WReadOnly]\n    public int totalHealth;\n\n    private void OnValidate()\n    {\n        totalHealth = baseHealth + healthBonus;\n    }\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>WReadOnly field showing totalHealth as a non-editable calculated value</p> <p>For detailed documentation on validation attributes, see Inspector Validation Attributes.</p> <p> </p>"},{"location":"features/editor-tools/editor-tools-guide/#automation-utilities","title":"Automation &amp; Utilities","text":""},{"location":"features/editor-tools/editor-tools-guide/#scriptableobject-singleton-creator","title":"ScriptableObject Singleton Creator","text":"<ul> <li>Type: Automatic (runs on editor load)</li> <li>Menu: N/A (automatic) - Uses <code>[InitializeOnLoad]</code></li> </ul> <p>Purpose: Automatically creates and maintains singleton ScriptableObject assets.</p> <p>See the base API guide for details on <code>ScriptableObjectSingleton&lt;T&gt;</code> usage, scenarios, and Odin compatibility: Singleton Utilities.</p> <p>How It Works:</p> Text Only<pre><code>1. Runs when Unity editor starts\n2. Scans all ScriptableObjectSingleton&lt;T&gt; derived types\n3. Creates missing assets in Assets/Resources/\n4. Moves misplaced singletons to correct locations\n5. Respects [ScriptableSingletonPath] attribute\n</code></pre> <p>Usage Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n// Define singleton:\npublic class GameSettings : ScriptableObjectSingleton&lt;GameSettings&gt;\n{\n    public float masterVolume = 1.0f;\n    public bool enableVSync = true;\n}\n\n// Optional custom path:\n[ScriptableSingletonPath(\"Settings/Audio\")]\npublic class AudioSettings : ScriptableObjectSingleton&lt;AudioSettings&gt;\n{\n    public float musicVolume = 0.8f;\n}\n\n// Assets created automatically:\n// - Assets/Resources/GameSettings.asset\n// - Assets/Resources/Settings/Audio/AudioSettings.asset\n\n// Access at runtime:\nfloat volume = GameSettings.Instance.masterVolume;\n</code></pre> <p>Folder Structure:</p> Text Only<pre><code>Assets/\n  Resources/\n    GameSettings.asset              (no path attribute)\n    Settings/\n      Audio/                        ([ScriptableSingletonPath(\"Settings/Audio\")])\n        AudioSettings.asset\n</code></pre> <p>Best For:</p> <ul> <li>Managing game settings as unique assets</li> <li>Centralizing configuration data</li> <li>Ensuring essential ScriptableObjects exist</li> <li>Team workflows to prevent missing asset errors</li> <li>Automatic project setup for new developers</li> </ul> <p>Customization:</p> <ul> <li>Set <code>IncludeTestAssemblies = true</code> to create test singletons</li> <li>Call <code>EnsureSingletonAssets()</code> manually to refresh</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#sprite-label-processor","title":"Sprite Label Processor","text":"<ul> <li>Type: Automatic asset processor</li> <li>Menu: N/A (automatic) - Uses <code>AssetPostprocessor</code></li> </ul> <p>Purpose: Automatically maintains a cache of all sprite labels in the project for fast lookup in editor tools.</p> <p>How It Works:</p> <ol> <li>Monitors sprite asset imports/reimports (PNG, JPG, JPEG)</li> <li>Detects changes to asset labels on sprites</li> <li>Updates global sprite label cache automatically</li> <li>Provides a cached label list to tools like Sprite Atlas Generator</li> </ol> <p>What Gets Cached:</p> <ul> <li>All unique asset labels across sprite assets</li> <li>Sorted alphabetically for a consistent display</li> <li>Updated on import, not at runtime</li> </ul> <p>Performance Benefits:</p> <ul> <li>\u2705 No need to scan the entire project for labels</li> <li>\u2705 Fast dropdown population in editors</li> <li>\u2705 Automatic cache invalidation on changes</li> <li>\u2705 Only processes sprite texture types</li> </ul> <p>Runtime Usage:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Access cached sprite labels\nstring[] labels = SpriteLabelCache.GetAllLabels();\n</code></pre> <p></p>"},{"location":"features/editor-tools/editor-tools-guide/#request-script-compilation","title":"Request Script Compilation","text":"<ul> <li>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Request Script Compilation</code></li> <li>Shortcut: <code>Ctrl/Cmd + Alt + R</code> (configurable in Unity's Shortcut Manager)</li> </ul> <p>Purpose: Manually trigger Unity script recompilation without needing to modify files or restart the editor.</p> <p>Visual Reference</p> <p></p> <p></p> <p>Key Features:</p> <ul> <li>One-click script recompilation</li> <li>Customizable keyboard shortcut (default: Ctrl/Cmd + Alt + R)</li> <li>Useful after external code generation or build processes</li> <li>Available in Unity's Shortcut Manager under \"Wallstop Studios / Request Script Compilation\"</li> </ul> <p>When to Use:</p> <ul> <li>After external code generation tools modify scripts</li> <li>When Unity doesn't auto-detect file changes</li> <li>To force recompilation without touching files</li> <li>In automated workflows that need explicit compilation steps</li> </ul> <p>Common Workflow:</p> Text Only<pre><code>1. Run external code generator\n2. Press Ctrl+Alt+R (or use menu item)\n3. Wait for Unity to recompile scripts\n4. Continue working with updated code\n</code></pre> <p></p>"},{"location":"features/editor-tools/editor-tools-guide/#project-settings-unity-helpers","title":"Project Settings: Unity Helpers","text":"<p>Menu: <code>Edit &gt; Project Settings &gt; Wallstop Studios &gt; Unity Helpers</code></p> <p>Purpose: Centralized configuration for Unity Helpers features, including buffer settings, pagination defaults, and inspector behavior.</p> <p>Visual Reference</p> <p> Centralized configuration panel in Unity's Project Settings</p> <p>Key Settings:</p>"},{"location":"features/editor-tools/editor-tools-guide/#coroutine-wait-buffer-defaults","title":"Coroutine Wait Buffer Defaults","text":"<p>Configure default behavior for <code>WaitForSeconds</code>, <code>WaitForFixedUpdate</code>, and other yield instructions:</p> C#<pre><code>// Settings affect runtime buffer pool behavior:\n- Quantization: How yield times are rounded (e.g., 0.1f rounds to nearest 0.1)\n- Entry Caps: Maximum number of cached wait instructions per type\n- LRU Mode: Least-recently-used eviction when caps are exceeded\n</code></pre> <p>Impact:</p> <ul> <li>Reduces GC allocations from repeated coroutine yields</li> <li>Settings apply automatically on domain reload and player start</li> <li>Can be overridden at runtime if needed</li> </ul> <p>Generated Asset: <code>Resources/WallstopStudios/UnityHelpers/UnityHelpersBufferSettings.asset</code></p>"},{"location":"features/editor-tools/editor-tools-guide/#inspector-pagination-defaults","title":"Inspector Pagination Defaults","text":"<ul> <li><code>StringInListPageSize</code>: Default page size for StringInList dropdowns (default: 50)</li> <li><code>WEnumToggleButtonsPageSize</code>: Default page size for enum toggle button grids (default: 20)</li> <li>Per-attribute overrides available on individual fields</li> </ul> <p>When to Adjust:</p> <ul> <li>Large option lists feel cramped (increase page size)</li> <li>Inspector feels sluggish with many options (decrease page size)</li> <li>Project-wide consistency for dropdown/toggle experiences</li> </ul> <p>Runtime Usage:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Get all known sprite labels\nstring[] allLabels = Helpers.GetAllSpriteLabelNames();\n\n// Used internally by StringInList attribute\n[StringInList(typeof(Helpers), nameof(Helpers.GetAllSpriteLabelNames))]\npublic List&lt;string&gt; selectedLabels;\n</code></pre> <p>Cache Updates:</p> <ul> <li>On sprite import/reimport</li> <li>When labels added/removed from sprites</li> <li>After asset database refresh</li> <li>Automatically during asset post-processing</li> </ul> <p>Best For:</p> <ul> <li>Tools requiring sprite label selection</li> <li>DropDown menus for label filtering</li> <li>Maintaining label consistency across the project</li> <li>Fast label-based sprite queries</li> </ul> <p>Technical Notes:</p> <ul> <li>Skips execution in batch mode and CI environments</li> <li>Uses efficient HashSet for uniqueness checks</li> <li>Sorted results for a consistent UI display</li> <li>Thread-safe cache updates</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#attribute-metadata-cache-generator","title":"Attribute Metadata Cache Generator","text":"<p>Type: Automatic (runs on editor load and domain reload)</p> <p>Purpose: Pre-generate attribute system metadata at edit time to eliminate runtime reflection overhead. The cache is automatically regenerated when scripts change. You can manually refresh it via the \"Purge &amp; Refresh Cache\" button in the <code>AttributeMetadataCache</code> asset inspector.</p> <p>What Gets Cached:</p> <ul> <li>All \"Attribute\" fields across AttributesComponent types</li> <li>Relational metadata ([ParentComponent], [ChildComponent], [SiblingComponent])</li> <li>Assembly-qualified type names for runtime resolution</li> <li>Field types (single, array, List, HashSet)</li> <li>Interface detection for polymorphic queries</li> </ul> <p>Performance Benefits:</p> <ul> <li>\u2705 Eliminates reflection overhead during attribute initialization</li> <li>\u2705 Reduces first-frame lag when attribute components awake</li> <li>\u2705 Enables fast attribute name lookups for UI</li> <li>\u2705 Optimizes relational component queries</li> <li>\u2705 Supports IL2CPP ahead-of-time compilation</li> </ul> <p>Runtime Usage:</p> C#<pre><code>// Cache is loaded automatically:\nAttributeMetadataCache cache = AttributeMetadataCache.Instance;\n\n// Get all known attribute names:\nstring[] allAttributes = cache.AllAttributeNames;\n\n// Check type for attribute fields:\nTypeFieldMetadata metadata = cache.GetMetadataForType(typeof(MyAttributesComponent));\nif (metadata != null)\n{\n    foreach (string fieldName in metadata.AttributeFieldNames)\n    {\n        Debug.Log($\"Found attribute field: {fieldName}\");\n    }\n}\n\n// Query relational component metadata:\nRelationalTypeMetadata relational = cache.GetRelationalMetadataForType(typeof(MyComponent));\n</code></pre> <p>Cache Regenerates:</p> <ul> <li>On Unity editor startup (automatic)</li> <li>After script recompilation (automatic)</li> <li>Manual trigger via menu item</li> <li>After domain reload in the editor</li> </ul> <p>Best For:</p> <ul> <li>Large projects with many attribute-based components</li> <li>Games using extensive parent/child relationships</li> <li>Optimizing startup time for complex prefabs</li> <li>IL2CPP builds where reflection is expensive</li> <li>Tools that need to enumerate available attributes</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#editor-utilities","title":"Editor Utilities","text":"<ul> <li>Type: Static utility class</li> <li>Namespace: <code>WallstopStudios.UnityHelpers.Editor.Utils</code></li> </ul> <p>Purpose: Helper methods for Unity Editor operations.</p> <p>Available Methods:</p>"},{"location":"features/editor-tools/editor-tools-guide/#getcurrentpathofprojectwindow","title":"<code>GetCurrentPathOfProjectWindow()</code>","text":"<p>Gets the currently selected folder in Unity's Project window.</p> C#<pre><code>// In an editor window or wizard:\nstring currentFolder = EditorUtilities.GetCurrentPathOfProjectWindow();\nif (!string.IsNullOrEmpty(currentFolder))\n{\n    string newAssetPath = $\"{currentFolder}/NewGeneratedAsset.asset\";\n    AssetDatabase.CreateAsset(myAsset, newAssetPath);\n}\nelse\n{\n    // Fallback to default location\n    AssetDatabase.CreateAsset(myAsset, \"Assets/NewGeneratedAsset.asset\");\n}\n</code></pre> <p>Returns: Asset-relative path (e.g., \"Assets/Scripts/Editor\") or empty string.</p> <p>Use Cases:</p> <ul> <li>Asset creation wizards defaulting to the selected folder</li> <li>Context menu extensions operating on the current location</li> <li>Batch processing tools respecting working directory</li> </ul> <p>Technical Notes:</p> <ul> <li>Uses reflection to access internal Unity API</li> <li>May break in future Unity versions</li> <li>Returns empty string on failure (no exceptions)</li> </ul> <p>Best For:</p> <ul> <li>Context-aware asset creation</li> <li>User-friendly editor tools</li> <li>Respecting the current working directory</li> </ul> <p></p>"},{"location":"features/editor-tools/editor-tools-guide/#failed-tests-exporter","title":"Failed Tests Exporter","text":"<ul> <li>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Export Failed Tests</code> / <code>Clear Failed Tests</code></li> <li>Settings: <code>Edit &gt; Project Settings &gt; Wallstop Studios &gt; Unity Helpers</code></li> </ul> <p>Purpose: Hooks into the Unity Test Runner API to automatically capture failed test results and export them to timestamped text files in a configurable directory (defaults to the project root). Disabled by default \u2014 enable in Project Settings.</p> <p>Key Features:</p> <ul> <li>Automatically records test name, failure message, and stack trace for each failed test</li> <li>Exports failures to <code>failed-tests-YYYY-MM-DD-HHmmss.txt</code> in the configured output directory (project root by default)</li> <li>Configurable output directory with folder picker \u2014 defaults to the project root</li> <li>Menu items to manually export or clear captured failures</li> <li>Useful for CI/CD pipelines and tracking intermittent test failures</li> </ul> <p>For full setup, usage, and API reference, see Failed Tests Exporter.</p>"},{"location":"features/editor-tools/editor-tools-guide/#quick-reference","title":"Quick Reference","text":""},{"location":"features/editor-tools/editor-tools-guide/#tools-by-category","title":"Tools by Category","text":"<p>Image Processing:</p> <ul> <li>Image Blur Tool - Gaussian blur effects</li> <li>Sprite Cropper - Remove transparent padding</li> <li>Texture Settings Applier - Batch import settings</li> <li>Sprite Settings Applier - Sprite-specific settings</li> <li>Sprite Pivot Adjuster - Pivot point adjustment</li> <li>Texture Resizer - Resize textures with bilinear/point algorithms</li> <li>Fit Texture Size - Auto-fit texture max size to source dimensions</li> </ul> <p>Animation:</p> <ul> <li>Sprite Animation Editor - Visual animation editing with preview</li> <li>Animation Event Editor - Visual animation event editing with sprite preview</li> <li>Animation Creator - Bulk-create clips from naming patterns</li> <li>Animation Copier - Duplicate and manage clips</li> <li>Sprite Sheet Animation Creator - Convert atlases to clips</li> </ul> <p>Sprite Atlases:</p> <ul> <li>Sprite Atlas Generator - Regex/label-based atlas creation and packing</li> </ul> <p>Quality &amp; Validation:</p> <ul> <li>Prefab Checker - Comprehensive prefab validation</li> </ul> <p>Custom Editors:</p> <ul> <li>MatchColliderToSprite Editor - Manual collider matching</li> <li>PolygonCollider2DOptimizer Editor - Collider simplification</li> <li>EnhancedImage Editor - HDR color and shape mask support</li> </ul> <p>Property Drawers:</p> <ul> <li>WShowIf - Conditional field visibility</li> <li>StringInList - DropDown selection for strings</li> <li>IntDropDown - DropDown selection for integers</li> <li>WReadOnly - Read-only inspector fields</li> </ul> <p>Automation:</p> <ul> <li>ScriptableObject Singleton Creator - Auto-create singletons</li> <li>Attribute Metadata Cache Generator - Performance optimization</li> <li>Sprite Label Processor - Automatic sprite label caching</li> <li>Failed Tests Exporter - Capture and export test failures</li> </ul> <p>Utilities:</p> <ul> <li>Editor Utilities - Helper methods for editor scripting</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#all-menu-items","title":"All Menu Items","text":"<p>Tools &gt; Wallstop Studios &gt; Unity Helpers:</p> <ul> <li>Animation Copier</li> <li>Animation Creator</li> <li>AnimationEvent Editor</li> <li>Fit Texture Size</li> <li>Image Blur</li> <li>Prefab Checker</li> <li>Sprite Animation Editor</li> <li>Sprite Atlas Generator</li> <li>Sprite Cropper</li> <li>Sprite Pivot Adjuster</li> <li>Sprite Settings Applier</li> <li>Sprite Sheet Animation Creator</li> <li>Sprite Sheet Extractor</li> <li>Texture Resizer</li> <li>Texture Settings Applier</li> <li>Export Failed Tests</li> <li>Clear Failed Tests</li> </ul> <p>Assets &gt; Create &gt; Wallstop Studios &gt; Unity Helpers:</p> <ul> <li>Scriptable Sprite Atlas Config</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#common-workflows","title":"Common Workflows","text":""},{"location":"features/editor-tools/editor-tools-guide/#setting-up-new-sprites","title":"Setting Up New Sprites","text":"Text Only<pre><code>1. Import sprites to Assets/Sprites/\n2. Use Sprite Cropper to remove padding\n3. Use Texture Settings Applier:\n   - Filter Mode: Bilinear\n   - Wrap Mode: Clamp\n   - Compression: CompressedHQ\n4. Use Sprite Settings Applier:\n   - Set consistent PPU (e.g., 32 or 64)\n5. Use Sprite Pivot Adjuster for consistent alignment\n</code></pre>"},{"location":"features/editor-tools/editor-tools-guide/#creating-and-editing-animations","title":"Creating and Editing Animations","text":"Text Only<pre><code>1. Prepare sprite frames in folder\n2. Open Sprite Animation Editor\n3. Click \"Browse Clips (Multi)...\" if clips exist, or\n4. Use Animation Creator to generate from sprites\n5. Edit in Sprite Animation Editor:\n   - Adjust frame order via drag-drop\n   - Set appropriate FPS\n6. Save clips\n7. (Optional) Add animation events:\n   a. Open AnimationEvent Editor\n   b. Drag Animator to \"Animator Object\" field\n   c. Select animation clip\n   d. Add events at specific frames\n   e. Configure event methods and parameters\n   f. Save changes\n</code></pre>"},{"location":"features/editor-tools/editor-tools-guide/#creating-sprite-atlases","title":"Creating Sprite Atlases","text":"Text Only<pre><code>1. Create atlas config:\n   a. Open Sprite Atlas Generator\n   b. Click \"Create New Config in 'Assets/Data'\"\n   c. Name your atlas configuration\n2. Configure source folders:\n   a. Click \"Add New Source Folder Entry\"\n   b. Select folder containing sprites\n   c. Add regex patterns (e.g., \"^character_.*\")\n   d. Or add labels for filtering\n3. Set packing options:\n   - Max texture size (2048 recommended)\n   - Padding (4px default)\n   - Compression settings\n4. Preview changes:\n   a. Click \"Scan Folders\"\n   b. Review sprites to add/remove\n5. Generate atlas:\n   a. Click \"Add X Sprites\" if satisfied\n   b. Click \"Generate/Update .spriteatlas ONLY\"\n   c. Click \"Pack All Generated Sprite Atlases\"\n</code></pre>"},{"location":"features/editor-tools/editor-tools-guide/#pre-commit-validation","title":"Pre-Commit Validation","text":"Text Only<pre><code>1. Open Prefab Checker\n2. Enable all critical checks:\n   - Missing Scripts \u2713\n   - Missing Required Components \u2713\n   - Null Object References \u2713\n3. Add changed prefab folders\n4. Click \"Run Checks\"\n5. Fix all reported issues\n6. Re-run to verify\n7. Commit changes\n</code></pre>"},{"location":"features/editor-tools/editor-tools-guide/#optimizing-textures-for-build","title":"Optimizing Textures for Build","text":"Text Only<pre><code>1. Use Sprite Cropper on all sprites (reduces memory)\n2. Use Texture Settings Applier with:\n   - Appropriate compression for platform\n   - Crunch compression enabled\n   - Proper max texture sizes\n3. Review build report for texture memory usage\n4. Iterate on settings as needed\n</code></pre>"},{"location":"features/editor-tools/editor-tools-guide/#keyboard-shortcuts-tips","title":"Keyboard Shortcuts &amp; Tips","text":"<p>Sprite Animation Editor:</p> <ul> <li><code>Enter</code> in frame order field: Apply frame reordering</li> <li>Drag frames: Reorder via visual feedback</li> <li>Drag clips: Reorder layer priority</li> </ul> <p>Prefab Checker:</p> <ul> <li>Click console errors: Selects problematic prefabs</li> <li>Toggle checks: Right-aligned checkboxes</li> </ul> <p>General:</p> <ul> <li>All tools remember last used directories</li> <li>Most tools support drag-and-drop folders</li> <li>Batch operations show progress in the console</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#performance-considerations","title":"Performance Considerations","text":"<p>Sprite Cropper:</p> <ul> <li>Uses parallel processing for pixel scanning</li> <li>Can process hundreds of sprites quickly</li> <li>Memory usage scales with sprite size</li> </ul> <p>Texture Settings Applier:</p> <ul> <li>Triggers Unity reimport for affected textures</li> <li>May take time on large texture sets</li> <li>Refresh only happens once after all changes</li> </ul> <p>Prefab Checker:</p> <ul> <li>Caches reflection metadata for speed</li> <li>Fast on repeated runs</li> <li>Scales linearly with prefab count</li> </ul> <p>Attribute Metadata Cache:</p> <ul> <li>Eliminates ~95% of runtime reflection overhead</li> <li>Startup time improvement: 50-200ms on large projects</li> <li>Critical for IL2CPP builds</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#troubleshooting","title":"Troubleshooting","text":"<p>Tool window won't open:</p> <ul> <li>Check the console for errors</li> <li>Verify that the package is in the correct location</li> <li>Try reimporting package</li> </ul> <p>Settings not applying:</p> <ul> <li>Ensure textures aren't in use</li> <li>Check the console for import errors</li> <li>Verify file permissions</li> </ul> <p>Cache not regenerating:</p> <ul> <li>Manually trigger via menu</li> <li>Check for script compilation errors</li> <li>Verify ScriptableObject singleton exists</li> </ul> <p>Prefab Checker missing issues:</p> <ul> <li>Ensure all relevant checks are enabled</li> <li>Verify folders are correct</li> <li>Check filter settings</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#best-practices","title":"Best Practices","text":"<p>Organization:</p> <ul> <li>Keep sprites in an organized folder structure</li> <li>Use consistent naming conventions</li> <li>Separate by type (UI, Characters, Environment)</li> </ul> <p>Performance:</p> <ul> <li>Run Sprite Cropper before creating atlases</li> <li>Use appropriate texture compression</li> <li>Enable crunch compression for mobile</li> </ul> <p>Quality:</p> <ul> <li>Run Prefab Checker before commits</li> <li>Use [ValidateAssignment] on critical fields</li> <li>Maintain consistent texture settings per category</li> <li>Use the manual recompilation menu/shortcut when iterating on packages to avoid reopening the project</li> </ul> <p>Workflow:</p> <ul> <li>Batch similar operations together</li> <li>Use multi-file selection where available</li> <li>Leverage automation tools (SingletonCreator, CacheGenerator)</li> </ul> <p>Compilation helpers:</p> <ul> <li>Menu path: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Request Script Compilation</code></li> <li>Shortcut: <code>Ctrl/Cmd + Alt + R</code> (configurable via Unity\u2019s Shortcut Manager under Wallstop / Request Script Compilation)</li> <li>Behavior: Forces an <code>AssetDatabase.Refresh</code> (synchronous import) before calling <code>CompilationPipeline.RequestScriptCompilation()</code> and logs whenever Unity is already compiling so scripts created outside the editor are imported immediately.</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#additional-resources","title":"Additional Resources","text":"<p>Attributes System:</p> <ul> <li>See <code>[ValidateAssignment]</code> for prefab validation</li> <li>See <code>[ScriptableSingletonPath]</code> for custom singleton paths</li> <li>See <code>[ParentComponent]</code>, <code>[ChildComponent]</code>, <code>[SiblingComponent]</code> for relational queries</li> </ul> <p>Related Components:</p> <ul> <li><code>ScriptableObjectSingleton&lt;T&gt;</code> - Base class for settings</li> <li><code>AttributesComponent</code> - Base class for the attribute system</li> <li><code>LayeredImage</code> - UI Toolkit multi-layer sprite rendering</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#version-information","title":"Version Information","text":"<p>Document Version: 2.0 Package: com.wallstop-studios.unity-helpers Last Updated: 2025-10-08</p> <p>What's New in v2.0:</p> <ul> <li>Added comprehensive Sprite Atlas Generator documentation</li> <li>Added Animation Event Editor documentation</li> <li>Added Texture Resizer and Fit Texture Size tools</li> <li>Added Custom Component Editors section</li> <li>Added Property Drawers &amp; Attributes section</li> <li>Added Sprite Label Processor documentation</li> <li>Expanded all existing tool documentation</li> <li>Added new workflow examples</li> <li>Complete menu item reference</li> <li>Enhanced quick reference section</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#summary","title":"Summary","text":"<p>This package provides 20+ editor tools across multiple categories:</p> <p>14 Editor Windows/Wizards:</p> <ul> <li>Image Blur Tool, Sprite Cropper, Texture Settings Applier, Sprite Settings Applier</li> <li>Sprite Pivot Adjuster, Texture Resizer, Fit Texture Size</li> <li>Sprite Animation Editor, Animation Event Editor, Animation Creator, Animation Copier</li> <li>Sprite Sheet Animation Creator, Sprite Atlas Generator, Prefab Checker</li> </ul> <p>3 Custom Component Editors:</p> <ul> <li>MatchColliderToSprite, PolygonCollider2DOptimizer, EnhancedImage</li> </ul> <p>4 Property Drawers:</p> <ul> <li>WShowIf, StringInList, IntDropDown, WReadOnly</li> </ul> <p>3 Automated Systems:</p> <ul> <li>ScriptableObject Singleton Creator</li> <li>Attribute Metadata Cache Generator</li> <li>Sprite Label Processor</li> </ul> <p>1 Utility Library:</p> <ul> <li>Editor Utilities</li> </ul> <p>All tools are designed to work together seamlessly and follow consistent design patterns for ease of use.</p> <p>For questions, issues, or feature requests, please contact the Wallstop Studios team.</p> <ul> <li>Integration note: The cache powers editor dropdowns and reflection shortcuts for the Effects system\u2019s <code>AttributeModification.attribute</code> field. See Effects System for how attributes, effects, and tags fit together.</li> </ul>"},{"location":"features/editor-tools/editor-tools-guide/#multifile-selector-ui-toolkit","title":"MultiFile Selector (UI Toolkit)","text":"<ul> <li>The <code>MultiFileSelectorElement</code> is primarily intended for Editor tooling. It can also be used in player builds, where it enumerates files under the application\u2019s data root. In the Editor it integrates with <code>EditorPrefs</code> and Reveal-in-Finder; at runtime it falls back to <code>PlayerPrefs</code> and omits Editor-only affordances.</li> </ul>"},{"location":"features/editor-tools/failed-tests-exporter/","title":"Failed Tests Exporter","text":""},{"location":"features/editor-tools/failed-tests-exporter/#overview","title":"Overview","text":"<p>The Failed Tests Exporter is an editor utility that hooks into the Unity Test Runner API to automatically capture failed test results. When a test run completes, it records each failure's name, message, and stack trace, and can export them to a timestamped text file in a configurable directory (defaults to the project root).</p> <p>This is especially useful for CI/CD pipelines where you need a machine-readable artifact of test failures, or for tracking intermittent test failures across multiple runs.</p>"},{"location":"features/editor-tools/failed-tests-exporter/#setup","title":"Setup","text":"<p>The Failed Tests Exporter is disabled by default. To enable it:</p> <ol> <li>Open Project Settings (<code>Edit &gt; Project Settings</code>)</li> <li>Navigate to Wallstop Studios &gt; Unity Helpers</li> <li>Expand the Failed Tests Exporter section</li> <li>Check Enable Failed Tests Exporter</li> </ol> <p>The exporter activates immediately when the setting is toggled. No domain reload is required.</p>"},{"location":"features/editor-tools/failed-tests-exporter/#output-directory","title":"Output Directory","text":"<p>By default, failed test result files are written to the project root. You can configure a different output directory:</p> <ol> <li>In the Failed Tests Exporter settings section, find the Output Directory row</li> <li>Click Browse\u2026 to open a folder picker dialog</li> <li>Select any folder within your project \u2014 the path is stored as a relative path from the project root</li> <li>Click the \u00d7 button to clear the setting and revert to the project root</li> </ol> <p>The output directory field is read-only to prevent typos \u2014 use the Browse\u2026 button to select a folder visually.</p> <p>Path Validation</p> <p>The output directory is validated on every use:</p> <ul> <li>If the configured directory no longer exists (e.g., it was renamed or deleted), the exporter automatically falls back to the project root</li> <li>Absolute paths, paths containing <code>..</code>, and paths outside the project root are rejected</li> <li>Invalid paths are automatically corrected when settings are loaded</li> </ul>"},{"location":"features/editor-tools/failed-tests-exporter/#usage","title":"Usage","text":""},{"location":"features/editor-tools/failed-tests-exporter/#automatic-export","title":"Automatic Export","text":"<p>When enabled, the exporter automatically:</p> <ol> <li>Clears previous failures when a new test run starts</li> <li>Records each individual test failure (name, message, stack trace)</li> <li>Exports all failures to a timestamped file when the test run completes</li> </ol> <p>The output file is written to the configured output directory (or the project root if none is set) with the format <code>failed-tests-YYYY-MM-DD-HHmmss.txt</code>.</p>"},{"location":"features/editor-tools/failed-tests-exporter/#manual-export","title":"Manual Export","text":"<p>You can manually export or clear captured failures using the menu items:</p> <ul> <li>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Export Failed Tests \u2014 Writes captured failures to a text file</li> <li>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Clear Failed Tests \u2014 Clears the in-memory failure list</li> </ul> <p>Both menu items are only enabled when there are captured failures.</p>"},{"location":"features/editor-tools/failed-tests-exporter/#output-format","title":"Output Format","text":"<p>Each failure in the exported file includes:</p> Text Only<pre><code>TEST_FAILURE_1\nName: MyNamespace.MyTestClass.MyTestMethod\nMessage: Expected 42 but was 0\nStack Trace:\nat MyNamespace.MyTestClass.MyTestMethod() in /path/to/file.cs:line 25\n\n---\n\nTEST_FAILURE_2\nName: MyNamespace.MyOtherClass.AnotherTest\nMessage: Object reference not set\nStack Trace:\n(no stack trace)\n</code></pre>"},{"location":"features/editor-tools/failed-tests-exporter/#api-reference","title":"API Reference","text":"<p>Note: The <code>FailedTestsExporter</code> class and its nested <code>FailedTestInfo</code> struct are <code>internal</code> and primarily intended for use within the Unity Helpers assembly. External consumers interact with this feature through the menu items and the settings UI described above.</p>"},{"location":"features/editor-tools/failed-tests-exporter/#failedtestsexporter","title":"FailedTestsExporter","text":"Member Type Description <code>Instance</code> <code>FailedTestsExporter</code> Static reference to the active exporter instance (null when disabled) <code>Failures</code> <code>IReadOnlyList&lt;FailedTestInfo&gt;</code> Read-only list of captured test failures <code>IsEnabled()</code> <code>bool</code> Whether the exporter is enabled in Unity Helpers settings"},{"location":"features/editor-tools/failed-tests-exporter/#failedtestinfo","title":"FailedTestInfo","text":"Field Type Description <code>name</code> <code>string</code> Fully qualified test name <code>message</code> <code>string</code> Failure message (or empty string) <code>stackTrace</code> <code>string</code> Stack trace (or empty string)"},{"location":"features/editor-tools/failed-tests-exporter/#see-also","title":"See Also","text":"<ul> <li>Editor Tools Guide \u2014 Complete reference for all Unity Helpers editor tools</li> <li>Inspector Settings \u2014 Configuring Unity Helpers project settings</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/","title":"Unity Method Analyzer","text":"<p>Detect C# inheritance issues and Unity lifecycle errors across your entire codebase.</p> <p>The Unity Method Analyzer scans your project's C# files and identifies common mistakes in method overrides, Unity lifecycle methods, and inheritance patterns\u2014before they cause runtime bugs. Use it during development to catch silent failures, missing base calls, and signature mismatches.</p>"},{"location":"features/editor-tools/unity-method-analyzer/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Overview</li> <li>Getting Started</li> <li>What It Detects</li> <li>Using the Analyzer</li> <li>Filtering Results</li> <li>Exporting Reports</li> <li>Suppressing Warnings</li> <li>Best Practices</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#overview","title":"Overview","text":"<p>The Unity Method Analyzer provides:</p> <ul> <li>Static code analysis without needing Roslyn or external tools</li> <li>Parallel scanning of thousands of files in seconds</li> <li>Issue categorization by severity and type</li> <li>One-click navigation to problematic code</li> <li>Export capabilities for CI/CD integration or team review</li> <li>Flexible filtering to focus on what matters</li> </ul> <p>Common issues detected:</p> Issue Type Example Missing <code>override</code> keyword Hiding base method instead of overriding Wrong method signature <code>OnCollisionEnter(Collider c)</code> instead of <code>OnCollisionEnter(Collision c)</code> Shadowed lifecycle methods Both base and derived class have <code>private void Start()</code> Return type mismatches <code>void Start()</code> vs <code>IEnumerator Start()</code> in inheritance chain Static lifecycle methods <code>static void Awake()</code> won't be called by Unity"},{"location":"features/editor-tools/unity-method-analyzer/#getting-started","title":"Getting Started","text":""},{"location":"features/editor-tools/unity-method-analyzer/#opening-the-analyzer","title":"Opening the Analyzer","text":"<p>Menu: <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Unity Method Analyzer</code></p> <p></p>"},{"location":"features/editor-tools/unity-method-analyzer/#your-first-scan","title":"Your First Scan","text":"<ol> <li>Open the analyzer from the menu</li> <li>Add source directories \u2014 By default, scans from the project root</li> <li>Click \"Analyze Code\" \u2014 Wait for the progress bar to complete</li> <li>Review results \u2014 Issues appear grouped by file, severity, or category</li> </ol>"},{"location":"features/editor-tools/unity-method-analyzer/#what-it-detects","title":"What It Detects","text":""},{"location":"features/editor-tools/unity-method-analyzer/#issue-categories","title":"Issue Categories","text":"<p>The analyzer groups issues into three categories:</p>"},{"location":"features/editor-tools/unity-method-analyzer/#unity-lifecycle-issues","title":"Unity Lifecycle Issues","text":"<p>Problems with Unity's magic methods (<code>Start</code>, <code>Update</code>, <code>OnCollisionEnter</code>, etc.):</p> C#<pre><code>// \u274c WRONG: Unexpected parameters - Unity won't call this\nvoid Update(float deltaTime)  // Update takes no parameters\n{\n    Move(deltaTime);\n}\n\n// \u2705 CORRECT: Proper signature\nvoid Update()\n{\n    Move(Time.deltaTime);\n}\n</code></pre> C#<pre><code>// \u274c WRONG: Static lifecycle method - Unity won't call it\nstatic void Awake()\n{\n    Initialize();\n}\n\n// \u2705 CORRECT: Instance method\nvoid Awake()\n{\n    Initialize();\n}\n</code></pre> C#<pre><code>// \u26a0\ufe0f SHADOWING: Both base and derived have private Start()\npublic class BaseEnemy : MonoBehaviour\n{\n    private void Start() { BaseInit(); }  // Called for BaseEnemy instances\n}\n\npublic class Boss : BaseEnemy\n{\n    private void Start() { BossInit(); }  // Only this is called for Boss instances\n}\n\n// \u2705 BETTER: Use virtual/override pattern\npublic class BaseEnemy : MonoBehaviour\n{\n    protected virtual void Start() { BaseInit(); }\n}\n\npublic class Boss : BaseEnemy\n{\n    protected override void Start()\n    {\n        base.Start();  // Calls BaseInit()\n        BossInit();\n    }\n}\n</code></pre>"},{"location":"features/editor-tools/unity-method-analyzer/#unity-inheritance-issues","title":"Unity Inheritance Issues","text":"<p>Problems when extending Unity base classes:</p> C#<pre><code>public class GameManager : MonoBehaviour\n{\n    // \u274c WRONG: Using 'new' hides the base method\n    public new void Awake()\n    {\n        Initialize();\n    }\n\n    // \u2705 CORRECT: Use virtual/override pattern if base is virtual\n    // Or just declare normally for MonoBehaviour lifecycle\n    private void Awake()\n    {\n        Initialize();\n    }\n}\n</code></pre>"},{"location":"features/editor-tools/unity-method-analyzer/#general-inheritance-issues","title":"General Inheritance Issues","text":"<p>Problems in your own class hierarchies:</p> C#<pre><code>public class BaseEnemy : MonoBehaviour\n{\n    public virtual void TakeDamage(int amount) { }\n}\n\npublic class Boss : BaseEnemy\n{\n    // \u274c WRONG: Missing 'override' keyword - hides base method\n    public void TakeDamage(int amount)\n    {\n        // This won't be called polymorphically!\n    }\n\n    // \u2705 CORRECT: Properly override\n    public override void TakeDamage(int amount)\n    {\n        base.TakeDamage(amount);\n        // Boss-specific logic\n    }\n}\n</code></pre>"},{"location":"features/editor-tools/unity-method-analyzer/#severity-levels","title":"Severity Levels","text":"Severity Description Example Critical Will cause runtime failures or silent bugs Missing override hiding virtual method High Likely unintended behavior Wrong Unity lifecycle signature Medium Potential issues worth reviewing Suspicious method hiding Low Style or minor concerns Non-standard access modifiers Info Informational notes Detected patterns for review"},{"location":"features/editor-tools/unity-method-analyzer/#using-the-analyzer","title":"Using the Analyzer","text":""},{"location":"features/editor-tools/unity-method-analyzer/#managing-source-directories","title":"Managing Source Directories","text":"<p>Configure which directories to scan:</p> <p></p> <ul> <li>Click \"+\" to add a new directory</li> <li>Click \"...\" to browse for a different path</li> <li>Click \"-\" to remove a directory</li> <li>Red paths indicate directories that don't exist</li> </ul> <p>Tip: Add only relevant directories (e.g., <code>Assets/Scripts</code>) to speed up analysis.</p>"},{"location":"features/editor-tools/unity-method-analyzer/#understanding-the-results-tree","title":"Understanding the Results Tree","text":"<p>The results are displayed in a hierarchical tree view:</p> <p></p> <ul> <li>Expand/collapse groups with the arrow</li> <li>Single-click an issue to see details in the panel below</li> <li>Double-click to open the file at the exact line number</li> <li>Right-click for context menu options</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#issue-detail-panel","title":"Issue Detail Panel","text":"<p>When you select an issue, the detail panel shows:</p> <p></p> <ul> <li>File path and line number</li> <li>Class and method names</li> <li>Issue type and severity</li> <li>Detailed description of the problem</li> <li>Recommended fix with specific guidance</li> <li>Base class information when relevant</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#filtering-results","title":"Filtering Results","text":""},{"location":"features/editor-tools/unity-method-analyzer/#group-by-options","title":"Group By Options","text":"<p>Organize results by:</p> <ul> <li>File \u2014 Group issues by source file (default)</li> <li>Severity \u2014 Group by Critical/High/Medium/Low/Info</li> <li>Category \u2014 Group by Unity Lifecycle/Unity Inheritance/General</li> </ul> <p></p>"},{"location":"features/editor-tools/unity-method-analyzer/#severity-filter","title":"Severity Filter","text":"<p>Focus on specific severity levels:</p> <p></p>"},{"location":"features/editor-tools/unity-method-analyzer/#category-filter","title":"Category Filter","text":"<p>Focus on specific issue categories:</p> <ul> <li>All \u2014 Show everything</li> <li>Unity Lifecycle \u2014 Only lifecycle method issues</li> <li>Unity Inheritance \u2014 Only Unity class inheritance issues</li> <li>General Inheritance \u2014 Only custom class inheritance issues</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#search-filter","title":"Search Filter","text":"<p>Free-text search across all issue fields:</p> <p></p> <p>Search matches against:</p> <ul> <li>File paths</li> <li>Class names</li> <li>Method names</li> <li>Issue descriptions</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#exporting-reports","title":"Exporting Reports","text":""},{"location":"features/editor-tools/unity-method-analyzer/#export-menu","title":"Export Menu","text":"<p>Click \"Export \u25be\" to access export options:</p> <p></p>"},{"location":"features/editor-tools/unity-method-analyzer/#copy-options","title":"Copy Options","text":"<ul> <li>Copy Selected as JSON \u2014 Copy the selected issue</li> <li>Copy Selected as Markdown \u2014 Copy the selected issue as readable text</li> <li>Copy All as JSON \u2014 Copy all filtered issues</li> <li>Copy All as Markdown \u2014 Copy all filtered issues as readable text</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#save-options","title":"Save Options","text":"<ul> <li>Save as JSON... \u2014 Export to a JSON file for CI/CD integration</li> <li>Save as Markdown... \u2014 Export to a Markdown file for documentation or review</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#json-export-format","title":"JSON Export Format","text":"JSON<pre><code>{\n  \"analysisDate\": \"2024-01-15T10:30:00Z\",\n  \"totalIssues\": 12,\n  \"issues\": [\n    {\n      \"filePath\": \"Assets/Scripts/Player/PlayerController.cs\",\n      \"className\": \"PlayerController\",\n      \"methodName\": \"OnCollisionEnter\",\n      \"issueType\": \"Unity Lifecycle Signature Mismatch\",\n      \"description\": \"Method 'OnCollisionEnter' has wrong parameter type\",\n      \"severity\": \"High\",\n      \"recommendedFix\": \"Change parameter from 'Collider' to 'Collision'\",\n      \"lineNumber\": 42,\n      \"category\": \"UnityLifecycle\"\n    }\n  ]\n}\n</code></pre>"},{"location":"features/editor-tools/unity-method-analyzer/#markdown-export-format","title":"Markdown Export Format","text":"Markdown<pre><code># Unity Method Analyzer Report\n\nGenerated: 2024-01-15 10:30:00\nTotal Issues: 12\n\n## Critical (2)\n\n### PlayerController.cs:42\n\n**Class:** PlayerController  \n**Method:** OnCollisionEnter  \n**Issue:** Unity Lifecycle Signature Mismatch  \n**Fix:** Change parameter from 'Collider' to 'Collision'\n</code></pre>"},{"location":"features/editor-tools/unity-method-analyzer/#suppressing-warnings","title":"Suppressing Warnings","text":"<p>For test code or intentional patterns, use <code>[SuppressAnalyzer]</code>:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Tests.Core;\n\n// Suppress entire class\n[SuppressAnalyzer(\"Test fixture for analyzer validation\")]\npublic class TestClassWithIntentionalIssues : BaseClass\n{\n    public void HiddenMethod() { }  // Won't trigger warning\n}\n\n// Or suppress specific methods\npublic class TestClass : BaseClass\n{\n    [SuppressAnalyzer(\"Testing method hiding detection\")]\n    public new void VirtualMethod() { }  // Won't trigger warning\n}\n</code></pre> <p>Note: <code>[SuppressAnalyzer]</code> is only available in test assemblies. Production code should fix issues rather than suppress them.</p>"},{"location":"features/editor-tools/unity-method-analyzer/#best-practices","title":"Best Practices","text":""},{"location":"features/editor-tools/unity-method-analyzer/#when-to-run","title":"When to Run","text":"<ul> <li>Before committing \u2014 Catch issues early</li> <li>During code review \u2014 Export reports for team review</li> <li>In CI/CD \u2014 Use JSON export for automated checks</li> <li>After refactoring \u2014 Verify inheritance chains remain correct</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#recommended-workflow","title":"Recommended Workflow","text":"<ol> <li>Run full scan on your <code>Assets/Scripts</code> folder</li> <li>Filter by Critical/High severity first</li> <li>Fix issues starting with Critical</li> <li>Re-scan to verify fixes</li> <li>Export report for documentation</li> </ol>"},{"location":"features/editor-tools/unity-method-analyzer/#performance-tips","title":"Performance Tips","text":"<ul> <li>Scan specific directories rather than the entire project</li> <li>Use filters to focus on relevant issues</li> <li>Close other editor windows during large scans</li> <li>Exclude generated code directories</li> </ul>"},{"location":"features/editor-tools/unity-method-analyzer/#integrating-with-cicd","title":"Integrating with CI/CD","text":"<p>Export JSON reports and parse them in your build pipeline:</p> Bash<pre><code># Example: Fail build if critical issues exist\nunity -batchmode -projectPath . -executeMethod AnalyzerRunner.RunAndExport\ncat analyzer-report.json | jq '.issues | map(select(.severity == \"Critical\")) | length'\n</code></pre>"},{"location":"features/editor-tools/unity-method-analyzer/#summary","title":"Summary","text":"Feature Description Menu Location <code>Tools &gt; Wallstop Studios &gt; Unity Helpers &gt; Unity Method Analyzer</code> Issue Categories Unity Lifecycle, Unity Inheritance, General Inheritance Severity Levels Critical, High, Medium, Low, Info Export Formats JSON, Markdown Suppression <code>[SuppressAnalyzer]</code> attribute (test assemblies only)"},{"location":"features/effects/effects-system-tutorial/","title":"Effects System Tutorial - Build Your First Buff in 5 Minutes","text":""},{"location":"features/effects/effects-system-tutorial/#what-youll-build","title":"What You'll Build","text":"<p>By the end of this tutorial, you'll have a complete working buff system with:</p> <ul> <li>A \"Haste\" buff that increases speed by 50%</li> <li>Visual particle effects that spawn/despawn with the buff</li> <li>A \"Stunned\" debuff that prevents player movement</li> <li>Tags you can query in gameplay code</li> </ul> <p>Time required: 5-10 minutes</p>"},{"location":"features/effects/effects-system-tutorial/#why-use-the-effects-system","title":"Why Use the Effects System?","text":"<p>The Old Way:</p> C#<pre><code>// 50-100 lines per effect type\npublic class HasteEffect : MonoBehaviour {\n    float duration;\n    float speedMultiplier;\n    GameObject particles;\n\n    void Update() {\n        duration -= Time.deltaTime;\n        if (duration &lt;= 0) RemoveSelf();\n    }\n\n    void RemoveSelf() {\n        // Remove speed modifier...\n        // Destroy particles...\n        // Handle stacking...\n        // 40 more lines...\n    }\n}\n</code></pre> <p>The New Way:</p> C#<pre><code>// Zero lines - everything configured in editor\nplayer.ApplyEffect(hasteEffect);  // Done!\n</code></pre> <p>Result: Designers create hundreds of effects without programmer involvement.</p>"},{"location":"features/effects/effects-system-tutorial/#step-1-create-your-first-attributescomponent-2-minutes","title":"Step 1: Create Your First AttributesComponent (2 minutes)","text":"<p>This component will hold the stats that effects can modify.</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Tags;\n\npublic class PlayerStats : AttributesComponent\n{\n    // Define attributes that effects can modify\n    public Attribute Speed = 5f;\n    public Attribute MaxHealth = 100f;\n    public Attribute AttackDamage = 10f;\n    public Attribute Defense = 5f;\n\n    protected override void Awake()\n    {\n        base.Awake();\n        // Optional: Log when attributes change\n        OnAttributeModified += (attributeName, oldVal, newVal) =&gt;\n            Debug.Log($\"{attributeName} changed: {oldVal} \u2192 {newVal}\");\n    }\n}\n</code></pre> <p>What's an Attribute?</p> <ul> <li>Holds a base value (e.g., Speed = 5)</li> <li>Tracks modifications from multiple sources</li> <li>Calculates final value automatically (Add \u2192 Multiply \u2192 Override)</li> <li>Raises events when value changes</li> </ul> <p>\u26a0\ufe0f Important: Use Attributes for \"max\" or \"rate\" values, NOT \"current\" depleting values!</p> <ul> <li>\u2705 MaxHealth - modified by buffs (good)</li> <li>\u274c CurrentHealth - modified by damage/healing from many systems (bad - causes state conflicts)</li> <li>\u2705 AttackDamage - modified by strength buffs (good)</li> <li>\u2705 Speed - modified by haste/slow effects (good)</li> </ul> <p>If a value is frequently modified by systems outside the effects system (like health being reduced by damage), use a regular field instead. See the main documentation for details.</p>"},{"location":"features/effects/effects-system-tutorial/#step-2-add-stats-to-your-player-30-seconds","title":"Step 2: Add Stats to Your Player (30 seconds)","text":"<ol> <li>Open your Player prefab/GameObject</li> <li>Add Component \u2192 <code>PlayerStats</code></li> <li>Set values in Inspector:</li> <li>Speed: <code>5</code></li> <li>MaxHealth: <code>100</code></li> <li>AttackDamage: <code>10</code></li> <li>Defense: <code>5</code></li> </ol> <p>That's it! Your player now has modifiable attributes.</p>"},{"location":"features/effects/effects-system-tutorial/#step-3-create-a-haste-effect-2-minutes","title":"Step 3: Create a Haste Effect (2 minutes)","text":""},{"location":"features/effects/effects-system-tutorial/#31-create-the-scriptableobject","title":"3.1 Create the ScriptableObject","text":"<ol> <li>In Project window: <code>Right-click</code> \u2192 <code>Create</code> \u2192 <code>Wallstop Studios</code> \u2192 <code>Unity Helpers</code> \u2192 <code>Attribute Effect</code></li> <li>Name it: <code>HasteEffect</code></li> </ol>"},{"location":"features/effects/effects-system-tutorial/#32-configure-the-effect","title":"3.2 Configure the Effect","text":"<p>Select <code>HasteEffect</code> and set these values in Inspector:</p> <p>Modifications:</p> <ul> <li>Click \"+\" to add a modification</li> <li>Attribute Name: <code>Speed</code> (must match field name exactly)</li> <li>Action: <code>Multiplication</code></li> <li>Value: <code>1.5</code> (150% of base speed)</li> </ul> <p>Duration:</p> <ul> <li>Modifier Duration Type: <code>Duration</code></li> <li>Duration: <code>5</code> (seconds)</li> <li>Can Reapply: \u2705 (checking this resets timer when reapplied)</li> </ul> <p>Tags:</p> <ul> <li>Effect Tags: Add <code>\"Haste\"</code> (used for both gameplay queries via <code>HasTag()</code> and effect organization)</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#33-add-visual-effects-optional","title":"3.3 Add Visual Effects (Optional)","text":"<p>Cosmetic Effects:</p> <ul> <li>Size: <code>1</code></li> <li>Element 0:</li> <li>Prefab: Drag a particle system prefab (or create one)</li> <li>Requires Instancing: \u2705 (creates a new instance per application)</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#step-4-apply-the-effect-30-seconds","title":"Step 4: Apply the Effect (30 seconds)","text":"<p>Add this code to test your effect:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Tags;\n\npublic class PlayerController : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect hasteEffect;\n    private PlayerStats stats;\n\n    void Start()\n    {\n        stats = GetComponent&lt;PlayerStats&gt;();\n    }\n\n    void Update()\n    {\n        // Apply haste when pressing H\n        if (Input.GetKeyDown(KeyCode.H))\n        {\n            this.ApplyEffect(hasteEffect);\n            Debug.Log($\"Speed is now: {stats.Speed.CurrentValue}\");\n        }\n\n        // Move with current speed\n        float h = Input.GetAxis(\"Horizontal\");\n        transform.position += Vector3.right * h * stats.Speed * Time.deltaTime;\n    }\n}\n</code></pre> <p>Test it:</p> <ol> <li>Assign <code>HasteEffect</code> to the Inspector field</li> <li>Press Play</li> <li>Press <code>H</code> to apply haste</li> <li>Notice: Speed increases to 7.5, particle effect spawns</li> <li>After 5 seconds: Speed returns to 5, particles disappear</li> </ol>"},{"location":"features/effects/effects-system-tutorial/#step-5-create-a-stun-debuff-2-minutes","title":"Step 5: Create a Stun Debuff (2 minutes)","text":"<p>Let's make a more complex effect that prevents movement.</p>"},{"location":"features/effects/effects-system-tutorial/#51-create-the-effect","title":"5.1 Create the Effect","text":"<ol> <li><code>Right-click</code> \u2192 <code>Create</code> \u2192 <code>Wallstop Studios</code> \u2192 <code>Unity Helpers</code> \u2192 <code>Attribute Effect</code></li> <li>Name it: <code>StunEffect</code></li> </ol>"},{"location":"features/effects/effects-system-tutorial/#52-configure-stun","title":"5.2 Configure Stun","text":"<p>Modifications:</p> <ul> <li>Attribute Name: <code>Speed</code></li> <li>Action: <code>Override</code></li> <li>Value: <code>0</code> (completely override speed to 0)</li> </ul> <p>Duration:</p> <ul> <li>Modifier Duration Type: <code>Duration</code></li> <li>Duration: <code>3</code></li> <li>Can Reapply: \u2705</li> </ul> <p>Tags:</p> <ul> <li>Effect Tags: <code>\"Stunned\"</code>, <code>\"Stun\"</code>, <code>\"Debuff\"</code>, <code>\"CC\"</code> (for gameplay queries and organization)</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#53-query-tags-in-gameplay","title":"5.3 Query Tags in Gameplay","text":"C#<pre><code>public class PlayerController : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect hasteEffect;\n    [SerializeField] private AttributeEffect stunEffect;\n    private PlayerStats stats;\n\n    void Update()\n    {\n        // Apply effects\n        if (Input.GetKeyDown(KeyCode.H)) this.ApplyEffect(hasteEffect);\n        if (Input.GetKeyDown(KeyCode.S)) this.ApplyEffect(stunEffect);\n\n        // Check if player is stunned before allowing movement\n        if (this.HasTag(\"Stunned\"))\n        {\n            Debug.Log(\"Player is stunned! Cannot move.\");\n            return;\n        }\n\n        // Normal movement\n        float h = Input.GetAxis(\"Horizontal\");\n        transform.position += Vector3.right * h * stats.Speed * Time.deltaTime;\n    }\n}\n</code></pre> <p>Test it:</p> <ol> <li>Press <code>S</code> to stun yourself</li> <li>Try to move - you can't!</li> <li>After 3 seconds, movement returns</li> </ol>"},{"location":"features/effects/effects-system-tutorial/#step-6-advanced-features-5-minutes","title":"Step 6: Advanced Features (5 minutes)","text":""},{"location":"features/effects/effects-system-tutorial/#stacking-effects","title":"Stacking Effects","text":"<p>Effects stack independently by default:</p> C#<pre><code>// Apply haste 3 times\nthis.ApplyEffect(hasteEffect);  // Speed = 7.5\nthis.ApplyEffect(hasteEffect);  // Speed = 11.25 (1.5 \u00d7 1.5 \u00d7 5)\nthis.ApplyEffect(hasteEffect);  // Speed = 16.875 (1.5 \u00d7 1.5 \u00d7 1.5 \u00d7 5)\n\n// Each stack has its own duration and can be removed independently\n</code></pre>"},{"location":"features/effects/effects-system-tutorial/#manual-removal","title":"Manual Removal","text":"C#<pre><code>// Apply and save handle\nEffectHandle? handle = this.ApplyEffect(hasteEffect);\n\n// Remove specific stack early\nif (handle.HasValue)\n{\n    this.RemoveEffect(handle.Value);\n}\n\n// Remove all haste effects\nthis.RemoveEffects(this.GetHandlesWithTag(\"Haste\"));\n</code></pre>"},{"location":"features/effects/effects-system-tutorial/#multiple-modifications-per-effect","title":"Multiple Modifications Per Effect","text":"<p>One effect can modify multiple attributes:</p> <p>Create \"Berserker Rage\" effect:</p> <ul> <li>Modification 1: Speed \u00d7 1.3</li> <li>Modification 2: AttackDamage \u00d7 2.0</li> <li>Modification 3: Defense \u00d7 0.5 (trade-off - more damage but less defense!)</li> <li>Duration: 10 seconds</li> <li>Tags: <code>\"Berserker\"</code>, <code>\"Buff\"</code></li> </ul>"},{"location":"features/effects/effects-system-tutorial/#infinite-duration-effects","title":"Infinite Duration Effects","text":"<p>For permanent buffs (e.g., equipment):</p> C#<pre><code>// In Inspector:\n// - Modifier Duration Type: Infinite\n// - (Duration field is ignored)\n\n// Apply permanent buff\nEffectHandle? handle = this.ApplyEffect(permanentStrengthBonus);\n\n// Later, remove when equipment is unequipped\nif (handle.HasValue)\n    this.RemoveEffect(handle.Value);\n</code></pre>"},{"location":"features/effects/effects-system-tutorial/#common-patterns","title":"Common Patterns","text":""},{"location":"features/effects/effects-system-tutorial/#damage-over-time-dot","title":"Damage Over Time (DOT)","text":"C#<pre><code>// Create \"Poison\" effect:\n// - periodicEffects: interval = 1s, maxTicks = 10, modifications = []\n// - behaviors: PoisonDamageBehavior (below)\n// - Duration: 10 seconds\n// - Tags: \"Poisoned\", \"DoT\", \"Debuff\"\n\nvoid ApplyPoison(GameObject target)\n{\n    target.ApplyEffect(poisonEffect);\n}\n\n[CreateAssetMenu(menuName = \"Combat/Effects/Poison Damage\")]\npublic sealed class PoisonDamageBehavior : EffectBehavior\n{\n    [SerializeField]\n    private float damagePerTick = 2f;\n\n    public override void OnPeriodicTick(\n        EffectBehaviorContext context,\n        PeriodicEffectTickContext tickContext\n    )\n    {\n        if (!context.Target.TryGetComponent(out PlayerHealth health))\n        {\n            return;\n        }\n\n        health.ApplyDamage(damagePerTick);\n    }\n}\n\npublic sealed class PlayerHealth : MonoBehaviour\n{\n    [SerializeField]\n    private float currentHealth = 100f;\n\n    public float CurrentHealth =&gt; currentHealth;\n\n    public void ApplyDamage(float amount)\n    {\n        currentHealth -= amount;\n\n        if (currentHealth &lt;= 0f)\n        {\n            currentHealth = 0f;\n            Die();\n        }\n    }\n\n    private void Die()\n    {\n        // Handle player death\n    }\n}\n</code></pre> <p>This keeps <code>CurrentHealth</code> as a regular gameplay field while the effect system triggers damage through behaviours.</p>"},{"location":"features/effects/effects-system-tutorial/#cooldown-reduction","title":"Cooldown Reduction","text":"C#<pre><code>// Create \"Haste\" effect (for abilities):\n// - Modification: CooldownRate \u00d7 1.5 (50% faster cooldowns)\n\npublic class AbilitySystem : AttributesComponent\n{\n    public Attribute CooldownRate = 1f;\n    private float cooldown;\n\n    public void UseAbility()\n    {\n        // Cooldown respects rate\n        cooldown = baseCooldown / CooldownRate.Value;\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system-tutorial/#conditional-effects","title":"Conditional Effects","text":"C#<pre><code>// Only apply effect if conditions met\nvoid TryApplyBuff(AttributeEffect effect)\n{\n    // Check if player already has max buffs\n    if (this.TryGetTagCount(\"Buff\", out int buffCount) &amp;&amp; buffCount &gt;= 5)\n    {\n        Debug.Log(\"Too many buffs active!\");\n        return;\n    }\n\n    // Check if effect is already active\n    if (this.HasTag(\"Haste\") &amp;&amp; effect == hasteEffect)\n    {\n        Debug.Log(\"Haste already active!\");\n        return;\n    }\n\n    this.ApplyEffect(effect);\n}\n</code></pre>"},{"location":"features/effects/effects-system-tutorial/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/effects/effects-system-tutorial/#should-i-use-currenthealth-as-an-attribute","title":"\"Should I use CurrentHealth as an Attribute?\"","text":"<ul> <li>No! Use <code>MaxHealth</code> as an Attribute (modified by buffs), but keep <code>CurrentHealth</code> as a regular field (modified by damage/healing)</li> <li>Why: CurrentHealth is modified by many systems (combat, regeneration, etc.). Using it as an Attribute causes state conflicts when effects and other systems both try to modify it</li> <li>Pattern: Attribute for max/cap, regular field for current/depleting value</li> <li>See: \"Understanding Attributes: What to Model and What to Avoid\" in the main documentation</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#attribute-speed-not-found","title":"\"Attribute 'Speed' not found\"","text":"<ul> <li>Cause: Attribute name in effect doesn't match the field name in AttributesComponent</li> <li>Fix: Names must match exactly (case-sensitive): <code>Speed</code> not <code>speed</code></li> <li>Tip: Use Attribute Metadata Cache generator for dropdown validation</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#effect-doesnt-apply","title":"Effect doesn't apply","text":"<ul> <li>Check: Does target GameObject have an <code>AttributesComponent</code>?</li> <li>Check: Is <code>EffectHandler</code> component added? (Usually added automatically)</li> <li>Check: Are there any errors in the console?</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#particles-dont-spawn","title":"Particles don't spawn","text":"<ul> <li>Check: Cosmetic Effects \u2192 Prefab is assigned</li> <li>Check: Prefab has a <code>CosmeticEffectData</code> component</li> <li>Check: Requires Instancing is checked if using per-application instances</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#value-isnt-changing","title":"Value isn't changing","text":"<ul> <li>Check: Attribute name matches exactly</li> <li>Check: Modification value is non-zero</li> <li>Check: Action type is correct (Multiplication needs &gt; 0, Addition can be negative)</li> <li>Debug: Log <code>attribute.Value</code> before and after applying effect</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#next-steps","title":"Next Steps","text":"<p>You now have a complete buff/debuff system! Here are some ideas to expand:</p>"},{"location":"features/effects/effects-system-tutorial/#create-more-effects","title":"Create More Effects","text":"<ul> <li>Shield: MaxHealth \u00d7 1.5, visual shield sprite</li> <li>Slow: Speed \u00d7 0.5, \"Slowed\" tag</li> <li>Critical Strike: AttackDamage \u00d7 2.0, \"CriticalHit\" tag, brief flash effect</li> <li>Invisibility: Just tags (\"Invisible\"), no stat changes, transparency effect</li> <li>Armor Buff: Defense + 10, metallic sheen cosmetic</li> <li>Strength Potion: AttackDamage \u00d7 1.5, red particle aura</li> </ul>"},{"location":"features/effects/effects-system-tutorial/#build-systems-around-tags","title":"Build Systems Around Tags","text":"C#<pre><code>// AI ignores invisible players\nif (!target.HasTag(\"Invisible\"))\n{\n    ChasePlayer(target);\n}\n\n// UI shows status icons\nif (player.HasTag(\"Poisoned\"))\n    ShowPoisonIcon();\n\n// Abilities check prerequisites\nif (player.HasTag(\"Stunned\") || player.HasTag(\"Silenced\"))\n    return;  // Can't cast\n\n// Interactions respect state\nif (player.HasTag(\"Invulnerable\"))\n    damage = 0;\n</code></pre>"},{"location":"features/effects/effects-system-tutorial/#designer-workflows","title":"Designer Workflows","text":"<ol> <li>Create an effect library (30+ common effects)</li> <li>Designers mix/match on items, abilities, enemies</li> <li>Programmers never touch effect code again!</li> </ol>"},{"location":"features/effects/effects-system-tutorial/#related-documentation","title":"\ud83d\udcda Related Documentation","text":"<p>Core Guides:</p> <ul> <li>Effects System Full Guide - Complete API reference and advanced patterns</li> <li>Getting Started - Your first 5 minutes with Unity Helpers</li> <li>Main README - Complete feature overview</li> </ul> <p>Related Features:</p> <ul> <li>Relational Components - Auto-wire components (pairs well with effects)</li> <li>Serialization - Save/load effects and attributes</li> </ul> <p>Need help? Open an issue</p>"},{"location":"features/effects/effects-system-tutorial/#made-with-by-wallstop-studios","title":"Made with \u2764\ufe0f by Wallstop Studios","text":"<p>Effects System tutorial complete! Your designers can now create gameplay effects without code.</p>"},{"location":"features/effects/effects-system/","title":"Effects, Attributes, and Tags \u2014 Deep Dive","text":""},{"location":"features/effects/effects-system/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>\u2b50 Build buff/debuff systems without writing custom code for every effect.</li> <li>Data\u2011driven ScriptableObjects: designers create 100s of effects, programmers build the system once.</li> <li>Reduces boilerplate with data-driven effect definitions; designers can iterate without code changes.</li> <li>Note: Attributes are optional - use the system purely for tag-based state management and timed cosmetic effects.</li> </ul>"},{"location":"features/effects/effects-system/#data-driven-effect-authoring","title":"Data-Driven Effect Authoring","text":"<p>The Problem - Hardcoded Effects:</p> C#<pre><code>// Every buff needs its own custom MonoBehaviour:\n\npublic class HasteEffect : MonoBehaviour\n{\n    private float duration = 5f;\n    private float originalSpeed;\n    private PlayerStats player;\n\n    void Start()\n    {\n        player = GetComponent&lt;PlayerStats&gt;();\n        originalSpeed = player.speed;\n        player.speed *= 1.5f;  // Apply speed boost\n    }\n\n    void Update()\n    {\n        duration -= Time.deltaTime;\n        if (duration &lt;= 0)\n        {\n            player.speed = originalSpeed;  // Restore\n            Destroy(this);\n        }\n    }\n}\n\n// 20 effects \u00d7 50 lines each = 1000 lines of repetitive code\n// Designers can't create effects without programmer\n</code></pre> <p>The Solution - Data-Driven:</p> C#<pre><code>// Programmers build system once (Unity Helpers provides this):\n// - AttributesComponent base class\n// - EffectHandler manages application/removal\n// - ScriptableObject authoring\n\n// Designers create effects in Editor (NO CODE):\n// 1. Right-click \u2192 Create \u2192 Attribute Effect\n// 2. Name: \"Haste\"\n// 3. Add modification: Speed \u00d7 1.5\n// 4. Duration: 5 seconds\n// 5. Done!\n\n// Apply at runtime (one line):\ntarget.ApplyEffect(hasteEffect);\n</code></pre> <p>Designer Workflow:</p> <ol> <li>Create the effect asset in the editor (no code)</li> <li>Test in-game immediately</li> <li>Tweak values and iterate freely</li> <li>Create variations (Haste II, Haste III) by duplicating assets</li> </ol> <p>Impact:</p> <ul> <li>Reduced boilerplate: Centralizes effect logic in a reusable system</li> <li>Designer workflow: Create and modify effects without code changes</li> <li>Faster iteration: Adjust values without recompiling</li> <li>Maintainability: All effects in one system vs. scattered scripts</li> </ul> <p>Data\u2011driven gameplay effects that modify stats, apply tags, and drive cosmetic presentation.</p> <p>This guide explains the concepts, how they work together, authoring patterns, recipes, best practices, and FAQs.</p>"},{"location":"features/effects/effects-system/#visual-reference","title":"Visual Reference","text":""},{"location":"features/effects/effects-system/#concepts","title":"Concepts","text":"<ul> <li><code>Attribute</code> \u2014 A dynamic numeric value with a base and a calculated current value. Current value applies all active modifications.</li> <li><code>AttributeModification</code> \u2014 Declarative change to an <code>Attribute</code>. Actions: Addition, Multiplication, Override. Applied in that order.</li> <li><code>AttributeEffect</code> \u2014 ScriptableObject asset bundling modifications, tags, cosmetic data, duration policy, periodic tick schedules, and optional runtime behaviours.</li> <li><code>EffectHandle</code> \u2014 Opaque identifier for a specific application instance (for Duration/Infinite effects). Used to remove one stack.</li> <li><code>AttributesComponent</code> \u2014 Base MonoBehaviour exposing modifiable <code>Attribute</code> fields (e.g., Health, Speed) on your character.</li> <li><code>EffectHandler</code> \u2014 Component that applies/removes effects, tracks durations, forwards modifications to <code>AttributesComponent</code>, applies tags and cosmetics.</li> <li><code>TagHandler</code> \u2014 Counts and queries string tags for gating gameplay (e.g., \"Stunned\"). Removes tags only when all sources are gone.</li> <li><code>CosmeticEffectData</code> \u2014 Prefab\u2011like container with <code>CosmeticEffectComponent</code> behaviours; reused or instanced per effect application.</li> </ul>"},{"location":"features/effects/effects-system/#how-it-works","title":"How It Works","text":"<ol> <li>You author an <code>AttributeEffect</code> with modifications, tags, cosmetics, and duration.</li> <li>You apply it to a GameObject: <code>EffectHandle? handle = target.ApplyEffect(effect);</code></li> <li><code>EffectHandler</code> will:</li> <li>Create an <code>EffectHandle</code> (for Duration/Infinite) and track expiration</li> <li>Apply tags via <code>TagHandler</code> (counted; multiple sources safe)</li> <li>Apply cosmetic behaviours (<code>CosmeticEffectData</code>)</li> <li>Forward <code>AttributeModification</code>s to all <code>AttributesComponent</code>s on the GameObject</li> <li>On removal (manual or expiration), all of the above are cleanly reversed.</li> </ol> <p>Instant effects modify base values permanently and return <code>null</code> instead of a handle.</p>"},{"location":"features/effects/effects-system/#authoring-guide","title":"Authoring Guide","text":"<ol> <li>Define stats:</li> </ol> C#<pre><code>public class CharacterStats : AttributesComponent\n{\n    public Attribute MaxHealth = 100f;\n    public Attribute Speed = 5f;\n    public Attribute AttackDamage = 10f;\n    public Attribute Defense = 10f;\n}\n</code></pre> <ol> <li>Create an <code>AttributeEffect</code> asset (Project view \u2192 Create \u2192 Wallstop Studios \u2192 Unity Helpers \u2192 Attribute Effect):</li> <li>modifications: e.g., <code>{ attribute: \"Speed\", action: Multiplication, value: 1.5f }</code></li> <li>durationType: <code>Duration</code> with <code>duration = 5</code></li> <li>resetDurationOnReapplication: true to refresh timer on re-apply</li> <li>effectTags: e.g., <code>[ \"Haste\" ]</code></li> <li> <p>cosmeticEffects: prefab with <code>CosmeticEffectData</code> + <code>CosmeticEffectComponent</code> scripts</p> </li> <li> <p>Apply/remove at runtime:</p> </li> </ol> C#<pre><code>GameObject player = ...;\nAttributeEffect haste = ...; // ScriptableObject reference\nEffectHandle? handle = player.ApplyEffect(haste);\n// ... later ...\nif (handle.HasValue)\n{\n    player.RemoveEffect(handle.Value);\n}\n</code></pre> <ol> <li>Query tags anywhere:</li> </ol> C#<pre><code>if (player.HasTag(\"Stunned\"))\n{\n    // Disable input, play animation, etc.\n}\n</code></pre>"},{"location":"features/effects/effects-system/#understanding-attributes-what-to-model-and-what-to-avoid","title":"Understanding Attributes: What to Model and What to Avoid","text":"<p>Important: Attributes are NOT required! The Effects System works well when used solely for tag-based state management and cosmetic effects.</p>"},{"location":"features/effects/effects-system/#what-makes-a-good-attribute","title":"What Makes a Good Attribute?","text":"<p>Attributes work best for values that are:</p> <ul> <li>Primarily modified by the effects system (buffs, debuffs, equipment)</li> <li>Derived from a base value (MaxHealth, Speed, AttackDamage, Defense)</li> <li>Calculated values where you need to see the result of all modifications</li> </ul>"},{"location":"features/effects/effects-system/#what-makes-a-poor-attribute","title":"What Makes a Poor Attribute?","text":"<p>\u274c DON'T use Attributes for \"current\" values like CurrentHealth, CurrentMana, or CurrentAmmo!</p> <p>Why? These values are frequently modified by multiple systems:</p> <ul> <li>Combat system subtracts health upon damage</li> <li>Healing system adds health</li> <li>Regeneration ticks add health over time</li> <li>Death system resets health to zero</li> <li>Save/load system restores health</li> </ul> <p>The Problem:</p> C#<pre><code>// \u274c BAD: CurrentHealth as an Attribute\npublic class PlayerStats : AttributesComponent\n{\n    public Attribute CurrentHealth = 100f; // DON'T DO THIS!\n    public Attribute MaxHealth = 100f;     // This is fine\n}\n\n// Multiple systems modify CurrentHealth:\nvoid TakeDamage(float damage)\n{\n    // Direct mutation bypasses the effects system\n    playerStats.CurrentHealth.BaseValue -= damage;\n\n    // Problem 1: If an effect was modifying CurrentHealth,\n    //           it still applies! Now calculations are wrong.\n    // Problem 2: If you remove an effect, it may restore\n    //           the ORIGINAL base value, undoing damage taken.\n    // Problem 3: Save/load becomes complicated - do you save\n    //           base or current? What about active modifiers?\n}\n</code></pre> <p>The Solution - Separate Current and Max:</p> C#<pre><code>// \u2705 GOOD: CurrentHealth is a regular field, MaxHealth is an Attribute\npublic class PlayerStats : AttributesComponent\n{\n    // Regular field - modified by combat/healing systems directly\n    private float currentHealth = 100f;\n\n    // Attribute - modified by buffs/effects\n    public Attribute MaxHealth = 100f;\n\n    public float CurrentHealth\n    {\n        get =&gt; currentHealth;\n        set =&gt; currentHealth = Mathf.Clamp(value, 0, MaxHealth.Value);\n    }\n\n    protected override void Awake()\n    {\n        base.Awake();\n        // Initialize current health to max\n        currentHealth = MaxHealth.Value;\n\n        // When max health changes, clamp current health\n        OnAttributeModified += (attributeName, oldVal, newVal) =&gt;\n        {\n            if (attributeName == nameof(MaxHealth))\n            {\n                // If max decreased, ensure current doesn't exceed new max\n                if (currentHealth &gt; newVal)\n                {\n                    currentHealth = newVal;\n                }\n            }\n        };\n    }\n}\n\n// Combat system can now safely modify current health\nvoid TakeDamage(float damage)\n{\n    playerStats.CurrentHealth -= damage; // Simple and correct\n}\n\n// Effects system modifies max health\nvoid ApplyHealthBuff()\n{\n    // MaxHealth \u00d7 1.5 (buffs max, current stays same)\n    player.ApplyEffect(healthBuffEffect);\n}\n</code></pre>"},{"location":"features/effects/effects-system/#attribute-best-practices","title":"Attribute Best Practices","text":"<p>\u2705 DO use Attributes for:</p> <ul> <li>MaxHealth, MaxMana, MaxStamina - caps that buffs modify</li> <li>Speed, MovementSpeed - continuous values modified by effects</li> <li>AttackDamage, Defense, CritChance - combat stats</li> <li>CooldownReduction, CastSpeed - multiplicative modifiers</li> <li>CarryCapacity, JumpHeight - gameplay parameters</li> </ul> <p>\u274c DON'T use Attributes for:</p> <ul> <li>CurrentHealth, CurrentMana - depleting resources with complex mutation</li> <li>Position, Rotation - physics/transform state</li> <li>Inventory count, Currency - discrete counts from multiple sources</li> <li>Quest progress, Level - progression state</li> <li>Input state, UI state - transient application state</li> </ul>"},{"location":"features/effects/effects-system/#why-this-matters","title":"Why This Matters","text":"<p>When you use Attributes for frequently mutated \"current\" values:</p> <ol> <li>State conflicts - The effects system and other systems fight over the value</li> <li>Save/load bugs - Unclear whether to save base value or current value with modifiers</li> <li>Unexpected restorations - Removing an effect may restore old base value, losing damage/healing</li> <li>Performance overhead - Recalculating modifications on every damage tick</li> <li>Complexity - Need to carefully coordinate between effects and direct mutations</li> </ol> <p>The Golden Rule: If a value is modified by systems outside the effects system regularly (combat, regeneration, consumption), it should NOT be an Attribute. Use a regular field instead and let Attributes handle the maximums/limits.</p>"},{"location":"features/effects/effects-system/#using-tags-without-attributes","title":"Using Tags WITHOUT Attributes","text":"<p>Even without any Attributes, the Effects System is useful for tag-based state management and cosmetic effects.</p>"},{"location":"features/effects/effects-system/#when-to-use-tags-without-attributes","title":"When to Use Tags Without Attributes","text":"<p>You should consider tag-only effects when:</p> <ul> <li>Managing categorical states (\"Stunned\", \"Invisible\", \"InDialogue\")</li> <li>Implementing temporary permissions (\"CanDash\", \"CanDoubleJump\")</li> <li>Coordinating system interactions (\"InCombat\", \"InCutscene\")</li> <li>Creating purely visual effects (particles, overlays) with timed lifetimes</li> <li>Building capability systems without numeric modifiers</li> </ul>"},{"location":"features/effects/effects-system/#example-pure-tag-effects","title":"Example: Pure Tag Effects","text":"C#<pre><code>// No AttributesComponent needed!\npublic class StealthCharacter : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect invisibilityEffect;\n    [SerializeField] private AttributeEffect stunnedEffect;\n\n    void Start()\n    {\n        // Apply invisibility for 5 seconds\n        // InvisibilityEffect.asset:\n        //   - durationType: Duration (5 seconds)\n        //   - effectTags: [\"Invisible\", \"Stealthy\"]\n        //   - modifications: (EMPTY - no attributes needed!)\n        //   - cosmeticEffects: shimmer particles\n        this.ApplyEffect(invisibilityEffect);\n    }\n\n    void Update()\n    {\n        // Check tags to gate behavior\n        if (this.HasTag(\"Stunned\"))\n        {\n            // Prevent all actions\n            return;\n        }\n\n        // AI can't detect invisible characters\n        if (!this.HasTag(\"Invisible\"))\n        {\n            BroadcastPosition();\n        }\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#example-tag-lifetimes-for-cosmetics","title":"Example: Tag Lifetimes for Cosmetics","text":"<p>Tags with durations provide automatic cleanup for visual effects:</p> C#<pre><code>// Create a \"ShowDamageIndicator\" effect:\n// DamageIndicator.asset:\n//   - durationType: Duration (1.5 seconds)\n//   - effectTags: [\"DamageIndicator\"]\n//   - modifications: (EMPTY)\n//   - cosmeticEffects: DamageNumbersPrefab\n\npublic class CombatFeedback : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect damageIndicator;\n\n    public void ShowDamage(float amount)\n    {\n        // Apply effect - cosmetic spawns automatically\n        this.ApplyEffect(damageIndicator);\n\n        // After 1.5 seconds, cosmetic is automatically cleaned up\n        // No manual cleanup code needed!\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#benefits-of-tag-only-usage","title":"Benefits of Tag-Only Usage","text":"<ul> <li>\u2705 Simpler setup - No AttributesComponent required</li> <li>\u2705 Automatic cleanup - Duration-based tags clean up themselves</li> <li>\u2705 Reference counting - Multiple sources work naturally</li> <li>\u2705 Cosmetic integration - Visual effects lifecycle managed automatically</li> <li>\u2705 System decoupling - Any system can query tags without dependencies</li> </ul>"},{"location":"features/effects/effects-system/#tag-only-patterns","title":"Tag-Only Patterns","text":"<p>1. Temporary Permissions:</p> C#<pre><code>// PowerUpEffect.asset:\n//   - durationType: Duration (10 seconds)\n//   - effectTags: [\"CanDash\", \"CanDoubleJump\", \"PoweredUp\"]\n//   - modifications: (EMPTY)\n\npublic void GrantPowerUp()\n{\n    player.ApplyEffect(powerUpEffect);\n    // Player now has special abilities for 10 seconds\n}\n</code></pre> <p>2. State Management:</p> C#<pre><code>// DialogueStateEffect.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"InDialogue\", \"InputDisabled\"]\n\nEffectHandle? dialogueHandle = player.ApplyEffect(dialogueState);\n// ... dialogue system runs ...\nplayer.RemoveEffect(dialogueHandle.Value);\n</code></pre> <p>3. Visual-Only Effects:</p> C#<pre><code>// LevelUpEffect.asset:\n//   - durationType: Duration (2 seconds)\n//   - effectTags: [\"LevelingUp\"]\n//   - cosmeticEffects: GlowParticles, LevelUpSound\n\nplayer.ApplyEffect(levelUpEffect);\n// Particles and sound play, then clean up automatically\n</code></pre>"},{"location":"features/effects/effects-system/#cosmetic-effects-complete-guide","title":"Cosmetic Effects - Complete Guide","text":"<p>Cosmetic effects handle the visual and audio presentation of effects. They provide a clean separation between gameplay logic (tags, attributes) and presentation (particles, sounds, UI).</p>"},{"location":"features/effects/effects-system/#architecture-overview","title":"Architecture Overview","text":"<p>Component Hierarchy:</p> Text Only<pre><code>CosmeticEffectData (Container GameObject/Prefab)\n  \u2514\u2500 CosmeticEffectComponent (Base class - abstract)\n       \u2514\u2500 Your custom implementations:\n           - ParticleCosmeticEffect\n           - AudioCosmeticEffect\n           - UICosmeticEffect\n           - AnimationCosmeticEffect\n</code></pre>"},{"location":"features/effects/effects-system/#creating-a-cosmetic-effect","title":"Creating a Cosmetic Effect","text":""},{"location":"features/effects/effects-system/#step-1-create-a-prefab-with-cosmeticeffectdata","title":"Step 1: Create a prefab with CosmeticEffectData","text":"<ol> <li>Create a new GameObject in the scene</li> <li>Add Component \u2192 <code>CosmeticEffectData</code></li> <li>Add your custom cosmetic components (particle systems, audio sources, etc.)</li> <li>Save as prefab</li> <li>Reference this prefab in your <code>AttributeEffect.cosmeticEffects</code> list</li> </ol>"},{"location":"features/effects/effects-system/#step-2-implement-cosmeticeffectcomponent-subclasses","title":"Step 2: Implement CosmeticEffectComponent subclasses","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Tags;\n\npublic class ParticleCosmeticEffect : CosmeticEffectComponent\n{\n    [SerializeField] private ParticleSystem particles;\n\n    // RequiresInstance = true creates a new instance per application\n    // RequiresInstance = false shares one instance across all applications\n    public override bool RequiresInstance =&gt; true;\n\n    // CleansUpSelf = true means you handle destruction yourself\n    // CleansUpSelf = false means EffectHandler destroys the GameObject\n    public override bool CleansUpSelf =&gt; false;\n\n    public override void OnApplyEffect(GameObject target)\n    {\n        base.OnApplyEffect(target);\n\n        // Attach cosmetic to target\n        transform.SetParent(target.transform);\n        transform.localPosition = Vector3.zero;\n\n        // Start visual effect\n        particles.Play();\n    }\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n\n        // Stop particles\n        particles.Stop();\n\n        // If CleansUpSelf = false, GameObject is destroyed automatically\n        // If CleansUpSelf = true, you must handle destruction\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#requiresinstance-shared-vs-instanced","title":"RequiresInstance: Shared vs Instanced","text":"<p>RequiresInstance = false (Shared):</p> <ul> <li>One cosmetic instance is reused for all applications</li> <li>Best for: UI overlays, status icons, shared audio managers</li> <li>Lower memory footprint</li> <li>All targets share the same cosmetic GameObject</li> </ul> C#<pre><code>public class StatusIconCosmetic : CosmeticEffectComponent\n{\n    public override bool RequiresInstance =&gt; false; // SHARED\n\n    [SerializeField] private Image iconImage;\n    private int activeCount = 0;\n\n    public override void OnApplyEffect(GameObject target)\n    {\n        base.OnApplyEffect(target);\n        activeCount++;\n\n        // Show icon if this is first application\n        if (activeCount == 1)\n        {\n            iconImage.enabled = true;\n        }\n    }\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n        activeCount--;\n\n        // Hide icon when no more applications\n        if (activeCount == 0)\n        {\n            iconImage.enabled = false;\n        }\n    }\n}\n</code></pre> <p>RequiresInstance = true (Instanced):</p> <ul> <li>New cosmetic instance created for each application</li> <li>Best for: Particles, per-effect animations, independent visuals</li> <li>Each application has an isolated state</li> <li>Higher memory cost, but full independence</li> </ul> C#<pre><code>public class FireParticleCosmetic : CosmeticEffectComponent\n{\n    public override bool RequiresInstance =&gt; true; // INSTANCED\n\n    [SerializeField] private ParticleSystem fireParticles;\n\n    public override void OnApplyEffect(GameObject target)\n    {\n        base.OnApplyEffect(target);\n\n        // Each instance is independent\n        transform.SetParent(target.transform);\n        transform.localPosition = Vector3.zero;\n        fireParticles.Play();\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#cleansupself-automatic-vs-manual-cleanup","title":"CleansUpSelf: Automatic vs Manual Cleanup","text":"<p>CleansUpSelf = false (Automatic - Default):</p> <ul> <li>EffectHandler destroys the GameObject when an effect is removed</li> <li>Simplest option for most cases</li> <li>Immediate cleanup</li> </ul> C#<pre><code>public class SimpleParticleEffect : CosmeticEffectComponent\n{\n    public override bool CleansUpSelf =&gt; false; // AUTOMATIC\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n        // GameObject destroyed automatically by EffectHandler\n    }\n}\n</code></pre> <p>CleansUpSelf = true (Manual Cleanup):</p> <ul> <li>You are responsible for destroying the GameObject</li> <li>Use when you need delayed cleanup (fade out animations, particle finish)</li> <li>More control over cleanup timing</li> </ul> C#<pre><code>public class FadeOutEffect : CosmeticEffectComponent\n{\n    public override bool CleansUpSelf =&gt; true; // MANUAL\n\n    [SerializeField] private float fadeOutDuration = 1f;\n    private bool isRemoving = false;\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n\n        if (!isRemoving)\n        {\n            isRemoving = true;\n            StartCoroutine(FadeOutAndDestroy());\n        }\n    }\n\n    private IEnumerator FadeOutAndDestroy()\n    {\n        // Fade out over time\n        float elapsed = 0f;\n        SpriteRenderer sprite = GetComponent&lt;SpriteRenderer&gt;();\n        Color originalColor = sprite.color;\n\n        while (elapsed &lt; fadeOutDuration)\n        {\n            elapsed += Time.deltaTime;\n            float alpha = 1f - (elapsed / fadeOutDuration);\n            sprite.color = new Color(originalColor.r, originalColor.g, originalColor.b, alpha);\n            yield return null;\n        }\n\n        // Now safe to destroy\n        Destroy(gameObject);\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#complete-cosmetic-examples","title":"Complete Cosmetic Examples","text":""},{"location":"features/effects/effects-system/#example-1-buff-visual-with-particles-and-sound","title":"Example 1: Buff Visual with Particles and Sound","text":"C#<pre><code>public class BuffCosmetic : CosmeticEffectComponent\n{\n    [SerializeField] private ParticleSystem buffParticles;\n    [SerializeField] private AudioSource audioSource;\n    [SerializeField] private AudioClip applySound;\n    [SerializeField] private AudioClip removeSound;\n\n    public override bool RequiresInstance =&gt; true;\n    public override bool CleansUpSelf =&gt; false;\n\n    public override void OnApplyEffect(GameObject target)\n    {\n        base.OnApplyEffect(target);\n\n        // Position cosmetic on target\n        transform.SetParent(target.transform);\n        transform.localPosition = Vector3.zero;\n\n        // Play effects\n        buffParticles.Play();\n        audioSource.PlayOneShot(applySound);\n    }\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n\n        audioSource.PlayOneShot(removeSound);\n        buffParticles.Stop();\n        // Automatic cleanup after this\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#example-2-status-ui-overlay-shared","title":"Example 2: Status UI Overlay (Shared)","text":"C#<pre><code>public class StatusOverlayCosmetic : CosmeticEffectComponent\n{\n    [SerializeField] private SpriteRenderer overlaySprite;\n    [SerializeField] private Color overlayColor = Color.red;\n\n    public override bool RequiresInstance =&gt; false; // SHARED\n    public override bool CleansUpSelf =&gt; false;\n\n    private SpriteRenderer targetSprite;\n\n    public override void OnApplyEffect(GameObject target)\n    {\n        base.OnApplyEffect(target);\n\n        targetSprite = target.GetComponent&lt;SpriteRenderer&gt;();\n        if (targetSprite != null)\n        {\n            // Tint the sprite\n            targetSprite.color = overlayColor;\n        }\n    }\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n\n        if (targetSprite != null)\n        {\n            // Restore original color\n            targetSprite.color = Color.white;\n        }\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#example-3-animation-trigger","title":"Example 3: Animation Trigger","text":"C#<pre><code>public class AnimationCosmetic : CosmeticEffectComponent\n{\n    [SerializeField] private string applyTrigger = \"BuffApplied\";\n    [SerializeField] private string removeTrigger = \"BuffRemoved\";\n\n    public override bool RequiresInstance =&gt; false;\n\n    public override void OnApplyEffect(GameObject target)\n    {\n        base.OnApplyEffect(target);\n\n        Animator animator = target.GetComponent&lt;Animator&gt;();\n        if (animator != null)\n        {\n            animator.SetTrigger(applyTrigger);\n        }\n    }\n\n    public override void OnRemoveEffect(GameObject target)\n    {\n        base.OnRemoveEffect(target);\n\n        Animator animator = target.GetComponent&lt;Animator&gt;();\n        if (animator != null)\n        {\n            animator.SetTrigger(removeTrigger);\n        }\n    }\n}\n</code></pre>"},{"location":"features/effects/effects-system/#combining-multiple-cosmetics","title":"Combining Multiple Cosmetics","text":"<p>A single effect can have multiple cosmetic components with different behaviors:</p> C#<pre><code>// PoisonEffect prefab:\n//   - CosmeticEffectData\n//   - PoisonParticles (RequiresInstance = true)  // One per stack\n//   - PoisonStatusIcon (RequiresInstance = false) // Shared UI element\n//   - PoisonAudioLoop (RequiresInstance = true)   // One audio loop per stack\n</code></pre>"},{"location":"features/effects/effects-system/#cosmetic-lifecycle","title":"Cosmetic Lifecycle","text":"<p>Application Flow:</p> <ol> <li><code>AttributeEffect</code> applied to GameObject</li> <li><code>EffectHandler</code> checks <code>cosmeticEffects</code> list</li> <li>For each <code>CosmeticEffectData</code>:</li> <li>If <code>RequiresInstancing = true</code>: Instantiate and parent to target</li> <li>If <code>RequiresInstancing = false</code>: Reuse existing instance</li> <li>Call <code>OnApplyEffect(target)</code> on all components</li> <li>Cosmetics remain active while an effect is active</li> </ol> <p>Removal Flow:</p> <ol> <li>Effect expires or is manually removed</li> <li><code>EffectHandler</code> calls <code>OnRemoveEffect(target)</code> on all components</li> <li>For each component:</li> <li>If <code>CleansUpSelf = false</code>: EffectHandler destroys GameObject immediately</li> <li>If <code>CleansUpSelf = true</code>: Component handles its own destruction</li> </ol>"},{"location":"features/effects/effects-system/#best-practices","title":"Best Practices","text":"<p>Performance:</p> <ul> <li>\u2705 Prefer <code>RequiresInstance = false</code> when possible (lower overhead)</li> <li>\u2705 Use object pooling for frequently spawned instanced cosmetics</li> <li>\u2705 Keep <code>OnApplyEffect</code> and <code>OnRemoveEffect</code> lightweight</li> <li>\u274c Avoid expensive operations in these callbacks</li> </ul> <p>Architecture:</p> <ul> <li>\u2705 One responsibility per cosmetic component (particles, audio, UI separate)</li> <li>\u2705 Store references in <code>OnApplyEffect</code>, use them in <code>OnRemoveEffect</code></li> <li>\u2705 Always call <code>base.OnApplyEffect()</code> and <code>base.OnRemoveEffect()</code></li> <li>\u274c Don't access gameplay logic from cosmetics (maintain separation)</li> </ul> <p>Cleanup:</p> <ul> <li>\u2705 Use <code>CleansUpSelf = false</code> unless you need delayed cleanup</li> <li>\u2705 If using <code>CleansUpSelf = true</code>, ensure you always destroy the GameObject</li> <li>\u2705 Handle null targets gracefully (target may be destroyed early)</li> <li>\u274c Don't leak GameObjects by forgetting to clean up</li> </ul>"},{"location":"features/effects/effects-system/#recipes","title":"Recipes","text":""},{"location":"features/effects/effects-system/#1-buff-with-speed-for-5s-refreshable","title":"1) Buff with % Speed for 5s (refreshable)","text":"<ul> <li>Effect: Multiplication <code>Speed *= 1.5f</code>, <code>Duration=5</code>, <code>resetDurationOnReapplication=true</code>, tag <code>Haste</code>.</li> <li>Apply to extend: reapply before expiry to reset the timer.</li> </ul>"},{"location":"features/effects/effects-system/#2-poison-poisoned-tag-10s-with-periodic-damage","title":"2) Poison: \"Poisoned\" tag 10s with periodic damage","text":"<ul> <li>periodicEffects: add a definition with <code>interval = 1s</code>, <code>maxTicks = 10</code>, and an empty <code>modifications</code> array (ticks drive behaviours)</li> <li>behaviors: attach a <code>PoisonDamageBehavior</code> that applies damage during <code>OnPeriodicTick</code> (sample below)</li> <li>durationType: Duration <code>10s</code> (or Infinite if the periodic schedule should drive expiry)</li> <li>effectTags: <code>[ \"Poisoned\" ]</code></li> <li>cosmetics: particles + UI icon</li> <li>Optional: add an immediate modification for on-apply burst damage</li> </ul> C#<pre><code>[CreateAssetMenu(menuName = \"Combat/Effects/Poison Damage\")]\npublic sealed class PoisonDamageBehavior : EffectBehavior\n{\n    [SerializeField]\n    private float damagePerTick = 5f;\n\n    public override void OnPeriodicTick(\n        EffectBehaviorContext context,\n        PeriodicEffectTickContext tickContext\n    )\n    {\n        if (!context.Target.TryGetComponent(out PlayerHealth health))\n        {\n            return;\n        }\n\n        health.ApplyDamage(damagePerTick);\n    }\n}\n</code></pre> <p>Pair this with a health component that owns mutable current-health state instead of modelling <code>CurrentHealth</code> as an Attribute.</p>"},{"location":"features/effects/effects-system/#3-equipment-aura-10-defense-while-equipped","title":"3) Equipment Aura: +10 Defense while equipped","text":"<ul> <li>durationType: Infinite</li> <li>modifications: Addition <code>{ attribute: \"Defense\", value: 10f }</code></li> <li>Apply on equip, store the handle, remove on unequip.</li> </ul>"},{"location":"features/effects/effects-system/#4-oneoff-permanent-bonus","title":"4) One\u2011off Permanent Bonus","text":"<ul> <li>durationType: Instant (returns null)</li> <li>modifications: Addition or Override on base value (no handle; cannot be removed).</li> </ul>"},{"location":"features/effects/effects-system/#5-stacking-multiple-instances","title":"5) Stacking Multiple Instances","text":"<ul> <li>Set <code>stackingMode</code> on the effect asset to control reapplication:</li> <li><code>Stack</code> keeps separate handles (respecting <code>maximumStacks</code>, trimming the oldest when the cap is reached).</li> <li><code>Refresh</code> reuses the first handle; set <code>resetDurationOnReapplication = true</code> if the timer should reset on reapplication.</li> <li><code>Replace</code> removes existing handles in the same group before adding a new one.</li> <li><code>Ignore</code> rejects duplicate applications.</li> <li>Use <code>stackGroup = CustomKey</code> with a shared <code>stackGroupKey</code> when different assets should share a stack identity.</li> <li>Inspect active stacks with <code>EffectHandler.GetEffectStackCount(effect)</code> or tag counts for debugging and UI.</li> </ul>"},{"location":"features/effects/effects-system/#6-shared-vs-instanced-cosmetics","title":"6) Shared vs. Instanced Cosmetics","text":"<ul> <li>In <code>CosmeticEffectData</code>, set a component\u2019s <code>RequiresInstance = true</code> for per\u2011application instances (e.g., particles).</li> <li>Keep <code>RequiresInstance = false</code> for shared presenters (e.g., status icon overlay).</li> </ul>"},{"location":"features/effects/effects-system/#periodic-tick-payloads","title":"Periodic Tick Payloads","text":"<ul> <li>Populate the <code>periodicEffects</code> list on an <code>AttributeEffect</code> to schedule damage/heal-over-time, resource regen, or scripted pulses without external coroutines.</li> <li>Each definition supports <code>initialDelay</code>, <code>interval</code>, and <code>maxTicks</code> (0 = infinite) plus its own <code>AttributeModification</code> bundle applied on every tick.</li> <li>Periodic payloads run only for Duration/Infinite effects; they automatically stop after <code>maxTicks</code> or when the effect handle is removed.</li> <li>Combine multiple definitions for mixed cadences (e.g., fast minor regen + slower burst heals).</li> </ul>"},{"location":"features/effects/effects-system/#effect-behaviours","title":"Effect Behaviours","text":"<ul> <li>Attach <code>EffectBehavior</code> ScriptableObjects to the <code>behaviors</code> list for per-handle runtime logic.</li> <li>The system clones behaviours on application and calls <code>OnApply</code>, <code>OnTick</code> (each frame), <code>OnPeriodicTick</code> (after periodic payloads fire), and <code>OnRemove</code>.</li> <li>Behaviours are ideal for integrating bespoke systems (e.g., camera shakes, AI hooks, quest tracking) while keeping designer-authored effects data-driven.</li> <li>Keep behaviours stateless or store per-handle state on the cloned instance; clean up in <code>OnRemove</code>.</li> </ul>"},{"location":"features/effects/effects-system/#best-practices_1","title":"Best Practices","text":"<ul> <li>Use Addition for flat changes; Multiplication for percentage changes; Override sparingly (wins last).</li> <li>Use the Attribute Metadata Cache generator to power editor dropdowns for <code>attribute</code> names and avoid typos.</li> <li>Centralize tag strings as constants to prevent mistakes and improve refactor safety.</li> <li>Prefer shared cosmetics where feasible; instantiate only when the state must be isolated per application.</li> <li>If reapplication should refresh timers, set <code>resetDurationOnReapplication = true</code> on the effect.</li> </ul>"},{"location":"features/effects/effects-system/#type-safe-effect-references-with-enums","title":"Type-Safe Effect References with Enums","text":"<p>Instead of managing effects through inspector references or Resources.Load calls, consider using an enum-based registry for centralized, type-safe access to all your effects:</p> <p>The Pattern:</p> C#<pre><code>// 1. Define an enum for all your effects\npublic enum EffectType\n{\n    HastePotion,\n    StrengthBuff,\n    PoisonDebuff,\n    ShieldBuff,\n    FireDamageOverTime,\n}\n\n// 2. Create a centralized registry\npublic class EffectRegistry : ScriptableObject\n{\n    [System.Serializable]\n    private class EffectEntry\n    {\n        public EffectType type;\n        public AttributeEffect effect;\n    }\n\n    [SerializeField] private EffectEntry[] effects;\n    private Dictionary&lt;EffectType, AttributeEffect&gt; effectLookup;\n\n    private void OnEnable()\n    {\n        effectLookup = effects.ToDictionary(e =&gt; e.type, e =&gt; e.effect);\n    }\n\n    public AttributeEffect GetEffect(EffectType type)\n    {\n        return effectLookup.TryGetValue(type, out AttributeEffect effect)\n            ? effect\n            : null;\n    }\n}\n\n// 3. Usage - type-safe and refactorable\npublic class PlayerAbilities : MonoBehaviour\n{\n    [SerializeField] private EffectRegistry effectRegistry;\n\n    public void DrinkHastePotion()\n    {\n        // Compiler ensures this effect exists\n        AttributeEffect haste = effectRegistry.GetEffect(EffectType.HastePotion);\n        this.ApplyEffect(haste);\n\n        // Typos are caught at compile time\n        // effectRegistry.GetEffect(EffectType.HastPotoin); // \u274c Won't compile\n    }\n}\n</code></pre> <p>Using DisplayName for Editor-Friendly Names:</p> C#<pre><code>using System.ComponentModel;\n\npublic enum EffectType\n{\n    [Description(\"Haste Potion\")]\n    HastePotion,\n\n    [Description(\"Strength Buff (10s)\")]\n    StrengthBuff,\n\n    [Description(\"Poison DoT\")]\n    PoisonDebuff,\n\n    [Description(\"Shield (+50 Defense)\")]\n    ShieldBuff,\n}\n\n// Custom PropertyDrawer can display Description in inspector\n// Or use Unity's [InspectorName] attribute in Unity 2021.2+:\n// [InspectorName(\"Haste Potion\")] HastePotion,\n</code></pre> <p>Cached Name Pattern for Performance:</p> <p>If you're doing frequent lookups or displaying effect names in UI, cache the enum-to-string mappings:</p> C#<pre><code>public static class EffectTypeExtensions\n{\n    private static readonly Dictionary&lt;EffectType, string&gt; DisplayNames = new()\n    {\n        { EffectType.HastePotion, \"Haste Potion\" },\n        { EffectType.StrengthBuff, \"Strength Buff\" },\n        { EffectType.PoisonDebuff, \"Poison\" },\n        { EffectType.ShieldBuff, \"Shield\" },\n    };\n\n    public static string GetDisplayName(this EffectType type)\n    {\n        return DisplayNames.TryGetValue(type, out string name)\n            ? name\n            : type.ToString();\n    }\n}\n\n// Usage in UI\nvoid UpdateEffectTooltip(EffectType effectType)\n{\n    tooltipText.text = effectType.GetDisplayName();\n    // No allocations, no typos, refactor-safe\n}\n</code></pre> <p>Benefits:</p> <ul> <li>\u2705 Type safety - Compiler catches typos and missing effects</li> <li>\u2705 Refactoring - Rename effects across the entire codebase reliably</li> <li>\u2705 Autocomplete - IDE suggests all available effects</li> <li>\u2705 Performance - Dictionary lookup avoids Resources.Load overhead</li> <li>\u2705 No magic strings - Effect references are code symbols, not brittle strings</li> </ul> <p>Drawbacks:</p> <ul> <li>\u26a0\ufe0f Centralization - All effects must be registered in the enum and registry</li> <li>\u26a0\ufe0f Designer friction - Programmers must add enum entries for new effects</li> <li>\u26a0\ufe0f Scalability - With 100+ effects, enum becomes unwieldy (consider categories)</li> <li>\u26a0\ufe0f Asset decoupling - Effects are tied to code enum, harder to add via mods/DLC</li> </ul> <p>When to Use:</p> <ul> <li>\u2705 Small to medium projects (&lt; 50 effects)</li> <li>\u2705 Programmer-driven effect creation</li> <li>\u2705 Need strong refactoring safety</li> <li>\u2705 Want compile-time validation</li> </ul> <p>When to Avoid:</p> <ul> <li>\u274c Designer-driven workflows (they can't add enum entries)</li> <li>\u274c Modding/DLC systems (effects defined outside codebase)</li> <li>\u274c Very large effect catalogs (enums become bloated)</li> <li>\u274c Rapid prototyping (slows iteration)</li> </ul> <p>Integration with Unity Helpers' Built-in Enum Utilities:</p> <p>This package already includes high-performance <code>EnumDisplayNameAttribute</code> and <code>ToCachedName()</code> extensions (see <code>EnumExtensions.cs:437-478</code>). You can use these for better performance:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\npublic enum EffectType\n{\n    [EnumDisplayName(\"Haste Potion\")]\n    HastePotion,\n\n    [EnumDisplayName(\"Strength Buff (10s)\")]\n    StrengthBuff,\n\n    [EnumDisplayName(\"Poison DoT\")]\n    PoisonDebuff,\n}\n\n// High-performance cached display name (zero allocation after first call)\nvoid UpdateEffectTooltip(EffectType effectType)\n{\n    tooltipText.text = effectType.ToDisplayName(); // Uses EnumDisplayNameCache&lt;T&gt;\n}\n\n// Or use ToCachedName() for the enum's field name without attributes\nvoid LogEffect(EffectType effectType)\n{\n    Debug.Log($\"Applied: {effectType.ToCachedName()}\"); // Uses EnumNameCache&lt;T&gt;\n}\n</code></pre> <p>Performance characteristics:</p> <ul> <li><code>ToDisplayName()</code>: O(1) lookup, zero allocations (array-based for enums \u2264256 values)</li> <li><code>ToCachedName()</code>: O(1) lookup, zero allocations, thread-safe with concurrent dictionary</li> <li>Both use aggressive inlining and avoid boxing</li> </ul> <p>This eliminates the need to manually maintain a <code>DisplayNames</code> dictionary as shown in the earlier example\u2014the package already provides optimized caching infrastructure.</p>"},{"location":"features/effects/effects-system/#api-reference","title":"API Reference","text":""},{"location":"features/effects/effects-system/#attributeeffect-query-methods","title":"AttributeEffect Query Methods","text":"<p>Checking for Tags:</p> C#<pre><code>// Check if effect has a specific tag\nbool hasTag = effect.HasTag(\"Haste\");\n\n// Check if effect has any of the specified tags\nbool hasAny = effect.HasAnyTag(new[] { \"Haste\", \"Speed\", \"Boost\" });\nbool hasAnyFromList = effect.HasAnyTag(myTagList); // IReadOnlyList&lt;string&gt; overload\n</code></pre> <p>Checking for Attribute Modifications:</p> C#<pre><code>// Check if effect modifies a specific attribute\nbool modifiesSpeed = effect.ModifiesAttribute(\"Speed\");\n\n// Get all modifications for a specific attribute\nusing var lease = Buffers&lt;AttributeModification&gt;.List.Get(out List&lt;AttributeModification&gt; mods);\neffect.GetModifications(\"Speed\", mods);\nforeach (AttributeModification mod in mods)\n{\n    Debug.Log($\"Action: {mod.action}, Value: {mod.value}\");\n}\n</code></pre>"},{"location":"features/effects/effects-system/#taghandler-query-methods","title":"TagHandler Query Methods","text":"<p>Basic Tag Queries:</p> C#<pre><code>// Check if a single tag is active\nif (player.HasTag(\"Stunned\"))\n{\n    DisableInput();\n}\n\n// Check if any of the tags are active\nif (player.HasAnyTag(new[] { \"Stunned\", \"Frozen\", \"Sleeping\" }))\n{\n    PreventMovement();\n}\n\n// Check if all tags are active\nif (player.HasAllTags(new[] { \"Wet\", \"Grounded\" }))\n{\n    ApplyElectricShock();\n}\n\n// Check if none of the tags are active\nif (player.HasNoneOfTags(new[] { \"Invulnerable\", \"Untargetable\" }))\n{\n    AllowDamage();\n}\n</code></pre> <p>Tag Count Queries:</p> C#<pre><code>// Get the active count for a tag\nif (player.TryGetTagCount(\"Poisoned\", out int stacks) &amp;&amp; stacks &gt;= 3)\n{\n    TriggerCriticalPoisonWarning();\n}\n\n// Get all active tags\nList&lt;string&gt; activeTags = player.GetActiveTags();\nforeach (string tag in activeTags)\n{\n    Debug.Log($\"Active tag: {tag}\");\n}\n</code></pre> <p>Collection Type Support:</p> <p>All tag query methods support multiple collection types with optimized implementations:</p> <ul> <li><code>IReadOnlyList&lt;string&gt;</code> (optimized with index-based iteration)</li> <li><code>List&lt;string&gt;</code></li> <li><code>HashSet&lt;string&gt;</code></li> <li><code>SortedSet&lt;string&gt;</code></li> <li><code>Queue&lt;string&gt;</code></li> <li><code>Stack&lt;string&gt;</code></li> <li><code>LinkedList&lt;string&gt;</code></li> <li>Any <code>IEnumerable&lt;string&gt;</code></li> </ul> C#<pre><code>// Example with different collection types\nHashSet&lt;string&gt; immunityTags = new() { \"Invulnerable\", \"Immune\" };\nif (player.HasAnyTag(immunityTags))\n{\n    PreventDamage();\n}\n\nList&lt;string&gt; crowdControlTags = new() { \"Stunned\", \"Rooted\", \"Silenced\" };\nif (player.HasNoneOfTags(crowdControlTags))\n{\n    EnableAllAbilities();\n}\n</code></pre>"},{"location":"features/effects/effects-system/#effecthandler-query-methods","title":"EffectHandler Query Methods","text":"<p>Effect State Queries:</p> C#<pre><code>// Check if a specific effect is currently active\nif (effectHandler.IsEffectActive(hasteEffect))\n{\n    ShowHasteIndicator();\n}\n\n// Get the stack count for an effect\nint hasteStacks = effectHandler.GetEffectStackCount(hasteEffect);\nDebug.Log($\"Haste stacks: {hasteStacks}\");\n\n// Get remaining duration for a specific effect instance\nif (effectHandler.TryGetRemainingDuration(effectHandle, out float remaining))\n{\n    UpdateDurationUI(remaining);\n}\n</code></pre> <p>Effect Manipulation:</p> C#<pre><code>// Refresh an effect's duration\nif (effectHandler.RefreshEffect(effectHandle))\n{\n    Debug.Log(\"Effect duration refreshed\");\n}\n\n// Refresh effect ignoring reapplication policy\neffectHandler.RefreshEffect(effectHandle, ignoreReapplicationPolicy: true);\n</code></pre>"},{"location":"features/effects/effects-system/#faq","title":"FAQ","text":"<p>Q: Should I use an Attribute for CurrentHealth?</p> <ul> <li>No! Use Attributes for values primarily modified by the effects system (MaxHealth, Speed, AttackDamage). CurrentHealth is modified by multiple systems (combat, healing, regeneration) and should be a regular field. See \"Understanding Attributes: What to Model and What to Avoid\" section above for details. Mixing direct mutations with effect modifications causes state conflicts and save/load bugs.</li> </ul> <p>Q: Why didn't I get an <code>EffectHandle</code>?</p> <ul> <li>Instant effects modify the base value permanently and do not return a handle (<code>null</code>). Duration/Infinite do.</li> </ul> <p>Q: Do modifications stack across multiple effects?</p> <ul> <li>Yes. Each <code>Attribute</code> applies all active modifications ordered by action: Addition \u2192 Multiplication \u2192 Override.</li> </ul> <p>Q: How do I remove just one instance of an effect?</p> <ul> <li>Keep the <code>EffectHandle</code> returned from <code>ApplyEffect</code> and pass it to <code>RemoveEffect(handle)</code>.</li> </ul> <p>Q: Two systems apply the same tag. Who owns removal?</p> <ul> <li>The tag is reference\u2011counted. Each application increments the count; removal decrements it. The tag is removed when the count reaches 0.</li> </ul> <p>Q: When should I use tags vs. checking stats?</p> <ul> <li>Use tags to represent categorical states (e.g., Stunned/Poisoned/Invulnerable) independent of numeric values. Check stats for numeric thresholds or calculations.</li> </ul> <p>Q: How do I check if an effect modifies a specific attribute?</p> <ul> <li>Use <code>effect.ModifiesAttribute(\"AttributeName\")</code> to check if an effect contains modifications for a specific attribute, or <code>effect.GetModifications(\"AttributeName\", buffer)</code> to retrieve all modifications for that attribute.</li> </ul> <p>Q: How do I query tag counts or check multiple tags at once?</p> <ul> <li>Use <code>TryGetTagCount(tag, out int count)</code> to get the active count for a tag, <code>HasAllTags(tags)</code> to check if all tags are active, or <code>HasNoneOfTags(tags)</code> to check if none are active.</li> </ul>"},{"location":"features/effects/effects-system/#troubleshooting","title":"Troubleshooting","text":"<ul> <li>Attribute name doesn\u2019t apply</li> <li>Ensure the <code>attribute</code> field matches a public/private <code>Attribute</code> field name on an <code>AttributesComponent</code> subclass.</li> <li> <p>Regenerate the Attribute Metadata Cache to update editor dropdowns.</p> </li> <li> <p>Effect didn\u2019t clean up cosmetics</p> </li> <li> <p>Confirm <code>RequiresInstance</code> is set correctly and components either clean up themselves (<code>CleansUpSelf</code>) or are destroyed by <code>EffectHandler</code>.</p> </li> <li> <p>Duration didn\u2019t refresh on reapplication</p> </li> <li>Set <code>resetDurationOnReapplication = true</code> on the <code>AttributeEffect</code>.</li> </ul>"},{"location":"features/effects/effects-system/#advanced-scenarios-beyond-buffs-and-debuffs","title":"Advanced Scenarios: Beyond Buffs and Debuffs","text":"<p>While the Effects System handles traditional buff/debuff mechanics well, it can also be used to build robust capability systems that drive complex gameplay decisions across your entire codebase. This section explores advanced patterns that use tags extensively for architectural purposes.</p>"},{"location":"features/effects/effects-system/#understanding-the-capability-pattern","title":"Understanding the Capability Pattern","text":"<p>The Problem with Flags:</p> <p>Many developers start with hardcoded boolean flags:</p> C#<pre><code>// \u274c OLD WAY: Scattered boolean flags\npublic class PlayerController : MonoBehaviour\n{\n    public bool isInvulnerable;\n    public bool canDash;\n    public bool hasDoubleJump;\n    public bool isInvisible;\n    // 50+ booleans later...\n\n    void TakeDamage(float damage)\n    {\n        if (isInvulnerable) return;\n        // ...\n    }\n\n    void Update()\n    {\n        if (Input.GetKeyDown(KeyCode.Space) &amp;&amp; canDash)\n            Dash();\n    }\n}\n\n// Problems:\n// 1. Every system needs direct references to check flags\n// 2. Adding temporary effects requires custom timers\n// 3. Multiple sources granting same capability = conflicts\n// 4. No centralized place to see what capabilities exist\n// 5. Difficult to debug \"why can't I do X?\"\n</code></pre> <p>The Solution - Tag-Based Capabilities:</p> C#<pre><code>// \u2705 NEW WAY: Tag-based capability system\npublic class PlayerController : MonoBehaviour\n{\n    void TakeDamage(float damage)\n    {\n        // Any system can grant \"Invulnerable\" tag\n        if (this.HasTag(\"Invulnerable\")) return;\n        // ...\n    }\n\n    void Update()\n    {\n        // Check capability before allowing action\n        if (Input.GetKeyDown(KeyCode.Space) &amp;&amp; this.HasTag(\"CanDash\"))\n            Dash();\n    }\n}\n\n// Benefits:\n// 1. Decoupled - systems query tags, don't need direct references\n// 2. Multiple sources work automatically (reference-counted)\n// 3. Temporary effects are free - just apply/remove tag\n// 4. Debuggable - inspect TagHandler to see all active tags\n// 5. Designer-friendly - add capabilities via ScriptableObjects\n</code></pre>"},{"location":"features/effects/effects-system/#when-to-use-this-pattern","title":"When to Use This Pattern","text":"<p>\u2705 Well-suited for:</p> <ul> <li>State management - \"Stunned\", \"Invisible\", \"Invulnerable\", \"Flying\"</li> <li>Capability gating - \"CanDash\", \"CanDoubleJump\", \"CanCastSpells\"</li> <li>System coordination - \"InCombat\", \"InCutscene\", \"InDialogue\"</li> <li>Permission systems - \"HasQuestItem\", \"UnlockedArea\", \"CompletedTutorial\"</li> <li>AI behavior - \"Aggressive\", \"Fleeing\", \"Alerted\", \"Patrolling\"</li> <li>Complex gameplay - \"Burning\", \"Wet\", \"Electrified\" (element interactions)</li> </ul> <p>\u274c Not ideal for:</p> <ul> <li>Simple one-off checks - If you only check in one place, a boolean is fine</li> <li>Continuous numeric values - Use Attributes for health, speed, damage</li> <li>Performance-critical inner loops - Cache tag checks outside hot paths</li> </ul>"},{"location":"features/effects/effects-system/#pattern-1-invulnerability-system","title":"Pattern 1: Invulnerability System","text":"<p>The Problem: Many different sources need to grant invulnerability (power-ups, cutscenes, dash moves, debug mode). Without tags, you need complex logic to track all sources.</p> <p>The Solution:</p> C#<pre><code>// === Setup (done once by programmer) ===\n\n// 1. Create invulnerability effects as ScriptableObjects\n// DashInvulnerability.asset:\n//   - durationType: Duration (0.3 seconds)\n//   - effectTags: [\"Invulnerable\", \"Dashing\"]\n//   - cosmeticEffects: flash sprite white\n\n// PowerStarInvulnerability.asset:\n//   - durationType: Duration (10 seconds)\n//   - effectTags: [\"Invulnerable\", \"PowerStar\"]\n//   - cosmeticEffects: rainbow sparkles + music\n\n// DebugInvulnerability.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"Invulnerable\", \"Debug\"]\n//   - cosmeticEffects: debug overlay\n\n// === Usage (everywhere in codebase) ===\n\n// Combat system\npublic class CombatSystem : MonoBehaviour\n{\n    public void TakeDamage(GameObject target, float damage)\n    {\n        // One simple check - doesn't care WHY they're invulnerable\n        if (target.HasTag(\"Invulnerable\"))\n        {\n            Debug.Log(\"Target is invulnerable!\");\n            return;\n        }\n\n        // Apply damage...\n    }\n}\n\n// Player dash ability\npublic class DashAbility : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect dashInvulnerability;\n\n    public void Dash()\n    {\n        // Grant 0.3s of invulnerability during dash\n        this.ApplyEffect(dashInvulnerability);\n        // Automatically removed after 0.3s\n    }\n}\n\n// Debug menu\npublic class DebugMenu : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect debugInvulnerability;\n    private EffectHandle? debugHandle;\n\n    public void ToggleInvulnerability()\n    {\n        if (debugHandle.HasValue)\n        {\n            player.RemoveEffect(debugHandle.Value);\n            debugHandle = null;\n        }\n        else\n        {\n            debugHandle = player.ApplyEffect(debugInvulnerability);\n        }\n    }\n}\n\n// Cutscene controller\npublic class CutsceneController : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect cutsceneInvulnerability;\n    private EffectHandle? cutsceneHandle;\n\n    void StartCutscene()\n    {\n        // Prevent player from taking damage during cutscenes\n        cutsceneHandle = player.ApplyEffect(cutsceneInvulnerability);\n    }\n\n    void EndCutscene()\n    {\n        if (cutsceneHandle.HasValue)\n            player.RemoveEffect(cutsceneHandle.Value);\n    }\n}\n\n// AI system\npublic class EnemyAI : MonoBehaviour\n{\n    void ChooseTarget()\n    {\n        // Don't waste time attacking invulnerable targets\n        List&lt;GameObject&gt; validTargets = allTargets\n            .Where(t =&gt; !t.HasTag(\"Invulnerable\"))\n            .ToList();\n\n        // Attack closest valid target...\n    }\n}\n</code></pre> <p>Why This Works:</p> <ul> <li>\u2705 Multiple sources - Dash, power-ups, debug mode all grant invulnerability independently</li> <li>\u2705 Reference-counted - All sources must end before invulnerability is removed</li> <li>\u2705 Decoupled - The combat system doesn't know about dash, debug, or cutscene systems</li> <li>\u2705 Designer-friendly - Create new invulnerability sources without code changes</li> <li>\u2705 Debuggable - Inspect TagHandler to see exactly why someone is invulnerable</li> </ul> <p>Common Pitfall to Avoid:</p> C#<pre><code>// \u274c DON'T: Check multiple specific tags\nif (target.HasTag(\"DashInvulnerable\") ||\n    target.HasTag(\"PowerStarInvulnerable\") ||\n    target.HasTag(\"DebugInvulnerable\"))\n{\n    // Now you need to update this everywhere you add a new invulnerability source!\n}\n\n// \u2705 DO: Check one general capability tag\nif (target.HasTag(\"Invulnerable\"))\n{\n    // Works with all current and future invulnerability sources\n}\n</code></pre>"},{"location":"features/effects/effects-system/#pattern-2-complex-ai-decision-making","title":"Pattern 2: Complex AI Decision-Making","text":"<p>The Problem: AI needs to make decisions based on complex state (player stealth, environmental conditions, buffs, etc.). Without a unified system, you end up with brittle if/else chains.</p> <p>The Solution:</p> C#<pre><code>// === Setup effects that grant capability tags ===\n\n// Stealth.asset:\n//   - effectTags: [\"Invisible\", \"Stealthy\"]\n//   - modifications: (none - just tags)\n\n// InWater.asset:\n//   - effectTags: [\"Wet\", \"Swimming\"]\n//   - modifications: Speed \u00d7 0.5\n\n// OnFire.asset:\n//   - effectTags: [\"Burning\", \"OnFire\"]\n//   - modifications: Health + (-5 per second)\n\n// === AI uses tags to make robust decisions ===\n\npublic class EnemyAI : MonoBehaviour\n{\n    public void UpdateAI()\n    {\n        GameObject player = FindPlayer();\n\n        // 1. Visibility checks\n        if (player.HasTag(\"Invisible\"))\n        {\n            // Can't see invisible targets - use last known position\n            PatrolToLastKnownPosition();\n            return;\n        }\n\n        // 2. Threat assessment\n        if (player.HasTag(\"Invulnerable\") &amp;&amp; player.HasTag(\"PowerStar\"))\n        {\n            // Player is powered up - flee!\n            Flee(player.transform.position);\n            return;\n        }\n\n        // 3. Environmental awareness\n        if (this.HasTag(\"Burning\"))\n        {\n            // On fire - prioritize finding water\n            GameObject water = FindNearestWater();\n            if (water != null)\n            {\n                MoveTowards(water.transform.position);\n                return;\n            }\n        }\n\n        // 4. Tactical decisions\n        if (player.HasTag(\"Stunned\") || player.HasTag(\"Slowed\"))\n        {\n            // Player is vulnerable - aggressive pursuit\n            AggressiveAttack(player);\n            return;\n        }\n\n        // 5. Element interactions\n        if (this.HasTag(\"Wet\") &amp;&amp; player.HasTag(\"ElectricWeapon\"))\n        {\n            // We're wet and player has electric weapon - dangerous!\n            MaintainDistance(player, minDistance: 10f);\n            return;\n        }\n\n        // Default behavior\n        ChaseAndAttack(player);\n    }\n\n    // Helper: Check multiple conditions easily\n    bool CanEngageInCombat()\n    {\n        // Can't fight if we're stunned, fleeing, or in a cutscene\n        return !this.HasTag(\"Stunned\") &amp;&amp;\n               !this.HasTag(\"Fleeing\") &amp;&amp;\n               !this.HasTag(\"InCutscene\");\n    }\n}\n</code></pre> <p>Why This Works:</p> <ul> <li>\u2705 Readable - AI logic is self-documenting (\"if player is invisible\")</li> <li>\u2705 Extensible - Add new capabilities without modifying AI code</li> <li>\u2705 Composable - Combine multiple tags for complex conditions</li> <li>\u2705 Testable - Apply tags in tests to verify AI behavior</li> <li>\u2705 Designer-friendly - Designers can create new effects that AI automatically responds to</li> </ul>"},{"location":"features/effects/effects-system/#pattern-3-permission-and-unlock-systems","title":"Pattern 3: Permission and Unlock Systems","text":"<p>The Problem: Games have many gated systems (abilities, areas, features). Tracking all unlockables with individual booleans becomes unwieldy.</p> <p>The Solution:</p> C#<pre><code>// === Setup unlock effects ===\n\n// UnlockDoubleJump.asset:\n//   - durationType: Infinite (permanent unlock)\n//   - effectTags: [\"CanDoubleJump\", \"HasUpgrade\"]\n\n// QuestKeyItem.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"HasKeyItem\", \"CanEnterDungeon\"]\n\n// TutorialComplete.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"TutorialComplete\", \"CanAccessMultiplayer\"]\n\n// === Usage throughout game systems ===\n\n// Ability system\npublic class PlayerAbilities : MonoBehaviour\n{\n    void Update()\n    {\n        // Jump\n        if (Input.GetKeyDown(KeyCode.Space))\n        {\n            if (isGrounded)\n            {\n                Jump();\n            }\n            // Double jump only works if unlocked\n            else if (this.HasTag(\"CanDoubleJump\") &amp;&amp; !hasUsedDoubleJump)\n            {\n                Jump();\n                hasUsedDoubleJump = true;\n            }\n        }\n\n        // Dash\n        if (Input.GetKeyDown(KeyCode.LeftShift))\n        {\n            if (this.HasTag(\"CanDash\"))\n            {\n                Dash();\n            }\n            else\n            {\n                ShowMessage(\"Unlock dash ability first!\");\n            }\n        }\n    }\n}\n\n// Level gate\npublic class DungeonGate : MonoBehaviour\n{\n    void OnTriggerEnter2D(Collider2D other)\n    {\n        GameObject player = other.gameObject;\n\n        if (player.HasTag(\"HasKeyItem\"))\n        {\n            // Has the key - open gate\n            OpenGate();\n        }\n        else\n        {\n            // Missing key - show hint\n            ShowMessage(\"You need the Ancient Key to enter.\");\n        }\n    }\n}\n\n// UI system\npublic class MainMenuUI : MonoBehaviour\n{\n    [SerializeField] private Button multiplayerButton;\n\n    void Update()\n    {\n        // Disable multiplayer until tutorial is complete\n        multiplayerButton.interactable = player.HasTag(\"TutorialComplete\");\n    }\n}\n\n// Save system\npublic class SaveSystem : MonoBehaviour\n{\n    public SaveData CreateSaveData(GameObject player)\n    {\n        // Save all permanent unlocks\n        var saveData = new SaveData\n        {\n            unlockedAbilities = new List&lt;string&gt;()\n        };\n\n        // Check all capability tags\n        if (player.HasTag(\"CanDoubleJump\"))\n            saveData.unlockedAbilities.Add(\"DoubleJump\");\n\n        if (player.HasTag(\"CanDash\"))\n            saveData.unlockedAbilities.Add(\"Dash\");\n\n        if (player.HasTag(\"HasKeyItem\"))\n            saveData.unlockedAbilities.Add(\"KeyItem\");\n\n        return saveData;\n    }\n\n    public void LoadSaveData(GameObject player, SaveData saveData)\n    {\n        // Reapply permanent unlocks\n        foreach (string ability in saveData.unlockedAbilities)\n        {\n            AttributeEffect unlock = LoadUnlockEffect(ability);\n            player.ApplyEffect(unlock);\n        }\n    }\n}\n</code></pre> <p>Why This Works:</p> <ul> <li>\u2705 Persistent - Infinite duration effects work like permanent flags</li> <li>\u2705 Serializable - Easy to save/load by checking tags</li> <li>\u2705 Discoverable - Inspect TagHandler to see all unlockables</li> <li>\u2705 No hardcoded strings - Create unlock effects as assets</li> <li>\u2705 Prevents duplication - Reference-counting handles multiple unlock sources</li> </ul>"},{"location":"features/effects/effects-system/#pattern-4-elemental-interaction-systems","title":"Pattern 4: Elemental Interaction Systems","text":"<p>The Problem: Complex element systems (wet + electric = shock, burning + ice = extinguish) require tracking multiple states and their interactions.</p> <p>The Solution:</p> C#<pre><code>// === Setup element effects ===\n\n// Wet.asset:\n//   - durationType: Duration (10 seconds)\n//   - effectTags: [\"Wet\", \"ConductsElectricity\"]\n//   - cosmeticEffects: water drips\n\n// Burning.asset:\n//   - durationType: Duration (5 seconds)\n//   - effectTags: [\"Burning\", \"OnFire\"]\n//   - modifications: Health + (-5 per second)\n//   - cosmeticEffects: fire particles\n\n// Frozen.asset:\n//   - durationType: Duration (3 seconds)\n//   - effectTags: [\"Frozen\", \"Immobilized\"]\n//   - modifications: Speed \u00d7 0\n\n// Electrified.asset:\n//   - durationType: Duration (4 seconds)\n//   - effectTags: [\"Electrified\", \"Stunned\"]\n//   - modifications: Speed \u00d7 0\n\n// === Interaction system ===\n\npublic class ElementalInteractions : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect wetEffect;\n    [SerializeField] private AttributeEffect burningEffect;\n    [SerializeField] private AttributeEffect frozenEffect;\n    [SerializeField] private AttributeEffect electrifiedEffect;\n\n    public void OnEnvironmentalEffect(GameObject target, string effectType)\n    {\n        switch (effectType)\n        {\n            case \"Water\":\n                // Apply wet\n                target.ApplyEffect(wetEffect);\n\n                // Water puts out fire\n                if (target.HasTag(\"Burning\"))\n                {\n                    target.RemoveEffects(target.GetHandlesWithTag(\"Burning\"));\n                    CreateSteamParticles(target.transform.position);\n                }\n                break;\n\n            case \"Fire\":\n                // Fire dries wet targets\n                if (target.HasTag(\"Wet\"))\n                {\n                    target.RemoveEffects(target.GetHandlesWithTag(\"Wet\"));\n                    CreateSteamParticles(target.transform.position);\n                }\n                else\n                {\n                    // Set on fire if dry\n                    target.ApplyEffect(burningEffect);\n                }\n                break;\n\n            case \"Ice\":\n                // Ice freezes wet targets (stronger effect)\n                if (target.HasTag(\"Wet\"))\n                {\n                    target.ApplyEffect(frozenEffect);\n                    target.RemoveEffects(target.GetHandlesWithTag(\"Wet\"));\n                }\n                break;\n\n            case \"Electric\":\n                // Electric shocks wet targets\n                if (target.HasTag(\"Wet\"))\n                {\n                    // Extra damage and stun\n                    target.ApplyEffect(electrifiedEffect);\n                    target.TakeDamage(20f); // Bonus damage\n                    CreateElectricParticles(target.transform.position);\n                }\n                break;\n        }\n    }\n\n    public float CalculateElementalDamageMultiplier(GameObject attacker, GameObject target)\n    {\n        float multiplier = 1f;\n\n        // Fire does extra damage to frozen targets (they thaw)\n        if (attacker.HasTag(\"FireWeapon\") &amp;&amp; target.HasTag(\"Frozen\"))\n            multiplier *= 1.5f;\n\n        // Electric does massive damage to wet targets\n        if (attacker.HasTag(\"ElectricWeapon\") &amp;&amp; target.HasTag(\"Wet\"))\n            multiplier *= 2.0f;\n\n        // Ice does extra damage to burning targets (extinguish)\n        if (attacker.HasTag(\"IceWeapon\") &amp;&amp; target.HasTag(\"Burning\"))\n            multiplier *= 1.5f;\n\n        return multiplier;\n    }\n}\n</code></pre> <p>Why This Works:</p> <ul> <li>\u2705 Composable - Elements interact naturally through tags</li> <li>\u2705 Discoverable - All active elements visible in TagHandler</li> <li>\u2705 Designer-friendly - Create new elements without code changes</li> <li>\u2705 Debuggable - See the exact element state at any moment</li> <li>\u2705 Extensible - Add new elements and interactions easily</li> </ul>"},{"location":"features/effects/effects-system/#pattern-5-state-machine-replacement","title":"Pattern 5: State Machine Replacement","text":"<p>The Problem: Traditional state machines become complex with many states and transitions. Tags can represent state more flexibly.</p> <p>Traditional Approach:</p> C#<pre><code>// \u274c OLD WAY: Rigid state machine\npublic enum PlayerState\n{\n    Idle,\n    Walking,\n    Running,\n    Jumping,\n    Attacking,\n    Stunned,\n    // What if player is jumping AND attacking?\n    // What if player is attacking AND stunned?\n    // Need combinatorial explosion of states!\n}\n\nprivate PlayerState currentState;\n\nvoid Update()\n{\n    switch (currentState)\n    {\n        case PlayerState.Stunned:\n            // Can't do anything when stunned\n            return;\n\n        case PlayerState.Attacking:\n            // Can't move while attacking\n            // But what if we want to allow movement during some attacks?\n            break;\n\n        // 50 more cases...\n    }\n}\n</code></pre> <p>Tag-Based Approach:</p> C#<pre><code>// \u2705 NEW WAY: Flexible tag-based state\nvoid Update()\n{\n    // States can overlap naturally\n    bool isGrounded = CheckGrounded();\n    bool isMoving = Input.GetAxis(\"Horizontal\") != 0;\n\n    // Check capabilities, not rigid states\n    if (this.HasTag(\"Stunned\") || this.HasTag(\"Frozen\"))\n    {\n        // Can't act while crowd-controlled\n        return;\n    }\n\n    // Movement\n    if (isMoving &amp;&amp; !this.HasTag(\"Immobilized\"))\n    {\n        Move();\n\n        // Can attack while moving (if not attacking already)\n        if (Input.GetButtonDown(\"Fire1\") &amp;&amp; !this.HasTag(\"Attacking\"))\n        {\n            Attack();\n        }\n    }\n\n    // Jumping\n    if (Input.GetButtonDown(\"Jump\") &amp;&amp; isGrounded)\n    {\n        if (this.HasTag(\"CanJump\") &amp;&amp; !this.HasTag(\"Jumping\"))\n        {\n            Jump();\n        }\n    }\n\n    // Special abilities\n    if (Input.GetButtonDown(\"Dash\"))\n    {\n        if (this.HasTag(\"CanDash\") &amp;&amp; !this.HasTag(\"Dashing\"))\n        {\n            Dash();\n        }\n    }\n}\n\n// Actions apply tags to themselves\nvoid Attack()\n{\n    // Apply \"Attacking\" tag for duration of attack\n    this.ApplyEffect(attackingEffect); // 0.5s duration\n    // Play animation...\n}\n\nvoid Dash()\n{\n    // Apply multiple tags during dash\n    this.ApplyEffect(dashingEffect);\n    // Effect grants: [\"Dashing\", \"Invulnerable\", \"FastMovement\"]\n    // All removed automatically after duration\n}\n</code></pre> <p>Why This Works:</p> <ul> <li>\u2705 Composable - Multiple states can be active simultaneously</li> <li>\u2705 Flexible - Easy to add conditional behavior based on tags</li> <li>\u2705 No spaghetti - Avoid complex state transition logic</li> <li>\u2705 Self-documenting - Tag names describe what's happening</li> <li>\u2705 Designer-friendly - Add new states via ScriptableObjects</li> </ul>"},{"location":"features/effects/effects-system/#pattern-6-debugging-and-cheat-codes","title":"Pattern 6: Debugging and Cheat Codes","text":"<p>The Problem: Debug tools and cheat codes need to temporarily grant capabilities without affecting production code.</p> <p>The Solution:</p> C#<pre><code>public class DebugConsole : MonoBehaviour\n{\n    [SerializeField] private AttributeEffect godModeEffect;\n    [SerializeField] private AttributeEffect noclipEffect;\n    [SerializeField] private AttributeEffect unlockAllEffect;\n\n    private Dictionary&lt;string, EffectHandle?&gt; activeDebugEffects = new();\n\n    void Update()\n    {\n        // God mode (invulnerable + infinite resources)\n        if (Input.GetKeyDown(KeyCode.F1))\n        {\n            ToggleDebugEffect(\"GodMode\", godModeEffect);\n        }\n\n        // Noclip (fly through walls)\n        if (Input.GetKeyDown(KeyCode.F2))\n        {\n            ToggleDebugEffect(\"Noclip\", noclipEffect);\n        }\n\n        // Unlock all abilities\n        if (Input.GetKeyDown(KeyCode.F3))\n        {\n            ApplyDebugEffect(\"UnlockAll\", unlockAllEffect);\n        }\n    }\n\n    void ToggleDebugEffect(string name, AttributeEffect effect)\n    {\n        if (activeDebugEffects.TryGetValue(name, out EffectHandle? handle) &amp;&amp; handle.HasValue)\n        {\n            player.RemoveEffect(handle.Value);\n            activeDebugEffects.Remove(name);\n            Debug.Log($\"Debug: {name} OFF\");\n        }\n        else\n        {\n            EffectHandle? newHandle = player.ApplyEffect(effect);\n            activeDebugEffects[name] = newHandle;\n            Debug.Log($\"Debug: {name} ON\");\n        }\n    }\n\n    void ApplyDebugEffect(string name, AttributeEffect effect)\n    {\n        player.ApplyEffect(effect);\n        Debug.Log($\"Debug: Applied {name}\");\n    }\n}\n\n// GodMode.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"Invulnerable\", \"InfiniteResources\", \"Debug\"]\n//   - modifications: Health \u00d7 999, Stamina \u00d7 999\n\n// Noclip.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"Noclip\", \"Flying\", \"Debug\"]\n//   - cosmeticEffects: ghost transparency\n\n// UnlockAll.asset:\n//   - durationType: Infinite\n//   - effectTags: [\"CanDoubleJump\", \"CanDash\", \"CanWallJump\", \"Debug\"]\n</code></pre> <p>Why This Works:</p> <ul> <li>\u2705 Non-invasive - Debug code doesn't pollute production code</li> <li>\u2705 Discoverable - Inspect TagHandler to see active debug effects</li> <li>\u2705 Reusable - Same effects can be used by actual gameplay</li> <li>\u2705 Safe - Easy to ensure debug effects don't ship (check for \"Debug\" tag)</li> </ul>"},{"location":"features/effects/effects-system/#comparison-to-other-approaches","title":"Comparison to Other Approaches","text":"Approach Pros Cons Boolean Flags Simple, fast Not composable, hard to debug, scattered Enums Type-safe, clear options Rigid, can't combine states Bitflags Combinable, fast Limited to 64 states, not designer-friendly State Machines Structured, predictable Complex with many states, rigid transitions Tag System (this!) Flexible, composable, designer-friendly Slightly slower than booleans, strings less type-safe <p>When to Use Tags vs Attributes:</p> Use Case Solution Example Binary state Tag \"Invulnerable\", \"CanDash\" Numeric value Attribute Health, Speed, Damage Temporary state Tag with Duration \"Stunned\" for 3 seconds Stacking bonuses Attribute with Multiplication Speed \u00d7 1.5 from multiple haste effects Category membership Tag \"Enemy\", \"Friendly\", \"Neutral\" Resource management Attribute Stamina, Mana Permission/unlock Tag with Infinite duration \"CanEnterDungeon\", \"TutorialComplete\" Complex interactions Multiple Tags \"Wet\" + \"Electrified\" = shocked"},{"location":"features/effects/effects-system/#best-practices-for-capability-systems","title":"Best Practices for Capability Systems","text":"<ol> <li>Namespace your tags - Use prefixes to avoid conflicts</li> </ol> C#<pre><code>// \u2705 Good: Clear categories\n\"Status_Stunned\"\n\"Ability_CanDash\"\n\"Quest_HasKeyItem\"\n\"Element_Burning\"\n\n// \u274c Bad: Ambiguous\n\"Stunned\"  // Status or ability?\n\"Fire\"     // On fire or has fire weapon?\n</code></pre> <ol> <li>Create tag constants - Avoid string typos</li> </ol> C#<pre><code>public static class GameplayTags\n{\n    // States\n    public const string Invulnerable = \"Invulnerable\";\n    public const string Stunned = \"Stunned\";\n    public const string Invisible = \"Invisible\";\n\n    // Capabilities\n    public const string CanDash = \"CanDash\";\n    public const string CanDoubleJump = \"CanDoubleJump\";\n\n    // Elements\n    public const string Burning = \"Burning\";\n    public const string Wet = \"Wet\";\n    public const string Frozen = \"Frozen\";\n}\n\n// Usage\nif (player.HasTag(GameplayTags.Invulnerable))\n{\n    // Compiler will catch typos!\n}\n</code></pre> <ol> <li>Document tag meanings - Keep a central registry</li> </ol> C#<pre><code>/// Tags Registry\n/// ===================================\n/// Invulnerable - Cannot take damage from any source\n/// Stunned - Cannot perform any actions (move, attack, cast)\n/// InCombat - Currently engaged in combat (prevents resting, saving)\n/// Invisible - Cannot be seen by AI or targeted\n/// CanDash - Has unlocked dash ability\n/// CanDoubleJump - Has unlocked double jump ability\n/// Wet - Conducts electricity, prevents fire, can be frozen\n/// Burning - Takes fire damage over time, can ignite others\n</code></pre> <ol> <li>Use effect tags for multiple purposes</li> </ol> C#<pre><code>// effectTags serve multiple purposes:\n// - Internal organization (removable via GetHandlesWithTag + RemoveEffects)\n// - Gameplay queries (checked via HasTag)\n// - Effect identification and categorization\n\n// Example effect:\n// HastePotion.asset:\n//   - effectTags: [\"Haste\", \"Potion\", \"Buff\", \"MovementBuff\"]\n//   - Use \"Haste\" for gameplay queries (player.HasTag(\"Haste\"))\n//   - Use \"Potion\" for finding/removing all potions\n//   - Use \"Buff\" for UI categorization\n</code></pre> <ol> <li>Test tag combinations - Verify interactions work correctly</li> </ol> C#<pre><code>[Test]\npublic void TestInvulnerability_MultipleSourcesStack()\n{\n    GameObject player = CreateTestPlayer();\n\n    // Apply invulnerability from two sources\n    EffectHandle? dash = player.ApplyEffect(dashInvulnerability);\n    EffectHandle? powerup = player.ApplyEffect(powerupInvulnerability);\n\n    Assert.IsTrue(player.HasTag(\"Invulnerable\"));\n\n    // Remove one source - should still be invulnerable\n    player.RemoveEffect(dash.Value);\n    Assert.IsTrue(player.HasTag(\"Invulnerable\"));\n\n    // Remove second source - now vulnerable\n    player.RemoveEffect(powerup.Value);\n    Assert.IsFalse(player.HasTag(\"Invulnerable\"));\n}\n</code></pre>"},{"location":"features/effects/effects-system/#performance-notes","title":"Performance Notes","text":"<ul> <li>Attribute field discovery is cached (and can be precomputed by the Attribute Metadata Cache generator).</li> <li>Tag queries provide overloads for lists to minimize allocations; prefer <code>IReadOnlyList&lt;string&gt;</code> overloads in hot paths.</li> <li>Cosmetics can be a significant cost; prefer shared presenters when possible.</li> </ul> <p>Related:</p> <ul> <li>README section: \"Effects, Attributes, and Tags\"</li> <li>Attribute Metadata Cache (Editor Tools) for dropdowns and performance</li> </ul>"},{"location":"features/inspector/inspector-button/","title":"Inspector Buttons (WButton)","text":"<p>Execute methods from the inspector with one click.</p> <p>The <code>[WButton]</code> attribute exposes methods as clickable buttons in the Unity inspector, complete with result history, async support, cancellation, custom styling, and automatic grouping. Test gameplay features, debug systems, and prototype rapidly without writing custom editors.</p>"},{"location":"features/inspector/inspector-button/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Basic Usage</li> <li>Parameters</li> <li>colorKey</li> <li>groupPriority</li> <li>groupPlacement</li> <li>Execution Types</li> <li>Result History</li> <li>Draw Order &amp; Positioning</li> <li>Grouping</li> <li>Color Theming</li> <li>Configuration</li> <li>Best Practices</li> <li>Examples</li> <li>Using WButton with Custom Editors</li> <li>Troubleshooting</li> </ul>"},{"location":"features/inspector/inspector-button/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class PlayerController : MonoBehaviour\n{\n    public int health = 100;\n\n    [WButton(\"Heal Player\")]\n    private void Heal()\n    {\n        health = 100;\n        Debug.Log(\"Player healed!\");\n    }\n\n    [WButton(\"Take Damage\")]\n    private void TakeDamage()\n    {\n        health -= 10;\n        Debug.Log($\"Player took damage! Health: {health}\");\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#parameters","title":"Parameters","text":"<p>The <code>[WButton]</code> attribute accepts several optional parameters to customize button appearance, behavior, and organization.</p>"},{"location":"features/inspector/inspector-button/#parameter-overview","title":"Parameter Overview","text":"C#<pre><code>[WButton(\n    string displayName = null,\n    int drawOrder = 0,\n    int historyCapacity = WButtonAttribute.UseGlobalHistory,\n    string colorKey = null,\n    string groupName = null,\n    int groupPriority = WButtonAttribute.NoGroupPriority,\n    WButtonGroupPlacement groupPlacement = WButtonGroupPlacement.UseGlobalSetting\n)]\n</code></pre>"},{"location":"features/inspector/inspector-button/#displayname-string-optional","title":"displayName (string, optional)","text":"<p>Controls the text shown on the button in the inspector.</p> <ul> <li>Default: Uses the method name (e.g., <code>\"RollDice\"</code> for <code>RollDice()</code>)</li> <li>When to use: Make buttons more readable or add context</li> </ul> C#<pre><code>// Without displayName - shows \"SpawnEnemy\"\n[WButton]\nprivate void SpawnEnemy() { }\n\n// With displayName - shows \"\ud83d\udd2b Spawn Enemy\"\n[WButton(\"\ud83d\udd2b Spawn Enemy\")]\nprivate void SpawnEnemy1() { }\n\n// More descriptive labels\n[WButton(\"Reset Player to Checkpoint\")]\nprivate void ResetPlayer() { }\n\n[WButton(\"Clear All Save Data\")]\nprivate void ClearSaveData() { }\n</code></pre> <p></p>"},{"location":"features/inspector/inspector-button/#draworder-int-optional","title":"drawOrder (int, optional)","text":"<p>Controls the sort order of buttons within their placement section (top or bottom).</p> <ul> <li>Lower values render first within the same placement section</li> <li>Buttons are sorted by drawOrder, then by declaration order for buttons with the same drawOrder</li> <li>Does NOT control placement \u2014 use <code>groupPlacement</code> to control whether buttons appear above or below properties</li> </ul> C#<pre><code>public class PlayerController1 : MonoBehaviour\n{\n    public int health = 100;\n    public float speed = 5f;\n\n    // These buttons render in order: Initialize, Validate, Debug Info\n    // Placement (above/below properties) is controlled by groupPlacement or global settings\n    [WButton(\"Initialize\", drawOrder: -1)]\n    private void Initialize() { }\n\n    [WButton(\"Validate\", drawOrder: 0)]\n    private void Validate() { }\n\n    [WButton(\"Debug Info\", drawOrder: 1)]\n    private void ShowDebugInfo() { }\n}\n</code></pre> <p>Visual layout:</p> <p></p>"},{"location":"features/inspector/inspector-button/#historycapacity-int-optional","title":"historyCapacity (int, optional)","text":"<p>Controls how many previous results are stored for methods that return values.</p> <ul> <li>Default: <code>WButtonAttribute.UseGlobalHistory</code> (uses project setting, typically 5)</li> <li>Range: <code>1</code> to <code>10</code> results, or <code>-1</code> for global default</li> <li>Applies to: Methods returning values, async tasks, or coroutines</li> <li>Does not affect: <code>void</code> methods (no history stored)</li> </ul> C#<pre><code>// Use global setting (default: 5 results)\n[WButton(\"Roll Dice\")]\nprivate int RollDice() =&gt; Random.Range(1, 7);\n\n// Store only last result\n[WButton(\"Get Timestamp\", historyCapacity: 1)]\nprivate string GetTimestamp() =&gt; DateTime.Now.ToString();\n\n// Store up to 10 results for detailed history\n[WButton(\"Measure Frame Time\", historyCapacity: 10)]\nprivate float MeasureFrameTime() =&gt; Time.deltaTime * 1000f;\n\n// Disable history completely (1 = minimum)\n[WButton(\"Ping Server\", historyCapacity: 1)]\nprivate async Task&lt;string&gt; PingServerAsync(CancellationToken ct)\n{\n    // ... ping logic\n    return \"Pong!\";\n}\n</code></pre> <p></p> <p>Performance tip: Use lower values (<code>1-3</code>) for methods called frequently to reduce memory usage.</p>"},{"location":"features/inspector/inspector-button/#colorkey-string-optional","title":"colorKey (string, optional)","text":"<p>Applies custom color themes to buttons using predefined color keys.</p> <ul> <li>Default: <code>null</code> (uses default button color)</li> <li>Common values: <code>\"Default\"</code>, <code>\"Default-Light\"</code>, <code>\"Default-Dark\"</code>, or custom keys</li> <li>Configure in: Edit \u2192 Project Settings \u2192 Unity Helpers \u2192 WButton Color Palettes</li> </ul> C#<pre><code>// Default blue button\n[WButton(\"Standard Action\")]\nprivate void StandardAction() { }\n\n// Custom themed button (requires setup in project settings)\n[WButton(\"Dangerous Action\", colorKey: \"Danger\")]\nprivate void DangerousAction() { }\n\n[WButton(\"Success Action\", colorKey: \"Success\")]\nprivate void SuccessAction() { }\n\n[WButton(\"Warning Action\", colorKey: \"Warning\")]\nprivate void WarningAction() { }\n</code></pre> <p></p> <p>Setting up custom colors:</p> <ol> <li>Open Edit \u2192 Project Settings \u2192 Unity Helpers</li> <li>Navigate to WButton Color Palettes</li> <li>Add a new color key (e.g., <code>\"Danger\"</code>)</li> <li>Set background/text colors</li> <li>Use the key in your <code>[WButton]</code> attribute</li> </ol> <p></p> <p></p>"},{"location":"features/inspector/inspector-button/#groupname-string-optional","title":"groupName (string, optional)","text":"<p>Organizes buttons under labeled group headers.</p> <ul> <li>Default: <code>null</code> (no group header)</li> <li>Behavior: All buttons with the same <code>groupName</code> are merged into a single group</li> <li>Best practice: Use with <code>groupPlacement</code> and <code>groupPriority</code> to control where groups appear</li> </ul> C#<pre><code>public class GameManager : MonoBehaviour\n{\n    // \"Debug Tools\" group - will appear based on groupPlacement or global settings\n    [WButton(\"Log State\", groupName: \"Debug Tools\", groupPlacement: WButtonGroupPlacement.Top)]\n    private void LogState() { }\n\n    [WButton(\"Clear Console\", groupName: \"Debug Tools\")]\n    private void ClearConsole() { }\n\n    // Inspector properties here\n    public int currentLevel = 1;\n    public bool debugMode = false;\n\n    // \"Save System\" group - explicitly placed at bottom\n    [WButton(\"Save Game\", groupName: \"Save System\", groupPlacement: WButtonGroupPlacement.Bottom)]\n    private void SaveGame() { }\n\n    [WButton(\"Load Game\", groupName: \"Save System\")]\n    private void LoadGame() { }\n\n    [WButton(\"Delete Save\", groupName: \"Save System\")]\n    private void DeleteSave() { }\n}\n</code></pre> <p></p> <p>Notes:</p> <ul> <li>Group headers are collapsible (click the arrow to expand/collapse)</li> <li>Groups can be configured to start expanded or collapsed in project settings</li> <li>The first button in a group determines the group's canonical properties (groupPlacement, groupPriority, drawOrder)</li> </ul>"},{"location":"features/inspector/inspector-button/#grouppriority-int-optional","title":"groupPriority (int, optional)","text":"<p>Controls the render order of button groups within a placement section.</p> <ul> <li>Default: <code>WButtonAttribute.NoGroupPriority</code> (renders after groups with explicit priorities)</li> <li>Lower values render first within the same placement (top or bottom)</li> <li>Only applies to buttons with a <code>groupName</code>; ungrouped buttons ignore this value</li> </ul> C#<pre><code>public class ActionPanel : MonoBehaviour\n{\n    // This group renders FIRST (priority 0)\n    [WButton(\"Quick Save\", groupName: \"Primary\", groupPriority: 0)]\n    private void QuickSave() { }\n\n    [WButton(\"Quick Load\", groupName: \"Primary\", groupPriority: 0)]\n    private void QuickLoad() { }\n\n    // This group renders SECOND (priority 10)\n    [WButton(\"Debug Info\", groupName: \"Debug\", groupPriority: 10)]\n    private void ShowDebugInfo() { }\n\n    // This group renders LAST (no explicit priority)\n    [WButton(\"Reset\", groupName: \"Misc\")]\n    private void Reset() { }\n}\n</code></pre> <p></p> <p>Important: The first declared button in a group sets the canonical priority for the entire group. If other buttons in the same group specify different priorities, they are ignored and a warning is displayed in the inspector.</p> C#<pre><code>// \u26a0\ufe0f WARNING: Conflicting priorities in the same group\n[WButton(\"Action A\", groupName: \"Tools\", groupPriority: 0)]  // This priority is used\nprivate void ActionA() { }\n\n[WButton(\"Action B\", groupName: \"Tools\", groupPriority: 10)] // Ignored! Warning shown\nprivate void ActionB() { }\n</code></pre>"},{"location":"features/inspector/inspector-button/#groupplacement-wbuttongroupplacement-optional","title":"groupPlacement (WButtonGroupPlacement, optional)","text":"<p>Controls where a button group renders, overriding the global Unity Helpers setting.</p> <ul> <li>Default: <code>WButtonGroupPlacement.UseGlobalSetting</code></li> <li>Options:</li> <li><code>UseGlobalSetting</code> \u2014 Respects the global setting in Project Settings</li> <li><code>Top</code> \u2014 Always render above inspector properties</li> <li><code>Bottom</code> \u2014 Always render below inspector properties</li> <li>Only applies to buttons with a <code>groupName</code>; ungrouped buttons ignore this value</li> </ul> C#<pre><code>public class MixedPlacementExample : MonoBehaviour\n{\n    public int health = 100;\n    public float speed = 5f;\n\n    // This group ALWAYS renders at the top, regardless of global setting\n    [WButton(\"Initialize\", groupName: \"Setup\", groupPlacement: WButtonGroupPlacement.Top)]\n    private void Initialize() { }\n\n    [WButton(\"Validate\", groupName: \"Setup\", groupPlacement: WButtonGroupPlacement.Top)]\n    private void Validate() { }\n\n    // Properties appear here (health, speed)\n\n    // This group ALWAYS renders at the bottom, regardless of global setting\n    [WButton(\"Cleanup\", groupName: \"Maintenance\", groupPlacement: WButtonGroupPlacement.Bottom)]\n    private void Cleanup() { }\n\n    [WButton(\"Reset All\", groupName: \"Maintenance\", groupPlacement: WButtonGroupPlacement.Bottom)]\n    private void ResetAll() { }\n}\n</code></pre> <p></p> <p>Important: Like <code>groupPriority</code>, the first declared button in a group sets the canonical placement for the entire group. Conflicting values from other buttons in the same group are ignored with a warning.</p>"},{"location":"features/inspector/inspector-button/#combining-grouppriority-and-groupplacement","title":"Combining groupPriority and groupPlacement","text":"<p>Use both parameters together for fine-grained control:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[CreateAssetMenu(\n    fileName = \"AdvancedButtonLayout\",\n    menuName = \"Wallstop Studios/Advanced Button Layout\"\n)]\npublic class AdvancedButtonLayout : ScriptableObject\n{\n    // TOP SECTION - ordered by priority\n    [WButton(\n        \"Validate Data\",\n        groupName: \"Validation\",\n        groupPriority: 1,\n        groupPlacement: WButtonGroupPlacement.Top\n    )]\n    private void ValidateData() { }\n\n    [WButton(\n        \"Generate IDs\",\n        groupName: \"Authoring\",\n        groupPriority: 0,\n        groupPlacement: WButtonGroupPlacement.Top\n    )]\n    private void GenerateIds() { }\n\n    // Properties appear here\n\n    public int property1;\n    public string property2;\n\n    // BOTTOM SECTION - ordered by priority\n\n    [WButton(\n        \"Submit to Server\",\n        groupName: \"Network\",\n        groupPriority: 10,\n        groupPlacement: WButtonGroupPlacement.Bottom\n    )]\n    private void Submit() { }\n\n    [WButton(\n        \"Export\",\n        groupName: \"IO\",\n        groupPriority: 0,\n        groupPlacement: WButtonGroupPlacement.Bottom\n    )]\n    private void Export() { }\n\n    [WButton(\n        \"Import\",\n        groupName: \"IO\",\n        groupPriority: 0,\n        groupPlacement: WButtonGroupPlacement.Bottom\n    )]\n    private void Import() { }\n}\n</code></pre> <p></p> <p>Rendering Order:</p> <ol> <li>Top Section (sorted by <code>groupPriority</code>):</li> <li>Authoring (priority 0)</li> <li>Validation (priority 1)</li> <li>Default Inspector Properties</li> <li>Bottom Section (sorted by <code>groupPriority</code>):</li> <li>IO (priority 0)</li> <li>Network (priority 10)</li> </ol>"},{"location":"features/inspector/inspector-button/#complete-example","title":"Complete Example","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class LevelManager : MonoBehaviour\n{\n    public int currentLevel = 1;\n    public bool debugMode = false;\n\n    // Setup group - explicitly placed at top, renders first due to groupPriority: 0\n    [WButton(\"Initialize Level\", groupName: \"Setup\", groupPriority: 0, groupPlacement: WButtonGroupPlacement.Top)]\n    private void Initialize()\n    {\n        Debug.Log(\"Level initialized!\");\n    }\n\n    [WButton(\"\u2713 Validate Configuration\", groupName: \"Setup\")]\n    private void ValidateConfig()\n    {\n        Debug.Log(\"Configuration valid!\");\n    }\n\n    // Debug group - explicitly placed at top, renders second due to groupPriority: 1\n    [WButton(\"Roll Dice\", historyCapacity: 10, groupName: \"Debug\", groupPriority: 1, groupPlacement: WButtonGroupPlacement.Top)]\n    private int RollDice() =&gt; Random.Range(1, 7);\n\n    [WButton(\"\ud83c\udfaf Spawn Test Enemy\", colorKey: \"Warning\", groupName: \"Debug\")]\n    private void SpawnTestEnemy()\n    {\n        // Spawn logic here\n    }\n\n    // Properties appear here in the inspector\n\n    // Actions group - explicitly placed at bottom, renders first in bottom section due to groupPriority: 0\n    [WButton(\"\u25b6 Start Level\", colorKey: \"Success\", groupName: \"Actions\", groupPriority: 0, groupPlacement: WButtonGroupPlacement.Bottom)]\n    private void StartLevel()\n    {\n        Debug.Log($\"Starting level {currentLevel}...\");\n    }\n\n    [WButton(\"\u23f8 Pause Game\", groupName: \"Actions\")]\n    private void PauseGame()\n    {\n        Time.timeScale = 0f;\n    }\n\n    [WButton(\"\ud83d\udd04 Restart Level\", colorKey: \"Danger\", groupName: \"Actions\")]\n    private void RestartLevel()\n    {\n        // Restart logic here\n    }\n\n    // Maintenance group - explicitly placed at bottom, renders last in bottom section due to groupPriority: 10\n    [WButton(\"Clear Cache\", historyCapacity: 1, groupName: \"Maintenance\", groupPriority: 10, groupPlacement: WButtonGroupPlacement.Bottom)]\n    private string ClearCache()\n    {\n        return $\"Cache cleared at {System.DateTime.Now:HH:mm:ss}\";\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#execution-types","title":"Execution Types","text":"<p>WButton supports four method signatures:</p>"},{"location":"features/inspector/inspector-button/#1-void-methods-immediate","title":"1. Void Methods (Immediate)","text":"C#<pre><code>[WButton(\"Log Message\")]\nprivate void LogMessage()\n{\n    Debug.Log(\"Button clicked!\");\n}\n</code></pre> <p>Behavior: Executes immediately, no return value shown</p>"},{"location":"features/inspector/inspector-button/#2-returning-values-with-history","title":"2. Returning Values (With History)","text":"C#<pre><code>[WButton(\"Roll Dice\", historyCapacity: 10, groupName: \"Debug\")]\nprivate int RollDice()\n{\n    return Random.Range(1, 7);\n}\n\n[WButton(\"Get Position\")]\nprivate Vector3 GetPlayerPosition()\n{\n    return transform.position;\n}\n</code></pre> <p>Behavior: Shows return value in a collapsible history panel</p>"},{"location":"features/inspector/inspector-button/#3-coroutines-ienumerator","title":"3. Coroutines (IEnumerator)","text":"C#<pre><code>[WButton(\"Fade Out\")]\nprivate IEnumerator FadeOut()\n{\n    SpriteRenderer sprite = GetComponent&lt;SpriteRenderer&gt;();\n    Color color = sprite.color;\n\n    for (float t = 1f; t &gt;= 0f; t -= Time.deltaTime)\n    {\n        color.a = t;\n        sprite.color = color;\n        yield return null;\n    }\n\n    Debug.Log(\"Fade complete!\");\n}\n</code></pre> <p>Behavior:</p> <ul> <li>Shows \"Running...\" status</li> <li>Spinner animation during execution</li> <li>\"Complete\" message when finished</li> </ul>"},{"location":"features/inspector/inspector-button/#4-async-methods-task-valuetask","title":"4. Async Methods (Task / ValueTask)","text":"C#<pre><code>using System.Threading;\nusing System.Threading.Tasks;\n\n[WButton(\"Load Data\")]\nprivate async Task&lt;string&gt; LoadDataAsync(CancellationToken ct)\n{\n    Debug.Log(\"Loading...\");\n    await Task.Delay(2000, ct);  // Simulate async work\n    return \"Data loaded successfully!\";\n}\n\n[WButton(\"Download Asset\")]\nprivate async ValueTask&lt;Texture2D&gt; DownloadAssetAsync(CancellationToken ct)\n{\n    Debug.Log(\"Downloading...\");\n    await Task.Delay(1000, ct);\n    // Simulate download\n    return new Texture2D(256, 256);\n}\n</code></pre> <p>Behavior:</p> <ul> <li>Automatic <code>CancellationToken</code> injection (optional parameter)</li> <li>\"Cancel\" button appears during execution</li> <li>Result shown in history when complete</li> <li>Exceptions logged to console</li> </ul> <p>Cancellation Example:</p> C#<pre><code>[WButton(\"Long Operation\")]\nprivate async Task LongOperationAsync(CancellationToken ct)\n{\n    for (int i = 0; i &lt; 10; i++)\n    {\n        ct.ThrowIfCancellationRequested();  // Check cancellation\n        Debug.Log($\"Step {i + 1}/10\");\n        await Task.Delay(500, ct);\n    }\n    return Task.CompletedTask;\n}\n</code></pre> <p>Supported Signatures:</p> <ul> <li><code>Task</code> (void async)</li> <li><code>Task&lt;T&gt;</code> (async with a result)</li> <li><code>ValueTask</code> (void async, no heap allocation)</li> <li><code>ValueTask&lt;T&gt;</code> (async with a result, no heap allocation)</li> </ul>"},{"location":"features/inspector/inspector-button/#result-history","title":"Result History","text":""},{"location":"features/inspector/inspector-button/#automatic-history","title":"Automatic History","text":"C#<pre><code>[WButton(\"Generate ID\", historyCapacity: 10)]\nprivate string GenerateId()\n{\n    return System.Guid.NewGuid().ToString().Substring(0, 8);\n}\n</code></pre> <p>Features:</p> <ul> <li>Per-method, per-target buffering (history survives inspector refresh)</li> <li>Collapsible foldout for each method</li> <li>Chronological order (newest first)</li> <li>Pagination when history exceeds the display threshold</li> </ul>"},{"location":"features/inspector/inspector-button/#history-capacity-options","title":"History Capacity Options","text":"C#<pre><code>// Use global setting (default: 5, configurable in UnityHelpersSettings)\n[WButton(\"Use Global\")]\nprivate int UseGlobal() =&gt; Random.Range(1, 100);\n\n// Custom capacity per method\n[WButton(\"Keep 20 Results\", historyCapacity: 20)]\nprivate float KeepMany() =&gt; Random.value;\n\n// Disable history (0 capacity)\n[WButton(\"No History\", historyCapacity: 0)]\nprivate void NoHistory() =&gt; Debug.Log(\"No history stored\");\n</code></pre> <p>Global Setting: <code>UnityHelpersSettings.WButtonHistorySize</code> (default: 5, range: 1-10)</p> <p> </p>"},{"location":"features/inspector/inspector-button/#draw-order-positioning","title":"Draw Order &amp; Positioning","text":"<p>Control the sort order of buttons within their placement section:</p> C#<pre><code>public class ButtonPositioning : MonoBehaviour\n{\n    // Buttons are sorted by drawOrder (lower values first)\n    [WButton(\"Top Button\", drawOrder: -1)]\n    private void TopButton() =&gt; Debug.Log(\"Renders first (lowest drawOrder)\");\n\n    [WButton(\"Middle Button\", drawOrder: 0)]\n    private void MiddleButton() =&gt; Debug.Log(\"Renders second\");\n\n    [WButton(\"Bottom Button\", drawOrder: 1)]\n    private void BottomButton() =&gt; Debug.Log(\"Renders last (highest drawOrder)\");\n\n    // Inspector fields - buttons appear above or below based on groupPlacement/global settings\n    public int someField = 10;\n}\n</code></pre> <p></p> <p>Positioning Rules:</p> <ul> <li>Lower <code>drawOrder</code> values render first within a placement section</li> <li>Placement (above/below properties) is controlled by <code>groupPlacement</code> or the global <code>WButtonPlacement</code> setting, not by <code>drawOrder</code></li> <li>Within the same <code>drawOrder</code>, buttons render in declaration order (source code order)</li> </ul>"},{"location":"features/inspector/inspector-button/#pagination-by-draw-order","title":"Pagination by Draw Order","text":"C#<pre><code>[WButton(drawOrder: 0)]\nprivate void Action1() {}\n\n[WButton( drawOrder: 0)]\nprivate void Action2() {}\n\n// ... 10 more buttons with drawOrder: 0 ...\n\n[WButton(drawOrder: 0)]\nprivate void Action12() {}\n</code></pre> <p>Pagination Settings:</p> <ul> <li>Page size controlled by <code>UnityHelpersSettings.WButtonPageSize</code> (default: 6)</li> <li>Pagination only applies within each draw order group</li> <li>Navigation: First, Previous, Next, Last buttons</li> </ul>"},{"location":"features/inspector/inspector-button/#grouping","title":"Grouping","text":"<p>Organize buttons into named sections:</p> C#<pre><code>[WButton(\"Spawn Enemy\", groupName: \"Combat\")]\nprivate void SpawnEnemy() =&gt; Debug.Log(\"Enemy spawned\");\n\n[WButton(\"Clear Enemies\", groupName: \"Combat\")]\nprivate void ClearEnemies() =&gt; Debug.Log(\"Enemies cleared\");\n\n[WButton(\"Save Game\", groupName: \"Persistence\")]\nprivate void SaveGame() =&gt; Debug.Log(\"Game saved\");\n\n[WButton(\"Load Game\", groupName: \"Persistence\")]\nprivate void LoadGame() =&gt; Debug.Log(\"Game loaded\");\n</code></pre> <p></p> <p>Grouping Behavior:</p> <ul> <li>Groups are created automatically based on <code>groupName</code></li> <li>Buttons within a group are organized by <code>drawOrder</code></li> <li>Groups can be collapsible (controlled by <code>UnityHelpersSettings.WButtonFoldoutBehavior</code>)</li> </ul>"},{"location":"features/inspector/inspector-button/#foldout-behavior","title":"Foldout Behavior","text":"<p>Global Setting: <code>UnityHelpersSettings.WButtonFoldoutBehavior</code></p> <p>Options:</p> <ul> <li><code>Always</code> - Always show group foldout triangles</li> <li><code>StartExpanded</code> - Collapsible, starts open</li> <li><code>StartCollapsed</code> - Collapsible, starts closed</li> </ul> <p>Animation:</p> <ul> <li>Enable/disable via <code>UnityHelpersSettings.WButtonFoldoutTweenEnabled</code></li> <li>Speed controlled by <code>UnityHelpersSettings.WButtonFoldoutSpeed</code> (default: 2.0, range: 2-12)</li> </ul> <p></p>"},{"location":"features/inspector/inspector-button/#color-theming","title":"Color Theming","text":"C#<pre><code>[WButton(\"Dangerous Action\", colorKey: \"Default-Dark\")]\nprivate void DangerousAction() =&gt; Debug.LogWarning(\"Dangerous!\");\n\n[WButton(\"Safe Action\", colorKey: \"Default-Light\")]\nprivate void SafeAction() =&gt; Debug.Log(\"Safe operation\");\n</code></pre> <p>Built-in Priorities (Color Keys):</p> <ul> <li><code>\"Default\"</code> - Theme-aware (adapts to Unity theme)</li> <li><code>\"Default-Dark\"</code> - Dark theme colors</li> <li><code>\"Default-Light\"</code> - Light theme colors</li> <li><code>\"WDefault\"</code> - Legacy vibrant blue</li> <li>Custom keys defined in <code>UnityHelpersSettings.WButtonCustomColors</code></li> </ul> <p>Define Custom Colors:</p> <ol> <li>Open <code>ProjectSettings/UnityHelpersSettings.asset</code></li> <li>Add entry to <code>WButtonCustomColors</code> dictionary</li> <li>Set a button background, text color, border</li> </ol> <p></p>"},{"location":"features/inspector/inspector-button/#configuration","title":"Configuration","text":""},{"location":"features/inspector/inspector-button/#global-settings","title":"Global Settings","text":"<p>All buttons respect project-wide settings defined in <code>UnityHelpersSettings</code>:</p> <p>Location: <code>ProjectSettings/UnityHelpersSettings.asset</code></p> <p>Settings:</p> <ul> <li><code>WButtonHistorySize</code> (default: 5, range: 1-10) - Results to keep per method</li> <li><code>WButtonPlacement</code> (Top or Bottom) - Default button position</li> <li><code>WButtonFoldoutBehavior</code> (Always, StartExpanded, StartCollapsed) - Group collapsibility</li> <li><code>WButtonFoldoutTweenEnabled</code> (bool) - Enable group animations</li> <li><code>WButtonFoldoutSpeed</code> (default: 2.0, range: 2-12) - Animation speed</li> <li><code>WButtonPageSize</code> (default: 6) - Buttons per page for pagination</li> <li><code>WButtonCustomColors</code> - Custom color palette dictionary</li> </ul> <p></p>"},{"location":"features/inspector/inspector-button/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/inspector-button/#1-clear-button-names","title":"1. Clear Button Names","text":"C#<pre><code>// \u2705 GOOD: Action-oriented, descriptive\n[WButton(\"Heal to Full\")]\nprivate void HealToFull() { ... }\n\n[WButton(\"Spawn 10 Enemies\")]\nprivate void SpawnEnemies() { ... }\n\n// \u274c BAD: Vague or technical\n[WButton(\"DoStuff\")]\nprivate void DoStuff() { ... }\n\n[WButton]  // Defaults to method name \"HandlePlayerDeath\"\nprivate void HandlePlayerDeath() { ... }\n</code></pre>"},{"location":"features/inspector/inspector-button/#2-group-related-actions","title":"2. Group Related Actions","text":"C#<pre><code>// \u2705 GOOD: Grouped by feature\n[WButton(\"Spawn Enemy\", groupName: \"Combat Testing\")]\nprivate void SpawnEnemy() { ... }\n\n[WButton(\"Kill All Enemies\", groupName: \"Combat Testing\")]\nprivate void KillAll() { ... }\n\n[WButton(\"Save Progress\", groupName: \"Persistence\")]\nprivate void Save() { ... }\n\n// \u274c BAD: No grouping, cluttered inspector\n[WButton(\"Spawn Enemy\")]\nprivate void SpawnEnemy() { ... }\n\n[WButton(\"Save Progress\")]\nprivate void Save() { ... }\n\n[WButton(\"Kill All Enemies\")]\nprivate void KillAll() { ... }\n</code></pre>"},{"location":"features/inspector/inspector-button/#3-use-history-for-randomvariable-results","title":"3. Use History for Random/Variable Results","text":"C#<pre><code>// \u2705 GOOD: History helps track random values\n[WButton(\"Roll Loot\", historyCapacity: 10)]\nprivate string RollLoot()\n{\n    return lootTable[PRNG.Instance.Next(0, lootTable.Length)];\n}\n\n// \u2705 GOOD: No history needed for fixed actions\n[WButton(\"Reset Position\", historyCapacity: 0)]\nprivate void ResetPosition()\n{\n    transform.position = Vector3.zero;\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#4-async-best-practices","title":"4. Async Best Practices","text":"C#<pre><code>// \u2705 GOOD: Accept CancellationToken, check it\n[WButton(\"Long Task\")]\nprivate async Task LongTaskAsync(CancellationToken ct)\n{\n    for (int i = 0; i &lt; 100; i++)\n    {\n        ct.ThrowIfCancellationRequested();\n        await Task.Delay(100, ct);\n    }\n}\n\n// \u2705 GOOD: Handle exceptions gracefully\n[WButton(\"Risky Operation\")]\nprivate async Task RiskyOperationAsync()\n{\n    try\n    {\n        await SomeRiskyApiCall();\n    }\n    catch (Exception ex)\n    {\n        Debug.LogError($\"Operation failed: {ex.Message}\");\n    }\n}\n\n// \u274c BAD: Long operation with no cancellation support\n[WButton(\"Infinite Loop\")]\nprivate async Task InfiniteLoopAsync()\n{\n    while (true)  // No way to stop this!\n    {\n        await Task.Delay(1000);\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#5-color-usage","title":"5. Color Usage","text":"C#<pre><code>// \u2705 GOOD: Use colors to indicate risk/importance\n[WButton(\"Delete All Data\", colorKey: \"Default-Dark\")]  // Dark = danger\nprivate void DeleteAllData() { ... }\n\n[WButton(\"Quick Save\", colorKey: \"Default-Light\")]  // Light = safe\nprivate void QuickSave() { ... }\n\n// \u274c BAD: Random colors without meaning\n[WButton(\"Log Message\", colorKey: \"CustomPurple\")]  // Why purple?\nprivate void LogMessage() { ... }\n</code></pre>"},{"location":"features/inspector/inspector-button/#examples","title":"Examples","text":""},{"location":"features/inspector/inspector-button/#example-1-gameplay-testing","title":"Example 1: Gameplay Testing","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class PlayerDebug : MonoBehaviour\n{\n    public int health = 100;\n    public int gold = 0;\n\n    [WButton(\"Heal\", groupName: \"Health\", colorKey: \"Default-Light\")]\n    private void Heal()\n    {\n        health = 100;\n        Debug.Log(\"Player healed!\");\n    }\n\n    [WButton(\"Take Damage\", groupName: \"Health\")]\n    private void TakeDamage()\n    {\n        health -= 25;\n        Debug.Log($\"Took damage! Health: {health}\");\n    }\n\n    [WButton(\"Kill Player\", groupName: \"Health\", colorKey: \"Default-Dark\")]\n    private void Kill()\n    {\n        health = 0;\n        Debug.LogWarning(\"Player died!\");\n    }\n\n    [WButton(\"Add Gold\", groupName: \"Economy\")]\n    private void AddGold()\n    {\n        gold += 100;\n        Debug.Log($\"Gold: {gold}\");\n    }\n\n    [WButton(\"Roll Reward\", groupName: \"Economy\", historyCapacity: 10)]\n    private int RollReward()\n    {\n        int amount = PRNG.Instance.Next(10, 100);\n        gold += amount;\n        return amount;\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#example-2-async-data-loading","title":"Example 2: Async Data Loading","text":"C#<pre><code>using UnityEngine;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class DataManager : MonoBehaviour\n{\n    [WButton(\"Load Player Data\")]\n    private async Task&lt;string&gt; LoadPlayerDataAsync(CancellationToken ct)\n    {\n        Debug.Log(\"Loading player data...\");\n\n        // Simulate network request\n        await Task.Delay(2000, ct);\n\n        string playerName = \"TestPlayer_\" + Random.Range(1000, 9999);\n        Debug.Log($\"Loaded: {playerName}\");\n\n        return playerName;\n    }\n\n    [WButton(\"Batch Load\", historyCapacity: 5)]\n    private async Task&lt;int&gt; BatchLoadAsync(CancellationToken ct)\n    {\n        int count = 0;\n        for (int i = 0; i &lt; 10; i++)\n        {\n            ct.ThrowIfCancellationRequested();\n            await Task.Delay(200, ct);\n            count++;\n            Debug.Log($\"Loaded item {count}/10\");\n        }\n        return count;\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#example-3-procedural-generation-testing","title":"Example 3: Procedural Generation Testing","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class LevelGenerator : MonoBehaviour\n{\n    [WButton(\"Generate Seed\", historyCapacity: 20)]\n    private int GenerateSeed()\n    {\n        return Random.Range(1000, 9999);\n    }\n\n    [WButton(\"Generate Level\")]\n    private void GenerateLevel()\n    {\n        int seed = Random.Range(1000, 9999);\n        Random.InitState(seed);\n        Debug.Log($\"Generating level with seed: {seed}\");\n        // ... generation logic ...\n    }\n\n    [WButton(\"Clear Level\", colorKey: \"Default-Dark\")]\n    private void ClearLevel()\n    {\n        // ... cleanup logic ...\n        Debug.Log(\"Level cleared\");\n    }\n\n    [WButton(\"Generate with Seed\")]\n    private void GenerateWithSeed(int seed)\n    {\n        Random.InitState(seed);\n        Debug.Log($\"Generating with seed: {seed}\");\n        // ... generation logic ...\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#example-4-coroutine-animation-testing","title":"Example 4: Coroutine Animation Testing","text":"C#<pre><code>using System.Collections;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class AnimationTester : MonoBehaviour\n{\n    public SpriteRenderer spriteRenderer;\n\n    private void Awake()\n    {\n        spriteRenderer = GetComponent&lt;SpriteRenderer&gt;();\n    }\n\n    [WButton(\"Fade Out\", groupName: \"Animations\")]\n    private IEnumerator FadeOutCoroutine()\n    {\n        Color color = spriteRenderer.color;\n        while (color.a &gt; 0f)\n        {\n            color.a -= Time.deltaTime;\n            spriteRenderer.color = color;\n            yield return null;\n        }\n        Debug.Log(\"Fade out complete\");\n    }\n\n    [WButton(\"Fade In\", groupName: \"Animations\")]\n    private IEnumerator FadeInCoroutine()\n    {\n        Color color = spriteRenderer.color;\n        while (color.a &lt; 1f)\n        {\n            color.a += Time.deltaTime;\n            spriteRenderer.color = color;\n            yield return null;\n        }\n        Debug.Log(\"Fade in complete\");\n    }\n\n    [WButton(\"Pulse\", groupName: \"Animations\")]\n    private IEnumerator PulseCoroutine()\n    {\n        Vector3 originalScale = transform.localScale;\n        for (int i = 0; i &lt; 3; i++)\n        {\n            transform.localScale = originalScale * 1.2f;\n            yield return new WaitForSeconds(0.2f);\n            transform.localScale = originalScale;\n            yield return new WaitForSeconds(0.2f);\n        }\n        Debug.Log(\"Pulse complete\");\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#using-wbutton-with-custom-editors","title":"Using WButton with Custom Editors","text":""},{"location":"features/inspector/inspector-button/#overview","title":"Overview","text":"<p>WButton works automatically with:</p> <ul> <li>All Unity Objects (<code>MonoBehaviour</code>, <code>ScriptableObject</code>, etc.)</li> <li>Odin Inspector's <code>SerializedMonoBehaviour</code> and <code>SerializedScriptableObject</code> (when <code>ODIN_INSPECTOR</code> is defined)</li> </ul> <p>When do you need <code>WButtonEditorHelper</code>?</p> <p>Only when you create custom Odin editors that override the default behavior. For example, if you create a <code>CustomEditor</code> that inherits from <code>OdinEditor</code> for a specific type, you'll need to integrate WButton manually.</p>"},{"location":"features/inspector/inspector-button/#automatic-integration-with-odin-inspector","title":"Automatic Integration with Odin Inspector","text":"<p>No setup required! WButton automatically works with Odin's base types:</p> C#<pre><code>#if ODIN_INSPECTOR\nusing Sirenix.OdinInspector;\n#endif\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n#if ODIN_INSPECTOR\npublic class MyComponent : SerializedMonoBehaviour\n#else\npublic class MyComponent : MonoBehaviour\n#endif\n{\n    [WButton(\"Test Button\")]\n    private void TestMethod()\n    {\n        Debug.Log(\"Button clicked!\");\n    }\n}\n</code></pre> <p>WButton will appear automatically in the inspector - no additional code needed!</p>"},{"location":"features/inspector/inspector-button/#integration-with-custom-odin-editors","title":"Integration with Custom Odin Editors","text":"<p>If you create a custom Odin editor for your type, you need to manually integrate WButton using <code>WButtonEditorHelper</code>:</p> C#<pre><code>#if UNITY_EDITOR &amp;&amp; ODIN_INSPECTOR\nusing Sirenix.OdinInspector.Editor;\nusing UnityEditor;\nusing WallstopStudios.UnityHelpers.Editor.Utils.WButton;\n\n// Custom editor for a specific type\n[CustomEditor(typeof(MyComponent))]\npublic class MyComponentEditor : OdinEditor\n{\n    private WButtonEditorHelper _wButtonHelper;\n\n    protected override void OnEnable()\n    {\n        base.OnEnable();\n        _wButtonHelper = new WButtonEditorHelper();\n    }\n\n    public override void OnInspectorGUI()\n    {\n        // Draw WButtons at top (optional - based on your settings)\n        _wButtonHelper.DrawButtonsAtTop(this);\n\n        // Draw Odin inspector\n        base.OnInspectorGUI();\n\n        // Draw WButtons at bottom and process any invocations\n        _wButtonHelper.DrawButtonsAtBottomAndProcessInvocations(this);\n    }\n}\n#endif\n</code></pre>"},{"location":"features/inspector/inspector-button/#integration-with-standard-custom-editors","title":"Integration with Standard Custom Editors","text":"<p>For standard Unity custom editors:</p> C#<pre><code>#if UNITY_EDITOR\nusing UnityEditor;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Editor.Utils.WButton;\n\n[CustomEditor(typeof(MyComponent))]\npublic class MyComponentEditor : Editor\n{\n    private WButtonEditorHelper _wButtonHelper;\n\n    private void OnEnable()\n    {\n        _wButtonHelper = new WButtonEditorHelper();\n    }\n\n    public override void OnInspectorGUI()\n    {\n        serializedObject.Update();\n\n        // Draw WButtons at top\n        _wButtonHelper.DrawButtonsAtTop(this);\n\n        // Draw your custom inspector\n        DrawDefaultInspector();\n\n        serializedObject.ApplyModifiedProperties();\n\n        // Draw WButtons at bottom and process invocations\n        _wButtonHelper.DrawButtonsAtBottomAndProcessInvocations(this);\n    }\n}\n#endif\n</code></pre>"},{"location":"features/inspector/inspector-button/#wbuttoneditorhelper-api","title":"WButtonEditorHelper API","text":"<p>The <code>WButtonEditorHelper</code> class provides several methods for different use cases:</p> Method Description <code>DrawButtonsAtTop(Editor)</code> Draws buttons configured for top placement <code>DrawButtonsAtBottom(Editor)</code> Draws buttons configured for bottom placement <code>ProcessInvocations()</code> Processes any triggered button invocations <code>DrawButtonsAtBottomAndProcessInvocations(Editor)</code> Convenience method combining bottom drawing + processing (most common) <code>DrawAllButtonsAndProcessInvocations(Editor)</code> Draws all buttons in one location regardless of placement settings <p>Key Points:</p> <ol> <li>Create one <code>WButtonEditorHelper</code> instance per editor (typically in <code>OnEnable</code>)</li> <li>Call <code>DrawButtonsAtTop</code> before your inspector content</li> <li>Call <code>DrawButtonsAtBottomAndProcessInvocations</code> after your inspector content</li> <li>Always call <code>ProcessInvocations()</code> after all button drawing is complete</li> </ol>"},{"location":"features/inspector/inspector-button/#single-location-button-drawing","title":"Single Location Button Drawing","text":"<p>If you prefer all buttons in one location regardless of placement settings:</p> C#<pre><code>public override void OnInspectorGUI()\n{\n    // Your inspector code here\n    DrawDefaultInspector();\n\n    // Draw all buttons at the end\n    _wButtonHelper.DrawAllButtonsAndProcessInvocations(this);\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/inspector/inspector-button/#button-not-appearing","title":"Button Not Appearing","text":"<p>Problem: Method has <code>[WButton]</code> but button doesn't show</p> <p>Solutions:</p> <ol> <li>Ensure method is <code>private</code> or <code>protected</code> (public methods may conflict)</li> <li>Verify the component is enabled and active</li> </ol>"},{"location":"features/inspector/inspector-button/#history-not-showing","title":"History Not Showing","text":"<p>Problem: Method returns a value but no history appears</p> <p>Solutions:</p> <ol> <li>Check <code>historyCapacity</code> - make sure it's &gt; 0</li> <li>Verify that the return type is serializable</li> <li>Check <code>UnityHelpersSettings.WButtonHistorySize</code> if using global setting</li> </ol>"},{"location":"features/inspector/inspector-button/#async-method-not-cancelling","title":"Async Method Not Cancelling","text":"<p>Problem: Cancel button doesn't stop an async method</p> <p>Solutions:</p> <ol> <li>Ensure method accepts <code>CancellationToken</code> parameter</li> <li>Check token periodically: <code>ct.ThrowIfCancellationRequested()</code></li> <li>Pass token to async operations: <code>await Task.Delay(1000, ct)</code></li> </ol> C#<pre><code>// \u2705 CORRECT: Cancellable\n[WButton(\"Long Task\")]\nprivate async Task LongTaskAsync(CancellationToken ct)\n{\n    for (int i = 0; i &lt; 100; i++)\n    {\n        ct.ThrowIfCancellationRequested();  // Check token\n        await Task.Delay(100, ct);  // Pass token\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-button/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Inspector Grouping Attributes - WGroup layouts</li> <li>Inspector Settings - Configuration reference</li> <li>Editor Tools Guide - Other editor utilities</li> </ul> <p>Next Steps:</p> <ul> <li>Add buttons to your components for quick testing</li> <li>Experiment with <code>groupName</code> to organize buttons</li> <li>Try async methods with <code>CancellationToken</code> support</li> <li>Customize colors in <code>UnityHelpersSettings.asset</code></li> </ul>"},{"location":"features/inspector/inspector-conditional-display/","title":"Inspector Conditional Display (WShowIf)","text":"<p>Show or hide fields dynamically based on runtime values.</p> <p>The <code>[WShowIf]</code> attribute creates dynamic inspector layouts that adapt to field values without writing custom PropertyDrawers. Perfect for reducing clutter, creating a contextual UI, and guiding designers toward valid configurations.</p>"},{"location":"features/inspector/inspector-conditional-display/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Basic Usage</li> <li>Comparison Operators</li> <li>Supported Types</li> <li>Advanced Features</li> <li>Best Practices</li> <li>Examples</li> </ul>"},{"location":"features/inspector/inspector-conditional-display/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WeaponConfig : MonoBehaviour\n{\n    public bool isRanged;\n\n    [WShowIf(nameof(isRanged))]\n    public float projectileSpeed = 10f;  // Only visible when isRanged == true\n\n    [WShowIf(nameof(isRanged), inverse: true)]\n    public float meleeRange = 2f;  // Only visible when isRanged == false\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#parameters","title":"Parameters","text":"C#<pre><code>[WShowIf(\n    string conditionField,                    // Required: Field/property name to evaluate\n    WShowIfComparison comparison = Equal,     // Comparison operator\n    bool inverse = false,                     // Invert the result\n    params object[] expectedValues            // Expected values for equality checks\n)]\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#comparison-operators","title":"Comparison Operators","text":"C#<pre><code>public enum WShowIfComparison\n{\n    Equal,                  // ==\n    NotEqual,               // !=\n    GreaterThan,            // &gt;\n    GreaterThanOrEqual,     // &gt;=\n    LessThan,               // &lt;\n    LessThanOrEqual,        // &lt;=\n    IsNull,                 // == null\n    IsNotNull,              // != null\n    IsNullOrEmpty,          // string.IsNullOrEmpty or collection.Count == 0\n    IsNotNullOrEmpty        // !string.IsNullOrEmpty and collection.Count &gt; 0\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#equal-notequal","title":"Equal / NotEqual","text":"C#<pre><code>public enum WeaponType { Melee, Ranged, Magic }\n\npublic WeaponType weaponType;\n\n[WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Ranged)]\npublic int ammoCapacity = 30;\n\n[WShowIf(nameof(weaponType), WShowIfComparison.NotEqual, WeaponType.Melee)]\npublic float castTime = 1f;  // Visible for Ranged or Magic\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#numeric-comparisons","title":"Numeric Comparisons","text":"C#<pre><code>public class Ability : ScriptableObject { }\n\n[Range(0, 100)]\npublic int playerLevel = 1;\n\n[WShowIf(nameof(playerLevel), WShowIfComparison.GreaterThan, 5)]\npublic Ability specialAbility;\n\n[WShowIf(nameof(playerLevel), WShowIfComparison.GreaterThanOrEqual, 10)]\npublic Ability ultimateAbility;\n\n[WShowIf(nameof(playerLevel), WShowIfComparison.LessThan, 5)]\npublic string tutorialMessage = \"Level up to unlock abilities\";\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#null-checks","title":"Null Checks","text":"C#<pre><code>public GameObject targetPrefab;\n\n[WShowIf(nameof(targetPrefab), WShowIfComparison.IsNotNull)]\npublic int spawnCount = 1;\n\n[WShowIf(nameof(targetPrefab), WShowIfComparison.IsNull)]\npublic string warningMessage = \"Assign a prefab to spawn!\";\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#collection-checks","title":"Collection Checks","text":"C#<pre><code>public List&lt;GameObject&gt; enemies;\n\n[WShowIf(nameof(enemies), WShowIfComparison.IsNotNullOrEmpty)]\npublic float spawnInterval = 2f;\n\n[WShowIf(nameof(enemies), WShowIfComparison.IsNullOrEmpty)]\npublic string hint = \"Add enemies to the list\";\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#supported-types","title":"Supported Types","text":""},{"location":"features/inspector/inspector-conditional-display/#boolean","title":"Boolean","text":"C#<pre><code>public bool enableFeature;\n\n[WShowIf(nameof(enableFeature))]\npublic float featurePower = 1.0f;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#numeric-int-float-double-byte-short-long-etc","title":"Numeric (int, float, double, byte, short, long, etc.)","text":"C#<pre><code>public float health = 100f;\n\n[WShowIf(nameof(health), WShowIfComparison.LessThanOrEqual, 30f)]\npublic Color lowHealthColor = Color.red;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#enum","title":"Enum","text":"C#<pre><code>public enum Difficulty { Easy, Normal, Hard }\n\npublic Difficulty difficulty;\n\n[WShowIf(nameof(difficulty), WShowIfComparison.Equal, Difficulty.Hard)]\npublic float hardModeMultiplier = 2.5f;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#string","title":"String","text":"C#<pre><code>public string playerName;\n\n[WShowIf(nameof(playerName), WShowIfComparison.IsNotNullOrEmpty)]\npublic Sprite playerAvatar;\n\n[WShowIf(nameof(playerName), WShowIfComparison.Equal, \"admin\")]\npublic bool debugMode;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#unityengineobject-gameobject-component-scriptableobject-etc","title":"UnityEngine.Object (GameObject, Component, ScriptableObject, etc.)","text":"C#<pre><code>public AudioSource audioSource;\n\n[WShowIf(nameof(audioSource), WShowIfComparison.IsNotNull)]\npublic AudioClip soundEffect;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#collections-list-array-ienumerable","title":"Collections (List, Array, IEnumerable)","text":"C#<pre><code>public GameObject[] spawnPoints;\n\n[WShowIf(nameof(spawnPoints), WShowIfComparison.IsNotNullOrEmpty)]\npublic GameObject enemyPrefab;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#advanced-features","title":"Advanced Features","text":""},{"location":"features/inspector/inspector-conditional-display/#inverse-logic","title":"Inverse Logic","text":"C#<pre><code>public bool useCustomColor;\n\n// Show when useCustomColor == false\n[WShowIf(nameof(useCustomColor), inverse: true)]\npublic string colorPreset = \"Default\";\n\n// Show when useCustomColor == true\n[WShowIf(nameof(useCustomColor))]\npublic Color customColor = Color.white;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#multiple-expected-values","title":"Multiple Expected Values","text":"C#<pre><code>public enum GameState { MainMenu, Playing, Paused, GameOver }\n\npublic GameState state;\n\n// Show when state is Playing OR Paused\n[WShowIf(nameof(state), WShowIfComparison.Equal, GameState.Playing, GameState.Paused)]\npublic float gameTimer;\n\n// Show when state is MainMenu OR GameOver\n[WShowIf(nameof(state), WShowIfComparison.Equal, GameState.MainMenu, GameState.GameOver)]\npublic string menuText;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#stacking-conditions","title":"Stacking Conditions","text":"C#<pre><code>public bool isEnabled;\npublic int level;\n\n// Both conditions must be true (AND logic)\n[WShowIf(nameof(isEnabled))]\n[WShowIf(nameof(level), WShowIfComparison.GreaterThan, 5)]\npublic Ability advancedFeature;\n</code></pre> <p>Note: Multiple <code>[WShowIf]</code> attributes create AND logic (all must be true).</p>"},{"location":"features/inspector/inspector-conditional-display/#properties-and-methods","title":"Properties and Methods","text":"C#<pre><code>public class Enemy : MonoBehaviour\n{\n    public float health = 100f;\n\n    // Property (computed)\n    public bool IsAlive =&gt; health &gt; 0f;\n\n    // Show when IsAlive == true\n    [WShowIf(nameof(IsAlive))]\n    public float damageResistance = 0.5f;\n\n    // Method (no parameters)\n    public bool ShouldShowDebug() =&gt; Application.isEditor &amp;&amp; Debug.isDebugBuild;\n\n    [WShowIf(nameof(ShouldShowDebug))]\n    public string debugInfo;\n}\n</code></pre> <p>Supported:</p> <ul> <li>Public fields</li> <li>Public properties (with getter)</li> <li>Public methods returning bool/int/float/enum/string/object (no parameters)</li> </ul>"},{"location":"features/inspector/inspector-conditional-display/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/inspector-conditional-display/#1-use-for-contextual-fields","title":"1. Use for Contextual Fields","text":"C#<pre><code>// \u2705 GOOD: Fields only relevant in specific contexts\npublic enum AbilityType { Passive, Active, Toggle }\n\npublic AbilityType abilityType;\n\n[WShowIf(nameof(abilityType), WShowIfComparison.Equal, AbilityType.Active)]\npublic float cooldown = 5f;\n\n[WShowIf(nameof(abilityType), WShowIfComparison.Equal, AbilityType.Toggle)]\npublic float energyDrainPerSecond = 10f;\n\n// \u274c BAD: Hiding core settings\n[WShowIf(nameof(advancedMode))]\npublic float maxHealth = 100f;  // Always needed, shouldn't hide!\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#2-provide-hints-when-hidden","title":"2. Provide Hints When Hidden","text":"C#<pre><code>// \u2705 GOOD: Show hint when condition is false\npublic GameObject weaponPrefab;\n\n[WShowIf(nameof(weaponPrefab), WShowIfComparison.IsNull)]\npublic string hint = \"\u26a0\ufe0f Assign a weapon prefab above\";\n\n[WShowIf(nameof(weaponPrefab), WShowIfComparison.IsNotNull)]\npublic int weaponDamage = 10;\n\n// \u274c BAD: No indication why fields are missing\n[WShowIf(nameof(weaponPrefab), WShowIfComparison.IsNotNull)]\npublic int weaponDamage = 10;  // User may not realize they need to assign prefab\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#3-avoid-deep-nesting","title":"3. Avoid Deep Nesting","text":"C#<pre><code>// \u2705 GOOD: Flat conditions\npublic bool featureA;\npublic bool featureB;\n\n[WShowIf(nameof(featureA))]\npublic float settingA;\n\n[WShowIf(nameof(featureB))]\npublic float settingB;\n\n// \u274c BAD: Overly complex dependencies\npublic bool enableAdvanced;\npublic bool enableExperimental;\npublic bool enableDangerous;\n\n[WShowIf(nameof(enableAdvanced))]\n[WShowIf(nameof(enableExperimental))]\n[WShowIf(nameof(enableDangerous))]\npublic float obscureSetting;  // Too many hoops to jump through!\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#4-use-enums-for-multi-state-ui","title":"4. Use Enums for Multi-State UI","text":"C#<pre><code>// \u2705 GOOD: Clean enum-based visibility\npublic enum InputMode { Keyboard, Gamepad, Touch }\n\npublic InputMode inputMode;\n\n[WShowIf(nameof(inputMode), WShowIfComparison.Equal, InputMode.Keyboard)]\npublic KeyCode actionKey = KeyCode.Space;\n\n[WShowIf(nameof(inputMode), WShowIfComparison.Equal, InputMode.Gamepad)]\npublic string gamepadButton = \"A\";\n\n[WShowIf(nameof(inputMode), WShowIfComparison.Equal, InputMode.Touch)]\npublic Vector2 touchRegion = new Vector2(100, 100);\n\n// \u274c BAD: Boolean spaghetti\npublic bool useKeyboard;\npublic bool useGamepad;\n\n[WShowIf(nameof(useKeyboard))]\npublic KeyCode actionKey;  // What if both are true?\n\n[WShowIf(nameof(useGamepad))]\npublic string gamepadButton;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#examples","title":"Examples","text":""},{"location":"features/inspector/inspector-conditional-display/#example-1-weapon-configuration","title":"Example 1: Weapon Configuration","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class Weapon : MonoBehaviour\n{\n    public enum WeaponType { Melee, Ranged, Magic }\n\n    public WeaponType weaponType;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Melee)]\n    public float meleeRange = 2f;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Melee)]\n    public float swingSpeed = 1f;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Ranged)]\n    public GameObject projectilePrefab;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Ranged)]\n    public int maxAmmo = 30;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Magic)]\n    public float manaCost = 15f;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Magic)]\n    public ParticleSystem spellEffect;\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#example-2-ai-configuration","title":"Example 2: AI Configuration","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class AIController : MonoBehaviour\n{\n    public bool enableAI = true;\n\n    [WShowIf(nameof(enableAI), inverse: true)]\n    public string disabledMessage = \"AI is disabled\";\n\n    [WShowIf(nameof(enableAI))]\n    public float detectionRange = 10f;\n\n    [WShowIf(nameof(enableAI))]\n    public float attackRange = 2f;\n\n    [WShowIf(nameof(enableAI))]\n    public bool canFlee = true;\n\n    [WShowIf(nameof(canFlee))]\n    [WShowIf(nameof(enableAI))]\n    public float fleeHealthThreshold = 0.3f;\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#example-3-level-based-progression","title":"Example 3: Level-Based Progression","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class PlayerProgression : MonoBehaviour\n{\n    public int level = 1;\n\n    [WShowIf(nameof(level), WShowIfComparison.LessThan, 5)]\n    public string beginnerTip = \"Complete quests to level up\";\n\n    [WShowIf(nameof(level), WShowIfComparison.GreaterThanOrEqual, 5)]\n    public Ability specialAbility;\n\n    [WShowIf(nameof(level), WShowIfComparison.GreaterThanOrEqual, 10)]\n    public Ability advancedAbility;\n\n    [WShowIf(nameof(level), WShowIfComparison.GreaterThanOrEqual, 20)]\n    public Ability ultimateAbility;\n\n    [WShowIf(nameof(level), WShowIfComparison.GreaterThanOrEqual, 20)]\n    public GameObject legendaryWeapon;\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#example-4-multiplayer-settings","title":"Example 4: Multiplayer Settings","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class MultiplayerConfig : MonoBehaviour\n{\n    public bool isHost = false;\n\n    [WShowIf(nameof(isHost))]\n    public int maxPlayers = 4;\n\n    [WShowIf(nameof(isHost))]\n    public string serverPassword;\n\n    [WShowIf(nameof(isHost), inverse: true)]\n    public string serverAddress = \"localhost\";\n\n    [WShowIf(nameof(isHost), inverse: true)]\n    public int serverPort = 7777;\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#example-5-graphics-quality","title":"Example 5: Graphics Quality","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class GraphicsSettings : MonoBehaviour\n{\n    public enum QualityLevel { Low, Medium, High, Ultra }\n\n    public QualityLevel quality = QualityLevel.Medium;\n\n    [WShowIf(nameof(quality), WShowIfComparison.GreaterThanOrEqual, QualityLevel.Medium)]\n    public bool enableShadows = true;\n\n    [WShowIf(nameof(quality), WShowIfComparison.GreaterThanOrEqual, QualityLevel.High)]\n    public bool enableReflections = true;\n\n    [WShowIf(nameof(quality), WShowIfComparison.Equal, QualityLevel.Ultra)]\n    public bool enableRayTracing = false;\n\n    [WShowIf(nameof(quality), WShowIfComparison.Equal, QualityLevel.Ultra)]\n    public int antiAliasingLevel = 8;\n}\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/inspector/inspector-conditional-display/#field-not-showing","title":"Field Not Showing","text":"<p>Problem: Field doesn't appear when a condition should be true</p> <p>Solutions:</p> <ol> <li>Check field name spelling in <code>conditionField</code> parameter</li> <li>Verify condition field is <code>public</code> (private fields not supported)</li> <li>Test with <code>inverse: true</code> to see if logic is inverted</li> <li>Use <code>WShowIfComparison.Equal</code> explicitly instead of relying on default</li> </ol>"},{"location":"features/inspector/inspector-conditional-display/#condition-field-not-found","title":"Condition Field Not Found","text":"<p>Problem: Error: \"Could not find field/property 'X'\"</p> <p>Solutions:</p> <ol> <li>Ensure the condition field exists and is spelled correctly</li> <li>Make condition field <code>public</code> (not <code>private</code> or <code>protected</code>)</li> <li>For properties, ensure they have a public getter</li> <li>For methods, ensure they're public and parameterless</li> </ol>"},{"location":"features/inspector/inspector-conditional-display/#multiple-conditions-not-working","title":"Multiple Conditions Not Working","text":"<p>Problem: Stacking <code>[WShowIf]</code> doesn't work as expected</p> <p>Current Behavior: Multiple attributes create AND logic (all must be true)</p> <p>Workaround for OR logic: Use a helper property</p> C#<pre><code>public bool condition1;\npublic bool condition2;\n\npublic bool EitherCondition =&gt; condition1 || condition2;\n\n[WShowIf(nameof(EitherCondition))]\npublic float myField;\n</code></pre>"},{"location":"features/inspector/inspector-conditional-display/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Inspector Grouping Attributes - WGroup layouts</li> <li>Inspector Selection Attributes - Dropdowns and toggles</li> <li>Editor Tools Guide - Other editor utilities</li> </ul> <p>Next Steps:</p> <ul> <li>Add conditional visibility to reduce inspector clutter</li> <li>Combine with <code>[WGroup]</code> for organized, dynamic layouts</li> <li>Use enums to create mode-based UIs</li> <li>Experiment with numeric comparisons for level/threshold-based features</li> </ul>"},{"location":"features/inspector/inspector-grouping-attributes/","title":"Inspector Grouping Attributes","text":"<p>Organize your inspector without writing custom editors.</p> <p>Unity Helpers provides powerful grouping attributes that create boxed sections and collapsible foldouts with zero boilerplate. These attributes rival commercial tools like Odin Inspector while offering unique features like auto-inclusion.</p>"},{"location":"features/inspector/inspector-grouping-attributes/#table-of-contents","title":"Table of Contents","text":"<ul> <li>WGroup &amp; WGroupEnd</li> <li>Common Features</li> <li>Configuration</li> <li>Best Practices</li> <li>Examples</li> </ul>"},{"location":"features/inspector/inspector-grouping-attributes/#wgroup-wgroupend","title":"WGroup &amp; WGroupEnd","text":"<p>Creates boxed inspector sections with optional collapsible headers and automatic field inclusion.</p> <p>\u26a0\ufe0f Important: <code>[WGroupEnd]</code> must be placed on the last field you want included in the group. The field with <code>[WGroupEnd]</code> IS included in the group, and then the group closes for subsequent fields.</p>"},{"location":"features/inspector/inspector-grouping-attributes/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class CharacterStatsWGroup : MonoBehaviour\n{\n    // Simple box with 4 fields\n    [WGroup(\"combat\", \"Combat Stats\")]\n    public float maxHealth = 100f;\n    public float defense = 10f;\n    public float attackPower = 25f;\n    [WGroupEnd(\"combat\")]  // criticalChance IS included, then group closes\n    public float criticalChance = 0.15f;\n\n    public string characterName; // Not in group (comes after WGroupEnd)\n}\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#parameters","title":"Parameters","text":"C#<pre><code>[WGroup(\n    string groupName,                    // Required: Unique identifier\n    string displayName = null,           // Optional: Header text (defaults to groupName)\n    int autoIncludeCount = UseGlobalAutoInclude,  // Auto-include N fields (or use global setting)\n    bool collapsible = false,            // Enable foldout behavior\n    bool startCollapsed = false,         // Initial collapsed state\n    bool hideHeader = false,             // Draw body without header bar\n    string parentGroup = null            // Optional: Nest inside another group\n)]\n</code></pre> <p>\ud83d\udca1 Use the optional <code>CollapseBehavior</code> named argument (or <code>startCollapsed: true</code>) to override the project-wide default configured under Project Settings \u25b8 Wallstop Studios \u25b8 Unity Helpers \u25b8 Start WGroups Collapsed. Example:</p> C#<pre><code>[WGroup(\n    \"advanced\",\n    collapsible: true,\n    CollapseBehavior = WGroupAttribute.WGroupCollapseBehavior.ForceExpanded\n)]\n</code></pre> <p><code>CollapseBehavior</code> options:</p> <ul> <li><code>UseProjectSetting</code> (default) \u2013 defers to the Unity Helpers project setting.</li> <li><code>ForceExpanded</code> \u2013 always starts expanded.</li> <li><code>ForceCollapsed</code> \u2013 always starts collapsed (equivalent to <code>startCollapsed: true</code>).</li> </ul>"},{"location":"features/inspector/inspector-grouping-attributes/#auto-inclusion-modes","title":"Auto-Inclusion Modes","text":""},{"location":"features/inspector/inspector-grouping-attributes/#1-explicit-count","title":"1. Explicit Count","text":"C#<pre><code>[WGroup(\"items\", \"Inventory\", autoIncludeCount: 3)]\npublic GameObject weapon;     // Field 1: in group\npublic GameObject armor;      // Field 2: in group (auto-included)\n[WGroupEnd(\"items\")]          // accessory IS included (field 3), then group closes\npublic GameObject accessory;  // Field 3: in group (last field)\n\npublic int gold;  // NOT in group (comes after WGroupEnd)\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#2-infinite-auto-include","title":"2. Infinite Auto-Include","text":"C#<pre><code>[WGroup(\"settings\", \"Settings\", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]\npublic bool enableSound;   // In group\npublic bool enableMusic;   // In group (auto-included)\npublic float volume;       // In group (auto-included)\n// ... 20 more fields ...  // All auto-included\n[WGroupEnd(\"settings\")]    // lastField IS included, then group closes\npublic bool lastField;     // In group (last field)\n\npublic int outsideGroup;   // NOT in group (comes after WGroupEnd)\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#3-global-default","title":"3. Global Default","text":"C#<pre><code>// Uses WGroupAutoIncludeRowCount from ProjectSettings/UnityHelpersSettings.asset (default: 4)\n[WGroup(\"stats\", \"Stats\")]  // autoIncludeCount defaults to UseGlobalAutoInclude\npublic int strength;        // Field 1: in group\npublic int intelligence;    // Field 2: in group (auto-included)\npublic int agility;         // Field 3: in group (auto-included)\n[WGroupEnd(\"stats\")]        // luck IS included (field 4), then group closes\npublic int luck;            // Field 4: in group (last field)\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#collapsible-groups","title":"Collapsible Groups","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WGroupEndExample : MonoBehaviour\n{\n    [WGroup(\"advanced\", \"Advanced Options\", collapsible: true, startCollapsed: true)]\n    public float raycastDistance = 100f;  // In group\n    public LayerMask collisionMask;       // In group (auto-included)\n\n    [WGroupEnd(\"advanced\")]               // debugDraw IS included, then group closes\n    public bool debugDraw;                // In group (last field)\n\n    public bool someOtherField;           // NOT in group (comes after WGroupEnd)\n}\n</code></pre> <p>Animation Settings:</p> <ul> <li>Speed controlled by <code>UnityHelpersSettings.WGroupFoldoutSpeed</code> (default: 2.0, range: 2.0-12.0)</li> <li>Enable/disable via <code>UnityHelpersSettings.WGroupFoldoutTweenEnabled</code> (default: enabled)</li> </ul> <p>Configure in Project Settings \u2192 Unity Helpers or see Inspector Settings for details.</p>"},{"location":"features/inspector/inspector-grouping-attributes/#hiding-headers","title":"Hiding Headers","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class HealthExample : MonoBehaviour\n{\n    [WGroup(\"stealth\", \"\", hideHeader: true)]\n    public float opacity = 1f;       // In group\n\n    [WGroupEnd(\"stealth\")]           // isVisible IS included, then group closes\n    public bool isVisible = true;    // In group (last field)\n}\n</code></pre> <p>Use Cases:</p> <ul> <li>Visual separation without labels</li> <li>Nested grouping styles</li> <li>Minimalist inspector layouts</li> </ul>"},{"location":"features/inspector/inspector-grouping-attributes/#nested-groups","title":"Nested Groups","text":"<p>Use the <code>parentGroup</code> parameter to nest one group inside another. Nested groups render visually inside their parent's box, with accumulated indentation and padding.</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class NestedGroupExample : MonoBehaviour\n{\n    [WGroup(\"outer\", \"Character\")]\n    public string characterName;      // In outer group\n\n    [WGroup(\"inner\", \"Stats\", parentGroup: \"outer\")]\n    public int level;                 // In inner group (nested in outer)\n    public int experience;            // In inner group (auto-included)\n\n    [WGroupEnd(\"inner\")]              // faction IS included in BOTH groups\n    [WGroupEnd(\"outer\")]              // Then both groups close\n    public string faction;            // In inner AND outer groups (last field)\n}\n</code></pre> <p></p> <p>How Nesting Works:</p> <ol> <li>Declare the parent group first with <code>[WGroup(\"outer\", ...)]</code></li> <li>Declare child group with <code>parentGroup: \"outer\"</code> parameter</li> <li>Child groups are rendered recursively inside parent content areas</li> <li>Indentation and padding accumulate for each nesting level</li> <li>Each group maintains its own foldout state when collapsible</li> </ol> <p>Multiple Nesting Levels:</p> C#<pre><code>[WGroup(\"level1\", \"Level 1\")]\npublic string field1;           // In level1 only\n\n[WGroup(\"level2\", \"Level 2\", parentGroup: \"level1\")]\npublic string field2;           // In level2 (nested in level1)\n\n[WGroup(\"level3\", \"Level 3\", parentGroup: \"level2\")]\npublic string field3;           // In level3 (nested in level2 \u2192 level1)\n[WGroupEnd(\"level3\")]           // field4 IS included in all three groups\n[WGroupEnd(\"level2\")]           // Then all groups close in order\n[WGroupEnd(\"level1\")]\npublic string field4;           // In level3, level2, AND level1 (last field)\n</code></pre> <p>Sibling Nested Groups:</p> C#<pre><code>[WGroup(\"parent\", \"Parent\")]\npublic string parentField;      // In parent group\n\n[WGroup(\"child1\", \"Child 1\", parentGroup: \"parent\")]\npublic string child1Field;      // In child1 (nested in parent)\n\n[WGroupEnd(\"child1\")]           // child2Field starts NEW group, so closes child1 first\n[WGroup(\"child2\", \"Child 2\", parentGroup: \"parent\")]\npublic string child2Field;      // In child2 (nested in parent)\n\n[WGroupEnd(\"child2\")]           // afterParent IS included in child2 AND parent\n[WGroupEnd(\"parent\")]           // Then both groups close\npublic string afterParent;      // In child2 AND parent groups (last field)\n</code></pre> <p>Important Notes:</p> <ul> <li>Parent group must be declared before or on the same property as the child</li> <li>Circular references are detected and logged as warnings; affected groups are treated as top-level</li> <li>If <code>parentGroup</code> references a non-existent group, the child is rendered as a top-level group</li> </ul>"},{"location":"features/inspector/inspector-grouping-attributes/#wgroupend-variants","title":"WGroupEnd Variants","text":"<p>\u26a0\ufe0f Key Point: <code>[WGroupEnd]</code> must always be attached to a field. The field with <code>[WGroupEnd]</code> IS included in the group (via auto-include), and then the group closes.</p>"},{"location":"features/inspector/inspector-grouping-attributes/#1-end-specific-group","title":"1. End Specific Group","text":"C#<pre><code>[WGroup(\"combat\", \"Combat Stats\")]\npublic int health;              // In group\npublic int defense;             // In group (auto-included)\n[WGroupEnd(\"combat\")]           // stamina IS included, then \"combat\" closes\npublic int stamina;             // In group (last field)\n\npublic int unrelatedField;      // NOT in group\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#2-end-multiple-groups","title":"2. End Multiple Groups","text":"<p>When closing nested groups, stack multiple <code>[WGroupEnd]</code> attributes on the last field:</p> C#<pre><code>[WGroup(\"outer\", \"Outer\")]\npublic int outerField;          // In outer\n\n[WGroup(\"inner\", \"Inner\", parentGroup: \"outer\")]\npublic int innerField;          // In inner (nested in outer)\n\n[WGroupEnd(\"inner\")]            // lastField IS included in both groups\n[WGroupEnd(\"outer\")]            // Then both groups close\npublic int lastField;           // In inner AND outer (last field)\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#3-close-all-active-groups","title":"3. Close All Active Groups","text":"<p>Omit the group name to close all currently active auto-include groups:</p> C#<pre><code>[WGroup(\"settings\", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]\npublic bool enableSound;        // In group\npublic float volume;            // In group (auto-included)\n[WGroupEnd]                     // lastSetting IS included, then ALL groups close\npublic bool lastSetting;        // In group (last field)\n\npublic int outsideAllGroups;    // NOT in any group\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#common-features","title":"Common Features","text":""},{"location":"features/inspector/inspector-grouping-attributes/#auto-include-constants","title":"Auto-Include Constants","text":"C#<pre><code>public class WGroupAttribute\n{\n    public const int InfiniteAutoInclude = -1;    // Include until WGroupEnd\n    public const int UseGlobalAutoInclude = -2;   // Default: use project setting\n}\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#shared-group-names","title":"Shared Group Names","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WGroupOutOfOrderExample : MonoBehaviour\n{\n    [WGroup(\"settings\", \"Game Settings\", autoIncludeCount: 1)]\n    public float masterVolume;\n    public float musicVolume;\n\n    public int numChannels;\n\n    // Later in the same script...\n    [WGroup(\"settings\", autoIncludeCount: 1)] // Reuses \"Game Settings\" header, included in original group\n    public float sfxVolume;\n\n    [WGroupEnd(\"settings\")]\n    public bool enableSound;\n}\n</code></pre> <p>Note: Multiple <code>[WGroup]</code> attributes with the same <code>groupName</code> merge into a single group instance. This allows for logical grouping of related fields that may not be contiguous in code.</p>"},{"location":"features/inspector/inspector-grouping-attributes/#configuration","title":"Configuration","text":""},{"location":"features/inspector/inspector-grouping-attributes/#global-settings","title":"Global Settings","text":"<p>All grouping attributes respect project-wide settings defined in <code>UnityHelpersSettings</code>:</p> <p>Location: <code>ProjectSettings/UnityHelpersSettings.asset</code></p> <p>Settings:</p> <ul> <li><code>WGroupAutoIncludeRowCount</code> (default: 4) - Default fields to auto-include</li> <li><code>WGroupStartCollapsed</code> (default: true) - Whether collapsible groups start collapsed</li> <li><code>WGroupFoldoutTweenEnabled</code> (default: true) - Enable expand/collapse animations</li> <li><code>WGroupFoldoutSpeed</code> (default: 2.0, range: 2-12) - Animation speed</li> </ul> <p></p>"},{"location":"features/inspector/inspector-grouping-attributes/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/inspector-grouping-attributes/#1-consistent-naming","title":"1. Consistent Naming","text":"C#<pre><code>// \u2705 GOOD: Clear, descriptive group names\n[WGroup(\"combat\", \"Combat Stats\")]\n[WGroup(\"movement\", \"Movement Settings\")]\n[WGroup(\"visuals\", \"Visual Effects\")]\n\n// \u274c BAD: Vague or inconsistent\n[WGroup(\"group1\", \"Stuff\")]\n[WGroup(\"misc\", \"Things\")]\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#2-auto-inclusion-strategy","title":"2. Auto-Inclusion Strategy","text":"C#<pre><code>// \u2705 GOOD: Explicit count for small groups\n[WGroup(\"position\", \"Position\", autoIncludeCount: 3)]\npublic Vector3 position;        // Field 1: in group\npublic Quaternion rotation;     // Field 2: in group (auto-included)\n[WGroupEnd(\"position\")]         // scale IS included (field 3), then group closes\npublic Vector3 scale;           // Field 3: in group (last field)\n\n// \u2705 GOOD: Infinite for dynamic/long lists\n[WGroup(\"inventory\", \"Items\", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]\npublic List&lt;GameObject&gt; weapons;      // In group\npublic List&lt;GameObject&gt; consumables;  // In group (auto-included)\n// ... many more fields ...           // All auto-included\n[WGroupEnd(\"inventory\")]              // lastItem IS included, then group closes\npublic int lastItem;                  // In group (last field)\n\n// \u274c BAD: Infinite without WGroupEnd (includes everything below!)\n[WGroup(\"bad\", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]\npublic int field1;\npublic int field2;\n// Oops, forgot [WGroupEnd]!\npublic string unrelatedField;  // Also included!\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#3-collapsible-vs-always-open","title":"3. Collapsible vs. Always-Open","text":"C#<pre><code>// \u2705 GOOD: Always-visible for frequently accessed data\n[WGroup(\"core\", \"Core Stats\", collapsible: false)]\npublic float health;          // In group\n[WGroupEnd(\"core\")]           // energy IS included, then group closes\npublic float energy;          // In group (last field)\n\n// \u2705 GOOD: Collapsible for optional/advanced features\n[WGroup(\"advanced\", \"Advanced\", collapsible: true, startCollapsed: true)]\npublic float debugParameter;       // In group\n[WGroupEnd(\"advanced\")]            // experimentalFeature IS included, then closes\npublic bool experimentalFeature;   // In group (last field)\n\n// \u274c BAD: Everything collapsible (hides important data)\n[WGroup(\"important\", \"Critical Settings\", collapsible: true, startCollapsed: true, autoIncludeCount: 0)]\npublic float maxHealth;  // Why hide this?\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#examples","title":"Examples","text":""},{"location":"features/inspector/inspector-grouping-attributes/#example-1-rpg-character-stats","title":"Example 1: RPG Character Stats","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class RPGCharacter : MonoBehaviour\n{\n    [WGroup(\"identity\", \"Identity\")]\n    public string characterName;           // In identity group\n    public Sprite portrait;                // In identity group (auto-included)\n    public string className;               // In identity group (auto-included)\n\n    [WGroupEnd(\"identity\")]                // strength IS included, then identity closes\n    [WGroup(\"attributes\", \"Base Attributes\", collapsible: true)]\n    public int strength = 10;              // In identity (last) AND starts attributes\n    public int agility = 10;               // In attributes (auto-included)\n    public int intelligence = 10;          // In attributes (auto-included)\n    public int vitality = 10;              // In attributes (auto-included)\n\n    [WGroupEnd(\"attributes\")]              // maxHealth IS included, then attributes closes\n    [WGroup(\"combat\", \"Combat Stats\")]\n    public float maxHealth = 100f;         // In attributes (last) AND starts combat\n    public float attackPower = 25f;        // In combat (auto-included)\n    public float defense = 15f;            // In combat (auto-included)\n\n    [WGroupEnd(\"combat\")]                  // learnedSkills IS included, then combat closes\n    [WGroup(\"skills\", \"Skills\", collapsible: true, startCollapsed: true)]\n    public List&lt;string&gt; learnedSkills = new(); // In combat (last) AND starts skills\n\n    [WGroupEnd(\"skills\")]                  // skillPoints IS included, then skills closes\n    public int skillPoints = 0;            // In skills (last field)\n}\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#example-2-weapon-configuration","title":"Example 2: Weapon Configuration","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic enum DamageType\n{\n    Physical,\n    Magic,\n}\n\npublic class WeaponConfig2 : MonoBehaviour\n{\n    [WGroup(\"basic\", \"Basic Info\", autoIncludeCount: 2)]\n    public string weaponName;              // Field 1: in basic group\n\n    [WGroupEnd(\"basic\")]                   // icon IS included (field 2), then closes\n    public Sprite icon;                    // Field 2: in basic (last field)\n\n    [WGroup(\"damage\", \"Damage\", collapsible: true)]\n    public float baseDamage = 10f;         // In damage group\n    public float criticalMultiplier = 2f; // In damage (auto-included)\n\n    [WGroupEnd(\"damage\")]                  // damageType IS included, then closes\n    public DamageType damageType;          // In damage (last field)\n\n    [WGroup(\"effects\", \"Special Effects\", collapsible: true, startCollapsed: true)]\n    public ParticleSystem hitEffect;       // In effects group\n    public AudioClip hitSound;             // In effects (auto-included)\n\n    [WGroupEnd(\"effects\")]                 // effectDuration IS included, then closes\n    public float effectDuration = 1f;      // In effects (last field)\n\n    [WGroup(\"advanced\", \"Advanced Settings\", collapsible: true, startCollapsed: true)]\n    public float projectileSpeed = 20f;    // In advanced group\n    public LayerMask targetLayers;         // In advanced (auto-included)\n\n    [WGroupEnd(\"advanced\")]                // debugMode IS included, then closes\n    public bool debugMode = false;         // In advanced (last field)\n}\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#example-3-dynamic-form-with-many-fields","title":"Example 3: Dynamic Form with Many Fields","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class LevelSettings : MonoBehaviour\n{\n    [WGroup(\"general\", \"General\", autoIncludeCount: 3)]\n    public string levelName;               // Field 1: in general group\n    public Sprite thumbnail;               // Field 2: in general (auto-included)\n\n    [WGroupEnd(\"general\")]                 // description IS included (field 3), then closes\n    public string description;             // Field 3: in general (last field)\n\n    [WGroup(\n        \"environment\",\n        \"Environment\",\n        collapsible: true,\n        startCollapsed: true,\n        autoIncludeCount: WGroupAttribute.InfiniteAutoInclude\n    )]\n    public Color skyColor;                 // In environment group\n    public Color fogColor;                 // In environment (auto-included)\n    public float fogDensity;               // In environment (auto-included)\n    public Light directionalLight;         // In environment (auto-included)\n    public Cubemap skybox;                 // In environment (auto-included)\n    public float ambientIntensity;         // In environment (auto-included)\n\n    [WGroupEnd(\"environment\")]             // sunIntensity IS included, then closes\n    public float sunIntensity;             // In environment (last field)\n\n    [WGroup(\"gameplay\", \"Gameplay Rules\", collapsible: true, startCollapsed: false)]\n    public int enemyCount = 10;            // In gameplay group\n    public float difficultyMultiplier = 1f; // In gameplay (auto-included)\n\n    [WGroupEnd(\"gameplay\")]                // allowRespawns IS included, then closes\n    public bool allowRespawns = true;      // In gameplay (last field)\n\n    [WGroup(\"debug\", \"Debug Options\", collapsible: true, startCollapsed: true)]\n    public bool godMode = false;           // In debug group\n    public bool unlimitedAmmo = false;     // In debug (auto-included)\n\n    [WGroupEnd(\"debug\")]                   // showHitboxes IS included, then closes\n    public bool showHitboxes = false;      // In debug (last field)\n}\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#example-4-nested-configuration","title":"Example 4: Nested Configuration","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class AIController : MonoBehaviour\n{\n    [WGroup(\"outer\", \"AI Configuration\")]\n    [WGroup(\"detection\", \"Detection\", parentGroup: \"outer\")]\n    public float sightRange = 10f;         // In detection (nested in outer)\n\n    [WGroupEnd(\"detection\")]               // hearingRange IS included, then detection closes\n    public float hearingRange = 5f;        // In detection (last) AND outer (auto-included)\n\n    [WGroup(\"behavior\", \"Behavior\", parentGroup: \"outer\")]\n    public float aggressionLevel = 0.5f;   // In behavior (nested in outer)\n\n    [WGroupEnd(\"behavior\")]                // retreatThreshold IS included in both\n    [WGroupEnd(\"outer\")]                   // Then both groups close\n    public float retreatThreshold = 0.2f;  // In behavior (last) AND outer (last)\n}\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/inspector/inspector-grouping-attributes/#group-not-appearing","title":"Group Not Appearing","text":"<p>Problem: Fields not showing in a group</p> <p>Solutions:</p> <ol> <li>Check <code>autoIncludeCount</code> - make sure it includes all desired fields</li> <li>Verify <code>WGroupEnd</code> placement - the field WITH <code>WGroupEnd</code> IS included, fields AFTER are excluded</li> <li>Ensure group names match between <code>WGroup</code> and <code>WGroupEnd</code></li> </ol> C#<pre><code>// \u274c WRONG: Count too low (only 2 fields included, but intelligence has WGroupEnd)\n[WGroup(\"stats\", autoIncludeCount: 2)]\npublic int strength;      // Field 1: in group\npublic int agility;       // Field 2: in group (auto-included)\n[WGroupEnd(\"stats\")]      // intelligence would be field 3, but count is only 2!\npublic int intelligence;  // NOT included - auto-include budget exhausted before WGroupEnd\n\n\n// \u2705 CORRECT: Increase count to include the WGroupEnd field\n[WGroup(\"stats\", autoIncludeCount: 3)]\npublic int strength;      // Field 1: in group\npublic int agility;       // Field 2: in group (auto-included)\n[WGroupEnd(\"stats\")]      // intelligence IS included (field 3), then group closes\npublic int intelligence;  // Field 3: in group (last field)\n</code></pre>"},{"location":"features/inspector/inspector-grouping-attributes/#animation-not-working","title":"Animation Not Working","text":"<p>Problem: Groups don't animate when collapsed/expanded</p> <p>Solutions:</p> <ol> <li>Check <code>UnityHelpersSettings.WGroupFoldoutTweenEnabled</code> is <code>true</code></li> <li>Ensure <code>collapsible: true</code> is set for WGroup</li> <li>Verify <code>WGroupFoldoutSpeed</code> isn't set too low (minimum is 2.0)</li> <li>Open Project Settings \u2192 Unity Helpers to review settings</li> </ol>"},{"location":"features/inspector/inspector-grouping-attributes/#compatibility","title":"Compatibility","text":"<p>WGroup operates at the inspector level, so existing property drawers and custom inspectors continue to work. Groups appear in the order of their first declaration, and multi-object editing remains fully supported.</p>"},{"location":"features/inspector/inspector-grouping-attributes/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Inspector Buttons - WButton for method invocation</li> <li>Inspector Settings - Configuration reference</li> <li>Editor Tools Guide - Other editor utilities</li> </ul> <p>Next Steps:</p> <ul> <li>Try grouping your existing scripts with <code>[WGroup]</code></li> <li>Explore <code>[WButton]</code> to add method buttons to your groups</li> </ul>"},{"location":"features/inspector/inspector-inline-editor/","title":"Inspector Inline Editor (WInLineEditor)","text":"<p>Edit nested objects without losing context.</p> <p>The <code>[WInLineEditor]</code> attribute embeds the inspector for object references (ScriptableObjects, Materials, Components, Textures, etc.) directly below the field. No more clicking through to edit configuration \u2014 everything stays in view.</p>"},{"location":"features/inspector/inspector-inline-editor/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Basic Usage</li> <li>Display Modes</li> <li>Configuration Options</li> <li>Animation Settings</li> <li>Best Practices</li> <li>Examples</li> </ul>"},{"location":"features/inspector/inspector-inline-editor/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class AbilityConfig : ScriptableObject\n{\n    public string displayName;\n    public float cooldown;\n    public Sprite icon;\n}\n\npublic class Character : MonoBehaviour\n{\n    [WInLineEditor]\n    public AbilityConfig primaryAbility;  // Editable inline!\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>WInLineEditor with embedded inspector for a ScriptableObject reference</p>"},{"location":"features/inspector/inspector-inline-editor/#display-modes","title":"Display Modes","text":"<p>Control how the inline editor appears using <code>WInLineEditorMode</code>:</p> Mode Behavior <code>AlwaysExpanded</code> Always shows the inline inspector (no foldout) <code>FoldoutExpanded</code> Shows a foldout that starts expanded <code>FoldoutCollapsed</code> Shows a foldout that starts collapsed (default) C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WInlineEditorModes : MonoBehaviour\n{\n    // Always visible\n    [WInLineEditor(WInLineEditorMode.AlwaysExpanded)]\n    public AbilityConfig alwaysVisibleConfig;\n\n    // Foldout, starts open\n    [WInLineEditor(WInLineEditorMode.FoldoutExpanded)]\n    public AbilityConfig expandedByDefault;\n\n    // Foldout, starts closed (default behavior)\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\n    public AbilityConfig collapsedByDefault;\n\n    // Uses global setting from Unity Helpers Settings\n    [WInLineEditor]\n    public AbilityConfig usesGlobalSetting;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Comparison of AlwaysExpanded, FoldoutExpanded, and FoldoutCollapsed modes</p>"},{"location":"features/inspector/inspector-inline-editor/#configuration-options","title":"Configuration Options","text":"<p>Fine-tune the presentation with constructor parameters:</p> C#<pre><code>[WInLineEditor(\n    WInLineEditorMode.FoldoutCollapsed,  // Display mode\n    inspectorHeight: 200f,                // Vertical space (min 160)\n    drawObjectField: true,                // Show object picker\n    drawHeader: true,                     // Show bold header with ping button\n    drawPreview: false,                   // Render preview area\n    previewHeight: 64f,                   // Preview area height\n    enableScrolling: true,                // Wrap in scroll view\n    minInspectorWidth: 520f               // Horizontal scroll threshold (0 = disabled)\n)]\npublic AbilityConfig detailedConfig;\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#parameter-reference","title":"Parameter Reference","text":"Parameter Default Description <code>inspectorHeight</code> 200 Vertical space for inspector body (minimum 160) <code>drawObjectField</code> true Show the object picker field next to label <code>drawHeader</code> true Show bold header with ping button <code>drawPreview</code> false Render preview area (if target editor supports it) <code>previewHeight</code> 64 Height of preview area when enabled <code>enableScrolling</code> true Wrap inspector body in scroll view <code>minInspectorWidth</code> 520 Width threshold for horizontal scrollbar (0 = disabled)"},{"location":"features/inspector/inspector-inline-editor/#examples-with-options","title":"Examples with Options","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Utils;\n\npublic class AbilityDatabase : ScriptableObjectSingleton&lt;AbilityDatabase&gt;\n{\n    // Compact: no header, no object field, fixed height\n    [WInLineEditor(\n        WInLineEditorMode.AlwaysExpanded,\n        inspectorHeight: 180f,\n        drawObjectField: false,\n        drawHeader: false\n    )]\n    public AbilityConfig compactView;\n\n    // Full featured: preview, scrolling, header with ping\n    [WInLineEditor(\n        WInLineEditorMode.FoldoutExpanded,\n        inspectorHeight: 300f,\n        drawPreview: true,\n        previewHeight: 80f,\n        enableScrolling: true\n    )]\n    public Sprite abilityIcon;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Different configuration combinations showing compact vs full-featured layouts</p>"},{"location":"features/inspector/inspector-inline-editor/#animation-settings","title":"Animation Settings","text":"<p>Foldout animations create smooth expand/collapse transitions. Configure globally via Edit &gt; Project Settings &gt; Unity Helpers:</p> Setting Default Description <code>InlineEditorFoldoutTweenEnabled</code> true Enable/disable smooth animations <code>InlineEditorFoldoutSpeed</code> 2.0 Animation speed (2.0 - 12.0) C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WInLineEditorAnimation : MonoBehaviour\n{\n    // Animation applies to foldout modes only\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)] // Animated\n    public AbilityConfig animatedFoldout;\n\n    [WInLineEditor(WInLineEditorMode.AlwaysExpanded)] // No animation\n    public AbilityConfig noAnimation;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Smooth expand/collapse animation with configurable speed</p> <p>See also: Inspector Settings Reference for complete settings documentation.</p>"},{"location":"features/inspector/inspector-inline-editor/#supported-types","title":"Supported Types","text":"<p>WInLineEditor works with any Unity Object reference:</p> <ul> <li>ScriptableObjects \u2014 Configuration assets, data containers</li> <li>Materials \u2014 Shader properties inline</li> <li>Textures \u2014 Texture import settings</li> <li>Components \u2014 Other MonoBehaviours on GameObjects</li> <li>Any UnityEngine.Object \u2014 Custom asset types</li> </ul> C#<pre><code>public class VisualConfig : MonoBehaviour\n{\n    [WInLineEditor]\n    public Material sharedMaterial;\n\n    [WInLineEditor(drawPreview: true, previewHeight: 128f)]\n    public Texture2D backgroundTexture;\n\n    [WInLineEditor]\n    public AudioClip soundEffect;\n}\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#features","title":"Features","text":"<ul> <li>Bespoke implementation \u2014 No Odin dependency, tailored for common workflows</li> <li>Native editor reuse \u2014 Respects custom inspectors, validation, and undo</li> <li>Optional scroll view \u2014 Keeps large inspectors usable without stealing space</li> <li>Preview support \u2014 For assets that implement <code>HasPreviewGUI</code></li> <li>Ping button \u2014 Quick navigation to assets in the Project window</li> <li>Smooth animations \u2014 Configurable expand/collapse transitions</li> </ul>"},{"location":"features/inspector/inspector-inline-editor/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/inspector-inline-editor/#1-use-foldouts-for-optional-content","title":"1. Use Foldouts for Optional Content","text":"C#<pre><code>// Collapsed by default - keeps inspector clean\n[WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\npublic AdvancedSettings advancedSettings;\n\n// Always visible for frequently-edited config\n[WInLineEditor(WInLineEditorMode.AlwaysExpanded)]\npublic CoreSettings coreSettings;\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#2-adjust-height-for-content-size","title":"2. Adjust Height for Content Size","text":"C#<pre><code>// Short config - minimal height\n[WInLineEditor(inspectorHeight: 160f)]\npublic SimpleConfig simple;\n\n// Complex config - more room\n[WInLineEditor(inspectorHeight: 400f, enableScrolling: true)]\npublic ComplexConfig complex;\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#3-combine-with-wgroup-for-organization","title":"3. Combine with WGroup for Organization","text":"C#<pre><code>[WGroup(\"Visual Settings\")]\n[WInLineEditor]\npublic Material material;      // In group\n\n[WInLineEditor]\n[WGroupEnd]                    // texture IS included, then group closes\npublic Texture2D texture;      // In group (last field)\n\n[WGroup(\"Audio Settings\")]\n[WInLineEditor]\n[WGroupEnd]                    // clip IS included, then group closes\npublic AudioClip clip;         // In group (last field)\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#4-disable-object-field-for-embedded-data","title":"4. Disable Object Field for Embedded Data","text":"C#<pre><code>// When the reference shouldn't change, hide the picker\n[WInLineEditor(drawObjectField: false)]\npublic FixedConfiguration config;\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#examples","title":"Examples","text":""},{"location":"features/inspector/inspector-inline-editor/#example-1-character-ability-system","title":"Example 1: Character Ability System","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[CreateAssetMenu(menuName = \"Game/Ability Config\")]\npublic class AbilityConfig : ScriptableObject\n{\n    public string displayName;\n    public Sprite icon;\n    public float cooldown = 1f;\n    public float damage = 10f;\n    public GameObject effectPrefab;\n}\n\npublic class CharacterAbilities : MonoBehaviour\n{\n    [WGroup(\"Primary Abilities\")]\n    [WInLineEditor(WInLineEditorMode.FoldoutExpanded)]\n    public AbilityConfig primaryAttack;   // In group\n\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\n    [WGroupEnd]                           // secondaryAttack IS included, then closes\n    public AbilityConfig secondaryAttack; // In group (last field)\n\n    [WGroup(\"Ultimate\")]\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed, inspectorHeight: 250f)]\n    [WGroupEnd]                           // ultimate IS included, then closes\n    public AbilityConfig ultimate;        // In group (last field)\n}\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#example-2-material-editor","title":"Example 2: Material Editor","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class VisualEffectController : MonoBehaviour\n{\n    [WInLineEditor(\n        WInLineEditorMode.FoldoutExpanded,\n        inspectorHeight: 300f,\n        drawPreview: true,\n        previewHeight: 100f)]\n    public Material effectMaterial;\n\n    [WInLineEditor(drawPreview: true, previewHeight: 64f)]\n    public Texture2D noiseTexture;\n}\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#example-3-audio-configuration","title":"Example 3: Audio Configuration","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[CreateAssetMenu(menuName = \"Audio/Sound Config\")]\npublic class SoundConfig : ScriptableObject\n{\n    public AudioClip clip;\n    [Range(0f, 1f)] public float volume = 1f;\n    [Range(0.5f, 2f)] public float pitch = 1f;\n    public bool loop;\n}\n\npublic class AudioManager : MonoBehaviour\n{\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\n    public SoundConfig backgroundMusic;\n\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\n    public SoundConfig buttonClick;\n\n    [WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\n    public SoundConfig victorySound;\n}\n</code></pre>"},{"location":"features/inspector/inspector-inline-editor/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Inspector Grouping Attributes - WGroup layouts</li> <li>Inspector Settings - Global configuration</li> </ul> <p>Next Steps:</p> <ul> <li>Add <code>[WInLineEditor]</code> to ScriptableObject references for inline editing</li> <li>Configure global defaults in Unity Helpers Settings</li> <li>Combine with <code>[WGroup]</code> for organized inspector layouts</li> </ul>"},{"location":"features/inspector/inspector-overview/","title":"Inspector &amp; Serialization Features Overview","text":"<p>Unity Helpers includes a suite of inspector attributes and serialization types that change how you author components and data. These features eliminate repetitive code and provide designer-friendly workflows.</p>"},{"location":"features/inspector/inspector-overview/#why-use-these-features","title":"Why Use These Features?","text":"<p>Time Savings:</p> <ul> <li>Grouping &amp; Organization: Can replace 50+ lines of custom editor code with a single <code>[WGroup]</code> attribute</li> <li>Method Buttons: Expose test methods in the inspector without writing custom editors</li> <li>Conditional Display: Show/hide fields based on values without PropertyDrawer boilerplate</li> <li>Selection Controls: Turn enums into toggle buttons, primitives into dropdowns - all declaratively</li> <li>Serialization: Store GUIDs, dictionaries, sets, and types with built-in Unity support</li> </ul> <p>Features:</p> <ul> <li>Designer-friendly interfaces reduce programmer bottlenecks</li> <li>Project-wide settings ensure consistent styling and behavior</li> <li>Pagination, animation, and polish - built-in</li> </ul>"},{"location":"features/inspector/inspector-overview/#feature-categories","title":"Feature Categories","text":""},{"location":"features/inspector/inspector-overview/#1-layout-organization","title":"1. Layout &amp; Organization","text":"<p>Control how fields are grouped and organized in the inspector:</p> <ul> <li>WGroup &amp; WGroupEnd - Boxed sections with optional collapse, auto-inclusion</li> </ul> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class TwoWGroupExample : MonoBehaviour\n{\n    [WGroup(\"Group 1\", collapsible: true)]\n    public float health;\n    public int intValue;\n    public string stringValue;\n\n    [WGroup(\"Group 2\", collapsible: true)]\n    public float speed;\n    public int otherIntValue;\n    public string otherStringValue;\n}\n</code></pre> <p></p> <p>\u2192 Full Guide: Inspector Grouping Attributes</p>"},{"location":"features/inspector/inspector-overview/#2-inline-editing","title":"2. Inline Editing","text":"<p>Edit nested objects without losing context:</p> <ul> <li>WInLineEditor - Embed inspectors for ScriptableObjects, Materials, Textures directly below the field</li> </ul> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[CreateAssetMenu(fileName = \"PowerUpDefinition\", menuName = \"Power-Up Definition\")]\npublic class PowerUpDefinition : ScriptableObject\n{\n    public string powerUpName;\n    public Sprite icon;\n}\n\npublic class WInLineEditorSimpleExample : MonoBehaviour\n{\n    [WInLineEditor]\n    public PowerUpDefinition powerUp;\n}\n</code></pre> <p> </p> <p>\u2192 Full Guide: Inspector Inline Editor</p>"},{"location":"features/inspector/inspector-overview/#3-method-invocation","title":"3. Method Invocation","text":"<p>Expose methods as clickable buttons in the inspector:</p> <ul> <li>WButton - One-click method execution with result history, async support, custom styling, grouping</li> </ul> C#<pre><code>using System;\nusing System.Collections;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\npublic class WButtonOverviewExample : MonoBehaviour\n{\n    [WButton]\n    private void Test1()\n    {\n        this.Log($\"We did it!\");\n    }\n\n    [WButton]\n    private IEnumerator KindaCoroutine()\n    {\n        this.Log($\"Starting coroutine...\");\n        yield return new WaitForSeconds(1f);\n        this.Log($\"Coroutine finished!\");\n    }\n\n    [WButton]\n    private async Task AsyncWorksToo()\n    {\n        await Task.Delay(TimeSpan.FromSeconds(1));\n        this.Log($\"Did it work?\");\n    }\n\n    [WButton]\n    private async Task AsyncWorksTooWithCancellationTokens(CancellationToken ct)\n    {\n        await Task.Delay(TimeSpan.FromSeconds(1), ct);\n        this.Log($\"Did it work?\");\n    }\n\n    [WButton]\n    private async ValueTask ValueTasks(CancellationToken ct)\n    {\n        await Task.Delay(TimeSpan.FromSeconds(1), ct);\n        this.Log($\"Did it work?\");\n    }\n}\n</code></pre> <p> </p> <p>\u2192 Full Guide: Inspector Buttons</p>"},{"location":"features/inspector/inspector-overview/#4-conditional-display","title":"4. Conditional Display","text":"<p>Show or hide fields based on runtime values:</p> <ul> <li>WShowIf - Visibility rules with comparison operators (Equal, GreaterThan, IsNull, etc.), inversion, stacking</li> </ul> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic enum ExampleEnum\n{\n    Option1,\n    Option2,\n    Option3,\n}\n\npublic class WShowIfExamples : MonoBehaviour\n{\n    public bool toggle;\n\n    [WShowIf(nameof(toggle))]\n    public string hiddenByBool;\n\n    public int intValue;\n\n    [WShowIf(nameof(intValue), WShowIfComparison.GreaterThan, 5)]\n    public string hiddenByInt;\n\n    public ExampleEnum enumValue;\n\n    [WShowIf(nameof(enumValue), ExampleEnum.Option2, ExampleEnum.Option3)]\n    public string hiddenByEnum;\n}\n</code></pre> <p></p> <p>\u2192 Full Guide: Inspector Conditional Display</p>"},{"location":"features/inspector/inspector-overview/#5-selection-dropdowns","title":"5. Selection &amp; Dropdowns","text":"<p>Provide designer-friendly selection controls:</p> <ul> <li>WEnumToggleButtons - Visual toggle buttons for enums and flag enums</li> <li>WValueDropDown - Generic dropdown for any type</li> <li>IntDropdown - Integer selection from predefined values</li> <li>StringInList - String selection with search and pagination</li> </ul> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WEnumToggleButtonOverview : MonoBehaviour\n{\n    [WEnumToggleButtons]\n    [IntDropDown(1, 2, 3, 4, 5, 6, 7, 8)]\n    public List&lt;int&gt; canToggle; // Works in lists!\n\n    [WEnumToggleButtons]\n    [IntDropDown(1, 2, 3, 4, 5, 6, 7, 8)]\n    public int canToggle2;\n\n    [StringInList(typeof(WEnumToggleButtonOverview), nameof(GetStringValues))]\n    public string canSelectString;\n\n    [WValueDropDown(typeof(WEnumToggleButtonOverview), nameof(GetFloatValues))]\n    public float canSelectFloat;\n\n    private IEnumerable&lt;string&gt; GetStringValues()\n    {\n        yield return \"String1\";\n        yield return \"String2\";\n        yield return \"String3\";\n    }\n\n    private IEnumerable&lt;float&gt; GetFloatValues()\n    {\n        yield return 1.0f;\n        yield return 2.0f;\n        yield return 3.0f;\n    }\n}\n</code></pre> <p></p> <p>\u2192 Full Guide: Inspector Selection Attributes</p>"},{"location":"features/inspector/inspector-overview/#6-validation-protection","title":"6. Validation &amp; Protection","text":"<p>Protect data integrity with validation attributes:</p> <ul> <li>WReadOnly - Display fields as read-only in the inspector</li> <li>WNotNull - Validate required references at runtime with <code>CheckForNulls()</code></li> </ul> <p>\u2192 Full Guide: Inspector Validation Attributes</p>"},{"location":"features/inspector/inspector-overview/#7-serialization-types","title":"7. Serialization Types","text":"<p>Unity-friendly wrappers for complex data:</p> <ul> <li>WGuid - Immutable GUID using two longs (optimized for Unity serialization)</li> <li>SerializableDictionary - Key/value pairs with custom drawer</li> <li>SerializableSet - HashSet and SortedSet with duplicate detection, pagination, reordering</li> <li>SerializableType - Type references that survive refactoring</li> <li>SerializableNullable - Nullable value types</li> </ul> <p></p> <p>\u2192 Full Guide: Serialization Types</p>"},{"location":"features/inspector/inspector-overview/#8-project-settings","title":"8. Project Settings","text":"<p>Centralized configuration for all inspector features:</p> <ul> <li>UnityHelpersSettings - Global settings for pagination, colors, animations, history</li> </ul> <p>Location: <code>ProjectSettings/UnityHelpersSettings.asset</code></p> <p>Settings:</p> <ul> <li>Pagination sizes (buttons, sets, dropdowns)</li> <li>Button placement and history capacity</li> <li>Color palettes for themes (light/dark/custom)</li> <li>Animation speeds for foldouts and groups</li> <li>Auto-include defaults</li> </ul> <p></p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WButtonSettingsExample : MonoBehaviour\n{\n    [WButton(colorKey: \"Documentation Example\")]\n    private void Button() { }\n}\n</code></pre> <p></p> <p>\u2192 Full Guide: Inspector Settings</p>"},{"location":"features/inspector/inspector-overview/#quick-start-example","title":"Quick Start Example","text":"<p>Here's a complete example showcasing multiple inspector features together:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class CharacterStats : MonoBehaviour\n{\n    // Grouped fields with collapsible sections\n    [WGroup(\"Combat\", \"Combat Stats\", collapsible: true)]\n    public float maxHealth = 100f;           // In group\n    public float defense = 10f;              // In group (auto-included)\n    [WGroupEnd(\"Combat\")]                    // attackPower IS included, then closes\n    public float attackPower = 25f;          // In group (last field)\n\n    // Conditional visibility based on enum\n    public enum WeaponType { Melee, Ranged, Magic }\n    public WeaponType weaponType;\n\n    [WShowIf(nameof(weaponType), WShowIfComparison.Equal, WeaponType.Ranged)]\n    public int ammoCapacity = 30;\n\n    // Flag enum as toggle buttons\n    [System.Flags]\n    public enum Abilities { None = 0, Jump = 1, Dash = 2, Block = 4 }\n\n    [WEnumToggleButtons(showSelectAll: true)]\n    public Abilities unlockedAbilities;\n\n    // Serializable collections\n    public SerializableDictionary&lt;string, int&gt; stats;\n    public WGuid entityId = WGuid.NewGuid();\n\n    // Inspector button\n    [WButton(\"Reset Stats\")]\n    private void ResetStats() =&gt; maxHealth = defense = attackPower = 100f;\n}\n</code></pre> <p>For individual feature examples, see the detailed guides linked above.</p>"},{"location":"features/inspector/inspector-overview/#feature-comparison","title":"Feature Comparison","text":"Feature Unity Default Odin Inspector Unity Helpers Grouping/Boxes Custom Editor <code>[BoxGroup]</code> <code>[WGroup]</code> Foldouts Custom Editor <code>[FoldoutGroup]</code> <code>[WGroup(collapsible: true)]</code> Method Buttons Custom Editor <code>[Button]</code> <code>[WButton]</code> Conditional Display Custom Drawer <code>[ShowIf]</code> <code>[WShowIf]</code> Enum Toggles Custom Drawer <code>[EnumToggleButtons]</code> <code>[WEnumToggleButtons]</code> Dictionaries Not Supported <code>[ShowInInspector]</code> <code>SerializableDictionary&lt;K,V&gt;</code> Sets Not Supported Custom <code>SerializableHashSet&lt;T&gt;</code> Type References Not Supported Custom <code>SerializableType</code> Nullable Values Not Supported Custom <code>SerializableNullable&lt;T&gt;</code> Color Themes Not Supported Built-in Project Settings Cost Free \\(55-\\)95 Free (MIT)"},{"location":"features/inspector/inspector-overview/#design-philosophy","title":"Design Philosophy","text":"<p>Declarative Over Imperative:</p> <ul> <li>Attributes describe what, not how</li> <li>No custom PropertyDrawers for common patterns</li> <li>Configuration over code</li> </ul> <p>Designer-Friendly:</p> <ul> <li>Visual controls for visual people</li> <li>Reduce programmer bottlenecks</li> <li>Iteration without recompiling</li> </ul> <p>Performance-Conscious:</p> <ul> <li>Uses cached reflection delegates to reduce overhead</li> <li>Uses pooled buffers for UI rendering to reduce allocations</li> <li>Aims to minimize GC allocations</li> </ul> <p>Project-Consistent:</p> <ul> <li>Centralized settings asset</li> <li>Predictable behavior across all inspectors</li> </ul>"},{"location":"features/inspector/inspector-overview/#getting-started","title":"Getting Started","text":"<ol> <li> <p>Install Unity Helpers - See Installation Guide</p> </li> <li> <p>Explore Examples - Check the guides linked above</p> </li> <li> <p>Configure Settings - Open <code>ProjectSettings/UnityHelpersSettings.asset</code> to customize pagination, colors, and animations</p> </li> <li> <p>Add Attributes - Start with <code>[WGroup]</code> and <code>[WButton]</code> for immediate impact</p> </li> <li> <p>Use Serialization Types - Replace custom wrappers with <code>SerializableDictionary</code>, <code>SerializableSet</code>, etc.</p> </li> </ol>"},{"location":"features/inspector/inspector-overview/#detailed-documentation","title":"Detailed Documentation","text":""},{"location":"features/inspector/inspector-overview/#inspector-attributes","title":"Inspector Attributes","text":"<ul> <li>Inspector Grouping Attributes - WGroup layout control</li> <li>Inspector Inline Editor - WInLineEditor for nested object editing</li> <li>Inspector Buttons - WButton for method invocation</li> <li>Inspector Conditional Display - WShowIf for dynamic visibility</li> <li>Inspector Selection Attributes - WEnumToggleButtons, WValueDropDown, IntDropdown, StringInList</li> <li>Inspector Validation Attributes - WReadOnly, WNotNull</li> </ul>"},{"location":"features/inspector/inspector-overview/#serialization","title":"Serialization","text":"<ul> <li>Serialization Types - WGuid, SerializableDictionary, SerializableSet, SerializableType, SerializableNullable</li> </ul>"},{"location":"features/inspector/inspector-overview/#configuration","title":"Configuration","text":"<ul> <li>Inspector Settings - UnityHelpersSettings asset reference</li> </ul>"},{"location":"features/inspector/inspector-overview/#see-also","title":"See Also","text":"<ul> <li>Odin Inspector Migration Guide - Step-by-step migration from Odin Inspector</li> <li>Editor Tools Guide - 20+ automation tools for sprites, animations, validation</li> <li>Relational Components - Auto-wire components with attributes</li> <li>Effects System - Data-driven buffs/debuffs</li> <li>Main Documentation - Complete feature list</li> </ul> <p>Next Steps:</p> <p>Choose a guide based on what you want to learn first:</p> <ul> <li>Want organized inspectors? \u2192 Inspector Grouping Attributes</li> <li>Want method buttons? \u2192 Inspector Buttons</li> <li>Want conditional fields? \u2192 Inspector Conditional Display</li> <li>Want better selection controls? \u2192 Inspector Selection Attributes</li> <li>Want data validation? \u2192 Inspector Validation Attributes</li> <li>Want to serialize complex data? \u2192 Serialization Types</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/","title":"Inspector Selection Attributes","text":"<p>Transform selection controls from dropdowns to designer-friendly interfaces.</p> <p>Unity Helpers provides four powerful selection attributes that replace standard dropdowns with toggle buttons, searchable lists, and type-safe selection controls. Perfect for flags, enums, predefined values, and dynamic option lists.</p>"},{"location":"features/inspector/inspector-selection-attributes/#table-of-contents","title":"Table of Contents","text":"<ul> <li>WEnumToggleButtons</li> <li>WValueDropDown</li> <li>IntDropDown</li> <li>StringInList</li> <li>Comparison Table</li> <li>Best Practices</li> <li>Examples</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#wenumtogglebuttons","title":"WEnumToggleButtons","text":"<p>Draws enum and flag enum fields as a toolbar of toggle buttons.</p>"},{"location":"features/inspector/inspector-selection-attributes/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class EntityPermissions : MonoBehaviour\n{\n    [System.Flags]\n    public enum Permissions\n    {\n        None = 0,\n        Move = 1 &lt;&lt; 0,\n        Attack = 1 &lt;&lt; 1,\n        UseItems = 1 &lt;&lt; 2,\n        CastSpells = 1 &lt;&lt; 3,\n    }\n\n    [WEnumToggleButtons]\n    public Permissions currentPermissions = Permissions.Move | Permissions.Attack;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Flag enum permissions rendered as toggle buttons</p>"},{"location":"features/inspector/inspector-selection-attributes/#parameters","title":"Parameters","text":"C#<pre><code>[WEnumToggleButtons(\n    int buttonsPerRow = 0,                  // Force column count (0 = automatic)\n    bool showSelectAll = false,             // Show \"Select All\" button (flags only)\n    bool showSelectNone = false,            // Show \"Select None\" button (flags only)\n    bool enablePagination = true,           // Allow pagination for many options\n    int pageSize = -1,                      // Override page size (defaults to project setting)\n    string colorKey = \"Default\"             // Color palette key\n)]\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#flag-enums","title":"Flag Enums","text":"C#<pre><code>[System.Flags]\npublic enum DamageTypes\n{\n    None = 0,\n    Physical = 1 &lt;&lt; 0,\n    Fire = 1 &lt;&lt; 1,\n    Ice = 1 &lt;&lt; 2,\n    Lightning = 1 &lt;&lt; 3,\n    Poison = 1 &lt;&lt; 4,\n}\n\n[WEnumToggleButtons(showSelectAll: true, showSelectNone: true, buttonsPerRow: 3)]\npublic DamageTypes resistances = DamageTypes.Fire | DamageTypes.Ice;\n</code></pre> <p>Visual Reference</p> <p></p> <p>Flag enum with Select All and Select None quick action buttons</p> <p>Behavior:</p> <ul> <li>Each flag value gets its own toggle button</li> <li>Multiple flags can be active simultaneously</li> <li>\"Select All\" and \"Select None\" buttons for quick configuration</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#standard-enums-radio-buttons","title":"Standard Enums (Radio Buttons)","text":"C#<pre><code>public enum WeaponType { Melee, Ranged, Magic }\n\n[WEnumToggleButtons(buttonsPerRow: 3, colorKey: \"Default-Dark\")]\npublic WeaponType weaponType = WeaponType.Melee;\n</code></pre> <p>Visual Reference</p> <p></p> <p>Standard enum rendered as radio-style toggle buttons (only one active)</p> <p>Behavior:</p> <ul> <li>Only one option can be selected at a time</li> <li>Clicking a button deselects others</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#pagination","title":"Pagination","text":"C#<pre><code>[System.Flags]\npublic enum AllAbilities\n{\n    None = 0,\n    Ability1 = 1 &lt;&lt; 0,\n    Ability2 = 1 &lt;&lt; 1,\n    // ... 20 more abilities ...\n    Ability22 = 1 &lt;&lt; 21,\n}\n\n[WEnumToggleButtons(enablePagination: true, pageSize: 10)]\npublic AllAbilities unlockedAbilities;\n</code></pre> <p>Visual Reference</p> <p></p> <p>Paginated toggle buttons with First, Previous, Next, Last navigation controls</p> <p>Features:</p> <ul> <li>Automatic pagination for many options</li> <li>Page size controlled by <code>pageSize</code> parameter or <code>UnityHelpersSettings.EnumToggleButtonsPageSize</code></li> <li>Navigation: First, Previous, Next, Last buttons</li> <li>Summary badge shows selections on other pages</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#layout-control","title":"Layout Control","text":"C#<pre><code>// Automatic layout (fits to inspector width)\n[WEnumToggleButtons]\npublic Permissions autoLayout;\n\n// Force 2 columns\n[WEnumToggleButtons(buttonsPerRow: 2)]\npublic Permissions twoColumns;\n\n// Force single column\n[WEnumToggleButtons(buttonsPerRow: 1)]\npublic Permissions singleColumn;\n</code></pre> <p>Visual Reference</p> <p></p> <p>Different column layouts: automatic, 2 columns, and single column</p>"},{"location":"features/inspector/inspector-selection-attributes/#color-theming","title":"Color Theming","text":"C#<pre><code>[WEnumToggleButtons(colorKey: \"Default-Dark\")]\npublic Permissions darkTheme;\n\n[WEnumToggleButtons(colorKey: \"Default-Light\")]\npublic Permissions lightTheme;\n</code></pre> <p>Visual Reference</p> <p></p> <p>Toggle buttons with dark and light color themes</p>"},{"location":"features/inspector/inspector-selection-attributes/#combining-with-other-attributes","title":"Combining with Other Attributes","text":"C#<pre><code>// Works with IntDropDown/StringInList/WValueDropDown!\n[IntDropDown(0, 30, 60, 120)]\n[WEnumToggleButtons]\npublic int frameRate = 60;  // Shows as toggle buttons instead of dropdown\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#composite-flags-behavior","title":"Composite Flags Behavior","text":"<p>The drawer intentionally filters out composite flag values (e.g., <code>ReadWrite = Read | Write</code>). This keeps the UI focused on atomic toggles and avoids ambiguous interactions. Use the \"Select All\" and \"Select None\" buttons for bulk operations.</p>"},{"location":"features/inspector/inspector-selection-attributes/#best-practices-for-wenumtogglebuttons","title":"Best Practices for WEnumToggleButtons","text":"<ul> <li>Keep option counts manageable \u2014 toggle groups work best for short lists where designers can see everything without scrolling</li> <li>Name enum members descriptively so automatic labels remain readable</li> <li>Combine with conditional attributes like <code>WShowIf</code> to build adaptive, context-aware authoring tools</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#wvaluedropdown","title":"WValueDropDown","text":"<p>Generic dropdown for any type with fixed values or provider methods.</p>"},{"location":"features/inspector/inspector-selection-attributes/#basic-usage-fixed-values","title":"Basic Usage (Fixed Values)","text":"C#<pre><code>// Primitive types\n[WValueDropDown(0, 25, 50, 100)]\npublic int staminaThreshold = 50;\n\n[WValueDropDown(0.5f, 1.0f, 1.5f, 2.0f)]\npublic float damageMultiplier = 1.0f;\n\n[WValueDropDown(\"Easy\", \"Normal\", \"Hard\", \"Insane\")]\npublic string difficulty = \"Normal\";\n</code></pre> <p>Visual Reference</p> <p></p> <p>Dropdown showing predefined integer, float, and string values</p>"},{"location":"features/inspector/inspector-selection-attributes/#provider-methods","title":"Provider Methods","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[CreateAssetMenu(fileName = \"PowerUpDefinition\", menuName = \"Power-Up Definition\")]\npublic class PowerUpDefinition : ScriptableObject\n{\n    public string name;\n    public Sprite icon;\n}\n\npublic class PowerUpConfig : MonoBehaviour\n{\n    [WValueDropDown(typeof(PowerUpLibrary), nameof(PowerUpLibrary.GetAvailablePowerUps))]\n    public PowerUpDefinition selectedPowerUp;\n}\n\npublic static class PowerUpLibrary\n{\n    public static IEnumerable&lt;PowerUpDefinition&gt; GetAvailablePowerUps()\n    {\n        // Return all power-ups from database/resources/etc.\n        return Resources.LoadAll&lt;PowerUpDefinition&gt;(\"PowerUps\");\n    }\n}\n</code></pre> <p>Visual Reference </p> <p>PowerUp definitions loaded from Resources folder</p> <p></p> <p>Dropdown populated dynamically from a provider method</p>"},{"location":"features/inspector/inspector-selection-attributes/#primitive-overloads","title":"Primitive Overloads","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class WValueDropDownPrimitives : MonoBehaviour\n{\n    // All primitive types supported:\n    [WValueDropDown(true, false)] // bool\n    public bool boolValue;\n\n    [WValueDropDown(true, false)] // bool\n    public List&lt;bool&gt; boolValues;\n\n    [WValueDropDown('A', 'B', 'C')] // char\n    public char charValue;\n\n    [WValueDropDown((byte)1, (byte)2, (byte)3)] // byte\n    public byte byteValue;\n\n    [WValueDropDown((short)10, (short)20, (short)30)] // short\n    public short shortValue;\n\n    [WValueDropDown(100, 200, 300)] // int\n    public int intValue;\n\n    [WValueDropDown(1000L, 2000L, 3000L)] // long\n    public long longValue;\n\n    [WValueDropDown(0.1f, 0.5f, 1.0f)] // float\n    public float floatValue;\n\n    [WValueDropDown(0.1, 0.5, 1.0)] // double\n    public double doubleValue;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Multiple dropdowns showing all supported primitive types</p>"},{"location":"features/inspector/inspector-selection-attributes/#instance-provider-context-aware","title":"Instance Provider (context-aware)","text":"C#<pre><code>public class DynamicOptions : MonoBehaviour\n{\n    public string prefix = \"Option\";\n    public int optionCount = 5;\n\n    // Instance method - uses object state to build options\n    [WValueDropDown(nameof(GetAvailableOptions), typeof(string))]\n    public string selectedOption;\n\n    private IEnumerable&lt;string&gt; GetAvailableOptions()\n    {\n        for (int i = 1; i &lt;= optionCount; i++)\n        {\n            yield return $\"{prefix}_{i}\";\n        }\n    }\n}\n</code></pre> <p>Passing only a method name instructs the drawer to search the decorated type for a parameterless method. Instance methods run on the serialized object; static methods are also supported. The second parameter specifies the value type for proper dropdown rendering.</p> <p>Alternate Constructor Forms:</p> C#<pre><code>// Form 1: Instance method with explicit value type\n[WValueDropDown(nameof(GetOptions), typeof(int))]\npublic int selection;\n\n// Form 2: Static provider from external type\n[WValueDropDown(typeof(DataProvider), nameof(DataProvider.GetItems))]\npublic Item selected;\n\n// Form 3: Static provider with explicit value type conversion\n[WValueDropDown(typeof(DataProvider), nameof(DataProvider.GetIds), typeof(int))]\npublic int selectedId;\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#custom-types","title":"Custom Types","text":"C#<pre><code>using System;\nusing System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[Serializable]\npublic class Preset\n{\n    public string name;\n    public float value;\n\n    public override string ToString() =&gt; name;  // Used for dropdown label\n}\n\npublic class Config : MonoBehaviour\n{\n    [WValueDropDown(typeof(Config), nameof(GetPresets))]\n    public Preset selectedPreset;\n\n    public static IEnumerable&lt;Preset&gt; GetPresets()\n    {\n        return new[]\n        {\n            new Preset { name = \"Low\", value = 0.5f },\n            new Preset { name = \"Medium\", value = 1.0f },\n            new Preset { name = \"High\", value = 2.0f },\n        };\n    }\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Dropdown showing custom serializable class options using ToString() for labels</p>"},{"location":"features/inspector/inspector-selection-attributes/#constructor-reference","title":"Constructor Reference","text":"Constructor Use Case <code>WValueDropDown(params T[] values)</code> Fixed list of primitive values (int, float, string, etc.) <code>WValueDropDown(Type provider, string method)</code> Static or instance method on external type <code>WValueDropDown(Type provider, string method, Type valueType)</code> Provider with explicit value type conversion <code>WValueDropDown(string method, Type valueType)</code> Instance/static method on the decorated type itself <code>WValueDropDown(Type valueType, params object[] values)</code> Fixed list with explicit type specification"},{"location":"features/inspector/inspector-selection-attributes/#best-practices-for-wvaluedropdown","title":"Best Practices for WValueDropDown","text":"<ul> <li>Override <code>ToString()</code> on custom types to control dropdown labels</li> <li>Use static providers for data shared across objects</li> <li>Use instance providers when options depend on the object state</li> <li>Provider methods are called each render \u2014 keep them efficient or cache results</li> <li>Return empty collection instead of null from providers</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#intdropdown","title":"IntDropDown","text":"<p>Integer field rendered as a dropdown with predefined options.</p>"},{"location":"features/inspector/inspector-selection-attributes/#basic-usage_1","title":"Basic Usage","text":"C#<pre><code>[IntDropDown(0, 30, 60, 120, 240)]\npublic int refreshRate = 60;\n\n[IntDropDown(1, 2, 4, 8, 16, 32)]\npublic int threadCount = 4;\n</code></pre> <p>Visual Reference</p> <p></p> <p>Integer dropdown showing predefined frame rate options</p>"},{"location":"features/inspector/inspector-selection-attributes/#provider-method","title":"Provider Method","text":"C#<pre><code>public class FrameRateConfig : MonoBehaviour\n{\n    [IntDropDown(typeof(FrameRateLibrary), nameof(FrameRateLibrary.GetSupportedFrameRates))]\n    public int targetFrameRate = 60;\n}\n\npublic static class FrameRateLibrary\n{\n    public static IEnumerable&lt;int&gt; GetSupportedFrameRates()\n    {\n        return new[] { 30, 60, 90, 120, 144, 240 };\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#fallback-behavior","title":"Fallback Behavior","text":"C#<pre><code>[IntDropDown(10, 20, 30)]\npublic int value = 25;  // Not in list! Shows as standard IntField\n</code></pre> <p>Note: If the current value isn't in the list, falls back to standard IntField for editing.</p>"},{"location":"features/inspector/inspector-selection-attributes/#stringinlist","title":"StringInList","text":"<p>String field constrained to a set of allowed values with an inline dropdown that opens a searchable popup.</p>"},{"location":"features/inspector/inspector-selection-attributes/#basic-usage_2","title":"Basic Usage","text":"C#<pre><code>[StringInList(\"Easy\", \"Normal\", \"Hard\", \"Nightmare\")]\npublic string difficulty = \"Normal\";\n\n[StringInList(\"Red\", \"Green\", \"Blue\", \"Yellow\", \"Purple\")]\npublic string teamColor = \"Red\";\n</code></pre> <p>Visual Reference</p> <p></p> <p>String dropdown with an inline popup showing search bar and results</p>"},{"location":"features/inspector/inspector-selection-attributes/#provider-method_1","title":"Provider Method","text":"C#<pre><code>public class LocalizationConfig : MonoBehaviour\n{\n    [StringInList(typeof(LocalizationKeys), nameof(LocalizationKeys.GetAllKeys))]\n    public string dialogKey;\n}\n\npublic static class LocalizationKeys\n{\n    public static IEnumerable&lt;string&gt; GetAllKeys()\n    {\n        // Return keys from localization database\n        return new[] { \"DIALOG_GREETING\", \"DIALOG_FAREWELL\", \"DIALOG_QUEST_START\" };\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#instance-provider-context-aware_1","title":"Instance Provider (context-aware)","text":"C#<pre><code>public class StateMachine : MonoBehaviour\n{\n    [StringInList(nameof(BuildAvailableStates))]\n    public string currentState;\n\n    private IEnumerable&lt;string&gt; BuildAvailableStates()\n    {\n        // Instance data drives the dropdown\n        yield return $\"{gameObject.name}_Idle\";\n        yield return $\"{gameObject.name}_Run\";\n    }\n\n    public static IEnumerable&lt;string&gt; StaticStates()\n    {\n        // Static helpers still work when referenced by name\n        return new[] { \"Global_A\", \"Global_B\" };\n    }\n}\n</code></pre> <p>Passing only a method name instructs the drawer to search the decorated type for a parameterless method. Instance methods run on the serialized object; static methods are also supported.</p>"},{"location":"features/inspector/inspector-selection-attributes/#search-and-pagination","title":"Search and Pagination","text":"C#<pre><code>[StringInList(typeof(SceneLibrary), nameof(SceneLibrary.GetAllSceneNames))]\npublic string targetScene;\n\npublic static class SceneLibrary\n{\n    public static IEnumerable&lt;string&gt; GetAllSceneNames()\n    {\n        // Return 100+ scene names\n        return EditorBuildSettings.scenes.Select(s =&gt; s.path);\n    }\n}\n</code></pre> <p>When the menu contains more entries than the configured limit, clicking the field opens a popup that embeds the search bar and pagination controls directly in the dropdown itself. The inspector row remains single-line, but the popup still supports fast filtering and page navigation.</p> <p>Visual Reference</p> <p></p> <p>Popup window with a search box filtering results in real-time</p> <p>Features:</p> <ul> <li>Dedicated popup hosts search + pagination when the option count exceeds <code>UnityHelpersSettings.StringInListPageLimit</code></li> <li>Inspector row stays single-line; the popup includes filtering, paging, and keyboard navigation</li> <li>Works in both IMGUI and UI Toolkit inspectors (including <code>SerializableTypeDrawer</code>)</li> <li>Accepts fixed lists, static provider methods, or instance provider methods resolved on the component</li> </ul>"},{"location":"features/inspector/inspector-selection-attributes/#use-cases","title":"Use Cases","text":"<p>Scene Names:</p> C#<pre><code>[StringInList(typeof(SceneHelper), nameof(SceneHelper.GetAllScenes))]\npublic string loadScene;\n</code></pre> <p>Animation States:</p> C#<pre><code>[StringInList(\"Idle\", \"Walk\", \"Run\", \"Jump\", \"Attack\")]\npublic string currentAnimation = \"Idle\";\n</code></pre> <p>Localization Keys:</p> C#<pre><code>[StringInList(typeof(LocaleManager), nameof(LocaleManager.GetKeys))]\npublic string textKey;\n</code></pre> <p>Tag/Layer Names:</p> C#<pre><code>[StringInList(\"Player\", \"Enemy\", \"Projectile\", \"Environment\")]\npublic string entityTag = \"Player\";\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#comparison-table","title":"Comparison Table","text":"Feature WEnumToggleButtons WValueDropDown IntDropDown StringInList Primary Use Enums, flag enums Any type Integers Strings Visual Style Toggle buttons Dropdown Dropdown Dropdown + search Multiple Selection Yes (flags) No No No Pagination Yes No No Yes Search No No No Yes Provider Methods No Yes Yes Yes Custom Types No Yes No No Color Theming Yes No No No"},{"location":"features/inspector/inspector-selection-attributes/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/inspector-selection-attributes/#1-choose-the-right-attribute","title":"1. Choose the Right Attribute","text":"C#<pre><code>// \u2705 GOOD: WEnumToggleButtons for flags (visual toggle state)\n[System.Flags]\npublic enum Features { None = 0, Feature1 = 1, Feature2 = 2, Feature3 = 4 }\n\n[WEnumToggleButtons]\npublic Features enabledFeatures;\n\n// \u2705 GOOD: WValueDropDown for fixed type-safe options\n[WValueDropDown(0.5f, 1.0f, 1.5f, 2.0f)]\npublic float speedMultiplier;\n\n// \u2705 GOOD: IntDropDown for integer-specific options\n[IntDropDown(30, 60, 120, 240)]\npublic int frameRate;\n\n// \u2705 GOOD: StringInList for string validation\n[StringInList(\"Easy\", \"Normal\", \"Hard\")]\npublic string difficulty;\n\n// \u274c BAD: Using strings when enum would be better\n[StringInList(\"Easy\", \"Normal\", \"Hard\")]\npublic string difficulty;  // Should be an enum!\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#2-use-provider-methods-for-dynamic-data","title":"2. Use Provider Methods for Dynamic Data","text":"C#<pre><code>// \u2705 GOOD: Provider method for data that changes\n[WValueDropDown(typeof(AssetDatabase), nameof(GetAllPrefabs))]\npublic GameObject prefab;\n\npublic static IEnumerable&lt;GameObject&gt; GetAllPrefabs()\n{\n    return Resources.LoadAll&lt;GameObject&gt;(\"Prefabs\");\n}\n\n// \u274c BAD: Hardcoded values for dynamic data\n[WValueDropDown/* hardcoded list of 50 prefabs */]\npublic GameObject prefab;  // Nightmare to maintain!\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#3-enable-helpful-ui-features","title":"3. Enable Helpful UI Features","text":"C#<pre><code>// \u2705 GOOD: Select All/None for flag enums\n[System.Flags]\npublic enum Permissions { None = 0, Read = 1, Write = 2, Execute = 4 }\n\n[WEnumToggleButtons(showSelectAll: true, showSelectNone: true)]\npublic Permissions permissions;\n\n// \u2705 GOOD: Pagination for many options\n[WEnumToggleButtons(enablePagination: true, pageSize: 10)]\npublic ManyOptionsEnum options;\n\n// \u274c BAD: No pagination with 50 options (cluttered inspector!)\n[WEnumToggleButtons(enablePagination: false)]\npublic ManyOptionsEnum options;\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#examples","title":"Examples","text":""},{"location":"features/inspector/inspector-selection-attributes/#example-1-damage-type-resistances","title":"Example 1: Damage Type Resistances","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class Armor : MonoBehaviour\n{\n    [System.Flags]\n    public enum DamageType\n    {\n        None = 0,\n        Physical = 1 &lt;&lt; 0,\n        Fire = 1 &lt;&lt; 1,\n        Ice = 1 &lt;&lt; 2,\n        Lightning = 1 &lt;&lt; 3,\n        Poison = 1 &lt;&lt; 4,\n        Magic = 1 &lt;&lt; 5,\n    }\n\n    [WEnumToggleButtons(showSelectAll: true, showSelectNone: true,\n                        buttonsPerRow: 3, colorKey: \"Default-Dark\")]\n    public DamageType resistances = DamageType.Physical;\n\n    [WEnumToggleButtons(showSelectAll: true, showSelectNone: true,\n                        buttonsPerRow: 3, colorKey: \"Default-Light\")]\n    public DamageType vulnerabilities = DamageType.None;\n}\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#example-2-graphics-presets","title":"Example 2: Graphics Presets","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class GraphicsConfig : MonoBehaviour\n{\n    [System.Serializable]\n    public class QualityPreset\n    {\n        public string name;\n        public int textureQuality;\n        public int shadowDistance;\n\n        public override string ToString() =&gt; name;\n    }\n\n    [WValueDropDown(typeof(GraphicsConfig), nameof(GetPresets))]\n    public QualityPreset selectedPreset;\n\n    public static IEnumerable&lt;QualityPreset&gt; GetPresets()\n    {\n        return new[]\n        {\n            new QualityPreset { name = \"Low\", textureQuality = 0, shadowDistance = 50 },\n            new QualityPreset { name = \"Medium\", textureQuality = 1, shadowDistance = 100 },\n            new QualityPreset { name = \"High\", textureQuality = 2, shadowDistance = 200 },\n            new QualityPreset { name = \"Ultra\", textureQuality = 3, shadowDistance = 500 },\n        };\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#example-3-scene-selection","title":"Example 3: Scene Selection","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing System.Linq;\n\npublic class SceneLoader : MonoBehaviour\n{\n    [StringInList(typeof(SceneLoader), nameof(GetAllSceneNames))]\n    public string mainMenuScene;\n\n    [StringInList(typeof(SceneLoader), nameof(GetAllSceneNames))]\n    public string gameplayScene;\n\n    [StringInList(typeof(SceneLoader), nameof(GetAllSceneNames))]\n    public string creditsScene;\n\n    public static IEnumerable&lt;string&gt; GetAllSceneNames()\n    {\n#if UNITY_EDITOR\n        return UnityEditor.EditorBuildSettings.scenes\n            .Select(s =&gt; System.IO.Path.GetFileNameWithoutExtension(s.path));\n#else\n        return new string[0];\n#endif\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#example-4-framerate-configuration","title":"Example 4: Framerate Configuration","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class PerformanceSettings : MonoBehaviour\n{\n    [IntDropDown(30, 60, 90, 120, 144, 240, -1)]\n    public int targetFrameRate = 60;  // -1 = unlimited\n\n    [IntDropDown(0, 1, 2, 3, 4)]\n    public int vsyncCount = 0;  // 0 = disabled\n\n    [WEnumToggleButtons]\n    public enum FrameLimitMode { Unlimited, Target, VSync }\n\n    public FrameLimitMode limitMode = FrameLimitMode.Target;\n\n    private void OnValidate()\n    {\n        Application.targetFrameRate = targetFrameRate;\n        QualitySettings.vSyncCount = vsyncCount;\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-selection-attributes/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Inspector Grouping Attributes - WGroup layouts</li> <li>Inspector Conditional Display - WShowIf</li> <li>Inspector Settings - Configuration reference</li> </ul> <p>Next Steps:</p> <ul> <li>Replace flag enum dropdowns with <code>[WEnumToggleButtons]</code> for better UX</li> <li>Use <code>[StringInList]</code> for scene names, animation states, localization keys</li> <li>Create provider methods for dynamic option lists</li> <li>Experiment with <code>[WValueDropDown]</code> for type-safe selection controls</li> </ul>"},{"location":"features/inspector/inspector-settings/","title":"Inspector Settings Reference","text":"<p>Centralized configuration for all inspector features.</p> <p>The <code>UnityHelpersSettings</code> asset provides project-wide configuration for pagination, colors, animations, and history capacity across all inspector attributes and custom drawers. Configure once, apply everywhere.</p>"},{"location":"features/inspector/inspector-settings/#table-of-contents","title":"Table of Contents","text":"<ul> <li>Accessing Settings</li> <li>Pagination Settings</li> <li>WButton Settings</li> <li>WGroup Settings</li> <li>Inline Editor Settings</li> <li>Color Palettes</li> <li>WEnumToggleButtons Settings</li> <li>Creating the Settings Asset</li> </ul>"},{"location":"features/inspector/inspector-settings/#accessing-settings","title":"Accessing Settings","text":"<p>Location: <code>ProjectSettings/UnityHelpersSettings.asset</code></p> <p>Access in Unity:</p> <ol> <li>Open Project Settings window (<code>Edit &gt; Project Settings</code>)</li> <li>Scroll to the \"Unity Helpers\" section (if available)</li> <li>Or navigate to <code>ProjectSettings/UnityHelpersSettings.asset</code> directly</li> </ol> <p>Note: The asset is created automatically on first use. If missing, any inspector feature will generate it.</p> <p></p>"},{"location":"features/inspector/inspector-settings/#pagination-settings","title":"Pagination Settings","text":"<p>Controls how many items are shown per page in various UI elements.</p>"},{"location":"features/inspector/inspector-settings/#stringinlistpagesize","title":"StringInListPageSize","text":"<ul> <li>Default: 25</li> <li>Range: 5 - 500</li> <li>Applies to: <code>[StringInList]</code> attribute</li> </ul> <p>Description: Number of string options shown per page in the dropdown.</p> <p>Usage:</p> C#<pre><code>[StringInList(typeof(SceneLibrary), nameof(SceneLibrary.GetAllSceneNames))]\npublic string sceneName;  // Uses StringInListPageSize\n</code></pre>"},{"location":"features/inspector/inspector-settings/#serializablesetpagesize","title":"SerializableSetPageSize","text":"<ul> <li>Default: 15</li> <li>Range: 5 - 500</li> <li>Applies to: <code>SerializableHashSet&lt;T&gt;</code>, <code>SerializableSortedSet&lt;T&gt;</code></li> </ul> <p>Description: Number of set elements shown per page in the inspector.</p> <p>Usage:</p> C#<pre><code>public SerializableHashSet&lt;string&gt; items;  // Uses SerializableSetPageSize\n</code></pre>"},{"location":"features/inspector/inspector-settings/#serializablesetstartcollapsed","title":"SerializableSetStartCollapsed","text":"<ul> <li>Default: On</li> <li>Applies to: <code>SerializableHashSet&lt;T&gt;</code>, <code>SerializableSortedSet&lt;T&gt;</code></li> </ul> <p>Description: Controls whether SerializableSet inspectors start collapsed the first time they are drawn. When enabled, sets render as a single foldout header until the user expands them; when disabled, the inspector opens automatically. This is only a default\u2014explicit script/test changes to <code>SerializedProperty.isExpanded</code> or <code>[WSerializableCollectionFoldout]</code> overrides still win.</p>"},{"location":"features/inspector/inspector-settings/#serializabledictionarypagesize","title":"SerializableDictionaryPageSize","text":"<ul> <li>Default: 15</li> <li>Range: 5 - 250</li> <li>Applies to: <code>SerializableDictionary&lt;TKey, TValue&gt;</code>, <code>SerializableSortedDictionary&lt;TKey, TValue&gt;</code></li> </ul> <p>Description: Number of dictionary entries shown per page in the inspector.</p> <p>Usage:</p> C#<pre><code>public SerializableDictionary&lt;string, GameObject&gt; prefabs;  // Uses SerializableDictionaryPageSize\n</code></pre>"},{"location":"features/inspector/inspector-settings/#serializabledictionarystartcollapsed","title":"SerializableDictionaryStartCollapsed","text":"<ul> <li>Default: On</li> <li>Applies to: <code>SerializableDictionary&lt;TKey, TValue&gt;</code>, <code>SerializableSortedDictionary&lt;TKey, TValue&gt;</code></li> </ul> <p>Description: Determines whether SerializableDictionary inspectors begin collapsed before any user interaction. Disable this to have dictionaries open automatically in newly created inspectors. Like the set toggle, this only establishes the default; <code>[WSerializableCollectionFoldout]</code> or manual changes to <code>SerializedProperty.isExpanded</code> take precedence on a per-field basis.</p>"},{"location":"features/inspector/inspector-settings/#serializablesetfoldouttweenenabled","title":"SerializableSetFoldoutTweenEnabled","text":"<ul> <li>Default: On</li> <li>Applies to: <code>SerializableHashSet&lt;T&gt;</code></li> </ul> <p>Description: Controls whether the manual entry foldout in SerializableSet inspectors animates when expanding or collapsing.</p>"},{"location":"features/inspector/inspector-settings/#serializablesetfoldoutspeed","title":"SerializableSetFoldoutSpeed","text":"<ul> <li>Default: 2</li> <li>Range: 2 - 12</li> <li>Applies to: <code>SerializableHashSet&lt;T&gt;</code></li> </ul> <p>Description: Animation speed for the SerializableSet manual entry foldout when <code>SerializableSetFoldoutTweenEnabled</code> is enabled.</p>"},{"location":"features/inspector/inspector-settings/#serializablesortedsetfoldouttweenenabled","title":"SerializableSortedSetFoldoutTweenEnabled","text":"<ul> <li>Default: On</li> <li>Applies to: <code>SerializableSortedSet&lt;T&gt;</code></li> </ul> <p>Description: Controls whether the manual entry foldout in SerializableSortedSet inspectors animate when expanding or collapsing.</p>"},{"location":"features/inspector/inspector-settings/#serializablesortedsetfoldoutspeed","title":"SerializableSortedSetFoldoutSpeed","text":"<ul> <li>Default: 2</li> <li>Range: 2 - 12</li> <li>Applies to: <code>SerializableSortedSet&lt;T&gt;</code></li> </ul> <p>Description: Animation speed for the SerializableSortedSet manual entry foldout when <code>SerializableSortedSetFoldoutTweenEnabled</code> is enabled.</p>"},{"location":"features/inspector/inspector-settings/#enumtogglebuttonspagesize","title":"EnumToggleButtonsPageSize","text":"<ul> <li>Default: 15</li> <li>Range: 5 - 50</li> <li>Applies to: <code>[WEnumToggleButtons]</code> attribute (when pagination enabled)</li> </ul> <p>Description: Number of toggle buttons shown per page for enums with many values.</p> <p>Usage:</p> C#<pre><code>[WEnumToggleButtons(enablePagination: true)]\npublic ManyOptionsEnum options;  // Uses EnumToggleButtonsPageSize\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wbuttonpagesize","title":"WButtonPageSize","text":"<ul> <li>Default: 6</li> <li>Range: 1 - 20</li> <li>Applies to: <code>[WButton]</code> attribute (grouped by draw order)</li> </ul> <p>Description: Number of button actions shown per page.</p> <p>Usage:</p> C#<pre><code>[WButton(\"Action 1\", drawOrder: 0)]\nprivate void Action1() { }\n\n// ... 10 more buttons with drawOrder: 0 ...\n// Pagination kicks in after WButtonPageSize buttons\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wbutton-settings","title":"WButton Settings","text":""},{"location":"features/inspector/inspector-settings/#wbuttonhistorysize","title":"WButtonHistorySize","text":"<ul> <li>Default: 5</li> <li>Range: 1 - 10</li> <li>Applies to: <code>[WButton]</code> methods with return values</li> </ul> <p>Description: Number of recent results to keep per method per target.</p> <p>Usage:</p> C#<pre><code>[WButton(\"Roll Dice\")]  // Uses WButtonHistorySize\nprivate int RollDice() =&gt; Random.Range(1, 7);\n\n[WButton(\"Custom History\", historyCapacity: 20)]  // Overrides global setting\nprivate int CustomHistory() =&gt; Random.Range(1, 100);\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wbuttonplacement","title":"WButtonPlacement","text":"<ul> <li>Default: Bottom</li> <li>Options: Top, Bottom</li> <li>Applies to: <code>[WButton]</code> buttons using <code>groupPlacement: WButtonGroupPlacement.UseGlobalSetting</code></li> </ul> <p>Description: Default placement of buttons in the inspector.</p> <ul> <li>Top: Buttons appear before default inspector fields</li> <li>Bottom: Buttons appear after default inspector fields</li> </ul> <p>Note: Use the <code>groupPlacement</code> parameter on individual buttons to override this setting:</p> <ul> <li><code>groupPlacement: WButtonGroupPlacement.Top</code> \u2192 Always render above inspector properties</li> <li><code>groupPlacement: WButtonGroupPlacement.Bottom</code> \u2192 Always render below inspector properties</li> <li><code>groupPlacement: WButtonGroupPlacement.UseGlobalSetting</code> \u2192 Follow this global setting (default)</li> </ul>"},{"location":"features/inspector/inspector-settings/#wbuttonfoldoutbehavior","title":"WButtonFoldoutBehavior","text":"<ul> <li>Default: StartExpanded</li> <li>Options: Always, StartExpanded, StartCollapsed</li> <li>Applies to: <code>[WButton]</code> grouped buttons</li> </ul> <p>Description: Controls foldout behavior for button groups.</p> <ul> <li>Always: Groups always show foldout triangles</li> <li>StartExpanded: Groups start open (can be collapsed)</li> <li>StartCollapsed: Groups start closed (can be expanded)</li> </ul>"},{"location":"features/inspector/inspector-settings/#wbuttonfoldouttweenenabled","title":"WButtonFoldoutTweenEnabled","text":"<ul> <li>Default: true</li> <li>Applies to: <code>[WButton]</code> grouped buttons</li> </ul> <p>Description: Enable smooth animation when expanding/collapsing button groups.</p>"},{"location":"features/inspector/inspector-settings/#wbuttonfoldoutspeed","title":"WButtonFoldoutSpeed","text":"<ul> <li>Default: 2.0</li> <li>Range: 2.0 - 12.0</li> <li>Applies to: <code>[WButton]</code> grouped buttons (when tween enabled)</li> </ul> <p>Description: Animation speed for button group fold/unfold.</p> <ul> <li>Lower values = slower animation</li> <li>Higher values = faster animation</li> </ul>"},{"location":"features/inspector/inspector-settings/#wgroup-settings","title":"WGroup Settings","text":""},{"location":"features/inspector/inspector-settings/#wgroupautoincluderowcount","title":"WGroupAutoIncludeRowCount","text":"<ul> <li>Default: 4</li> <li>Range: 0 - 32</li> <li>Applies to: <code>[WGroup]</code> attributes using <code>UseGlobalAutoInclude</code></li> </ul> <p>Description: Default number of fields to auto-include in a WGroup.</p> <p>Usage:</p> C#<pre><code>// Uses WGroupAutoIncludeRowCount (default: 4)\n[WGroup(\"stats\", \"Stats\")]\npublic int strength;           // Field 1: in group\npublic int agility;            // Field 2: in group (auto-included)\npublic int intelligence;       // Field 3: in group (auto-included)\n[WGroupEnd(\"stats\")]           // luck IS included (field 4), then group closes\npublic int luck;               // Field 4: in group (last field)\n\n// Explicit override\n[WGroup(\"combat\", \"Combat\", autoIncludeCount: 2)]\npublic float health;           // Field 1: in group\n[WGroupEnd(\"combat\")]          // mana IS included (field 2), then group closes\npublic float mana;             // Field 2: in group (last field)\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wgroupstartcollapsed","title":"WGroupStartCollapsed","text":"<ul> <li>Default: true</li> <li>Applies to: <code>[WGroup]</code> with <code>collapsible: true</code> when <code>startCollapsed</code> is omitted</li> </ul> <p>Description: Controls the initial foldout state for collapsible WGroups. Disable this to have collapsible groups start expanded unless the attribute explicitly passes <code>startCollapsed: true</code>.</p> <p>Projects can still override per group via the <code>startCollapsed</code> constructor argument or the <code>CollapseBehavior</code> named argument:</p> C#<pre><code>[WGroup(\n    \"advanced\",\n    collapsible: true,\n    CollapseBehavior = WGroupAttribute.WGroupCollapseBehavior.ForceExpanded\n)]\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wgrouptweenenabled","title":"WGroupTweenEnabled","text":"<ul> <li>Default: true</li> <li>Applies to: <code>[WGroup]</code> with <code>collapsible: true</code></li> </ul> <p>Description: Enable smooth animation when expanding/collapsing groups.</p>"},{"location":"features/inspector/inspector-settings/#wgrouptweenspeed","title":"WGroupTweenSpeed","text":"<ul> <li>Default: 2.0</li> <li>Range: 2.0 - 12.0</li> <li>Applies to: <code>[WGroup]</code> with <code>collapsible: true</code> (when tween enabled)</li> </ul> <p>Description: Animation speed for group fold/unfold.</p>"},{"location":"features/inspector/inspector-settings/#inline-editor-settings","title":"Inline Editor Settings","text":"<p>Controls behavior for the <code>[WInLineEditor]</code> attribute that embeds nested inspectors inline.</p>"},{"location":"features/inspector/inspector-settings/#inlineeditorfoldoutbehavior","title":"InlineEditorFoldoutBehavior","text":"<ul> <li>Default: StartCollapsed</li> <li>Options: AlwaysExpanded, StartExpanded, StartCollapsed</li> <li>Applies to: <code>[WInLineEditor]</code> without explicit mode</li> </ul> <p>Description: Default foldout behavior for inline editors.</p> <ul> <li>AlwaysExpanded: Always draws the inline inspector (no foldout)</li> <li>StartExpanded: Shows a foldout that starts expanded</li> <li>StartCollapsed: Shows a foldout that starts collapsed</li> </ul> <p>Note: Use the <code>mode</code> parameter on individual attributes to override this setting:</p> C#<pre><code>// Uses global setting\n[WInLineEditor]\npublic AbilityConfig config;\n\n// Always shows inline inspector\n[WInLineEditor(WInLineEditorMode.AlwaysExpanded)]\npublic AbilityConfig alwaysVisible;\n\n// Starts collapsed regardless of global setting\n[WInLineEditor(WInLineEditorMode.FoldoutCollapsed)]\npublic AbilityConfig collapsedByDefault;\n</code></pre>"},{"location":"features/inspector/inspector-settings/#inlineeditorfoldouttweenenabled","title":"InlineEditorFoldoutTweenEnabled","text":"<ul> <li>Default: true</li> <li>Applies to: <code>[WInLineEditor]</code> with foldout modes</li> </ul> <p>Description: Enable smooth animation when expanding/collapsing inline editors.</p>"},{"location":"features/inspector/inspector-settings/#inlineeditorfoldoutspeed","title":"InlineEditorFoldoutSpeed","text":"<ul> <li>Default: 2.0</li> <li>Range: 2.0 - 12.0</li> <li>Applies to: <code>[WInLineEditor]</code> with foldout modes (when tween enabled)</li> </ul> <p>Description: Animation speed for inline editor fold/unfold.</p> <ul> <li>Lower values = slower animation</li> <li>Higher values = faster animation</li> </ul>"},{"location":"features/inspector/inspector-settings/#color-palettes","title":"Color Palettes","text":"<p>Palette keys keep WButton and WEnumToggleButtons visuals consistent across the project. Open the Color Palettes foldout inside <code>UnityHelpersSettings</code> to add or edit entries. Each key is matched at draw time against the <code>colorKey</code> parameter on the corresponding attribute; unknown keys fall back to theme-aware defaults.</p>"},{"location":"features/inspector/inspector-settings/#wbuttoncustomcolors","title":"WButtonCustomColors","text":"<ul> <li>Applies to: <code>[WButton]</code> via the <code>colorKey</code> parameter</li> <li>Reserved keys: <code>Default</code>, <code>Default-Light</code>, <code>Default-Dark</code>, <code>WDefault</code> (legacy)</li> <li>Description: Each entry stores a button color and a readable text color. Reserved keys auto-sync to the current editor skin and cannot be deleted. Custom keys are ideal for highlighting dangerous or primary actions across multiple inspectors.</li> </ul> <p>Usage:</p> <ol> <li>Expand Color Palettes \u2192 WButton Custom Colors.</li> <li>Add an entry (e.g., <code>Highlight</code>) and pick button/text colors.</li> <li>Reference the key from your button:</li> </ol> C#<pre><code>[WButton(\"Submit\", colorKey: \"Highlight\")]\nprivate void Submit() { }\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wenumtogglebuttonscustomcolors","title":"WEnumToggleButtonsCustomColors","text":"<ul> <li>Applies to: <code>[WEnumToggleButtons]</code> via the <code>ColorKey</code> property</li> <li>Reserved keys: <code>Default</code>, <code>Default-Light</code>, <code>Default-Dark</code></li> <li>Description: Each entry defines four colors (selected background/text and inactive background/text). Use this dictionary to align enum toggle palettes with the rest of your UI or to clearly separate different tool contexts.</li> </ul> <p>Usage: Add a key under WEnumToggleButtons Custom Colors, then assign it per field:</p> C#<pre><code>[WEnumToggleButtons(ColorKey = \"Difficulty\")]\npublic DifficultyLevel difficulty;\n</code></pre>"},{"location":"features/inspector/inspector-settings/#wenumtogglebuttons-settings","title":"WEnumToggleButtons Settings","text":""},{"location":"features/inspector/inspector-settings/#enumtogglebuttonspagesize_1","title":"EnumToggleButtonsPageSize","text":"<p>See Pagination Settings.</p>"},{"location":"features/inspector/inspector-settings/#creating-the-settings-asset","title":"Creating the Settings Asset","text":"<p>The <code>UnityHelpersSettings</code> asset is automatically created on first use, but you can create it manually:</p>"},{"location":"features/inspector/inspector-settings/#method-1-automatic-creation","title":"Method 1: Automatic Creation","text":"<ol> <li>Use any inspector attribute (WGroup, WButton, etc.)</li> <li>Open the inspector</li> <li>Asset is created at <code>ProjectSettings/UnityHelpersSettings.asset</code></li> </ol>"},{"location":"features/inspector/inspector-settings/#method-2-force-creation","title":"Method 2: Force Creation","text":"<ol> <li>Open any script with an inspector attribute</li> <li>Select the GameObject/asset in the inspector</li> <li>Settings asset is generated automatically</li> </ol>"},{"location":"features/inspector/inspector-settings/#method-3-via-api-editor-script","title":"Method 3: Via API (Editor Script)","text":"C#<pre><code>#if UNITY_EDITOR\nusing WallstopStudios.UnityHelpers.Editor.Settings;\n\nUnityHelpersSettings settings = UnityHelpersSettings.Instance;\n// Settings asset is now created\n#endif\n</code></pre>"},{"location":"features/inspector/inspector-settings/#example-configurations","title":"Example Configurations","text":""},{"location":"features/inspector/inspector-settings/#high-density-ui-more-items-per-page","title":"High-Density UI (More items per page)","text":"Text Only<pre><code>StringInListPageSize: 50\nSerializableSetPageSize: 30\nEnumToggleButtonsPageSize: 25\nWButtonPageSize: 12\n</code></pre> <p>Use case: Large monitors, scrolling preference over pagination</p>"},{"location":"features/inspector/inspector-settings/#low-density-ui-fewer-items-per-page","title":"Low-Density UI (Fewer items per page)","text":"Text Only<pre><code>StringInListPageSize: 10\nSerializableSetPageSize: 5\nEnumToggleButtonsPageSize: 8\nWButtonPageSize: 4\n</code></pre> <p>Use case: Laptop screens, prefer focused views</p>"},{"location":"features/inspector/inspector-settings/#performance-focused-disable-animations","title":"Performance-Focused (Disable animations)","text":"Text Only<pre><code>WButtonFoldoutTweenEnabled: false\nWGroupTweenEnabled: false\nInlineEditorFoldoutTweenEnabled: false\n</code></pre> <p>Use case: Slower machines, prefer instant feedback</p>"},{"location":"features/inspector/inspector-settings/#smooth-animations-fast-tweens","title":"Smooth Animations (Fast tweens)","text":"Text Only<pre><code>WButtonFoldoutSpeed: 8.0\nWGroupTweenSpeed: 8.0\nInlineEditorFoldoutSpeed: 8.0\n</code></pre> <p>Use case: Snappy UI feel</p>"},{"location":"features/inspector/inspector-settings/#extensive-history-more-button-results","title":"Extensive History (More button results)","text":"Text Only<pre><code>WButtonHistorySize: 10\n</code></pre> <p>Use case: Heavy testing/debugging workflows</p>"},{"location":"features/inspector/inspector-settings/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/inspector/inspector-settings/#settings-asset-missing","title":"Settings Asset Missing","text":"<p>Problem: Can't find <code>UnityHelpersSettings.asset</code></p> <p>Solution:</p> <ol> <li>Use any inspector attribute in a script</li> <li>Select an object in the inspector</li> <li>Asset is created automatically</li> <li>Check <code>ProjectSettings/UnityHelpersSettings.asset</code></li> </ol>"},{"location":"features/inspector/inspector-settings/#changes-not-applied","title":"Changes Not Applied","text":"<p>Problem: Modified settings but inspector doesn't update</p> <p>Solution:</p> <ol> <li>Ensure settings asset is saved (<code>Ctrl+S</code> or <code>Cmd+S</code>)</li> <li>Refresh inspector (click away and back)</li> <li>Reimport scripts if needed (<code>Assets &gt; Reimport All</code>)</li> </ol>"},{"location":"features/inspector/inspector-settings/#color-palette-not-working","title":"Color Palette Not Working","text":"<p>Problem: Custom color key doesn't apply</p> <p>Solution:</p> <ol> <li>Check color key spelling (case-sensitive)</li> <li>Verify entry exists in the appropriate dictionary (WButtonCustomColors, WEnumToggleButtonsCustomColors)</li> <li>Ensure colors are set (not transparent/default)</li> <li>Save settings asset</li> </ol>"},{"location":"features/inspector/inspector-settings/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Inspector Grouping Attributes - WGroup layouts</li> <li>Inspector Buttons - WButton</li> <li>Inspector Selection Attributes - WEnumToggleButtons</li> </ul> <p>Next Steps:</p> <ul> <li>Customize pagination sizes for your workflow</li> <li>Create custom color palettes for project theming</li> <li>Adjust animation speeds to your preference</li> <li>Configure default button history capacity</li> </ul>"},{"location":"features/inspector/inspector-validation-attributes/","title":"Inspector Validation Attributes","text":"<p>Protect your data with declarative validation and read-only presentation.</p> <p>Unity Helpers provides validation attributes that help maintain data integrity and prevent accidental modifications. These attributes work seamlessly with the Unity inspector and can validate fields at runtime.</p>"},{"location":"features/inspector/inspector-validation-attributes/#table-of-contents","title":"Table of Contents","text":"<ul> <li>WReadOnly</li> <li>WNotNull</li> <li>ValidateAssignment</li> <li>Best Practices</li> </ul>"},{"location":"features/inspector/inspector-validation-attributes/#wreadonly","title":"WReadOnly","text":"<p>Displays a field in the inspector as read-only, preventing accidental modifications while keeping the value visible.</p>"},{"location":"features/inspector/inspector-validation-attributes/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class GameManagerReadOnly : MonoBehaviour\n{\n    [WReadOnly]\n    public string sessionId = \"abc-123-xyz\";\n\n    [WReadOnly]\n    public float elapsedTime = 0f;\n\n    [WReadOnly]\n    [SerializeField]\n    private int internalScore = 100;\n}\n</code></pre> <p>Behavior:</p> <ul> <li>Field appears grayed out in the inspector</li> <li>Value is visible but cannot be edited through the inspector</li> <li>Useful for displaying computed values, debug info, or auto-generated IDs</li> <li>Works with any serializable field type</li> </ul> <p>Visual Reference</p> <p></p> <p>Fields marked with [WReadOnly] appear grayed out and cannot be edited</p>"},{"location":"features/inspector/inspector-validation-attributes/#common-use-cases","title":"Common Use Cases","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class EntityReadOnly : MonoBehaviour\n{\n    // Auto-generated unique identifier\n    [WReadOnly]\n    public string entityId = System.Guid.NewGuid().ToString();\n\n    // Computed property exposed for debugging\n    [WReadOnly]\n    [SerializeField]\n    private float _currentHealth;\n\n    // Reference that should only be set via code\n    [WReadOnly]\n    public Transform cachedTarget;\n\n    // Frame counter for debugging\n    [WReadOnly]\n    public int framesSinceLastUpdate;\n}\n</code></pre> <p>Why Use WReadOnly:</p> <ul> <li>Prevent accidents: Stop designers from accidentally modifying auto-generated values</li> <li>Debug visibility: Show internal state without allowing modification</li> <li>Documentation: Make it clear which fields are managed by code vs. configured in the editor</li> <li>Data integrity: Protect computed or cached values from manual overrides</li> </ul> <p>Visual Reference</p> <p></p> <p>Multiple field types (string, float, Transform, int) displayed as read-only</p>"},{"location":"features/inspector/inspector-validation-attributes/#wnotnull","title":"WNotNull","text":"<p>Validates that a field is not null, providing both visual inspector feedback and runtime validation. When a field marked with <code>[WNotNull]</code> is null, the inspector displays a warning or error HelpBox. Additionally, calling <code>CheckForNulls()</code> on an object will throw an <code>ArgumentNullException</code> for any null <code>[WNotNull]</code> fields.</p>"},{"location":"features/inspector/inspector-validation-attributes/#basic-usage_1","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class PlayerControllerNotNull : MonoBehaviour\n{\n    [WNotNull]\n    public Rigidbody2D rb;\n\n    [WNotNull]\n    public SpriteRenderer spriteRenderer;\n\n    [WNotNull]\n    [SerializeField]\n    private AudioSource audioSource;\n\n    private void Awake()\n    {\n        // Validates all [WNotNull] fields are assigned\n        // Throws ArgumentNullException if any are null in Editor ONLY\n        this.CheckForNulls();\n    }\n}\n</code></pre> <p>Behavior:</p> <ul> <li>Inspector feedback: Displays a HelpBox warning (yellow) or error (red) when the field is null</li> <li>Runtime validation: Call <code>this.CheckForNulls()</code> to validate all <code>[WNotNull]</code> fields</li> <li>Throws <code>ArgumentNullException</code> with the field name if any marked field is null</li> <li>Works with both Unity <code>Object</code> types and plain C# objects</li> <li>All validation runs only in the Unity Editor (stripped in builds for performance)</li> </ul> <p>Visual Reference</p> <p></p> <p>Fields marked with [WNotNull] display a HelpBox in the inspector when null</p>"},{"location":"features/inspector/inspector-validation-attributes/#wnotnullmessagetype-enum","title":"WNotNullMessageType Enum","text":"<p>The <code>WNotNullMessageType</code> enum controls how null fields are displayed in the inspector:</p> Value Description <code>Warning</code> Displays a yellow warning HelpBox (default) <code>Error</code> Displays a red error HelpBox"},{"location":"features/inspector/inspector-validation-attributes/#constructor-overloads","title":"Constructor Overloads","text":"<p>The <code>[WNotNull]</code> attribute supports multiple constructor overloads for flexibility:</p>"},{"location":"features/inspector/inspector-validation-attributes/#default-warning","title":"Default Warning","text":"C#<pre><code>// Default: warning message type with auto-generated message\n[WNotNull]\npublic GameObject target;\n// Inspector shows: \"target must be assigned\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#specify-message-type","title":"Specify Message Type","text":"C#<pre><code>// Error message type with auto-generated message\n[WNotNull(WNotNullMessageType.Error)]\npublic AudioSource criticalAudioSource;\n// Inspector shows red error: \"criticalAudioSource must be assigned\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#custom-message","title":"Custom Message","text":"C#<pre><code>// Warning with custom message\n[WNotNull(\"Player needs a target to attack\")]\npublic Transform attackTarget;\n// Inspector shows yellow warning: \"Player needs a target to attack\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#full-customization","title":"Full Customization","text":"C#<pre><code>// Error with custom message\n[WNotNull(WNotNullMessageType.Error, \"Audio source is required for sound effects\")]\npublic AudioSource audioSource;\n// Inspector shows red error: \"Audio source is required for sound effects\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#inspector-display-examples","title":"Inspector Display Examples","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class EnemyAI : MonoBehaviour\n{\n    // Yellow warning - nice to have\n    [WNotNull]\n    public ParticleSystem hitEffect;\n\n    // Red error - critical reference\n    [WNotNull(WNotNullMessageType.Error)]\n    public Transform patrolPath;\n\n    // Warning with helpful context\n    [WNotNull(\"Assign the player tag or the enemy won't detect the player\")]\n    public string playerTag;\n\n    // Error with specific instructions\n    [WNotNull(WNotNullMessageType.Error, \"Drag the NavMeshAgent component here - required for movement\")]\n    public UnityEngine.AI.NavMeshAgent agent;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Warning (yellow) and Error (red) HelpBoxes for null fields in the inspector</p>"},{"location":"features/inspector/inspector-validation-attributes/#runtime-validation-with-checkfornulls","title":"Runtime Validation with CheckForNulls()","text":"<p>The <code>CheckForNulls()</code> extension method validates all <code>[WNotNull]</code> fields at runtime:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class EnemySpawner : MonoBehaviour\n{\n    [WNotNull]\n    public GameObject enemyPrefab;\n\n    [WNotNull]\n    public Transform spawnPoint;\n\n    [WNotNull(WNotNullMessageType.Error)]\n    public EnemyManager enemyManager;\n\n    private void Start()\n    {\n        // If any [WNotNull] field is null, this throws with the field name\n        // Example: ArgumentNullException(\"enemyPrefab\")\n        this.CheckForNulls();\n\n        // Safe to use - we know these are assigned\n        SpawnEnemy();\n    }\n\n    private void SpawnEnemy()\n    {\n        GameObject enemy = Instantiate(enemyPrefab, spawnPoint.position, Quaternion.identity);\n        enemyManager.RegisterEnemy(enemy);\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#editor-only-validation","title":"Editor-Only Validation","text":"<p>Both the inspector HelpBox display and the <code>CheckForNulls()</code> extension method are only active in the Unity Editor:</p> C#<pre><code>// The validation code only runs in UNITY_EDITOR\n// In builds, CheckForNulls() does nothing (stripped for performance)\nthis.CheckForNulls();\n</code></pre> <p>This means:</p> <ul> <li>Development: Visual feedback in inspector + runtime null checking with detailed exception messages</li> <li>Production: Zero runtime cost (all validation code is stripped)</li> </ul>"},{"location":"features/inspector/inspector-validation-attributes/#combining-with-other-attributes","title":"Combining with Other Attributes","text":"C#<pre><code>public class UIManager : MonoBehaviour\n{\n    // Required reference with error severity - validated at runtime\n    [WNotNull(WNotNullMessageType.Error)]\n    [SerializeField]\n    private Canvas mainCanvas;\n\n    // Optional reference - not validated\n    [SerializeField]\n    private AudioSource clickSound;\n\n    // Read-only and required\n    [WReadOnly]\n    [WNotNull]\n    public RectTransform cachedRect;\n\n    // Group with validation and custom message\n    [WGroup(\"UI Elements\")]\n    [WNotNull(\"Start button required for main menu\")]\n    public Button startButton;                     // In group\n\n    [WNotNull(WNotNullMessageType.Error, \"Quit button required for main menu\")]\n    [WGroupEnd(\"UI Elements\")]                     // quitButton IS included, then closes\n    public Button quitButton;                      // In group (last field)\n}\n</code></pre> <p>Why Use WNotNull:</p> <ul> <li>Visual feedback: See missing references immediately in the inspector without running the game</li> <li>Severity control: Use warnings for nice-to-have references, errors for critical ones</li> <li>Custom messages: Provide helpful context about why a reference is needed</li> <li>Early failure: Catch missing references at game start with <code>CheckForNulls()</code>, not when first used</li> <li>Clear errors: Get the exact field name in the exception message</li> <li>Documentation: Make required references explicit in code</li> <li>Zero runtime cost: All validation stripped from builds</li> </ul>"},{"location":"features/inspector/inspector-validation-attributes/#validateassignment","title":"ValidateAssignment","text":"<p>Validates that a field is properly assigned, providing visual inspector feedback when validation fails. Unlike <code>[WNotNull]</code> which only checks for null references, <code>[ValidateAssignment]</code> validates that fields are \"properly assigned\" based on their type\u2014including checking for empty strings, empty collections, and null references.</p>"},{"location":"features/inspector/inspector-validation-attributes/#what-validateassignment-validates","title":"What ValidateAssignment Validates","text":"Field Type Validation Rule Unity <code>Object</code> references Not null Strings Not null or whitespace <code>IList</code> (arrays, List) Has at least one element <code>ICollection</code> Has at least one element <code>IEnumerable</code> Has at least one element Other types Not null"},{"location":"features/inspector/inspector-validation-attributes/#basic-usage_2","title":"Basic Usage","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class EnemySpawner : MonoBehaviour\n{\n    [ValidateAssignment]\n    public GameObject enemyPrefab;\n\n    [ValidateAssignment]\n    public string enemyName;\n\n    [ValidateAssignment]\n    public List&lt;Transform&gt; spawnPoints;\n\n    private void Start()\n    {\n        // Logs warnings for any invalid [ValidateAssignment] fields\n        this.ValidateAssignments();\n    }\n}\n</code></pre> <p>Behavior:</p> <ul> <li>Inspector feedback: Displays a HelpBox warning (yellow) or error (red) when the field is invalid</li> <li>Runtime validation: Call <code>this.ValidateAssignments()</code> to log warnings for invalid fields</li> <li>Programmatic checking: Call <code>this.AreAnyAssignmentsInvalid()</code> to check if any fields are invalid</li> <li>Works with Unity <code>Object</code> types, strings, collections, and any reference type</li> <li>Inspector validation runs only in the Unity Editor (stripped in builds for performance)</li> </ul> <p>Visual Reference</p> <p></p> <p>Fields marked with [ValidateAssignment] display a HelpBox in the inspector when invalid</p>"},{"location":"features/inspector/inspector-validation-attributes/#validateassignmentmessagetype-enum","title":"ValidateAssignmentMessageType Enum","text":"<p>The <code>ValidateAssignmentMessageType</code> enum controls how invalid fields are displayed in the inspector:</p> Value Description <code>Warning</code> Displays a yellow warning HelpBox (default) <code>Error</code> Displays a red error HelpBox"},{"location":"features/inspector/inspector-validation-attributes/#constructor-overloads_1","title":"Constructor Overloads","text":"<p>The <code>[ValidateAssignment]</code> attribute supports multiple constructor overloads for flexibility:</p>"},{"location":"features/inspector/inspector-validation-attributes/#default-warning_1","title":"Default Warning","text":"C#<pre><code>// Default: warning message type with auto-generated message\n[ValidateAssignment]\npublic GameObject target;\n// Inspector shows: \"target must be assigned\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#specify-message-type_1","title":"Specify Message Type","text":"C#<pre><code>// Error message type with auto-generated message\n[ValidateAssignment(ValidateAssignmentMessageType.Error)]\npublic AudioSource criticalAudioSource;\n// Inspector shows red error: \"criticalAudioSource must be assigned\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#custom-message_1","title":"Custom Message","text":"C#<pre><code>// Warning with custom message\n[ValidateAssignment(\"Enemy name is required for UI display\")]\npublic string enemyName;\n// Inspector shows yellow warning: \"Enemy name is required for UI display\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#full-customization_1","title":"Full Customization","text":"C#<pre><code>// Error with custom message\n[ValidateAssignment(ValidateAssignmentMessageType.Error, \"Spawn points list cannot be empty\")]\npublic List&lt;Transform&gt; spawnPoints;\n// Inspector shows red error: \"Spawn points list cannot be empty\"\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#inspector-display-examples_1","title":"Inspector Display Examples","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing System.Collections.Generic;\n\npublic class GameConfig : MonoBehaviour\n{\n    // Yellow warning - validates not null\n    [ValidateAssignment]\n    public GameObject playerPrefab;\n\n    // Red error - validates string not empty/whitespace\n    [ValidateAssignment(ValidateAssignmentMessageType.Error)]\n    public string gameTitle;\n\n    // Warning with helpful context - validates list not empty\n    [ValidateAssignment(\"Add at least one difficulty level\")]\n    public List&lt;string&gt; difficultyLevels;\n\n    // Error with specific instructions - validates array not empty\n    [ValidateAssignment(ValidateAssignmentMessageType.Error, \"Spawn points are required - add Transform references\")]\n    public Transform[] spawnPoints;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Warning (yellow) and Error (red) HelpBoxes for various invalid field types</p>"},{"location":"features/inspector/inspector-validation-attributes/#runtime-validation-methods","title":"Runtime Validation Methods","text":"<p>Two extension methods are available for runtime validation of <code>[ValidateAssignment]</code> fields:</p>"},{"location":"features/inspector/inspector-validation-attributes/#validateassignments","title":"ValidateAssignments()","text":"<p>Logs warnings to the console for all invalid fields:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class LevelManager : MonoBehaviour\n{\n    [ValidateAssignment]\n    public GameObject[] enemyPrefabs;\n\n    [ValidateAssignment]\n    public string levelName;\n\n    private void Start()\n    {\n        // Logs a warning for each invalid [ValidateAssignment] field\n        this.ValidateAssignments();\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#areanyassignmentsinvalid","title":"AreAnyAssignmentsInvalid()","text":"<p>Returns <code>true</code> if any <code>[ValidateAssignment]</code> field is invalid:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class SpawnManager : MonoBehaviour\n{\n    [ValidateAssignment]\n    public GameObject spawnPrefab;\n\n    [ValidateAssignment]\n    public List&lt;Transform&gt; spawnLocations;\n\n    private void Start()\n    {\n        if (this.AreAnyAssignmentsInvalid())\n        {\n            Debug.LogError(\"SpawnManager has invalid assignments - spawning disabled\");\n            enabled = false;\n            return;\n        }\n\n        // Safe to proceed - all assignments are valid\n        SpawnEnemies();\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#validateassignment-vs-wnotnull","title":"ValidateAssignment vs WNotNull","text":"<p>Both attributes validate fields and provide inspector feedback, but they serve different purposes:</p> Feature ValidateAssignment WNotNull Null object check \u2713 \u2713 Empty string check \u2713 \u2717 Empty collection check \u2713 \u2717 Runtime behavior Logs warnings Throws <code>ArgumentNullException</code> Best for Comprehensive field validation Strict null reference checking <p>When to use which:</p> <ul> <li>Use <code>[ValidateAssignment]</code> when you need to validate that strings are non-empty or collections have elements</li> <li>Use <code>[WNotNull]</code> when you want strict null checking with an exception thrown at runtime</li> <li>Use <code>[ValidateAssignment]</code> when you want softer runtime validation (warnings instead of exceptions)</li> <li>Use <code>[WNotNull]</code> for critical references where the game should fail fast if not assigned</li> </ul> C#<pre><code>public class PlayerSetup : MonoBehaviour\n{\n    // Use WNotNull for critical references - throws if null\n    [WNotNull(WNotNullMessageType.Error)]\n    public Rigidbody2D rb;\n\n    // Use ValidateAssignment for string validation\n    [ValidateAssignment]\n    public string playerName;\n\n    // Use ValidateAssignment for collection validation\n    [ValidateAssignment(ValidateAssignmentMessageType.Error, \"Add at least one weapon\")]\n    public List&lt;GameObject&gt; startingWeapons;\n\n    private void Awake()\n    {\n        // Throws ArgumentNullException if rb is null\n        this.CheckForNulls();\n\n        // Logs warnings if playerName is empty or startingWeapons is empty\n        this.ValidateAssignments();\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#combining-with-other-attributes_1","title":"Combining with Other Attributes","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing System.Collections.Generic;\n\npublic class UIManager : MonoBehaviour\n{\n    // Group with validation\n    [WGroup(\"Required UI Elements\")]\n    [ValidateAssignment(ValidateAssignmentMessageType.Error)]\n    public Canvas mainCanvas;                      // In group\n\n    [ValidateAssignment(\"Button text cannot be empty\")]\n    public string startButtonText;                 // In group (auto-included)\n\n    [ValidateAssignment(ValidateAssignmentMessageType.Error, \"Add menu items to display\")]\n    [WGroupEnd(\"Required UI Elements\")]            // menuItems IS included, then closes\n    public List&lt;GameObject&gt; menuItems;             // In group (last field)\n\n    // Conditional validation\n    [WShowIf(nameof(useCustomTheme))]\n    [ValidateAssignment(\"Custom theme name required when using custom theme\")]\n    public string customThemeName;\n\n    public bool useCustomTheme;\n}\n</code></pre> <p>Why Use ValidateAssignment:</p> <ul> <li>Comprehensive validation: Validates strings, collections, and references\u2014not just null checks</li> <li>Visual feedback: See invalid fields immediately in the inspector</li> <li>Severity control: Use warnings for nice-to-have fields, errors for critical ones</li> <li>Custom messages: Provide helpful context about validation requirements</li> <li>Non-throwing validation: Use <code>ValidateAssignments()</code> for warnings instead of exceptions</li> <li>Programmatic checking: Use <code>AreAnyAssignmentsInvalid()</code> for conditional logic</li> <li>Zero runtime cost: All validation stripped from builds</li> </ul>"},{"location":"features/inspector/inspector-validation-attributes/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/inspector-validation-attributes/#1-validate-early","title":"1. Validate Early","text":"<p>Call <code>CheckForNulls()</code> and <code>ValidateAssignments()</code> in <code>Awake()</code> or <code>Start()</code> to catch missing references immediately:</p> C#<pre><code>private void Awake()\n{\n    // Throws for critical null references\n    this.CheckForNulls();\n    // Logs warnings for empty strings, collections, etc.\n    this.ValidateAssignments();\n}\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#2-choose-the-right-validation-attribute","title":"2. Choose the Right Validation Attribute","text":"C#<pre><code>// Use WNotNull for critical object references (throws on null)\n[WNotNull(WNotNullMessageType.Error)]\npublic Rigidbody2D rb;\n\n// Use ValidateAssignment for strings (validates not empty/whitespace)\n[ValidateAssignment]\npublic string playerName;\n\n// Use ValidateAssignment for collections (validates not empty)\n[ValidateAssignment(ValidateAssignmentMessageType.Error)]\npublic List&lt;Transform&gt; waypoints;\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#3-use-wreadonly-for-computed-values","title":"3. Use WReadOnly for Computed Values","text":"C#<pre><code>[WReadOnly]\npublic float Speed =&gt; rb.velocity.magnitude;\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#4-combine-with-relational-components","title":"4. Combine with Relational Components","text":"C#<pre><code>// Auto-wired but protected from manual changes\n[WReadOnly]\n[SiblingComponent]\npublic Collider2D col;\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#5-document-intent","title":"5. Document Intent","text":"C#<pre><code>// Required: Must be assigned in inspector\n[WNotNull]\npublic AudioClip attackSound;\n\n// Required: Must not be empty\n[ValidateAssignment]\npublic string characterName;\n\n// Optional: May be null\npublic AudioClip hitSound;\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#6-use-with-scriptableobjects","title":"6. Use with ScriptableObjects","text":"C#<pre><code>[CreateAssetMenu]\npublic class GameConfig : ScriptableObject\n{\n    [WNotNull]\n    public GameObject playerPrefab;\n\n    [WNotNull]\n    public Material defaultMaterial;\n\n    [ValidateAssignment(\"Game title cannot be empty\")]\n    public string gameTitle;\n\n    [ValidateAssignment(ValidateAssignmentMessageType.Error)]\n    public List&lt;string&gt; supportedLanguages;\n\n    private void OnEnable()\n    {\n        this.CheckForNulls();\n        this.ValidateAssignments();\n    }\n}\n</code></pre>"},{"location":"features/inspector/inspector-validation-attributes/#see-also","title":"See Also","text":"<ul> <li>Inspector Grouping Attributes - Organize related fields</li> <li>Inspector Conditional Display - Show/hide fields conditionally</li> <li>Relational Components - Auto-wire component references</li> </ul>"},{"location":"features/inspector/utility-components/","title":"Utility Components Guide","text":""},{"location":"features/inspector/utility-components/#tldr-why-use-these","title":"TL;DR \u2014 Why Use These","text":"<p>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.</p>"},{"location":"features/inspector/utility-components/#contents","title":"Contents","text":"<ul> <li>Oscillator \u2014 Automatic circular/elliptical motion</li> <li>ChildSpawner \u2014 Conditional prefab instantiation</li> <li>CollisionProxy \u2014 Event-based collision detection</li> <li>CircleLineRenderer \u2014 Visual circle debugging</li> <li>MatchTransform \u2014 Follow another transform</li> <li>SpriteRendererSync \u2014 Mirror sprite renderer state</li> <li>SpriteRendererMetadata \u2014 Stacked visual modifications</li> <li>CenterPointOffset \u2014 Define logical center points</li> <li>AnimatorEnumStateMachine \u2014 Type-safe animator control</li> <li>CoroutineHandler \u2014 Singleton coroutine host</li> <li>StartTracker \u2014 Lifecycle tracking</li> <li>MatchColliderToSprite \u2014 Auto-sync colliders</li> <li>PolygonCollider2DOptimizer \u2014 Simplify collider shapes</li> </ul>"},{"location":"features/inspector/utility-components/#oscillator","title":"Oscillator","text":"<p>What it does: Automatically moves a GameObject in a circular or elliptical pattern. Think \"floating pickup\" or \"idle hover animation\" without animators.</p> <p>Problem it solves: Creating simple repetitive motion (hovering, bobbing, orbiting) usually requires animation curves or custom update loops. Oscillator handles it with three parameters.</p>"},{"location":"features/inspector/utility-components/#when-to-use","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Floating/hovering UI elements</li> <li>Pickup items that gently bob</li> <li>Decorative objects with idle motion</li> <li>Circular patrol paths</li> <li>Simple pendulum motion</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Complex animation sequences (use Animator)</li> <li>Physics-based motion (use Rigidbody)</li> <li>Player/enemy movement (too rigid)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use","title":"How to Use","text":"<ol> <li>Add <code>Oscillator</code> component to any GameObject</li> <li>Configure three parameters:</li> <li>speed: Rotation speed (radians per second)</li> <li>width: Horizontal amplitude (X-axis movement range)</li> <li>height: Vertical amplitude (Y-axis movement range)</li> </ol> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Via code\nOscillator osc = gameObject.AddComponent&lt;Oscillator&gt;();\nosc.speed = 2f;    // Two radians/second\nosc.width = 1f;    // \u00b11 unit horizontally\nosc.height = 0.5f; // \u00b10.5 units vertically\n</code></pre>"},{"location":"features/inspector/utility-components/#examples","title":"Examples","text":"<p>Gentle hover (coin pickup):</p> Text Only<pre><code>speed = 3\nwidth = 0\nheight = 0.2\n</code></pre> <p>Figure-8 motion:</p> Text Only<pre><code>speed = 2\nwidth = 1\nheight = 1\n</code></pre> <p>Horizontal sway:</p> Text Only<pre><code>speed = 1\nwidth = 0.5\nheight = 0\n</code></pre>"},{"location":"features/inspector/utility-components/#important-notes","title":"Important Notes","text":"<ul> <li>Updates <code>transform.localPosition</code> in Update()</li> <li>Motion is relative to the original local position</li> <li>Starts from current time offset (unique per instance)</li> <li>Zero allocation per frame</li> <li>Works in 2D and 3D (only affects X and Y)</li> </ul>"},{"location":"features/inspector/utility-components/#childspawner","title":"ChildSpawner","text":"<p>What it does: Conditionally instantiates prefabs as children based on environment (editor/development/release) with automatic duplicate prevention.</p> <p>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.</p>"},{"location":"features/inspector/utility-components/#when-to-use_1","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Debug UI overlays (FPS counters, console)</li> <li>Analytics managers (only in release builds)</li> <li>Development tools (cheat menus, level select)</li> <li>Platform-specific managers</li> <li>Scene-independent singleton spawners</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Regular gameplay objects (use Instantiate)</li> <li>One-time spawns (just call Instantiate)</li> <li>Objects that need complex initialization</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_1","title":"How to Use","text":"<p>Add <code>ChildSpawner</code> to a GameObject (often on a scene manager or empty GameObject):</p> <p>Inspector configuration:</p> <ul> <li>Prefabs: Always spawned</li> <li>Editor Only Prefabs: Only in Unity Editor</li> <li>Development Only Prefabs: Only in Development builds</li> <li>Spawn Method: When to spawn (Awake/OnEnable/Start)</li> <li>Dont Destroy On Load: Persist across scenes</li> </ul> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Via code\nChildSpawner spawner = gameObject.AddComponent&lt;ChildSpawner&gt;();\nspawner._prefabs = new[] { analyticsPrefab };\nspawner._developmentOnlyPrefabs = new[] { debugMenuPrefab };\nspawner._spawnMethod = ChildSpawnMethod.Awake;\nspawner._dontDestroyOnLoad = true;\n</code></pre>"},{"location":"features/inspector/utility-components/#deduplication-behavior","title":"Deduplication Behavior","text":"<p>ChildSpawner prevents duplicate instantiation:</p> C#<pre><code>// Spawns DebugCanvas once\nChildSpawner spawner1 = obj1.AddComponent&lt;ChildSpawner&gt;();\nspawner1._prefabs = new[] { debugCanvasPrefab };\n\n// This will NOT spawn a second DebugCanvas (detects existing instance)\nChildSpawner spawner2 = obj2.AddComponent&lt;ChildSpawner&gt;();\nspawner2._prefabs = new[] { debugCanvasPrefab };\n</code></pre> <p>Deduplication uses prefab asset path matching.</p>"},{"location":"features/inspector/utility-components/#spawn-methods","title":"Spawn Methods","text":"<ul> <li>Awake: Spawns before anything else (use for foundational systems)</li> <li>OnEnable: Spawns when a component is enabled (use for dynamic spawning)</li> <li>Start: Spawns after all Awake calls (use when dependencies are needed)</li> </ul>"},{"location":"features/inspector/utility-components/#dontdestroyonload","title":"DontDestroyOnLoad","text":"<p>When enabled:</p> <ul> <li>Spawned objects persist across scene loads</li> <li>Deduplication works across scene transitions</li> <li>Objects aren't destroyed when loading new scenes</li> </ul> <p>Typical use case:</p> Text Only<pre><code>Scene 1: ChildSpawner spawns AnalyticsManager with DontDestroyOnLoad\nScene 2 loads: Same ChildSpawner detects existing AnalyticsManager, doesn't spawn duplicate\n</code></pre> <p></p>"},{"location":"features/inspector/utility-components/#collisionproxy","title":"CollisionProxy","text":"<p>What it does: Exposes Unity's 2D collision callbacks as C# events, enabling composition-based collision handling without inheriting from MonoBehaviour.</p> <p>Problem it solves: To receive collision events in Unity, you traditionally override <code>OnCollisionEnter2D</code> etc. in a MonoBehaviour subclass. CollisionProxy lets you subscribe to events instead, supporting multiple listeners and decoupled architectures.</p>"},{"location":"features/inspector/utility-components/#when-to-use_2","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Composition over inheritance designs</li> <li>Multiple systems reacting to the same collision</li> <li>Decoupling collision logic from GameObject code</li> <li>Testing collision responses</li> <li>Dynamic behavior attachment/detachment</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Simple single-handler cases (override is fine)</li> <li>3D collisions (only supports 2D)</li> <li>High-frequency collisions (event overhead)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_2","title":"How to Use","text":"<ol> <li>Add <code>CollisionProxy</code> to GameObject with Collider2D</li> <li>Subscribe to events from other scripts</li> </ol> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nCollisionProxy proxy = gameObject.AddComponent&lt;CollisionProxy&gt;();\n\n// Subscribe to enter event\nproxy.OnCollisionEnter += HandleCollision;\nproxy.OnTriggerEnter += HandleTrigger;\n\nvoid HandleCollision(Collision2D collision)\n{\n    Debug.Log($\"Hit {collision.gameObject.name}\");\n}\n\nvoid HandleTrigger(Collider2D other)\n{\n    Debug.Log($\"Triggered by {other.gameObject.name}\");\n}\n\n// Cleanup\nvoid OnDestroy()\n{\n    proxy.OnCollisionEnter -= HandleCollision;\n    proxy.OnTriggerEnter -= HandleTrigger;\n}\n</code></pre>"},{"location":"features/inspector/utility-components/#available-events","title":"Available Events","text":"<p>Collision events (Collision2D parameter):</p> <ul> <li><code>OnCollisionEnter</code></li> <li><code>OnCollisionStay</code></li> <li><code>OnCollisionExit</code></li> </ul> <p>Trigger events (Collider2D parameter):</p> <ul> <li><code>OnTriggerEnter</code></li> <li><code>OnTriggerStay</code></li> <li><code>OnTriggerExit</code></li> </ul>"},{"location":"features/inspector/utility-components/#multiple-subscribers-example","title":"Multiple Subscribers Example","text":"C#<pre><code>// Health system subscribes\nhealthSystem.OnDamageTaken += proxy.OnCollisionEnter;\n\n// Sound system subscribes to same event\nsoundSystem.PlayImpactSound += proxy.OnCollisionEnter;\n\n// Analytics subscribes\nanalytics.TrackCollision += proxy.OnCollisionEnter;\n\n// All three systems react to the same collision independently\n</code></pre>"},{"location":"features/inspector/utility-components/#circlelinerenderer","title":"CircleLineRenderer","text":"<p>What it does: Visualizes CircleCollider2D with a dynamically drawn circle using LineRenderer, with randomized appearance for visual variety.</p> <p>Problem it solves: Seeing collision bounds at runtime for debugging, or creating dynamic range indicators (ability ranges, explosion radii) without pre-made sprites.</p>"},{"location":"features/inspector/utility-components/#when-to-use_3","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Debug visualization of collision bounds</li> <li>Dynamic range indicators (attack range, detection radius)</li> <li>Area-of-effect visualization</li> <li>Circular UI elements</li> <li>Animated selection rings</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Production graphics (performance overhead)</li> <li>Static circles (use a sprite)</li> <li>Thousands of circles (expensive)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_3","title":"How to Use","text":"<ol> <li>Add <code>CircleLineRenderer</code> to GameObject with <code>CircleCollider2D</code></li> <li>Component automatically:</li> <li>Adds LineRenderer if not present</li> <li>Syncs circle size to collider radius</li> <li>Randomizes line width for visual variety</li> </ol> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nCircleLineRenderer circleVis = gameObject.AddComponent&lt;CircleLineRenderer&gt;();\ncircleVis.color = Color.red;\ncircleVis.minLineWidth = 0.05f;\ncircleVis.maxLineWidth = 0.15f;\ncircleVis.updateRateSeconds = 0.5f; // Refresh twice per second\n</code></pre>"},{"location":"features/inspector/utility-components/#configuration","title":"Configuration","text":"<ul> <li>minLineWidth / maxLineWidth: Random line thickness range</li> <li>numSegments: Circle smoothness (more segments = smoother, more expensive)</li> <li>baseSegments: Minimum segments (scaled by radius)</li> <li>updateRateSeconds: How often to randomize appearance</li> <li>color: Line color</li> </ul>"},{"location":"features/inspector/utility-components/#update-rate","title":"Update Rate","text":"<p>Lower values = more frequent randomization = more visual variety but higher CPU cost</p> Text Only<pre><code>0.1f = Very active (10 updates/sec)\n0.5f = Moderate (2 updates/sec)\n2.0f = Subtle (0.5 updates/sec)\n</code></pre> <p></p>"},{"location":"features/inspector/utility-components/#matchtransform","title":"MatchTransform","text":"<p>What it does: Makes one transform follow another with configurable update timing and offset.</p> <p>Problem it solves: Following transforms (UI name plates, camera targets, position constraints) usually require custom scripts. MatchTransform handles it declaratively.</p>"},{"location":"features/inspector/utility-components/#when-to-use_4","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>UI name plates following 3D objects</li> <li>Camera targets</li> <li>Object attachments (weapon to hand)</li> <li>Position constraints</li> <li>Simple parent-child alternatives</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Smooth following (use Vector3.Lerp in Update)</li> <li>Physics-based following (use joints/springs)</li> <li>Complex multi-axis constraints (use Unity Constraints)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_4","title":"How to Use","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nMatchTransform matcher = uiPlate.AddComponent&lt;MatchTransform&gt;();\nmatcher.toMatch = enemyTransform;\nmatcher.localOffset = new Vector3(0, 2, 0); // 2 units above target\nmatcher.mode = MatchTransform.Mode.LateUpdate; // Update after camera\n</code></pre>"},{"location":"features/inspector/utility-components/#update-modes","title":"Update Modes","text":"<ul> <li>Update: Standard update timing (most common)</li> <li>FixedUpdate: For physics-synced following</li> <li>LateUpdate: After all Updates (best for camera followers)</li> <li>Awake: Set once at startup, then never update</li> <li>Start: Set once after Awake, then never update</li> </ul>"},{"location":"features/inspector/utility-components/#local-offset","title":"Local Offset","text":"C#<pre><code>// Offset is added to target position\nmatcher.localOffset = new Vector3(1, 0, 0); // 1 unit to the right\n</code></pre>"},{"location":"features/inspector/utility-components/#self-matching","title":"Self-Matching","text":"<p>If <code>toMatch</code> is the same GameObject, applies offset once then disables:</p> C#<pre><code>matcher.toMatch = transform; // Self-reference\nmatcher.localOffset = new Vector3(5, 0, 0);\n// GameObject moves 5 units right once, then MatchTransform disables itself\n</code></pre> <p> </p>"},{"location":"features/inspector/utility-components/#spriterenderersync","title":"SpriteRendererSync","text":"<p>What it does: Mirrors one SpriteRenderer's properties (sprite, color, material, sorting) to another, with selective property matching.</p> <p>Problem it solves: Creating shadow sprites, duplicate renderers for effects, or layered rendering often requires manually keeping multiple SpriteRenderers in sync.</p>"},{"location":"features/inspector/utility-components/#when-to-use_5","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Shadow sprites (black silhouette following character)</li> <li>Duplicate renderers for effects (outlines, glows)</li> <li>Mirrored sprites (reflection effects)</li> <li>Synchronized sprite swapping</li> <li>VFX layers</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Single sprite rendering</li> <li>Particle effects (use ParticleSystem)</li> <li>Complex multi-layer rendering (use LayeredImage for UI)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_5","title":"How to Use","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// On the \"follower\" sprite renderer\nSpriteRendererSync syncer = shadowRenderer.AddComponent&lt;SpriteRendererSync&gt;();\nsyncer.toMatch = characterRenderer;\nsyncer.matchColor = false; // Don't copy color (shadow should be black)\nsyncer.matchMaterial = true;\nsyncer.matchSortingLayer = true;\nsyncer.matchOrderInLayer = true;\n</code></pre>"},{"location":"features/inspector/utility-components/#configuration-options","title":"Configuration Options","text":"<p>What to sync:</p> <ul> <li><code>matchColor</code>: Copy color tint</li> <li><code>matchMaterial</code>: Copy material</li> <li><code>matchSortingLayer</code>: Copy sorting layer</li> <li><code>matchOrderInLayer</code>: Copy order in layer</li> <li>Sprite, flipX, flipY are always copied</li> </ul> <p>Dynamic source:</p> C#<pre><code>// Change what to match at runtime\nsyncer.DynamicToMatch = () =&gt; GetCurrentWeaponRenderer();\n</code></pre> <p>Sorting override:</p> C#<pre><code>// Override order in layer dynamically\nsyncer.DynamicSortingOrderOverride = () =&gt; characterRenderer.sortingOrder - 1; // Always behind\n</code></pre>"},{"location":"features/inspector/utility-components/#update-timing","title":"Update Timing","text":"<p>Syncs in <code>LateUpdate()</code> to ensure source renderer has updated first.</p>"},{"location":"features/inspector/utility-components/#example-shadow-effect","title":"Example: Shadow Effect","text":"C#<pre><code>// Create shadow GameObject\nGameObject shadow = new GameObject(\"Shadow\");\nshadow.transform.parent = character.transform;\nshadow.transform.localPosition = new Vector3(0.2f, -0.2f, 0); // Offset\n\nSpriteRenderer shadowRenderer = shadow.AddComponent&lt;SpriteRenderer&gt;();\nSpriteRendererSync syncer = shadow.AddComponent&lt;SpriteRendererSync&gt;();\n\nsyncer.toMatch = character.GetComponent&lt;SpriteRenderer&gt;();\nsyncer.matchColor = false;\nshadowRenderer.color = new Color(0, 0, 0, 0.5f); // Semi-transparent black\n</code></pre>"},{"location":"features/inspector/utility-components/#spriterenderermetadata","title":"SpriteRendererMetadata","text":"<p>What it does: Stack-based color and material management for SpriteRenderers, allowing multiple systems to modify visuals with automatic priority handling and restoration.</p> <p>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.</p>"},{"location":"features/inspector/utility-components/#when-to-use_6","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Damage flashes (red tint on hit)</li> <li>Status effects (poison = green, frozen = blue)</li> <li>Power-up visuals (glow effects)</li> <li>Multiple overlapping visual modifiers</li> <li>Temporary material swaps</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Single, exclusive color changes (just set color directly)</li> <li>Animations (use Animator)</li> <li>Permanent changes (just set the property)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_6","title":"How to Use","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nSpriteRenderer renderer = GetComponent&lt;SpriteRenderer&gt;();\nSpriteRendererMetadata metadata = renderer.GetComponent&lt;SpriteRendererMetadata&gt;();\nif (metadata == null)\n    metadata = renderer.gameObject.AddComponent&lt;SpriteRendererMetadata&gt;();\n\n// Component A pushes red color\nmetadata.PushColor(this, Color.red);\n\n// Component B pushes blue color (takes precedence)\nmetadata.PushColor(otherComponent, Color.blue);\n// Renderer is now blue\n\n// Component B pops its color\nmetadata.PopColor(otherComponent);\n// Renderer reverts to red (Component A's color)\n\n// Component A pops its color\nmetadata.PopColor(this);\n// Renderer reverts to original color\n</code></pre>"},{"location":"features/inspector/utility-components/#stack-operations","title":"Stack Operations","text":"<p>Push/Pop (LIFO - Last In, First Out):</p> C#<pre><code>metadata.PushColor(owner, Color.red);    // Add to top of stack\nmetadata.PopColor(owner);                 // Remove from top (must match owner)\n</code></pre> <p>PushBack (add to bottom, lower priority):</p> C#<pre><code>metadata.PushBackColor(owner, Color.yellow);  // Added to bottom, doesn't change current color unless stack is empty\n</code></pre>"},{"location":"features/inspector/utility-components/#component-ownership","title":"Component Ownership","text":"<p>Each color/material is tagged with the Component that pushed it:</p> C#<pre><code>public class DamageFlash : MonoBehaviour\n{\n    void OnDamage()\n    {\n        metadata.PushColor(this, Color.red);\n        Invoke(nameof(RemoveFlash), 0.1f);\n    }\n\n    void RemoveFlash()\n    {\n        metadata.PopColor(this); // Only removes if this component owns top of stack\n    }\n}\n</code></pre> <p>This prevents Component A from accidentally removing Component B's color.</p>"},{"location":"features/inspector/utility-components/#material-stacking","title":"Material Stacking","text":"<p>Works identically for materials:</p> C#<pre><code>metadata.PushMaterial(this, glowMaterial);\n// ... later\nmetadata.PopMaterial(this);\n</code></pre>"},{"location":"features/inspector/utility-components/#original-state","title":"Original State","text":"C#<pre><code>Color original = metadata.OriginalColor;     // Color before any modifications\nColor current = metadata.CurrentColor;       // Current top-of-stack color\n\nMaterial originalMat = metadata.OriginalMaterial;\nMaterial currentMat = metadata.CurrentMaterial;\n</code></pre>"},{"location":"features/inspector/utility-components/#important-notes_1","title":"Important Notes","text":"<ul> <li>Automatically detects and stores original color/material in <code>Awake()</code></li> <li>Survives enable/disable cycles</li> <li>Priority is determined by push order (last push wins)</li> <li>Cleanup happens automatically when a component is destroyed</li> <li>If a non-owner tries to pop, the operation is ignored (defensive)</li> </ul>"},{"location":"features/inspector/utility-components/#centerpointoffset","title":"CenterPointOffset","text":"<p>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.</p> <p>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.</p>"},{"location":"features/inspector/utility-components/#when-to-use_7","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Sprites with off-center pivots that need gameplay center</li> <li>Rotation pivots different from visual pivot</li> <li>Targeting reticles</li> <li>AI targeting points</li> <li>Center-of-mass definitions</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Centered sprites (just use transform.position)</li> <li>Complex multi-point definitions</li> <li>Physics center of mass (use Rigidbody2D.centerOfMass)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_7","title":"How to Use","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nCenterPointOffset centerDef = gameObject.AddComponent&lt;CenterPointOffset&gt;();\ncenterDef.offset = new Vector2(0, 0.5f); // Center is 0.5 units above transform\ncenterDef.spriteUsesOffset = true; // Flag for sprite-specific logic\n\n// Get world-space center point\nVector2 centerInWorld = centerDef.CenterPoint;\n\n// Use for targeting\ntargetingSystem.AimAt(centerDef.CenterPoint);\n</code></pre>"},{"location":"features/inspector/utility-components/#offset-scaling","title":"Offset Scaling","text":"<p>Offset is multiplied by <code>transform.localScale</code>:</p> Text Only<pre><code>transform.position = (0, 0)\noffset = (1, 0)\ntransform.localScale = (2, 2, 2)\n\nCenterPoint = (0, 0) + (1, 0) * (2, 2) = (2, 0)\n</code></pre> <p>This ensures the center point scales with the object.</p>"},{"location":"features/inspector/utility-components/#sprite-flag","title":"Sprite Flag","text":"<p><code>spriteUsesOffset</code> is a boolean flag you can check in other systems:</p> C#<pre><code>if (center.spriteUsesOffset)\n{\n    // Apply sprite-specific logic\n}\n</code></pre> <p></p>"},{"location":"features/inspector/utility-components/#animatorenumstatemachine","title":"AnimatorEnumStateMachine","text":"<p>What it does: Type-safe, enum-based Animator state control. Maps enum values to Animator boolean parameters for exclusive state control.</p> <p>Problem it solves: Setting Animator bools with magic strings (<code>animator.SetBool(\"IsJumping\", true)</code>) is error-prone and hard to refactor. This provides compile-time safety and automatic cleanup of previous states.</p>"},{"location":"features/inspector/utility-components/#when-to-use_8","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Complex state machines (player states, enemy AI)</li> <li>Type-safe animation control</li> <li>State pattern implementations</li> <li>Refactor-friendly animation code</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Simple trigger-based animations (use animator.SetTrigger)</li> <li>Float/int parameters (only supports bools)</li> <li>Blend trees (use animator.SetFloat)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_8","title":"How to Use","text":"<p>1. Define an enum matching your Animator parameters:</p> C#<pre><code>public enum PlayerState\n{\n    Idle,    // Maps to Animator bool \"Idle\"\n    Running, // Maps to Animator bool \"Running\"\n    Jumping, // Maps to Animator bool \"Jumping\"\n    Falling  // Maps to Animator bool \"Falling\"\n}\n</code></pre> <p>2. Create the state machine:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nAnimator animator = GetComponent&lt;Animator&gt;();\nAnimatorEnumStateMachine&lt;PlayerState&gt; stateMachine;\n\nvoid Awake()\n{\n    stateMachine = new AnimatorEnumStateMachine&lt;PlayerState&gt;(animator, PlayerState.Idle);\n}\n</code></pre> <p>3. Set state:</p> C#<pre><code>void Jump()\n{\n    stateMachine.Value = PlayerState.Jumping;\n    // Automatically sets Animator bools:\n    //   Idle = false\n    //   Running = false\n    //   Jumping = true\n    //   Falling = false\n}\n</code></pre>"},{"location":"features/inspector/utility-components/#automatic-state-management","title":"Automatic State Management","text":"<p>Setting <code>stateMachine.Value</code> automatically:</p> <ol> <li>Sets ALL enum-named bools to <code>false</code></li> <li>Sets ONLY the matching bool to <code>true</code></li> </ol> <p>This ensures exclusive state control (only one state active).</p>"},{"location":"features/inspector/utility-components/#animator-setup","title":"Animator Setup","text":"<p>Your Animator needs bool parameters matching enum names:</p> Text Only<pre><code>Animator parameters:\n- Idle (bool)\n- Running (bool)\n- Jumping (bool)\n- Falling (bool)\n\nTransitions:\n- Any State \u2192 Idle: Idle == true\n- Any State \u2192 Running: Running == true\n- Any State \u2192 Jumping: Jumping == true\n- Any State \u2192 Falling: Falling == true\n</code></pre>"},{"location":"features/inspector/utility-components/#serialization","title":"Serialization","text":"<p><code>AnimatorEnumStateMachine&lt;T&gt;</code> is serializable for debugging in Inspector.</p> <p></p>"},{"location":"features/inspector/utility-components/#coroutinehandler","title":"CoroutineHandler","text":"<p>What it does: Singleton MonoBehaviour that provides a global coroutine host for non-MonoBehaviour classes.</p> <p>Problem it solves: Coroutines require a MonoBehaviour to start. Static classes, plain C# objects, and ScriptableObjects can't start coroutines directly.</p>"},{"location":"features/inspector/utility-components/#when-to-use_9","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Starting coroutines from static utility classes</li> <li>Coroutines in plain C# objects</li> <li>ScriptableObjects that need coroutines</li> <li>Global/scene-independent coroutines</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>MonoBehaviours (just use StartCoroutine)</li> <li>Short-lived coroutines (might outlive the object)</li> <li>Frame-perfect timing (singleton has overhead)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_9","title":"How to Use","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// From anywhere\nCoroutineHandler.Instance.StartCoroutine(MyCoroutine());\n\nIEnumerator MyCoroutine()\n{\n    yield return new WaitForSeconds(1f);\n    Debug.Log(\"Done!\");\n}\n</code></pre>"},{"location":"features/inspector/utility-components/#lifetime","title":"Lifetime","text":"<p>CoroutineHandler persists across scene loads (<code>DontDestroyOnLoad</code>), so coroutines survive scene transitions.</p>"},{"location":"features/inspector/utility-components/#stopping-coroutines","title":"Stopping Coroutines","text":"C#<pre><code>Coroutine routine = CoroutineHandler.Instance.StartCoroutine(MyCoroutine());\n// ... later\nCoroutineHandler.Instance.StopCoroutine(routine);\n</code></pre>"},{"location":"features/inspector/utility-components/#starttracker","title":"StartTracker","text":"<p>What it does: Simple component that tracks whether <code>MonoBehaviour.Start()</code> has been called.</p> <p>Problem it solves: Sometimes you need to know if initialization (Start) has completed, especially in the editor or during complex initialization orders.</p>"},{"location":"features/inspector/utility-components/#when-to-use_10","title":"When to Use","text":"<p>\u2705 Use for:</p> <ul> <li>Initialization order checking</li> <li>Conditional setup logic</li> <li>Editor tools validating scene state</li> <li>Testing initialization</li> </ul> <p>\u274c Don't use for:</p> <ul> <li>Production gameplay logic (architectural smell)</li> <li>Most scenarios (rethink if you need this)</li> </ul>"},{"location":"features/inspector/utility-components/#how-to-use_10","title":"How to Use","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Add to GameObject\nStartTracker tracker = gameObject.AddComponent&lt;StartTracker&gt;();\n\n// Later, check if Start has been called\nif (tracker.Started)\n{\n    // Initialization complete\n}\n</code></pre>"},{"location":"features/inspector/utility-components/#matchcollidertosprite","title":"MatchColliderToSprite","text":"<p>Automatically syncs <code>PolygonCollider2D</code> shape to sprite's physics shape.</p> <p>See: Editor Tools Guide - MatchColliderToSprite</p> <p></p>"},{"location":"features/inspector/utility-components/#polygoncollider2doptimizer","title":"PolygonCollider2DOptimizer","text":"<p>Reduces PolygonCollider2D point count using Douglas-Peucker simplification.</p> <p>See: Editor Tools Guide - PolygonCollider2DOptimizer</p>"},{"location":"features/inspector/utility-components/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/utility-components/#general","title":"General","text":"<ul> <li>One utility per GameObject: Don't stack unrelated utilities on the same GameObject</li> <li>Configure in Awake/Start: Set properties before first Update</li> <li>Remove when done: Disable/destroy utilities that are no longer needed</li> <li>Test in builds: Some utilities behave differently in editor vs. builds (ChildSpawner)</li> </ul>"},{"location":"features/inspector/utility-components/#performance","title":"Performance","text":"<ul> <li>CircleLineRenderer: Use sparingly, each instance updates line vertices</li> <li>SpriteRendererSync: Updates every LateUpdate, don't use for hundreds of sprites</li> <li>MatchTransform: Choose an appropriate update mode (FixedUpdate for physics, LateUpdate for camera)</li> </ul>"},{"location":"features/inspector/utility-components/#architecture","title":"Architecture","text":"<ul> <li>CollisionProxy: Great for composition, but don't overuse events everywhere</li> <li>SpriteRendererMetadata: Document ownership in team code (who can push/pop)</li> <li>AnimatorEnumStateMachine: Keep enum names matching Animator parameters</li> </ul>"},{"location":"features/inspector/utility-components/#related-documentation","title":"Related Documentation","text":"<ul> <li>Math &amp; Extensions - Extension methods used by utilities</li> <li>Editor Tools Guide - Editor components</li> <li>Helpers Guide - Non-component helper classes</li> </ul>"},{"location":"features/inspector/visual-components/","title":"Visual Components Guide","text":""},{"location":"features/inspector/visual-components/#tldr-why-use-these","title":"TL;DR \u2014 Why Use These","text":"<ul> <li>AnimatedSpriteLayer: Data structure for packaging sprite animation frames with per-frame offsets and transparency</li> <li>LayeredImage: UI Toolkit element that composites multiple sprite animation layers into a single animated image</li> <li>EnhancedImage: Extended Unity UI Image with HDR color support and shape masking</li> </ul> <p>These components solve the problem of creating complex, multi-layer sprite animations without pre-rendering every combination into massive sprite sheets.</p>"},{"location":"features/inspector/visual-components/#animatedspritelayer","title":"AnimatedSpriteLayer","text":"<p>What it is: An immutable data structure (struct) that packages a sprite animation sequence with per-frame position offsets and layer-wide alpha transparency.</p> <p>Why it exists: When building complex sprite animations (like a character with equipment, effects, or layered body parts), you need a standardized way to represent each layer with its timing, positioning, and transparency. This struct is the building block for the LayeredImage composition system.</p> <p>Problem it solves:</p> <ul> <li>Eliminates manually syncing sprite frames, offsets, and alpha values across multiple systems</li> <li>Provides type-safe storage for animation layer data</li> <li>Automatically converts world-space offsets to pixel-space for rendering</li> <li>Validates texture readability at construction time (catches import setting errors early)</li> </ul>"},{"location":"features/inspector/visual-components/#when-to-use","title":"When to Use","text":"<p>\u2705 Use when:</p> <ul> <li>Building <code>LayeredImage</code> compositions</li> <li>Creating character animations with separate layers for body parts</li> <li>Combining sprite effects (glow, shadow, outline) with base sprites</li> <li>You need frame-by-frame position adjustments (bobbing, recoil, etc.)</li> <li>Working with sprite-based animations that need dynamic layering</li> </ul> <p>\u274c Don't use when:</p> <ul> <li>You only have a single sprite sequence (just use <code>Animator</code>)</li> <li>Sprites don't need per-frame offsets (just use sprite arrays)</li> <li>You're working with UI Toolkit animations (use USS transitions)</li> <li>Performance is critical and you can pre-render combinations</li> </ul>"},{"location":"features/inspector/visual-components/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Visuals;\nusing UnityEngine;\n\n// Create a layer from sprite sequence\nSprite[] walkCycleFrames = LoadWalkCycleSprites(); // Must have Read/Write enabled!\n\n// Optional: per-frame offsets in world space (e.g., for bobbing motion)\nVector2[] walkBobOffsets = new[]\n{\n    new Vector2(0, 0.1f),   // Frame 0: slight up\n    new Vector2(0, 0),      // Frame 1: neutral\n    new Vector2(0, 0.1f),   // Frame 2: slight up\n    new Vector2(0, 0)       // Frame 3: neutral\n};\n\nAnimatedSpriteLayer bodyLayer = new AnimatedSpriteLayer(\n    sprites: walkCycleFrames,\n    worldSpaceOffsets: walkBobOffsets,\n    alpha: 1f  // Fully opaque\n);\n\n// Create equipment layer (same frame count, different sprites)\nSprite[] helmetFrames = LoadHelmetSprites();\nAnimatedSpriteLayer helmetLayer = new AnimatedSpriteLayer(\n    sprites: helmetFrames,\n    worldSpaceOffsets: null, // No offsets\n    alpha: 0.9f  // Slightly transparent\n);\n\n// Combine in LayeredImage (see below)\n</code></pre>"},{"location":"features/inspector/visual-components/#editor-only-creating-from-animationclip","title":"Editor-Only: Creating from AnimationClip","text":"<p>In editor code, you can create layers directly from AnimationClips:</p> C#<pre><code>#if UNITY_EDITOR\nusing UnityEditor;\n\nAnimationClip walkClip = AssetDatabase.LoadAssetAtPath&lt;AnimationClip&gt;(\"Assets/Animations/Walk.anim\");\nAnimatedSpriteLayer layer = new AnimatedSpriteLayer(\n    clip: walkClip,\n    worldSpaceOffsets: null,\n    alpha: 1f\n);\n#endif\n</code></pre>"},{"location":"features/inspector/visual-components/#important-notes","title":"Important Notes","text":"<p>Texture Readability: All sprites must have Read/Write Enabled in their texture import settings. The constructor validates this and logs errors for non-readable textures.</p> <p>To fix:</p> <ol> <li>Select the texture asset</li> <li>In Inspector, check \"Read/Write Enabled\"</li> <li>Click Apply</li> </ol> <p>Frame Rate: Default frame rate is 12 fps (stored in <code>AnimatedSpriteLayer.FrameRate</code> constant). This matches classic sprite animation timing.</p> <p>Offset Conversion: World-space offsets are automatically converted to pixel-space using sprite pixels-per-unit. This ensures offsets scale correctly with sprite resolution.</p>"},{"location":"features/inspector/visual-components/#layeredimage","title":"LayeredImage","text":"<p>What it is: A UI Toolkit <code>VisualElement</code> that composites multiple <code>AnimatedSpriteLayer</code> instances into a single animated image with alpha blending, automatic cropping, and frame timing.</p> <p>Why it exists: Creating character customization systems (body + equipment), visual effects (base + glow), or any multi-layer sprite animation traditionally requires pre-rendering every combination into massive sprite sheets. LayeredImage composes layers dynamically at runtime.</p> <p>Problem it solves:</p> <ul> <li>Sprite sheet explosion: Instead of 10 bodies \u00d7 20 helmets \u00d7 15 armors = 3,000 pre-rendered sprites, you have 10 + 20 + 15 = 45 source sprites</li> <li>Memory efficiency: Only active layers are loaded, not every possible combination</li> <li>Runtime flexibility: Change equipment/effects without new assets</li> <li>Automatic composition: Handles alpha blending, pivot alignment, and cropping</li> </ul>"},{"location":"features/inspector/visual-components/#when-to-use_1","title":"When to Use","text":"<p>\u2705 Use when:</p> <ul> <li>Character customization systems (swap equipment, clothing, accessories)</li> <li>Visual effects that layer over base sprites (shields, auras, damage flashes)</li> <li>Procedural sprite generation from components</li> <li>UI that needs animated, multi-layer sprites</li> <li>You want to avoid combinatorial explosion of pre-rendered sprites</li> </ul> <p>\u274c Don't use when:</p> <ul> <li>Single-layer animations (use Unity's <code>Image</code> or <code>Animator</code>)</li> <li>3D models (use skinned mesh renderers)</li> <li>Performance is absolutely critical and you can afford pre-rendered sheets</li> <li>Sprites don't share the same frame count/timing</li> <li>Working in UGUI (use <code>EnhancedImage</code> for UGUI, though it doesn't support layering)</li> </ul>"},{"location":"features/inspector/visual-components/#basic-usage_1","title":"Basic Usage","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Visuals.UIToolkit;\nusing UnityEngine.UIElements;\n\n// Create layers (see AnimatedSpriteLayer section above)\nAnimatedSpriteLayer[] layers = new[]\n{\n    bodyLayer,    // Base character\n    armorLayer,   // Equipment layer 1\n    helmetLayer,  // Equipment layer 2\n    glowLayer     // Effect layer\n};\n\n// Create LayeredImage\nLayeredImage characterImage = new LayeredImage(\n    inputSpriteLayers: layers,\n    backgroundColor: null,  // Transparent background (or use Color for solid background)\n    fps: 12f,               // Animation speed\n    updatesSelf: true,      // Automatically advances frames (uses Unity editor ticks or coroutines)\n    pixelCutoff: 0.01f      // Alpha threshold for cropping transparent pixels\n);\n\n// Add to UI Toolkit hierarchy\nrootVisualElement.Add(characterImage);\n</code></pre>"},{"location":"features/inspector/visual-components/#manual-frame-control","title":"Manual Frame Control","text":"<p>If you need precise control over frame advancement:</p> C#<pre><code>LayeredImage manualImage = new LayeredImage(\n    inputSpriteLayers: layers,\n    backgroundColor: null,\n    fps: 12f,\n    updatesSelf: false,  // Disable automatic updates\n    pixelCutoff: 0.01f\n);\n\n// In your update loop\nvoid Update()\n{\n    manualImage.Update(force: false); // Advances frame based on elapsed time\n}\n\n// Or force immediate frame advance\nmanualImage.Update(force: true);\n</code></pre>"},{"location":"features/inspector/visual-components/#changing-animation-speed","title":"Changing Animation Speed","text":"C#<pre><code>// Set frames per second at runtime\ncharacterImage.Fps = 24f; // Speed up animation\ncharacterImage.Fps = 6f;  // Slow down animation\n</code></pre>"},{"location":"features/inspector/visual-components/#visual-demo","title":"Visual Demo","text":"<p>LayeredImage in Action</p> <p></p> <p>Character with dynamically composited layers animating at runtime</p> <p></p> <p>Swapping equipment layers at runtime without pre-rendered sprite sheets</p>"},{"location":"features/inspector/visual-components/#how-compositing-works","title":"How Compositing Works","text":"<p>LayeredImage performs these steps each frame:</p> <ol> <li>Allocates canvas: Creates a texture large enough to hold all layers with their offsets</li> <li>Alpha blending: Layers are composited back-to-front with alpha blending</li> <li>Pivot alignment: Each sprite's pivot point is respected during positioning</li> <li>Offset application: Per-frame pixel offsets are applied</li> <li>Cropping: Transparent borders are trimmed (configurable via <code>pixelCutoff</code>)</li> <li>Rendering: Final composited texture is displayed</li> </ol> <p>Performance optimization:</p> <ul> <li>Uses parallel processing for large sprites (2048+ pixels total)</li> <li>Employs array pooling to minimize GC allocations</li> <li>Caches composited frames when possible</li> </ul>"},{"location":"features/inspector/visual-components/#pixel-cutoff-parameter","title":"Pixel Cutoff Parameter","text":"<p>Controls how aggressive transparent pixel cropping is:</p> C#<pre><code>// More aggressive cropping (removes near-transparent pixels)\nlayeredImage.pixelCutoff = 0.05f;\n\n// Less aggressive (keeps more semi-transparent pixels)\nlayeredImage.pixelCutoff = 0.001f;\n\n// No cropping (includes fully transparent border)\nlayeredImage.pixelCutoff = 0f;\n</code></pre> <p>Higher values = smaller final image, but may clip soft edges (glows, shadows).</p>"},{"location":"features/inspector/visual-components/#important-notes_1","title":"Important Notes","text":"<p>Frame Synchronization: All layers must have the same number of frames. Mixing 4-frame and 8-frame animations will cause visual glitches.</p> <p>Performance Considerations:</p> <ul> <li>Compositing happens every frame for animated images</li> <li>Large sprite resolutions (1024\u00d71024+) will impact performance</li> <li>Consider pre-rendering if targeting low-end devices</li> <li>Parallel processing threshold is 2048 pixels (width \u00d7 height)</li> </ul> <p>Editor vs Runtime:</p> <ul> <li>In Editor: Uses Unity's editor update ticks for animation</li> <li>In Runtime: Uses coroutines for frame timing</li> <li>Both honor <code>updatesSelf</code> setting</li> </ul>"},{"location":"features/inspector/visual-components/#enhancedimage-ugui","title":"EnhancedImage (UGUI)","text":"<p>What it is: An extended version of Unity's UI <code>Image</code> component with HDR color support and texture-based shape masking.</p> <p>Why it exists: Unity's standard <code>Image</code> component doesn't support:</p> <ul> <li>HDR colors (for bloom/glow effects)</li> <li>Complex shape masks (beyond sprite masks)</li> <li>Shader-based shape rendering</li> </ul> <p>See full documentation: Editor Tools Guide - EnhancedImage</p> <p>Visual Demo</p> <p></p> <p>HDR color values above 1.0 create bloom effects when post-processing is enabled</p> <p>Quick example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Visuals.UGUI;\n\nEnhancedImage image = GetComponent&lt;EnhancedImage&gt;();\n\n// HDR color for bloom\nimage.HdrColor = new Color(2f, 0.5f, 0.1f, 1f); // RGB values &gt; 1 for bloom\n\n// Shape mask\nimage.shapeMask = myMaskTexture; // Black areas are transparent\n</code></pre>"},{"location":"features/inspector/visual-components/#best-practices","title":"Best Practices","text":""},{"location":"features/inspector/visual-components/#animatedspritelayer_1","title":"AnimatedSpriteLayer","text":"<ul> <li>Always enable Read/Write on source textures (build will fail otherwise)</li> <li>Keep frame counts consistent across layers for the same animation</li> <li>Use world-space offsets for consistent motion across different sprite resolutions</li> <li>Cache layer instances when using the same animation repeatedly (they're immutable)</li> </ul>"},{"location":"features/inspector/visual-components/#layeredimage_1","title":"LayeredImage","text":"<ul> <li>Layer order matters: Layers are rendered front-to-back in array order</li> <li>Optimize sprite sizes: Trim transparent borders before importing (use Sprite Editor's \"Tight\" mode)</li> <li>Profile on target hardware: Mobile devices may struggle with 512\u00d7512+ composites at 60fps</li> <li>Use manual updates when syncing with non-UI systems (like gameplay state)</li> <li>Pre-render if combinations are limited and performance is critical</li> </ul>"},{"location":"features/inspector/visual-components/#enhancedimage","title":"EnhancedImage","text":"<ul> <li>Don't mix with Image: EnhancedImage replaces Unity's Image, don't use both</li> <li>Material cleanup is automatic but test in edit mode transitions</li> <li>HDR requires post-processing: Ensure Bloom is enabled in your camera's post-processing</li> </ul>"},{"location":"features/inspector/visual-components/#related-documentation","title":"Related Documentation","text":"<ul> <li>Editor Tools Guide - EnhancedImage editor integration</li> <li>Samples - Example projects in <code>Samples~</code> folder in the repository</li> <li>Math &amp; Extensions - Color utilities used internally</li> </ul>"},{"location":"features/inspector/visual-components/#faq","title":"FAQ","text":"<p>Q: Can I mix different frame counts in LayeredImage? A: No, all layers must have the same frame count. Pad shorter animations with duplicate frames if needed.</p> <p>Q: Why are my layers not aligned correctly? A: Check that sprite pivots are set correctly (usually center). LayeredImage respects sprite pivot points.</p> <p>Q: Can I change layers at runtime? A: Currently no, LayeredImage is immutable after construction. Create a new instance with updated layers.</p> <p>Q: Performance impact vs pre-rendered sprites? A: Compositing costs ~1-3ms per image on modern hardware. Pre-rendered is faster but uses more memory/storage.</p> <p>Q: Does this work with Unity's Animator? A: No, LayeredImage is independent. It's designed for UI Toolkit programmatic control.</p> <p>Q: Can I export the composited result? A: Not directly, but you could capture the rendered texture using <code>Texture2D.ReadPixels</code> in a render texture setup.</p>"},{"location":"features/logging/logging-extensions/","title":"Unity Logging Extensions &amp; Tag Formatter","text":"<p>Bring structured, color-coded logs to any Unity project without sprinkling <code>Debug.Log</code> everywhere. <code>WallstopStudiosLogger</code> adds extension methods (<code>this.Log</code>, <code>this.LogWarn</code>, <code>this.LogError</code>, <code>this.LogDebug</code>) that automatically capture component metadata, thread info, timestamps, and user-defined tags rendered by <code>UnityLogTagFormatter</code>.</p> <ul> <li>Thread-safe: Logs are marshaled back to the Unity main thread when required (via <code>UnityMainThreadDispatcher</code> / <code>UnityMainThreadGuard</code>).</li> <li>Readable output: Pretty mode prefixes <code>time|GameObject[Component]</code> when logging on the main thread and inserts <code>|thread|</code> only when background workers emit messages, keeping logs deterministic without extra noise.</li> <li>Tag formatter: Apply rich text decorations inline (<code>$\"{name:b,color=cyan}\"</code>) without string concatenation. Tags deduplicate automatically and can be stacked in any order.</li> </ul> <p>These helpers live in <code>Runtime/Core/Extension/WallstopStudiosLogger.cs</code> and <code>Runtime/Core/Helper/Logging/UnityLogTagFormatter.cs</code>. Tests at <code>Tests/Runtime/Extensions/LoggingExtensionTests.cs</code> demonstrate every supported scenario.</p>"},{"location":"features/logging/logging-extensions/#sample-scene","title":"Sample Scene","text":"<ul> <li>Import the <code>Logging \u2013 Tag Formatter</code> package sample and open <code>Samples~/Logging - Tag Formatter/Scenes/LoggingDemo.unity</code>.</li> <li>Press Play to use the on-screen toggles (global logging, component logging, pretty output) and emit Info/Warn/Error logs that showcase the decorators.</li> <li>Review <code>LoggingDemoBootstrap</code> (decorator registration) and <code>LoggingDemoController</code> (runtime toggles + <code>this.Log*</code> usage) to copy the patterns into your project.</li> </ul>"},{"location":"features/logging/logging-extensions/#quick-start","title":"Quick Start","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\npublic sealed class EnemyHUD : MonoBehaviour\n{\n    private void Start()\n    {\n        string mode = Application.isEditor ? \"Test\" : \"Live\";\n        int hp = 42;\n\n        this.Log(\n            $\"Player {\"Rogue-17\":b,color=orange} :: HP {hp:color=#FF4444} ({mode:italic})\"\n        );\n    }\n}\n</code></pre> <ul> <li>Pass interpolated strings directly; the formatter applies tags before Unity renders the message.</li> <li>Use <code>pretty: false</code> if you only want the decorated text without the timestamp (or optional thread) prefix.</li> <li>Call <code>this.LogWarn</code>, <code>this.LogError</code>, or <code>this.LogDebug</code> for severity-specific output; all overloads accept <code>Exception e</code> to append stack traces.</li> </ul>"},{"location":"features/logging/logging-extensions/#enabling-logging-in-builds","title":"Enabling logging in builds","text":"<p><code>ENABLE_UBERLOGGING</code> is defined automatically for <code>DEBUG</code>, <code>DEVELOPMENT_BUILD</code>, and <code>UNITY_EDITOR</code>. Define it manually (or <code>DEBUG_LOGGING</code> / <code>WARN_LOGGING</code> / <code>ERROR_LOGGING</code>) in Player Settings if you need the extensions in release builds.</p>"},{"location":"features/logging/logging-extensions/#default-tag-reference","title":"Default Tag Reference","text":"Tag syntax Effect Notes <code>:b</code>, <code>:bold</code>, <code>:!</code> Wraps value in <code>&lt;b&gt;</code> Editor-only (uses Unity rich text) <code>:i</code>, <code>:italic</code>, <code>:_</code> Wraps value in <code>&lt;i&gt;</code> Editor-only <code>:json</code> Serializes value via <code>ToJson()</code> Works in player builds <code>:#color</code>, <code>:color=name</code>, <code>:color=#hex</code> Wraps with <code>&lt;color=...&gt;</code> Named colors resolve to <code>UnityEngine.Color</code> constants <code>:42</code>, <code>:size=42</code> Wraps with <code>&lt;size=42&gt;</code> Integers 1\u2013100 (or any positive int) <ul> <li>Combine tags using commas: <code>$\"{stats:json,b,color=yellow}\"</code> emits bold, colored JSON.</li> <li>Tags are applied in priority order and deduplicate automatically, so repeating <code>:b</code> has no effect.</li> </ul>"},{"location":"features/logging/logging-extensions/#custom-decorations","title":"Custom Decorations","text":"<p>Register project-specific tags at startup (for example, in an <code>InitializeOnLoad</code> editor script or a runtime bootstrapper):</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\nusing WallstopStudios.UnityHelpers.Core.Helper.Logging;\n\n[InitializeOnLoad]\ninternal static class LoggingBootstrap\n{\n    static LoggingBootstrap()\n    {\n        UnityLogTagFormatter formatter = WallstopStudiosLogger.LogInstance;\n\n        formatter.AddDecoration(\n            predicate: tag =&gt; tag.StartsWith(\"stat:\", StringComparison.OrdinalIgnoreCase),\n            format: (tag, value) =&gt;\n            {\n                string label = tag.Substring(\"stat:\".Length);\n                return $\"&lt;color=#7AD7FF&gt;[{label}]&lt;/color&gt; {value}\";\n            },\n            tag: \"StatLabel\",\n            priority: -10 // run before built-ins\n        );\n    }\n}\n</code></pre> <p>Key APIs:</p> <ul> <li><code>AddDecoration(string match, Func&lt;object,string&gt; format, string tag, int priority = 0, bool editorOnly = false, bool force = false)</code></li> <li><code>AddDecoration(Func&lt;string,bool&gt; predicate, Func&lt;string,object,string&gt; format, string tag, int priority = 0, bool editorOnly = false, bool force = false)</code></li> <li><code>RemoveDecoration(string tag, out Decoration removed)</code> to swap or disable decorators at runtime.</li> <li><code>UnityLogTagFormatter.Separator (',')</code> controls how stacked tags are parsed.</li> </ul> <p>Use negative priorities for \u201couter\u201d wrappers (run earlier) and higher numbers for final passes. Setting <code>force: true</code> replaces existing tags with the same name.</p>"},{"location":"features/logging/logging-extensions/#extension-method-cheat-sheet","title":"Extension Method Cheat Sheet","text":"API Description <code>component.Log(FormattableString, Exception e = null, bool pretty = true)</code> Sends an info log through the formatter. Guarded by <code>ENABLE_UBERLOGGING</code>/<code>DEBUG_LOGGING</code>. <code>component.LogWarn(...)</code>, <code>component.LogError(...)</code>, <code>component.LogDebug(...)</code> Severity-specific variants with the same signature. <code>component.GenericToString()</code> Serializes all public fields/properties into JSON (used by the formatter when you pass <code>:json</code>). <code>component.EnableLogging()</code> / <code>component.DisableLogging()</code> Per-object toggle. Disabled components are skipped without allocations. <code>component.GlobalEnableLogging()</code> / <code>component.GlobalDisableLogging()</code> / <code>SetGlobalLoggingEnabled(bool)</code> Global kill switch suitable for in-game consoles or dev toggles. <code>WallstopStudiosLogger.IsGlobalLoggingEnabled()</code> Query current state (useful for tooling UIs). <p>Additional behavior:</p> <ul> <li>Thread routing: If a log originates off the main thread, the extension tries <code>UnityMainThreadDispatcher.TryDispatchToMainThread</code> first. If unavailable, it falls back to <code>UnityMainThreadGuard.TryPostToMainThread</code> and, if that fails, emits an \u201coffline\u201d log with a <code>[WallstopMainThreadLogger:*]</code> prefix.</li> <li>Pretty output: Keeps logs uniform (<code>timestamp|GameObject[Component]|message</code> on the main thread, inserting <code>|thread|</code> only for worker threads). Pass <code>pretty: false</code> when emitting data the Unity console already decorates (for example, performance CSV dumps).</li> <li>Context awareness: Unity context objects are forwarded to <code>Debug.Log*</code>, preserving click-to-focus navigation even when logs originate from pooled helper classes.</li> </ul>"},{"location":"features/logging/logging-extensions/#best-practices","title":"Best Practices","text":"<ol> <li>Register tags once \u2014 Use static constructors or <code>[RuntimeInitializeOnLoadMethod]</code> to register project-wide tags. Avoid allocating per-frame delegates.</li> <li>Prefer interpolation \u2014 <code>$\"{health:json}\"</code> keeps minimal formatting allocations compared to <code>string.Format</code>.</li> <li>Use <code>pretty: false</code> for exporters \u2014 When writing to files or parsing logs, disable prefixes to simplify downstream tooling.</li> <li>Gate release builds \u2014 If you plan to leave logging enabled in production, explicitly define <code>ENABLE_UBERLOGGING</code> (or <code>DEBUG_LOGGING</code> / <code>WARN_LOGGING</code> / <code>ERROR_LOGGING</code>) and make sure log volume is acceptable (or wrap noisy calls in your own <code>#define</code>s).</li> <li>Leverage tests \u2014 <code>Tests/Runtime/Extensions/LoggingExtensionTests.cs</code> covers every default tag and stacking scenario. Copy those patterns when adding new decorations to ensure behavior stays deterministic.</li> </ol>"},{"location":"features/logging/logging-extensions/#related-topics","title":"Related Topics","text":"<ul> <li>Unity Main Thread Dispatcher \u2014 Ensures background logs can find the main thread safely.</li> <li>Helper Utilities Overview \u2014 Highlights other runtime helpers.</li> </ul>"},{"location":"features/logging/unity-main-thread-dispatcher/","title":"Unity Main Thread Dispatcher &amp; Guard","text":"<p><code>UnityMainThreadDispatcher</code> and <code>UnityMainThreadGuard</code> provide thread-safe access to Unity's main thread from background workers, ensuring callbacks execute correctly and preventing common threading errors.</p>"},{"location":"features/logging/unity-main-thread-dispatcher/#overview","title":"Overview","text":"Component Purpose Usage Pattern <code>UnityMainThreadDispatcher</code> Dispatch work to the main thread <code>dispatcher.RunOnMainThread(() =&gt; ...)</code> <code>UnityMainThreadGuard</code> Assert code is on the main thread <code>UnityMainThreadGuard.EnsureMainThread()</code>"},{"location":"features/logging/unity-main-thread-dispatcher/#unitymainthreaddispatcher","title":"UnityMainThreadDispatcher","text":"<p>The <code>UnityMainThreadDispatcher</code> is the package-wide bridge for marshaling callbacks from worker threads back onto Unity's main thread. The dispatcher is implemented as a <code>RuntimeSingleton&lt;UnityMainThreadDispatcher&gt;</code>, is marked <code>[ExecuteAlways]</code>, and runs both in Edit Mode and Play Mode so background logging, importers, and build scripts can all enqueue work safely.</p>"},{"location":"features/logging/unity-main-thread-dispatcher/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Helper;\n\npublic class BackgroundProcessor\n{\n    public void ProcessOnWorkerThread()\n    {\n        // Queue work from any thread to execute on Unity's main thread\n        UnityMainThreadDispatcher.Instance.RunOnMainThread(() =&gt;\n        {\n            // This code executes on the main thread during the next Update\n            Debug.Log(\"Safe to call Unity APIs here\");\n            Object.FindObjectOfType&lt;Player&gt;().UpdateState();\n        });\n    }\n}\n</code></pre>"},{"location":"features/logging/unity-main-thread-dispatcher/#api","title":"API","text":"C#<pre><code>// Queue action to run on main thread (logs warning if queue is full)\nUnityMainThreadDispatcher.Instance.RunOnMainThread(Action action);\n\n// Queue action silently (returns false if queue is full, no warning)\nbool queued = UnityMainThreadDispatcher.Instance.TryRunOnMainThread(Action action);\n\n// Check current queue depth\nint pending = UnityMainThreadDispatcher.Instance.PendingActionCount;\n\n// Configure queue limit (0 = unlimited)\nUnityMainThreadDispatcher.Instance.PendingActionLimit = 4096;\n</code></pre>"},{"location":"features/logging/unity-main-thread-dispatcher/#use-cases","title":"Use Cases","text":"C#<pre><code>// Async/await with Unity API access\npublic async Task&lt;GameObject&gt; LoadAssetAsync(string path)\n{\n    GameObject asset = await LoadFromDiskAsync(path);\n\n    // Marshal instantiation to main thread\n    TaskCompletionSource&lt;GameObject&gt; tcs = new();\n    UnityMainThreadDispatcher.Instance.RunOnMainThread(() =&gt;\n    {\n        tcs.SetResult(Object.Instantiate(asset));\n    });\n\n    return await tcs.Task;\n}\n\n// Thread Pool work\nThreadPool.QueueUserWorkItem(_ =&gt;\n{\n    ComputedData data = ProcessHeavyComputation();\n\n    UnityMainThreadDispatcher.Instance.RunOnMainThread(() =&gt;\n    {\n        ApplyToScene(data);  // Safe Unity API calls\n    });\n});\n\n// Task continuation\nTask.Run(() =&gt; ComputeData())\n    .ContinueWith(task =&gt;\n    {\n        UnityMainThreadDispatcher.Instance.RunOnMainThread(() =&gt;\n        {\n            DisplayResults(task.Result);\n        });\n    });\n</code></pre>"},{"location":"features/logging/unity-main-thread-dispatcher/#unitymainthreadguard","title":"UnityMainThreadGuard","text":"<p>The <code>UnityMainThreadGuard</code> is a guard/assertion that throws an exception if called from a background thread. Use it to protect Unity API access points that must never be called off-thread.</p> <p>\u26a0\ufe0f Important: <code>EnsureMainThread()</code> does NOT dispatch work to the main thread. It throws <code>InvalidOperationException</code> if called from a background thread. To dispatch work, use <code>UnityMainThreadDispatcher.Instance.RunOnMainThread()</code>.</p> <p>\ud83d\udce6 Internal API: <code>UnityMainThreadGuard</code> is an <code>internal</code> class, accessible only within the Unity Helpers assembly or via <code>[InternalsVisibleTo]</code>. For most use cases, prefer using <code>UnityMainThreadDispatcher</code> directly which is <code>public</code>.</p>"},{"location":"features/logging/unity-main-thread-dispatcher/#basic-usage_1","title":"Basic Usage","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\npublic class PlayerManager\n{\n    private Player _cachedPlayer;\n\n    public Player Instance\n    {\n        get\n        {\n            // Throws if accessed from background thread\n            UnityMainThreadGuard.EnsureMainThread();\n            return _cachedPlayer;\n        }\n    }\n\n    public void RefreshUI()\n    {\n        // Optional context string for better error messages\n        UnityMainThreadGuard.EnsureMainThread(\"Refreshing player UI\");\n        // Safe to interact with Unity objects here\n    }\n}\n</code></pre>"},{"location":"features/logging/unity-main-thread-dispatcher/#why-it-exists","title":"Why It Exists","text":"<p>Problem: Unity APIs must be called from the main thread, but bugs can allow background thread access that causes silent corruption or crashes.</p> <p>Solution: <code>UnityMainThreadGuard.EnsureMainThread()</code> fails fast with a clear error message, catching threading bugs during development.</p>"},{"location":"features/logging/unity-main-thread-dispatcher/#api_1","title":"API","text":"C#<pre><code>// Throws InvalidOperationException if not on main thread\nUnityMainThreadGuard.EnsureMainThread();\nUnityMainThreadGuard.EnsureMainThread(\"optional context\");\n\n// Check without throwing (internal API)\nbool isMainThread = UnityMainThreadGuard.IsMainThread;\n</code></pre>"},{"location":"features/logging/unity-main-thread-dispatcher/#default-bootstrapping-flow","title":"Default Bootstrapping Flow","text":"<ul> <li>A <code>[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]</code> (<code>EnsureDispatcherBootstrap</code>) spins up the dispatcher before user assemblies receive their own <code>RuntimeInitializeOnLoadMethod</code> callbacks. This guarantees that worker threads can enqueue diagnostics as soon as your game loads.</li> <li>Inside the editor, <code>[InitializeOnLoadMethod]</code> (<code>EnsureDispatcherBootstrapInEditor</code>) mirrors the same behavior for Edit Mode tools. The bootstrap politely skips when play mode is already running, so it does not duplicate the instance scene-side.</li> <li>Both entry points run through <code>UnityMainThreadGuard.EnsureMainThread(...)</code> before touching Unity APIs, so the dispatcher is always created on the real main thread even when background code tries to force instantiation.</li> </ul> <p>With no configuration, the dispatcher therefore always exists, enforces the queue bound exposed via <code>PendingActionLimit</code>, and is hidden from the hierarchy during Edit Mode by <code>HideFlags.HideInHierarchy | HideFlags.HideInInspector | HideFlags.NotEditable</code>.</p>"},{"location":"features/logging/unity-main-thread-dispatcher/#opting-inout-of-auto-creation","title":"Opting In/Out of Auto-Creation","text":"<p>Occasionally (especially in tests) you want to control exactly when the dispatcher is created so that scenes unload cleanly and Unity does not warn about hidden objects surviving domain reloads. Use the internal switch:</p> C#<pre><code>UnityMainThreadDispatcher.SetAutoCreationEnabled(false);\n// ... destroy any dispatcher instance if you want a completely clean slate ...\nUnityMainThreadDispatcher.SetAutoCreationEnabled(true);\n</code></pre> <ul> <li>The toggle is global to the current domain; disabling it prevents the runtime/editor bootstrap hooks from creating fresh GameObjects.</li> <li>When you re-enable auto-creation the very next call to <code>UnityMainThreadDispatcher.Instance</code> will recreate the singleton on the main thread (and Play Mode bootstrap will recreate it on the following domain reload).</li> <li>Always destroy the existing dispatcher GameObject (<code>UnityMainThreadDispatcher.DestroyExistingDispatcher</code> or the test helper) before turning the feature back on so that stale instances are removed from <code>Resources.FindObjectsOfTypeAll</code>.</li> </ul>"},{"location":"features/logging/unity-main-thread-dispatcher/#autocreationscope-scoped-toggles","title":"AutoCreationScope (scoped toggles)","text":"<p><code>UnityMainThreadDispatcher.AutoCreationScope</code> wraps the toggle/cleanup pattern above in an <code>IDisposable</code> so you cannot forget to restore the previous state:</p> C#<pre><code>using UnityMainThreadDispatcher.AutoCreationScope scope =\n    UnityMainThreadDispatcher.AutoCreationScope.Disabled(\n        destroyExistingInstanceOnEnter: true,\n        destroyInstancesOnDispose: true,\n        destroyImmediate: true   // prefer true in tests, false in runtime code\n    );\n\n// Auto-creation is disabled inside the scope; manually enable or create instances as needed.\n</code></pre> <ul> <li>On enter, the scope captures the previous <code>AutoCreationEnabled</code> value, switches to the desired state, and optionally destroys any existing dispatcher instances.</li> <li>On dispose, the scope restores the original toggle and (when requested) destroys any dispatcher that may have been created during the scoped work, ensuring follow-up tests inherit a clean slate.</li> </ul>"},{"location":"features/logging/unity-main-thread-dispatcher/#test-bootstrap-workflow","title":"Test Bootstrap Workflow","text":"<p>Two dedicated bootstraps live under <code>Tests/*/TestUtils</code> and keep dispatcher lifetimes predictable during automated suites:</p>"},{"location":"features/logging/unity-main-thread-dispatcher/#runtime-playmode-testsruntimetestutilsunitymainthreaddispatchertestbootstrapcs","title":"Runtime / PlayMode (<code>Tests/Runtime/TestUtils/UnityMainThreadDispatcherTestBootstrap.cs</code>)","text":"C#<pre><code>[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]\nprivate static void DisableAutoCreation()\n{\n    UnityMainThreadDispatcher.SetAutoCreationEnabled(false);\n    UnityMainThreadDispatcherTestHelper.DestroyDispatcherIfExists(immediate: true);\n}\n</code></pre> <ul> <li>Runs before every play-mode domain reload.</li> <li>Forces any hidden dispatcher left over from a prior scene to be <code>DestroyImmediate</code>'d so play-mode tests can opt into dispatcher usage explicitly.</li> <li>Guarantees a clean slate for suites that create and destroy scenes repeatedly (see <code>CommonTestBase.BaseSetUp</code>).</li> </ul>"},{"location":"features/logging/unity-main-thread-dispatcher/#editor-editmode-testseditortestutilsunitymainthreaddispatchereditortestbootstrapcs","title":"Editor / EditMode (<code>Tests/Editor/TestUtils/UnityMainThreadDispatcherEditorTestBootstrap.cs</code>)","text":"C#<pre><code>[InitializeOnLoad]\ninternal static class UnityMainThreadDispatcherEditorTestBootstrap\n{\n    static UnityMainThreadDispatcherEditorTestBootstrap()\n    {\n        UnityMainThreadDispatcher.SetAutoCreationEnabled(false);\n        // DestroyImmediate the hidden dispatcher object, if any.\n    }\n}\n</code></pre> <ul> <li>Executes once per editor domain reload (before EditMode tests run).</li> <li>Ensures the hidden dispatcher object is removed from the hierarchy so Unity's test runner does not flag leaked objects between assemblies.</li> </ul>"},{"location":"features/logging/unity-main-thread-dispatcher/#re-enabling-per-test","title":"Re-enabling per Test","text":"<p>The runtime and editor <code>CommonTestBase</code> fixtures demonstrate the intended per-test lifecycle via <code>UnityMainThreadDispatcher.AutoCreationScope</code>:</p> <ol> <li>At <code>[SetUp]</code> it grabs <code>UnityMainThreadDispatcher.CreateTestScope(destroyImmediate: true)</code> which internally disables auto-creation, destroys stragglers, and then re-enables auto-creation so the test can access <code>Instance</code> normally.</li> <li>Production code can create/destroy the dispatcher freely; the scope tracks everything automatically.</li> <li>During every teardown stage it disposes the scope, restoring the previous auto-creation flag and destroying any dispatcher created while the test runs \u2014 no manual try/finally blocks required.</li> </ol> <p>Downstream packages can copy the exact pattern:</p> <ol> <li>Add a <code>[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]</code> that disables auto-creation and destroys lingering instances.</li> <li>Add an <code>[InitializeOnLoad]</code> editor bootstrap that mirrors the same behavior for Edit Mode.</li> <li>When a single test needs to temporarily disable the dispatcher, wrap the custom logic in <code>using var scope = UnityMainThreadDispatcher.AutoCreationScope.Disabled(...)</code> so cleanup always runs.</li> </ol> <p>Following this workflow keeps Unity from warning about hidden GameObjects during test teardown, prevents worker threads from instantiating the dispatcher off-thread, and documents exactly when auto-creation is in effect for your package.</p>"},{"location":"features/relational-components/relational-components/","title":"Relational Component Attributes","text":"<p>Visual</p> <p></p> <p>Auto-wire components in your hierarchy without <code>GetComponent</code> boilerplate. These attributes make common relationships explicit, robust, and easy to maintain.</p> <ul> <li><code>SiblingComponent</code> \u2014 same GameObject</li> <li><code>ParentComponent</code> \u2014 up the transform hierarchy</li> <li><code>ChildComponent</code> \u2014 down the transform hierarchy (breadth-first)</li> </ul> <p>Collection Type Support: Each attribute works with:</p> <ul> <li>Single fields (e.g., <code>Transform</code>)</li> <li>Arrays (e.g., <code>Collider2D[]</code>)</li> <li>Lists (e.g., <code>List&lt;Rigidbody2D&gt;</code>)</li> <li>HashSets (e.g., <code>HashSet&lt;Renderer&gt;</code>)</li> </ul> <p>All attributes support optional assignment, filters (tag/name), depth limits, max results, and interface/base-type resolution.</p> <p>Having issues? Jump to Troubleshooting: see Troubleshooting.</p> <p>Related systems: For data\u2011driven gameplay effects (attributes, tags, cosmetics), see Effects System and the README section Effects, Attributes, and Tags.</p> <p>Curious how these attributes stack up against manual <code>GetComponent*</code> loops? Check the Relational Component Performance Benchmarks for operations-per-second and allocation snapshots.</p>"},{"location":"features/relational-components/relational-components/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>\u2b50 Replace 20+ lines of repetitive GetComponent boilerplate with 3 attributes + 1 method call.</li> <li>Self\u2011documenting, supports interfaces, filters, and validation.</li> <li>Time saved: 10-20 minutes per script \u00d7 hundreds of scripts = weeks of development time.</li> </ul>"},{"location":"features/relational-components/relational-components/#the-productivity-advantage","title":"The Productivity Advantage","text":"<p>Before (The Old Way):</p> C#<pre><code>void Awake()\n{\n    sprite = GetComponent&lt;SpriteRenderer&gt;();\n    if (sprite == null) Debug.LogError(\"Missing SpriteRenderer!\");\n\n    rigidbody = GetComponentInParent&lt;Rigidbody2D&gt;();\n    if (rigidbody == null) Debug.LogError(\"Missing Rigidbody2D in parent!\");\n\n    colliders = GetComponentsInChildren&lt;Collider2D&gt;();\n    if (colliders.Length == 0) Debug.LogWarning(\"No colliders in children!\");\n\n    // Repeat for every component...\n    // 15-30 lines of boilerplate per script\n}\n</code></pre> <p>After (Relational Components):</p> C#<pre><code>[SiblingComponent] private SpriteRenderer sprite;\n[ParentComponent] private Rigidbody2D rigidbody;\n[ChildComponent] private Collider2D[] colliders;\n\nvoid Awake() =&gt; this.AssignRelationalComponents();\n// That's it. 4 lines total, all wired automatically with validation.\n</code></pre> <p>Pick the right attribute</p> <ul> <li>Same GameObject? Use <code>SiblingComponent</code>.</li> <li>Search up the hierarchy? Use <code>ParentComponent</code>.</li> <li>Search down the hierarchy? Use <code>ChildComponent</code>.</li> </ul> <p>One\u2011minute setup</p> C#<pre><code>[SiblingComponent] private SpriteRenderer sprite;\n[ParentComponent(OnlyAncestors = true)] private Rigidbody2D rb;\n[ChildComponent(OnlyDescendants = true, MaxDepth = 1)] private Collider2D[] childColliders;\n\nvoid Awake() =&gt; this.AssignRelationalComponents();\n</code></pre>"},{"location":"features/relational-components/relational-components/#why-use-these","title":"Why Use These?","text":"<ul> <li>Replace repetitive <code>GetComponent</code> and fragile manual wiring</li> <li>Make intent clear and local to the field that needs it</li> <li>Fail fast with useful errors (or opt-in to optional fields)</li> <li>Filter results precisely and control traversal cost</li> <li>Support interfaces for clean architecture</li> </ul>"},{"location":"features/relational-components/relational-components/#quick-start","title":"Quick Start","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class Player : MonoBehaviour\n{\n    // Same-GameObject\n    [SiblingComponent] private SpriteRenderer sprite;\n\n    // First matching ancestor (excluding self)\n    [ParentComponent(OnlyAncestors = true)] private Rigidbody2D ancestorRb;\n\n    // Immediate children only, collect many\n    [ChildComponent(OnlyDescendants = true, MaxDepth = 1)]\n    private Collider2D[] immediateChildColliders;\n\n    private void Awake()\n    {\n        // Wires up all relational fields on this component\n        this.AssignRelationalComponents();\n    }\n}\n</code></pre>"},{"location":"features/relational-components/relational-components/#how-it-works","title":"How It Works","text":"<p>Decorate private (or public) fields on a <code>MonoBehaviour</code> with a relational attribute, then call one of:</p> <ul> <li><code>this.AssignRelationalComponents()</code> \u2014 assign all three categories</li> <li><code>this.AssignSiblingComponents()</code> \u2014 only siblings</li> <li><code>this.AssignParentComponents()</code> \u2014 only parents</li> <li><code>this.AssignChildComponents()</code> \u2014 only children</li> </ul> <p>Assignments happen at runtime (e.g., <code>Awake</code>/<code>OnEnable</code>), not at edit-time serialization.</p>"},{"location":"features/relational-components/relational-components/#visual-search-patterns","title":"Visual Search Patterns","text":"Text Only<pre><code>ParentComponent (searches UP the hierarchy):\n\n  Grandparent \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 (included unless OnlyAncestors = true)\n      \u2191\n      \u2502\n    Parent \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 (always included)\n      \u2191\n      \u2502\n   [YOU] \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500  Component with [ParentComponent]\n      \u2502\n    Child\n      \u2502\n   Grandchild\n\n\nChildComponent (searches DOWN the hierarchy, breadth-first):\n\n  Grandparent\n      \u2502\n    Parent\n      \u2502\n   [YOU] \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500  Component with [ChildComponent]\n      \u2193\n      \u251c\u2500 Child 1 \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 (depth = 1)\n      \u2502    \u251c\u2500 Grandchild 1  (depth = 2)\n      \u2502    \u2514\u2500 Grandchild 2  (depth = 2)\n      \u2502\n      \u2514\u2500 Child 2 \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 (depth = 1)\n           \u2514\u2500 Grandchild 3  (depth = 2)\n\n  Breadth-first means all Children (depth 1) are checked\n  before any Grandchildren (depth 2).\n\n\nSiblingComponent (searches same GameObject):\n\n  Parent\n    \u2502\n    \u2514\u2500 [GameObject] \u2190\u2500\u2500\u2500\u2500\u2500\u2500 All components on this GameObject\n         \u251c\u2500 [YOU] \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Component with [SiblingComponent]\n         \u251c\u2500 Component A\n         \u251c\u2500 Component B\n         \u2514\u2500 Component C\n</code></pre>"},{"location":"features/relational-components/relational-components/#key-options","title":"Key Options","text":"<p>OnlyAncestors / OnlyDescendants:</p> <ul> <li><code>OnlyAncestors = true</code> \u2192 Excludes self, searches only parents/grandparents</li> <li><code>OnlyDescendants = true</code> \u2192 Excludes self, searches only children/grandchildren</li> <li>Default (false) \u2192 Includes self in search</li> </ul> <p>MaxDepth:</p> <ul> <li>Limits how far up/down the hierarchy to search</li> <li><code>MaxDepth = 1</code> with <code>OnlyDescendants = true</code> \u2192 immediate children only</li> <li><code>MaxDepth = 2</code> \u2192 children + grandchildren (or parents + grandparents)</li> </ul> <p>\ud83d\udca1 Having Issues? Components not being assigned? Fields staying null? Jump to Troubleshooting for solutions to common problems.</p>"},{"location":"features/relational-components/relational-components/#attribute-reference","title":"Attribute Reference","text":""},{"location":"features/relational-components/relational-components/#siblingcomponent","title":"SiblingComponent","text":"<ul> <li>Scope: Same <code>GameObject</code></li> <li>Use for: Standard component composition patterns</li> </ul> <p>Examples:</p> C#<pre><code>[SiblingComponent] private Animator animator;                 // required by default\n[SiblingComponent(Optional = true)] private Rigidbody2D rb;   // optional\n[SiblingComponent(TagFilter = \"Visual\", NameFilter = \"Sprite\")] private Component[] visuals;\n[SiblingComponent(MaxCount = 2)] private List&lt;Collider2D&gt; firstTwo;  // List&lt;T&gt; supported\n[SiblingComponent] private HashSet&lt;Renderer&gt; allRenderers;     // HashSet&lt;T&gt; supported\n</code></pre> <p>Performance note: Sibling lookups do not cache results between calls. In profiling we found these assignments typically run once per GameObject (e.g., during <code>Awake</code>), so the extra bookkeeping and invalidation cost of a cache outweighed the benefits. If you need updated references later, call <code>AssignSiblingComponents</code> again after the hierarchy changes.</p>"},{"location":"features/relational-components/relational-components/#parentcomponent","title":"ParentComponent","text":"<ul> <li>Scope: Up the transform chain (optionally excluding self)</li> <li>Controls: <code>OnlyAncestors</code>, <code>MaxDepth</code></li> </ul> <p>Examples:</p> C#<pre><code>// Immediate parent only\n[ParentComponent(OnlyAncestors = true, MaxDepth = 1)] private Transform directParent;\n\n// Up to 3 levels with a tag\n[ParentComponent(OnlyAncestors = true, MaxDepth = 3, TagFilter = \"Player\")] private Collider2D playerAncestor;\n\n// Interface/base-type resolution is supported by default\n[ParentComponent] private IHealth healthProvider;\n</code></pre>"},{"location":"features/relational-components/relational-components/#childcomponent","title":"ChildComponent","text":"<ul> <li>Scope: Down the transform chain (breadth-first; optionally excluding self)</li> <li>Controls: <code>OnlyDescendants</code>, <code>MaxDepth</code></li> </ul> <p>Examples:</p> C#<pre><code>// Immediate children only\n[ChildComponent(OnlyDescendants = true, MaxDepth = 1)] private Transform[] immediateChildren;\n\n// First matching descendant with a tag\n[ChildComponent(OnlyDescendants = true, TagFilter = \"Weapon\")] private Collider2D weaponCollider;\n\n// Gather into a List (preserves insertion order)\n[ChildComponent(OnlyDescendants = true)] private List&lt;MeshRenderer&gt; childRenderers;\n\n// Gather into a HashSet (unique results, no duplicates) and limit count\n[ChildComponent(OnlyDescendants = true, MaxCount = 10)] private HashSet&lt;Rigidbody2D&gt; firstTenRigidbodies;\n</code></pre> <p>Performance note: When you avoid depth limits and interface filtering, child assignments run through a cached <code>GetComponentsInChildren&lt;T&gt;()</code> delegate to stay allocation-free. Turning on <code>MaxDepth</code> or interface searches still works, but the assigner reverts to the breadth-first traversal to honour those constraints.</p>"},{"location":"features/relational-components/relational-components/#common-options-all-attributes","title":"Common Options (All Attributes)","text":"<ul> <li><code>Optional</code> (default: false)</li> <li>If <code>false</code>, logs a descriptive error when no match is found</li> <li> <p>If <code>true</code>, suppresses the error (field remains null/empty)</p> </li> <li> <p><code>IncludeInactive</code> (default: true)</p> </li> <li>If <code>true</code>, includes disabled components and inactive GameObjects</li> <li> <p>If <code>false</code>, only assigns enabled components on active-in-hierarchy objects</p> </li> <li> <p><code>SkipIfAssigned</code> (default: false)</p> </li> <li> <p>If <code>true</code>, preserves existing non-null value (single) or non-empty collection</p> </li> <li> <p><code>MaxCount</code> (default: 0 = unlimited)</p> </li> <li> <p>Applies to arrays, lists, and hash sets; ignored for single fields</p> </li> <li> <p><code>TagFilter</code></p> </li> <li> <p>Exact tag match using <code>CompareTag</code></p> </li> <li> <p><code>NameFilter</code></p> </li> <li> <p>Case-sensitive substring match on the GameObject name</p> </li> <li> <p><code>AllowInterfaces</code> (default: true)</p> </li> <li>If <code>true</code>, can assign by interface or base type; set <code>false</code> to restrict to concrete types</li> </ul>"},{"location":"features/relational-components/relational-components/#choosing-the-right-collection-type","title":"Choosing the Right Collection Type","text":"<p>Use Arrays (<code>T[]</code>) when:</p> <ul> <li>Collection size is fixed or rarely changes</li> <li>Need the smallest memory footprint</li> <li>Interoperating with APIs that require arrays</li> </ul> <p>Use Lists (<code>List&lt;T&gt;</code>) when:</p> <ul> <li>Need insertion order preserved</li> <li>Plan to add/remove elements after assignment</li> <li>Want indexed access with <code>[]</code> operator</li> <li>Need compatibility with most LINQ operations</li> </ul> <p>Use HashSets (<code>HashSet&lt;T&gt;</code>) when:</p> <ul> <li>Need guaranteed uniqueness (no duplicates)</li> <li>Performing frequent membership tests (<code>Contains()</code>)</li> <li>Order doesn't matter</li> <li>Want O(1) lookup performance</li> </ul> C#<pre><code>// Arrays: Fixed size, minimal overhead\n[ChildComponent] private Collider2D[] colliders;\n\n// Lists: Dynamic, ordered, index-based access\n[ChildComponent] private List&lt;Renderer&gt; renderers;\n\n// HashSets: Unique, fast lookups, unordered\n[ChildComponent] private HashSet&lt;AudioSource&gt; audioSources;\n</code></pre>"},{"location":"features/relational-components/relational-components/#recipes","title":"Recipes","text":"<ul> <li>UI hierarchy references</li> </ul> C#<pre><code>[ParentComponent(OnlyAncestors = true, MaxDepth = 2)] private Canvas canvas;\n[ChildComponent(OnlyDescendants = true, NameFilter = \"Button\")] private Button[] buttons;\n</code></pre> <ul> <li>Sensors/components living on children</li> </ul> C#<pre><code>[ChildComponent(OnlyDescendants = true, TagFilter = \"Sensor\")] private Collider[] sensors;\n</code></pre> <ul> <li>Modular systems via interfaces</li> </ul> C#<pre><code>public interface IInputProvider { Vector2 Move { get; } }\n[ParentComponent] private IInputProvider input; // PlayerInput, AIInput, etc.\n</code></pre>"},{"location":"features/relational-components/relational-components/#best-practices","title":"Best Practices","text":"<ul> <li>Call in <code>Awake()</code> or <code>OnEnable()</code> so references exist early</li> <li>Prefer selective calls (<code>AssignSibling/Parent/Child</code>) when you only use one category</li> <li>Use <code>MaxDepth</code> to cap traversal cost in deep trees</li> <li>Use <code>MaxCount</code> to reduce allocations when you only need a subset</li> <li>Mark non-critical references <code>Optional = true</code> to avoid noise</li> </ul>"},{"location":"features/relational-components/relational-components/#explicit-initialization-prewarm","title":"Explicit Initialization (Prewarm)","text":"<p>Relational components build high\u2011performance reflection helpers on first use. To eliminate this lazy cost and avoid first\u2011frame stalls on large projects or IL2CPP builds, explicitly pre\u2011initialize caches at startup:</p> C#<pre><code>// Call during bootstrap/loading\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\nvoid Start()\n{\n    RelationalComponentInitializer.Initialize();\n}\n</code></pre> <p>Notes:</p> <ul> <li>Uses AttributeMetadataCache when available, with reflection fallback per type if not cached.</li> <li>Logs warnings for missing fields/types and logs errors for unexpected exceptions; processing continues.</li> <li>Scope the work by providing specific types: <code>RelationalComponentInitializer.Initialize(new[]{ typeof(MyComponent) });</code></li> <li>To auto\u2011prewarm on app load, enable the toggle on the AttributeMetadataCache asset: \u201cPrewarm Relational On Load\u201d.</li> </ul>"},{"location":"features/relational-components/relational-components/#dependency-injection-integrations","title":"Dependency Injection Integrations","text":"<p>Stop choosing between DI and clean hierarchy references - Unity Helpers provides seamless integrations with Zenject/Extenject, VContainer, and Reflex that automatically wire up your relational component fields right after dependency injection completes.</p>"},{"location":"features/relational-components/relational-components/#the-di-pain-point","title":"The DI Pain Point","text":"<p>Without these integrations, you're stuck writing <code>Awake()</code> methods full of <code>GetComponent</code> boilerplate even when using a DI framework:</p> C#<pre><code>public class Enemy : MonoBehaviour\n{\n    [Inject] private IHealthSystem _health;  // \u2705 DI handles this\n\n    private Animator _animator;               // \u274c Still manual boilerplate\n    private Rigidbody2D _rigidbody;          // \u274c Still manual boilerplate\n\n    void Awake()\n    {\n        _animator = GetComponent&lt;Animator&gt;();\n        _rigidbody = GetComponent&lt;Rigidbody2D&gt;();\n        // ... 15 more lines of GetComponent hell\n    }\n}\n</code></pre>"},{"location":"features/relational-components/relational-components/#the-integration-solution","title":"The Integration Solution","text":"<p>With the DI integrations, everything just works:</p> C#<pre><code>public class Enemy : MonoBehaviour\n{\n    [Inject] private IHealthSystem _health;         // \u2705 DI injection\n    [SiblingComponent] private Animator _animator;  // \u2705 Relational auto-wiring\n    [SiblingComponent] private Rigidbody2D _rigidbody; // \u2705 Relational auto-wiring\n\n    // No Awake() needed! Both DI and hierarchy references wired automatically\n}\n</code></pre>"},{"location":"features/relational-components/relational-components/#why-use-the-di-integrations","title":"Why Use the DI Integrations","text":"<ul> <li>Zero boilerplate - No <code>Awake()</code> method needed, no manual <code>GetComponent</code> calls, no validation code</li> <li>Consistent behavior - Works seamlessly with constructor/property/field injection and runtime instantiation</li> <li>Safe fallback - Gracefully degrades to standard behavior if DI binding is missing</li> <li>Risk-free adoption - Use incrementally, mix DI and non-DI components freely</li> </ul>"},{"location":"features/relational-components/relational-components/#supported-packages-auto-detected","title":"Supported Packages (Auto-detected)","text":"<p>Unity Helpers automatically detects these packages via UPM:</p> <ul> <li>Zenject/Extenject: <code>com.extenject.zenject</code>, <code>com.modesttree.zenject</code>, <code>com.svermeulen.extenject</code></li> <li>VContainer: <code>jp.cysharp.vcontainer</code>, <code>jp.hadashikick.vcontainer</code></li> <li>Reflex: <code>com.gustavopsantos.reflex</code></li> </ul> <p>\ud83d\udca1 UPM packages work out-of-the-box - No scripting defines needed!</p>"},{"location":"features/relational-components/relational-components/#manual-or-source-imports-non-upm","title":"Manual or Source Imports (Non-UPM)","text":"<p>If you import Zenject/VContainer/Reflex as source code, .unitypackage, or raw DLLs (not via UPM), you need to manually add scripting defines:</p> <ol> <li>Open <code>Project Settings &gt; Player &gt; Other Settings &gt; Scripting Define Symbols</code></li> <li>Add the appropriate define(s) for your target platforms:</li> <li><code>ZENJECT_PRESENT</code> - When using Zenject/Extenject</li> <li><code>VCONTAINER_PRESENT</code> - When using VContainer</li> <li><code>REFLEX_PRESENT</code> - When using Reflex</li> <li>Unity will recompile and the integration assemblies under <code>Runtime/Integrations/*</code> will activate automatically</li> </ol>"},{"location":"features/relational-components/relational-components/#vcontainer-at-a-glance","title":"VContainer at a Glance","text":"<ul> <li>Enable once per scope</li> </ul> C#<pre><code>builder.RegisterRelationalComponents(\n    new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true),\n    enableAdditiveSceneListener: true\n);\n</code></pre> <ul> <li>Runtime helpers</li> <li><code>_resolver.InstantiateComponentWithRelations(componentPrefab, parent)</code></li> <li><code>_resolver.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)</code></li> <li><code>_resolver.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)</code></li> <li> <p><code>RelationalObjectPools.CreatePoolWithRelations(...)</code> + <code>pool.GetWithRelations(resolver)</code></p> </li> <li> <p>Full walkthrough: See <code>Samples~/DI - VContainer</code> folder in the repository</p> </li> </ul>"},{"location":"features/relational-components/relational-components/#zenject-at-a-glance","title":"Zenject at a Glance","text":"<ul> <li>Install once per scene</li> <li>Add <code>RelationalComponentsInstaller</code> to your <code>SceneContext</code>.</li> <li> <p>Toggles cover include-inactive scanning, single-pass strategy, and additive-scene listening.</p> </li> <li> <p>Runtime helpers</p> </li> <li><code>_container.InstantiateComponentWithRelations(componentPrefab, parent)</code></li> <li><code>_container.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)</code></li> <li><code>_container.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)</code></li> <li> <p>Subclass <code>RelationalMemoryPool&lt;T&gt;</code> to hydrate pooled items on spawn.</p> </li> <li> <p>Full walkthrough: See <code>Samples~/DI - Zenject</code> folder in the repository</p> </li> </ul>"},{"location":"features/relational-components/relational-components/#reflex-at-a-glance","title":"Reflex at a Glance","text":"<ul> <li>Install once per scene</li> <li>Reflex creates a <code>SceneScope</code> in each scene. Add <code>RelationalComponentsInstaller</code> to the same GameObject (or a child) to bind the relational assigner, run the initial scene scan, and optionally register the additive-scene listener.</li> <li> <p>Toggles mirror the runtime helpers: include inactive objects, choose the scan strategy, and enable additive listening.</p> </li> <li> <p>Runtime helpers</p> </li> <li><code>_container.InjectWithRelations(existingComponent)</code> to inject DI fields and hydrate relational attributes on existing objects.</li> <li><code>_container.InstantiateComponentWithRelations(componentPrefab, parent)</code> for component prefabs.</li> <li><code>_container.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)</code> for full hierarchies.</li> <li> <p><code>_container.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)</code> to hydrate arbitrary hierarchies after manual instantiation.</p> </li> <li> <p>Full walkthrough: See <code>Samples~/DI - Reflex</code> folder in the repository</p> </li> <li> <p>Reflex shares the same fallback behaviour: if the assigner is not bound, the helpers call <code>AssignRelationalComponents()</code> directly so you can adopt incrementally.</p> </li> </ul> <p>Notes</p> <ul> <li>Both integrations fall back to the built-in <code>component.AssignRelationalComponents()</code> call path if the DI container does not expose the assigner binding, so you can adopt them incrementally without breaking existing behaviour.</li> </ul>"},{"location":"features/relational-components/relational-components/#troubleshooting","title":"Troubleshooting","text":"<ul> <li>Fields remain null in the Inspector</li> <li> <p>Expected in Edit Mode. These attributes are assigned at runtime only and are not serialized. They are checked at runtime and log errors if they fail to find a match.</p> </li> <li> <p>Nothing assigned at runtime</p> </li> <li>Ensure you call <code>AssignRelationalComponents()</code> or the specific <code>Assign*Components()</code> in <code>Awake()</code> or <code>OnEnable()</code>.</li> <li>Verify filters: <code>TagFilter</code> must match an existing tag; <code>NameFilter</code> is case-sensitive.</li> <li>Check depth limits: <code>OnlyAncestors</code>/<code>OnlyDescendants</code> may exclude self; <code>MaxDepth</code> may be too small.</li> <li> <p>For interface/base type fields, confirm <code>AllowInterfaces = true</code> (default) or use a concrete type.</p> </li> <li> <p>Inactive or disabled components unexpectedly included</p> </li> <li> <p>These are included by default. Set <code>IncludeInactive = false</code> to restrict to enabled components on active GameObjects.</p> </li> <li> <p>Too many results or large allocations</p> </li> <li> <p>Cap with <code>MaxCount</code> and/or <code>MaxDepth</code>. Prefer <code>List&lt;T&gt;</code> or <code>HashSet&lt;T&gt;</code> when you plan to mutate the collection after assignment.</p> </li> <li> <p>Child search doesn\u2019t find the nearest match you expect</p> </li> <li> <p>Children are traversed breadth-first. If you want the nearest by hierarchy level, this is correct; if you need a custom order, gather a collection and sort manually.</p> </li> <li> <p>I only need one category (e.g., parents)</p> </li> <li>Call the specific helper (<code>AssignParentComponents</code> / <code>AssignChildComponents</code> / <code>AssignSiblingComponents</code>) instead of the all-in-one method for clarity and potentially less work.</li> </ul>"},{"location":"features/relational-components/relational-components/#faq","title":"FAQ","text":"<p>Q: Does this run in Edit Mode or serialize values?</p> <ul> <li>No. Assignment occurs at runtime only; values are not serialized by Unity.</li> </ul> <p>Q: Are interfaces supported?</p> <ul> <li>Yes, when <code>AllowInterfaces = true</code> (default). Set it to <code>false</code> to restrict to concrete types.</li> </ul> <p>Q: What about performance?</p> <ul> <li>Work scales with the number of attributed fields and the search space. Use <code>MaxDepth</code>, <code>TagFilter</code>, <code>NameFilter</code>, and <code>MaxCount</code> to limit work. Sibling lookups are O(1) when no filters are applied.</li> </ul> <p>For quick examples in context, see the README\u2019s \u201cAuto Component Discovery\u201d section. For API docs, hover the attributes in your IDE for XML summaries and examples.</p>"},{"location":"features/relational-components/relational-components/#di-integrations-testing-and-edge-cases","title":"DI Integrations: Testing and Edge Cases","text":"<p>Beginner-friendly overview</p> <ul> <li>Optional DI integrations compile only when symbols are present (<code>ZENJECT_PRESENT</code>, <code>VCONTAINER_PRESENT</code>). With UPM, these are added via asmdef <code>versionDefines</code>. Without UPM (manual import), add them in Project Settings \u2192 Player \u2192 Scripting Define Symbols.</li> <li>Both integrations register an assigner (<code>IRelationalComponentAssigner</code>) and provide a scene initializer/entry point to hydrate relational fields once the container is ready.</li> </ul> <p>VContainer (1.16.x)</p> <ul> <li>Runtime usage (LifetimeScope): Call <code>builder.RegisterRelationalComponents()</code> in <code>LifetimeScope.Configure</code>. The entry point runs automatically after the container builds. You can enable an additive-scene listener and customize scan options:</li> </ul> C#<pre><code>using VContainer;\nusing VContainer.Unity;\nusing WallstopStudios.UnityHelpers.Integrations.VContainer;\n\nprotected override void Configure(IContainerBuilder builder)\n{\n    // Single-pass scan + additive scene listener\n    var options = new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true);\n    builder.RegisterRelationalComponents(options, enableAdditiveSceneListener: true);\n}\n</code></pre> <ul> <li>Tests without LifetimeScope: Construct the entry point and call <code>Initialize()</code> yourself, and register your <code>AttributeMetadataCache</code> instance so the assigner uses it:</li> </ul> C#<pre><code>var cache = ScriptableObject.CreateInstance&lt;AttributeMetadataCache&gt;();\n// populate cache._relationalTypeMetadata with your test component types\ncache.ForceRebuildForTests(); // rebuild lookups so the initializer can discover your types\nvar builder = new ContainerBuilder();\nbuilder.RegisterInstance(cache).AsSelf();\nbuilder.Register&lt;RelationalComponentAssigner&gt;(Lifetime.Singleton)\n       .As&lt;IRelationalComponentAssigner&gt;()\n       .AsSelf();\nvar resolver = builder.Build();\nvar entry = new RelationalComponentEntryPoint(\n    resolver.Resolve&lt;IRelationalComponentAssigner&gt;(),\n    cache,\n    RelationalSceneAssignmentOptions.Default\n);\nentry.Initialize();\n</code></pre> <ul> <li>Inject vs BuildUp: Use <code>resolver.InjectWithRelations(component)</code> to inject + assign in one call, or <code>resolver.Inject(component)</code> then <code>resolver.AssignRelationalComponents(component)</code>.</li> <li> <p>Prefabs &amp; GameObjects: <code>resolver.InstantiateComponentWithRelations(prefab, parent)</code> or <code>resolver.InstantiateGameObjectWithRelations(prefab, parent)</code>; to inject existing hierarchies use <code>resolver.InjectGameObjectWithRelations(root)</code>.</p> </li> <li> <p>EditMode reliability: In EditMode tests, prefer <code>[UnityTest]</code> and <code>yield return null</code> after creating objects and after initializing the entry point so Unity has a frame to register new objects before <code>FindObjectsOfType</code> runs and to allow assignments to complete.</p> </li> <li>Active scene filter: Entry points operate on the active scene only. In EditMode, create a new scene with <code>SceneManager.CreateScene</code>, set it active, and move your test hierarchy into it before calling <code>Initialize()</code>.</li> <li>IncludeInactive: Control with <code>RelationalSceneAssignmentOptions(includeInactive: bool)</code>.</li> </ul> <p>Zenject/Extenject</p> <ul> <li>Runtime usage: Add <code>RelationalComponentsInstaller</code> to your <code>SceneContext</code>. It binds <code>IRelationalComponentAssigner</code> and runs <code>RelationalComponentSceneInitializer</code> once the container is ready. The installer exposes toggles to assign on initialize and to listen for additive scenes.</li> <li>Tests: Bind a concrete <code>AttributeMetadataCache</code> instance and construct the assigner with that cache. Then resolve <code>IInitializable</code> and call <code>Initialize()</code>.</li> <li>EditMode reliability: As with VContainer, consider <code>[UnityTest]</code> with a <code>yield return null</code> after creating objects and after calling <code>Initialize()</code> to allow Unity to register objects and complete assignments.</li> <li>Active scene filter: Initial one-time scan operates on the active scene only. The additive-scene listener processes only newly loaded scenes (not all loaded scenes).</li> <li>Prefabs &amp; GameObjects: <code>container.InstantiateComponentWithRelations(...)</code>, <code>container.InstantiateGameObjectWithRelations(...)</code>, or <code>container.InjectGameObjectWithRelations(root)</code>; to inject + assign a single instance: <code>container.InjectWithRelations(component)</code>.</li> </ul>"},{"location":"features/relational-components/relational-components/#object-pools-di-aware","title":"Object Pools (DI-aware)","text":"<ul> <li>Zenject: use <code>RelationalMemoryPool&lt;T&gt;</code> (or <code>&lt;TParam, T&gt;</code>) to assign relational fields in <code>OnSpawned</code> automatically.</li> <li>VContainer: create pools with <code>RelationalObjectPools.CreatePoolWithRelations(...)</code> and rent via <code>pool.GetWithRelations(resolver)</code> to inject + assign.</li> </ul> <p>Common pitfalls and how to avoid them</p> <ul> <li>\"No such registration \u2026 RelationalComponentEntryPoint\": You're resolving in a plain container without <code>LifetimeScope</code>. Construct the entry point manually as shown above.</li> <li>Optional integrations don't compile: Ensure the scripting define symbols are present. UPM adds them automatically via <code>versionDefines</code>; manual imports require adding them in Player Settings.</li> <li>Fields remain null in tests: Ensure your test <code>AttributeMetadataCache</code> has the relational metadata for your test component types and that the DI container uses the same cache instance (register it and prefer constructors that accept the cache).</li> </ul>"},{"location":"features/relational-components/relational-components/#related-documentation","title":"\ud83d\udcda Related Documentation","text":"<p>Core Guides:</p> <ul> <li>Getting Started - Your first 5 minutes with Unity Helpers</li> <li>Main README - Complete feature overview</li> <li>Feature Index - Alphabetical reference</li> </ul> <p>Related Features:</p> <ul> <li>Effects System - Data-driven buffs/debuffs with attributes and tags</li> <li>Singletons - Runtime and ScriptableObject singleton patterns</li> <li>Editor Tools - Attribute Metadata Cache generator</li> </ul> <p>DI Integration Samples:</p> <ul> <li>VContainer Integration - See <code>Samples~/DI - VContainer</code> folder in the repository</li> <li>Zenject Integration - See <code>Samples~/DI - Zenject</code> folder in the repository</li> <li>Reflex Integration - See <code>Samples~/DI - Reflex</code> folder in the repository</li> </ul> <p>Need help? Open an issue | Troubleshooting</p>"},{"location":"features/serialization/serialization-types/","title":"Serialization Types","text":"<p>Unity-friendly wrappers for complex data.</p> <p>Unity Helpers provides serializable wrappers for types that Unity can't serialize natively: GUIDs, dictionaries, sets, type references, and nullable values. All types include custom property drawers for a seamless inspector experience and support JSON/Protobuf serialization.</p>"},{"location":"features/serialization/serialization-types/#table-of-contents","title":"Table of Contents","text":"<ul> <li>WGuid</li> <li>SerializableDictionary</li> <li>SerializableHashSet &amp; SerializableSortedSet</li> <li>SerializableType</li> <li>SerializableNullable</li> <li>Best Practices</li> <li>Examples</li> </ul>"},{"location":"features/serialization/serialization-types/#wguid","title":"WGuid","text":"<p>Immutable version-4 GUID wrapper using two longs for efficient Unity serialization.</p>"},{"location":"features/serialization/serialization-types/#why-wguid","title":"Why WGuid?","text":"<ul> <li>Problem: Unity doesn't serialize <code>System.Guid</code> directly</li> <li>Solution: <code>WGuid</code> stores as two <code>long</code> fields (<code>_low</code> and <code>_high</code>) for fast Unity serialization</li> </ul> <p>Performance:</p> <ul> <li>2x faster serialization than string-based GUID storage</li> <li>Smaller memory footprint (16 bytes vs. 36 bytes for string)</li> <li>Immutable design prevents accidental modification</li> </ul>"},{"location":"features/serialization/serialization-types/#basic-usage","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class Entity : MonoBehaviour\n{\n    public WGuid entityId = WGuid.NewGuid();\n}\n</code></pre> <p>Visual Reference</p> <p></p>"},{"location":"features/serialization/serialization-types/#creating-guids","title":"Creating GUIDs","text":"C#<pre><code>// Generate new GUID\nWGuid id1 = WGuid.NewGuid();\n\n// From System.Guid\nSystem.Guid sysGuid = System.Guid.NewGuid();\nWGuid id2 = (WGuid)sysGuid;\n\n// Parse from string\nWGuid id3 = WGuid.Parse(\"12345678-1234-1234-1234-123456789abc\");\n\n// Try parse (safe)\nif (WGuid.TryParse(\"...\", out WGuid id4))\n{\n    Debug.Log($\"Parsed: {id4}\");\n}\n\n// Empty GUID\nWGuid empty = WGuid.EmptyGuid;\n</code></pre>"},{"location":"features/serialization/serialization-types/#inspector-features","title":"Inspector Features","text":"<p>Custom Drawer:</p> <ul> <li>Text field displays GUID in standard format</li> <li>\"Generate\" button creates new GUID</li> <li>Validation warns if GUID is not version-4</li> <li>Undo/redo support</li> </ul> <p></p> <p></p> <p></p>"},{"location":"features/serialization/serialization-types/#conversions","title":"Conversions","text":"C#<pre><code>// WGuid &lt;-&gt; System.Guid\nWGuid wguid = WGuid.NewGuid();\nSystem.Guid sysGuid = wguid.ToGuid();\nWGuid back = (WGuid)sysGuid;\n\n// ToString() formats\nstring standard = wguid.ToString();  // \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\nstring formatted = wguid.ToString(\"N\");  // Without hyphens\n</code></pre>"},{"location":"features/serialization/serialization-types/#equality-comparison","title":"Equality &amp; Comparison","text":"C#<pre><code>WGuid id1 = WGuid.NewGuid();\nWGuid id2 = id1;\n\n// Implements IEquatable&lt;WGuid&gt;\nbool equal = id1.Equals(id2);  // true\nbool opEqual = id1 == id2;     // true\n\n// Implements IComparable&lt;WGuid&gt;\nint comparison = id1.CompareTo(id2);  // 0\n</code></pre>"},{"location":"features/serialization/serialization-types/#serialization-support","title":"Serialization Support","text":"<ul> <li>Unity: Serialized as two <code>long</code> fields</li> <li>JSON: Serialized as GUID string</li> <li>Protobuf: Serialized as two <code>long</code> fields</li> </ul> C#<pre><code>using ProtoBuf;\n\n[ProtoContract]\npublic class SaveData\n{\n    [ProtoMember(1)] public WGuid playerId;\n    [ProtoMember(2)] public WGuid sessionId;\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#serializabledictionary","title":"SerializableDictionary","text":"<p>Unity-friendly dictionary with synchronized key/value arrays and custom drawer.</p>"},{"location":"features/serialization/serialization-types/#why-serializabledictionary","title":"Why SerializableDictionary?","text":"<ul> <li>Problem: Unity doesn't serialize <code>Dictionary&lt;TKey, TValue&gt;</code></li> <li>Solution: <code>SerializableDictionary&lt;TKey, TValue&gt;</code> maintains synchronized arrays for Unity serialization and a runtime dictionary for fast lookups</li> </ul>"},{"location":"features/serialization/serialization-types/#basic-usage_1","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class PrefabRegistry : MonoBehaviour\n{\n    public GameObject enemyPrefab;\n    public GameObject playerPrefab;\n\n    public SerializableDictionary&lt;string, GameObject&gt; prefabs;\n\n    private void Start()\n    {\n        // Access entries\n        if (prefabs.TryGetValue(\"Enemy\", out GameObject prefab))\n        {\n            Instantiate(prefab);\n        }\n\n        // Count\n        Debug.Log($\"Prefab count: {prefabs.Count}\");\n\n        // Iteration\n        foreach (var kvp in prefabs)\n        {\n            Debug.Log($\"{kvp.Key}: {kvp.Value}\");\n        }\n\n        /*\n            Add entries (or overwrite)\n            WARNING: This is just for demo purposes! SerializableDictionary is meant for editor-mode persistence.\n            Nothing stops you from changing this at runtime, but it will be lost on next playthrough.\n        */\n        prefabs[\"Player\"] = playerPrefab;\n        prefabs[\"Enemy\"] = enemyPrefab;\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#inspector-features_1","title":"Inspector Features","text":"<p>Visual Reference</p> <p></p> <p>Dictionary inspector showing key-value pairs with pagination and inline editing</p> <p>Custom Drawer:</p> <ul> <li>Key/value pair editing</li> <li>Add/Remove buttons</li> <li>Reorderable list</li> <li>Duplicate key detection (visual warning)</li> <li>Null value highlighting</li> <li>Pagination for large dictionaries</li> </ul> <p></p>"},{"location":"features/serialization/serialization-types/#dictionary-operations","title":"Dictionary Operations","text":"C#<pre><code>// Implements IDictionary&lt;TKey, TValue&gt; and IReadOnlyDictionary&lt;TKey, TValue&gt;\nSerializableDictionary&lt;int, string&gt; dict = new();\n\n// Add\ndict.Add(1, \"One\");\ndict[2] = \"Two\";\n\n// Read\nstring value = dict[1];\nbool exists = dict.TryGetValue(2, out string val);\nbool contains = dict.ContainsKey(1);\n\n// Update\ndict[1] = \"First\";\n\n// Remove\ndict.Remove(1);\ndict.Clear();\n\n// Iteration\nforeach (KeyValuePair&lt;int, string&gt; kvp in dict)\n{\n    Debug.Log($\"{kvp.Key} = {kvp.Value}\");\n}\n\n// Keys/Values collections\nICollection&lt;int&gt; keys = dict.Keys;\nICollection&lt;string&gt; values = dict.Values;\n</code></pre>"},{"location":"features/serialization/serialization-types/#specialized-dictionaries","title":"Specialized Dictionaries","text":"C#<pre><code>// Sorted dictionary (maintains key order)\npublic SerializableSortedDictionary&lt;int, string&gt; sortedDict;\n</code></pre> <p>Note: <code>SerializableSortedDictionary</code> uses <code>SortedDictionary&lt;TKey, TValue&gt;</code> internally for ordered keys.</p>"},{"location":"features/serialization/serialization-types/#serialization-support_1","title":"Serialization Support","text":"<ul> <li>Unity: Synchronized <code>_keys</code> and <code>_values</code> arrays</li> <li>JSON: Standard dictionary format</li> <li>Protobuf: Supported via surrogates</li> </ul> C#<pre><code>// JSON example\n{\n    \"prefabs\": {\n        \"Enemy\": { \"instanceId\": 12345 },\n        \"Player\": { \"instanceId\": 67890 }\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#serializablehashset-serializablesortedset","title":"SerializableHashSet &amp; SerializableSortedSet","text":"<p>Unity-friendly set collections with duplicate detection and custom drawers.</p>"},{"location":"features/serialization/serialization-types/#why-serializable-sets","title":"Why Serializable Sets?","text":"<ul> <li>Problem: Unity doesn't serialize <code>HashSet&lt;T&gt;</code> or <code>SortedSet&lt;T&gt;</code></li> <li>Solution: <code>SerializableHashSet&lt;T&gt;</code> and <code>SerializableSortedSet&lt;T&gt;</code> maintain a serialized array and runtime set for fast lookups</li> </ul>"},{"location":"features/serialization/serialization-types/#basic-usage_2","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class UniqueItemTracker : MonoBehaviour\n{\n    public SerializableHashSet&lt;string&gt; collectedItems;\n\n    private void Start()\n    {\n        foreach (var item in collectedItems)\n        {\n            Debug.Log($\"Found item: {item}\");\n        }\n    }\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Set inspector with add/remove controls, duplicate highlighting, and pagination</p>"},{"location":"features/serialization/serialization-types/#inspector-features_2","title":"Inspector Features","text":"<p>Custom Drawer:</p> <ul> <li>Reorderable list</li> <li>Add/Remove/Clear/Sort buttons</li> <li>Duplicate detection with visual highlighting (shake animation + color)</li> <li>Null entry highlighting (red background)</li> <li>Pagination for large sets</li> <li>Move Up/Down buttons</li> <li>Current selection badge for items on other pages</li> <li>New Entry foldout to stage values before adding them to the runtime set (tune its animation via Project Settings \u25b8 Wallstop Studios \u25b8 Unity Helpers \u25b8 Set Foldouts)</li> </ul> <p>Visual Reference</p> <p></p> <p>Visual feedback for duplicate entries (yellow shake) and null values (red background) </p>"},{"location":"features/serialization/serialization-types/#set-operations","title":"Set Operations","text":"C#<pre><code>// Implements ISet&lt;T&gt; and IReadOnlyCollection&lt;T&gt;\nSerializableHashSet&lt;int&gt; set = new();\n\n// Add (returns true if new)\nbool added = set.Add(42);\n\n// Read\nbool contains = set.Contains(42);\nint count = set.Count;\n\n// Remove\nbool removed = set.Remove(42);\nset.Clear();\n\n// Set operations\nHashSet&lt;int&gt; other = new HashSet&lt;int&gt; { 1, 2, 3 };\nset.UnionWith(other);        // Add all from other\nset.IntersectWith(other);    // Keep only common elements\nset.ExceptWith(other);       // Remove elements in other\nbool overlaps = set.Overlaps(other);\n\n// Iteration\nforeach (int item in set)\n{\n    Debug.Log(item);\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#new-entry-foldout","title":"New Entry Foldout","text":"<p>Expandable \"New Entry\" controls let you configure the exact value that will be inserted, which is especially helpful for complex structs, managed references, or ScriptableObjects. The foldout supports the same field variety as the inline list and respects your duplicate/null validation. Animation for the New Entry foldout is governed by the Serializable Set Foldouts settings; adjust tweening and speed independently for <code>SerializableHashSet&lt;T&gt;</code> and <code>SerializableSortedSet&lt;T&gt;</code>.</p>"},{"location":"features/serialization/serialization-types/#foldout-defaults-overrides","title":"Foldout Defaults &amp; Overrides","text":"<p>By default, SerializableSet inspectors start collapsed until you open them. This baseline comes from Project Settings \u25b8 Wallstop Studios \u25b8 Unity Helpers via the Serializable Set Start Collapsed toggle (and the equivalent Serializable Dictionary Start Collapsed toggle for dictionaries). You can override the default per-field with <code>[WSerializableCollectionFoldout]</code>:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Attributes;\n\n[WSerializableCollectionFoldout(WSerializableCollectionFoldoutBehavior.StartExpanded)]\npublic SerializableHashSet&lt;string&gt; unlockedBadges = new();\n</code></pre> <ul> <li>Project setting establishes the initial state only.</li> <li><code>[WSerializableCollectionFoldout]</code> can request expanded or collapsed behavior for specific collections.</li> <li>Explicit changes to <code>SerializedProperty.isExpanded</code> (scripts, custom inspectors, or tests) take ultimate precedence. The drawer now respects those manual decisions, so opting-in via code no longer gets undone by the attribute or the global default.</li> </ul> <p>The attribute applies to both <code>SerializableHashSet&lt;T&gt;</code>/<code>SerializableSortedSet&lt;T&gt;</code> and the dictionary equivalents, making it straightforward to mix project-wide defaults with per-field intentions.</p>"},{"location":"features/serialization/serialization-types/#sorted-sets","title":"Sorted Sets","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class ThresholdLogger : MonoBehaviour\n{\n    public SerializableSortedSet&lt;int&gt; scoreThresholds = new();\n\n    [WButton]\n    private string LogThresholds()\n    {\n        foreach (int threshold in scoreThresholds)\n        {\n            Debug.Log(threshold);\n        }\n        return $\"Logged {scoreThresholds.Count} thresholds\";\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#serialization-support_2","title":"Serialization Support","text":"<ul> <li>Unity: Serialized <code>_items</code> array</li> <li>JSON: Array format</li> <li>Protobuf: Supported via collection surrogates</li> </ul> C#<pre><code>// JSON example\n{\n    \"collectedItems\": [\"item_001\", \"item_042\", \"item_137\"]\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#serializabletype","title":"SerializableType","text":"<p>Unity-friendly type reference that survives refactoring and namespace changes.</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class SerializableTypeExample : MonoBehaviour\n{\n    public SerializableType type;\n}\n</code></pre> <p>Visual Reference</p> <p></p> <p>Type selection with searchable dropdown, namespace filtering, and validation</p>"},{"location":"features/serialization/serialization-types/#why-serializabletype","title":"Why SerializableType?","text":"<ul> <li>Problem: Unity doesn't serialize <code>System.Type</code>, and type names break when refactoring</li> <li>Solution: <code>SerializableType</code> stores assembly-qualified names with fallback resolution on rename/namespace changes</li> </ul>"},{"location":"features/serialization/serialization-types/#basic-usage_3","title":"Basic Usage","text":"C#<pre><code>using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\nusing WallstopStudios.UnityHelpers.Core.Helper;\n\npublic class BehaviorSpawner : MonoBehaviour\n{\n    [WValueDropDown(typeof(BehaviorSpawner), nameof(GetAllMonoBehaviourNames))]\n    public SerializableType behaviorType;\n\n    private void SpawnBehavior()\n    {\n        if (behaviorType.IsEmpty)\n        {\n            Debug.LogWarning(\"No behavior type assigned!\");\n            return;\n        }\n\n        Type type = behaviorType.Value;\n        if (type != null)\n        {\n            GameObject go = new GameObject(type.Name);\n            go.AddComponent(type);\n        }\n    }\n\n    private static IEnumerable&lt;Type&gt; GetAllMonoBehaviourNames()\n    {\n        return ReflectionHelpers\n            .GetAllLoadedTypes()\n            .Where(type =&gt; typeof(MonoBehaviour).IsAssignableFrom(type) &amp;&amp; !type.IsAbstract);\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#inspector-features_3","title":"Inspector Features","text":"<p>Custom Drawer (Two-Line):</p> <ul> <li>Search row: Text field for filtering types</li> <li>Popup row: Dropdown showing matched types</li> <li>Clear button to unset the type</li> <li>Pagination for large type catalogs</li> <li>Auto-complete suggestions</li> </ul>"},{"location":"features/serialization/serialization-types/#type-operations","title":"Type Operations","text":"C#<pre><code>// Create\nSerializableType typeRef = new SerializableType(typeof(PlayerController));\n\n// Resolve\nType resolvedType = typeRef.Value;\nif (resolvedType != null)\n{\n    object instance = Activator.CreateInstance(resolvedType);\n}\n\n// Check\nbool isEmpty = typeRef.IsEmpty;\nstring displayName = typeRef.DisplayName;  // User-friendly name\n\n// Equality\nbool equal = typeRef.Equals(new SerializableType(typeof(PlayerController)));\n</code></pre>"},{"location":"features/serialization/serialization-types/#refactoring-resilience","title":"Refactoring Resilience","text":"<p>Scenario: You rename <code>PlayerController</code> to <code>PlayerBehavior</code> or move it to a new namespace.</p> <ul> <li>Standard Approach: Type reference breaks, data loss</li> <li>SerializableType: Automatically resolves via assembly scanning and fallback matching</li> </ul> <p>How it works:</p> <ol> <li>Stores assembly-qualified name (e.g., <code>Namespace.PlayerController, Assembly-CSharp</code>)</li> <li>On deserialization, tries exact match first</li> <li>If the exact match fails, it scans assemblies for the best partial match</li> <li>Updates internal name if resolved to the new type</li> </ol>"},{"location":"features/serialization/serialization-types/#serialization-support_3","title":"Serialization Support","text":"<ul> <li>Unity: Stores assembly-qualified name string</li> <li>JSON: Type name string with custom converter</li> <li>Protobuf: Supported via string surrogates</li> </ul> C#<pre><code>// JSON example\n{\n    \"behaviorType\": \"MyNamespace.PlayerController, Assembly-CSharp\"\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#serializablenullable","title":"SerializableNullable","text":"<p>Unity-friendly nullable value type wrapper.</p>"},{"location":"features/serialization/serialization-types/#why-serializablenullable","title":"Why SerializableNullable?","text":"<ul> <li>Problem: Unity doesn't serialize <code>Nullable&lt;T&gt;</code> (e.g., <code>int?</code>, <code>float?</code>)</li> <li>Solution: <code>SerializableNullable&lt;T&gt;</code> wraps any value type with <code>HasValue</code> and <code>Value</code> properties</li> </ul>"},{"location":"features/serialization/serialization-types/#basic-usage_4","title":"Basic Usage","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class BonusConfig : MonoBehaviour\n{\n    public SerializableNullable&lt;float&gt; criticalHitMultiplier;\n\n    private float GetDamage(float baseDamage, bool isCritical)\n    {\n        if (isCritical &amp;&amp; criticalHitMultiplier.HasValue)\n        {\n            return baseDamage * criticalHitMultiplier.Value;\n        }\n        return baseDamage;\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#inspector-features_4","title":"Inspector Features","text":"<p>Custom Drawer:</p> <ul> <li>Checkbox for <code>HasValue</code> state</li> <li>Inline value field (enabled when <code>HasValue == true</code>)</li> <li>Height adapts based on the nullable state</li> </ul>"},{"location":"features/serialization/serialization-types/#nullable-operations","title":"Nullable Operations","text":"C#<pre><code>// Create with value\nSerializableNullable&lt;int&gt; nullableInt = new SerializableNullable&lt;int&gt;(42);\n\n// Create without value (null)\nSerializableNullable&lt;int&gt; nullInt = new SerializableNullable&lt;int&gt;();\n\n// Check\nif (nullableInt.HasValue)\n{\n    int value = nullableInt.Value;\n    Debug.Log($\"Value: {value}\");\n}\n\n// Implicit conversion from T\nSerializableNullable&lt;float&gt; nullableFloat = 3.14f;\n\n// Conversion to Nullable&lt;T&gt;\nint? systemNullable = nullableInt.HasValue ? nullableInt.Value : null;\n</code></pre>"},{"location":"features/serialization/serialization-types/#use-cases","title":"Use Cases","text":"<p>Optional Configuration:</p> C#<pre><code>public SerializableNullable&lt;float&gt; overrideSpeed;  // null = use default\n</code></pre> <p>Conditional Bonuses:</p> C#<pre><code>public SerializableNullable&lt;int&gt; bonusGold;  // null = no bonus\n</code></pre> <p>Dynamic Properties:</p> C#<pre><code>public SerializableNullable&lt;Color&gt; customColor;  // null = use preset\n</code></pre>"},{"location":"features/serialization/serialization-types/#serialization-support_4","title":"Serialization Support","text":"<ul> <li>Unity: Stores <code>_hasValue</code> bool and <code>_value</code> T fields</li> <li>JSON: Standard nullable format</li> <li>Protobuf: Supported via nullable surrogates</li> </ul> C#<pre><code>// JSON example (has value)\n{\n    \"criticalHitMultiplier\": 2.5\n}\n\n// JSON example (null)\n{\n    \"criticalHitMultiplier\": null\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#best-practices","title":"Best Practices","text":""},{"location":"features/serialization/serialization-types/#1-choose-the-right-type","title":"1. Choose the Right Type","text":"C#<pre><code>// \u2705 GOOD: WGuid for entity IDs\npublic WGuid entityId = WGuid.NewGuid();\n\n// \u2705 GOOD: SerializableDictionary for key/value mappings\npublic SerializableDictionary&lt;string, GameObject&gt; prefabRegistry;\n\n// \u2705 GOOD: SerializableHashSet for unique collections\npublic SerializableHashSet&lt;string&gt; uniqueItemIds;\n\n// \u2705 GOOD: SerializableSortedSet for ordered unique values\npublic SerializableSortedSet&lt;int&gt; scoreThresholds;\n\n// \u2705 GOOD: SerializableType for type references\npublic SerializableType behaviorType;\n\n// \u2705 GOOD: SerializableNullable for optional values\npublic SerializableNullable&lt;float&gt; overrideSpeed;\n\n// \u274c BAD: String-based GUID\npublic string entityId = System.Guid.NewGuid().ToString();  // Use WGuid!\n\n// \u274c BAD: Parallel arrays instead of dictionary\npublic string[] keys;\npublic GameObject[] values;  // Use SerializableDictionary!\n\n// \u274c BAD: List with manual duplicate checking\npublic List&lt;string&gt; uniqueItems;  // Use SerializableHashSet!\n</code></pre>"},{"location":"features/serialization/serialization-types/#2-initialize-collections","title":"2. Initialize Collections","text":"C#<pre><code>// \u2705 GOOD: Initialize in field declaration\npublic SerializableDictionary&lt;string, int&gt; scores = new();\npublic SerializableHashSet&lt;string&gt; tags = new();\n\n// \u274c BAD: Null collections (NullReferenceException!)\npublic SerializableDictionary&lt;string, int&gt; scores;  // null!\n\nprivate void Start()\n{\n    scores.Add(\"player\", 100);  // Crash!\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#3-use-sorted-variants-for-ordered-data","title":"3. Use Sorted Variants for Ordered Data","text":"C#<pre><code>// \u2705 GOOD: SortedSet for ordered priorities\npublic SerializableSortedSet&lt;int&gt; unlockLevels;\n\n// \u2705 GOOD: SortedDictionary for ordered display\npublic SerializableSortedDictionary&lt;string, string&gt; alphabeticalNames;\n\n// \u274c BAD: HashSet for ordered data (no guaranteed order!)\npublic SerializableHashSet&lt;int&gt; unlockLevels;  // Order is random!\n</code></pre>"},{"location":"features/serialization/serialization-types/#4-handle-wguid-generation-carefully","title":"4. Handle WGuid Generation Carefully","text":"C#<pre><code>// \u2705 GOOD: Generate once, then immutable\npublic WGuid entityId = WGuid.NewGuid();\n\n// \u2705 GOOD: Generate in Awake if needed\nprivate void Awake()\n{\n    if (entityId == WGuid.EmptyGuid)\n    {\n        entityId = WGuid.NewGuid();\n    }\n}\n\n// \u274c BAD: Regenerating on every access\npublic WGuid EntityId =&gt; WGuid.NewGuid();  // New GUID every time!\n</code></pre>"},{"location":"features/serialization/serialization-types/#examples","title":"Examples","text":""},{"location":"features/serialization/serialization-types/#example-1-item-database-with-dictionary","title":"Example 1: Item Database with Dictionary","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\n[System.Serializable]\npublic class ItemData\n{\n    public string name;\n    public Sprite icon;\n    public int value;\n}\n\npublic class ItemDatabase : MonoBehaviour\n{\n    public SerializableDictionary&lt;string, ItemData&gt; items;\n\n    public bool TryGetItem(string itemId, out ItemData data)\n    {\n        return items.TryGetValue(itemId, out data);\n    }\n\n    public void AddItem(string itemId, ItemData data)\n    {\n        items[itemId] = data;\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#example-2-player-achievement-tracking","title":"Example 2: Player Achievement Tracking","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class PlayerProfile : MonoBehaviour\n{\n    public WGuid playerId = WGuid.NewGuid();\n    public SerializableHashSet&lt;string&gt; unlockedAchievements;\n    public SerializableSortedSet&lt;int&gt; highScores;\n\n    public void UnlockAchievement(string achievementId)\n    {\n        if (unlockedAchievements.Add(achievementId))\n        {\n            Debug.Log($\"Unlocked: {achievementId}\");\n            // Trigger UI notification, etc.\n        }\n    }\n\n    public void RecordScore(int score)\n    {\n        highScores.Add(score);\n\n        // Keep only top 10\n        while (highScores.Count &gt; 10)\n        {\n            highScores.Remove(highScores.Min);\n        }\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#example-3-dynamic-behavior-spawning","title":"Example 3: Dynamic Behavior Spawning","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\nusing System;\n\npublic class BehaviorFactory : MonoBehaviour\n{\n    [StringInList(typeof(TypeHelper), nameof(TypeHelper.GetAllMonoBehaviours))]\n    public SerializableType defaultBehavior;\n\n    public SerializableDictionary&lt;string, SerializableType&gt; namedBehaviors;\n\n    public GameObject SpawnWithBehavior(string behaviorName = null)\n    {\n        SerializableType typeToSpawn = defaultBehavior;\n\n        if (!string.IsNullOrEmpty(behaviorName) &amp;&amp;\n            namedBehaviors.TryGetValue(behaviorName, out SerializableType namedType))\n        {\n            typeToSpawn = namedType;\n        }\n\n        if (typeToSpawn.IsEmpty)\n        {\n            Debug.LogWarning(\"No behavior type specified!\");\n            return null;\n        }\n\n        Type type = typeToSpawn.Value;\n        if (type == null || !typeof(MonoBehaviour).IsAssignableFrom(type))\n        {\n            Debug.LogError($\"Invalid behavior type: {typeToSpawn.DisplayName}\");\n            return null;\n        }\n\n        GameObject go = new GameObject(type.Name);\n        go.AddComponent(type);\n        return go;\n    }\n}\n\npublic static class TypeHelper\n{\n    public static IEnumerable&lt;Type&gt; GetAllMonoBehaviours()\n    {\n        return AppDomain.CurrentDomain.GetAssemblies()\n            .SelectMany(a =&gt; a.GetTypes())\n            .Where(t =&gt; typeof(MonoBehaviour).IsAssignableFrom(t) &amp;&amp; !t.IsAbstract);\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#example-4-optional-configuration-with-nullable","title":"Example 4: Optional Configuration with Nullable","text":"C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class CharacterConfig : MonoBehaviour\n{\n    public float baseSpeed = 5f;\n    public SerializableNullable&lt;float&gt; speedOverride;  // null = use baseSpeed\n\n    public Color defaultColor = Color.white;\n    public SerializableNullable&lt;Color&gt; colorOverride;  // null = use defaultColor\n\n    private void Start()\n    {\n        float actualSpeed = speedOverride.HasValue ? speedOverride.Value : baseSpeed;\n        Color actualColor = colorOverride.HasValue ? colorOverride.Value : defaultColor;\n\n        Debug.Log($\"Speed: {actualSpeed}, Color: {actualColor}\");\n    }\n}\n</code></pre>"},{"location":"features/serialization/serialization-types/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features overview</li> <li>Serialization Guide - JSON/Protobuf serialization</li> <li>Data Structures - Other data structures</li> <li>Editor Tools Guide - Editor utilities</li> </ul> <p>Next Steps:</p> <ul> <li>Replace string/int-based IDs with <code>WGuid</code></li> <li>Use <code>SerializableDictionary</code> instead of parallel arrays</li> <li>Track unique collections with <code>SerializableHashSet</code></li> <li>Store type references with <code>SerializableType</code></li> <li>Add optional configuration with <code>SerializableNullable</code></li> </ul>"},{"location":"features/serialization/serialization/","title":"Serialization Guide","text":""},{"location":"features/serialization/serialization/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>Save/load data and configs reliably with JSON or Protobuf using one unified API.</li> <li>Unity\u2011aware converters handle common engine types; pooled buffers keep GC low.</li> <li>Pick Pretty/Normal for human\u2011readable; Fast/FastPOCO for hot paths.</li> </ul> <p>Visuals</p> <p></p> <p>This package provides fast, compact serialization for save systems, configuration, and networking with a unified API.</p> <ul> <li>Json \u2014 System.Text.Json with Unity-aware converters</li> <li>Protobuf \u2014 protobuf-net for compact, schema-evolvable binary</li> <li>SystemBinary \u2014 .NET BinaryFormatter for legacy/trusted-only scenarios</li> </ul> <p>All formats are exposed via <code>WallstopStudios.UnityHelpers.Core.Serialization.Serializer</code> and selected with <code>SerializationType</code>.</p>"},{"location":"features/serialization/serialization/#formats-provided","title":"Formats Provided","text":""},{"location":"features/serialization/serialization/#json","title":"Json","text":"<p>Human-readable; ideal for settings, debug, modding, and Git diffs.</p> <ul> <li>Includes converters for Unity types (ignores cycles, includes fields by default, case-insensitive by default; enums as strings in Normal/Pretty):</li> <li>Vector2, Vector3, Vector4, Vector2Int, Vector3Int</li> <li>Color, Color32, ColorBlock</li> <li>Quaternion, Matrix4x4, Pose, Plane, SphericalHarmonicsL2</li> <li>Bounds, BoundsInt, Rect, RectInt, RectOffset, RangeInt</li> <li>Ray, Ray2D, RaycastHit, BoundingSphere</li> <li>Resolution, RenderTextureDescriptor, LayerMask, Hash128, Scene</li> <li>AnimationCurve, Gradient, Touch, GameObject</li> <li>ParticleSystem.MinMaxCurve, ParticleSystem.MinMaxGradient</li> <li>System.Type (type metadata)</li> <li>Profiles: Normal, Pretty, Fast, FastPOCO (see below)</li> </ul>"},{"location":"features/serialization/serialization/#protobuf-protobuf-net","title":"Protobuf (protobuf-net)","text":"<p>\u2b50 Killer Feature: Schema Evolution \u2014 Players can load saves from older game versions without breaking! Add new fields, remove old ones, rename types\u2014all while maintaining compatibility.</p> <ul> <li>Small and fast; best for networking and large save payloads.</li> <li>Forward/backward compatible message evolution (see the Schema Evolution guide below).</li> </ul>"},{"location":"features/serialization/serialization/#systembinary-binaryformatter","title":"SystemBinary (BinaryFormatter)","text":"<p>Only for legacy or trusted, same-version, local data. Avoid for long-term persistence or untrusted input.</p> <ul> <li>\u26a0\ufe0f Cannot handle version changes - a single field addition breaks all existing saves.</li> </ul>"},{"location":"features/serialization/serialization/#when-to-use-what","title":"When To Use What","text":"<p>Use this decision flowchart to pick the right serialization format:</p> Text Only<pre><code>START: What are you serializing?\n  \u2502\n  \u251c\u2500 Game settings / Config files\n  \u2502   \u2502\n  \u2502   \u251c\u2500 Need human-readable / Git-friendly?\n  \u2502   \u2502   \u2192 JSON (Normal or Pretty) \u2713\n  \u2502   \u2502\n  \u2502   \u2514\u2500 Performance critical (large files)?\n  \u2502       \u2192 JSON (Fast or FastPOCO) \u2713\n  \u2502\n  \u251c\u2500 Save game data\n  \u2502   \u2502\n  \u2502   \u251c\u2500 First save system / Need debugging?\n  \u2502   \u2502   \u2192 JSON (Pretty) \u2713\n  \u2502   \u2502\n  \u2502   \u251c\u2500 Mobile / Size matters?\n  \u2502   \u2502   \u2192 Protobuf \u2713\n  \u2502   \u2502\n  \u2502   \u2514\u2500 Need cross-version compatibility?\n  \u2502       \u2192 Protobuf \u2713\n  \u2502\n  \u251c\u2500 Network messages (multiplayer)\n  \u2502   \u2502\n  \u2502   \u2514\u2500 Bandwidth is critical\n  \u2502       \u2192 Protobuf \u2713\n  \u2502\n  \u251c\u2500 Editor-only / Temporary cache (trusted environment)\n  \u2502   \u2502\n  \u2502   \u2514\u2500 Same Unity version, local only\n  \u2502       \u2192 SystemBinary (\u26a0\ufe0f legacy, consider JSON Fast)\n  \u2502\n  \u2514\u2500 Hot path / Per-frame serialization\n      \u2502\n      \u251c\u2500 Pure C# objects (no Unity types)?\n      \u2502   \u2192 JSON (FastPOCO) \u2713\n      \u2502\n      \u2514\u2500 Mixed with Unity types?\n          \u2192 JSON (Fast) \u2713\n</code></pre>"},{"location":"features/serialization/serialization/#quick-reference","title":"Quick Reference","text":"<ul> <li>Use JSON for:</li> <li>Player/tool settings, human-readable saves, serverless workflows, text diffs</li> <li>Quick iteration and debugging</li> <li> <p>First-time save system implementation</p> </li> <li> <p>Use Protobuf for:</p> </li> <li>Network payloads and large, bandwidth-sensitive saves</li> <li>Cases where schema evolves across versions</li> <li> <p>Mobile games where save file size matters</p> </li> <li> <p>Use SystemBinary only for:</p> </li> <li>Transient caches in trusted environments with exact version match</li> <li>\u26a0\ufe0f Consider JSON Fast instead - SystemBinary is legacy</li> </ul>"},{"location":"features/serialization/serialization/#json-examples-unity-aware","title":"JSON Examples (Unity-aware)","text":"<ul> <li>Serialize/deserialize and write/read files</li> </ul> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Serialization;\n\npublic class SaveData\n{\n    public Vector3 position;\n    public Color playerColor;\n    public List&lt;GameObject&gt; inventory;\n}\n\nvar data = new SaveData\n{\n    position = new Vector3(1, 2, 3),\n    playerColor = Color.cyan,\n    inventory = new List&lt;GameObject&gt;()\n};\n\n// Serialize to UTF-8 JSON bytes (Unity types supported)\nbyte[] jsonBytes = Serializer.JsonSerialize(data);\n\n// Pretty stringify for human readability\nstring jsonText = Serializer.JsonStringify(data, pretty: true);\n\n// Parse from string (convert to bytes first)\nbyte[] textBytes = System.Text.Encoding.UTF8.GetBytes(jsonText);\nSaveData fromText = Serializer.JsonDeserialize&lt;SaveData&gt;(textBytes);\n\n// File helpers\nSerializer.WriteToJsonFile(data, path: \"save.json\", pretty: true);\nSaveData fromFile = Serializer.ReadFromJsonFile&lt;SaveData&gt;(\"save.json\");\n\n// Generic entry points (choose format at runtime)\nbyte[] bytes = Serializer.Serialize(data, SerializationType.Json);\nSaveData loaded = Serializer.Deserialize&lt;SaveData&gt;(bytes, SerializationType.Json);\n</code></pre>"},{"location":"features/serialization/serialization/#advanced-json-apis","title":"Advanced JSON APIs","text":"<p>Unity Helpers provides several advanced APIs for high-performance and robust file operations.</p>"},{"location":"features/serialization/serialization/#async-file-operations","title":"Async File Operations","text":"<p>For non-blocking file I/O (useful in loading screens or background saves):</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Serialization;\n\n// Async read from file\nSaveData data = await Serializer.ReadFromJsonFileAsync&lt;SaveData&gt;(\"save.json\");\n\n// Async write to file\nawait Serializer.WriteToJsonFileAsync(data, \"save.json\", pretty: true);\n\n// With cancellation token (for interruptible operations)\nvar cts = new CancellationTokenSource();\nSaveData data = await Serializer.ReadFromJsonFileAsync&lt;SaveData&gt;(\"save.json\", cts.Token);\nawait Serializer.WriteToJsonFileAsync(data, \"save.json\", pretty: true, cts.Token);\n</code></pre> <p>When to use async:</p> <ul> <li>Loading screens where you don't want to block the main thread</li> <li>Auto-save systems running in the background</li> <li>Large save files that may take noticeable time</li> </ul>"},{"location":"features/serialization/serialization/#safe-try-pattern-apis","title":"Safe Try-Pattern APIs","text":"<p>For graceful error handling without try-catch blocks:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Serialization;\n\n// TryRead - returns false if file missing or invalid JSON\nif (Serializer.TryReadFromJsonFile&lt;SaveData&gt;(\"save.json\", out SaveData data))\n{\n    // File exists and parsed successfully\n    LoadGame(data);\n}\nelse\n{\n    // File missing or corrupted - start new game\n    StartNewGame();\n}\n\n// TryWrite - returns false if write failed\nif (!Serializer.TryWriteToJsonFile(data, \"save.json\"))\n{\n    Debug.LogError(\"Failed to save game!\");\n    ShowSaveErrorDialog();\n}\n</code></pre> <p>When to use Try-pattern:</p> <ul> <li>Loading saves that may not exist (new players)</li> <li>Handling corrupted save files gracefully</li> <li>Writing to paths that may not be writable</li> </ul>"},{"location":"features/serialization/serialization/#fast-serialization-hot-paths","title":"Fast Serialization (Hot Paths)","text":"<p>For performance-critical scenarios where you serialize/deserialize frequently:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Serialization;\n\n// Fast serialize - stricter options, Unity converters, minimal validation\nbyte[] fastBytes = Serializer.JsonSerializeFast(networkMessage);\n\n// Fast deserialize\nNetworkMessage msg = Serializer.JsonDeserializeFast&lt;NetworkMessage&gt;(fastBytes);\n\n// Fast serialize with buffer reuse (zero-allocation after warmup)\nbyte[] buffer = null;\nint length = Serializer.JsonSerializeFast(networkMessage, ref buffer);\n// Use buffer[0..length], buffer is reused on subsequent calls\n</code></pre> <p>Fast options differences:</p> Setting Normal/Pretty Fast Case-insensitive \u2705 \u274c Comments allowed \u2705 \u274c Trailing commas \u2705 \u274c Include fields \u2705 \u274c Reference handling Safe Disabled Unity type converters \u2705 \u2705"},{"location":"features/serialization/serialization/#creating-custom-options","title":"Creating Custom Options","text":"<p>Create your own options based on the Fast presets:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Serialization;\nusing System.Text.Json;\n\n// Get a copy of Fast options to customize\nJsonSerializerOptions myOptions = Serializer.CreateFastJsonOptions();\nmyOptions.WriteIndented = true;  // Add pretty-printing\n\n// FastPOCO - for pure C# objects with NO Unity types (fastest)\nJsonSerializerOptions pocoOptions = Serializer.CreateFastPocoJsonOptions();\n\n// Use with any serialize method\nbyte[] bytes = Serializer.JsonSerialize(data, myOptions);\nSerializer.WriteToJsonFile(data, \"file.json\", myOptions);\n</code></pre> <p>Option profiles:</p> <ul> <li><code>CreateFastJsonOptions()</code> \u2014 Fast parsing + Unity type converters (Vector3, Color, etc.)</li> <li><code>CreateFastPocoJsonOptions()</code> \u2014 Fastest, no converters, pure C# objects only</li> </ul>"},{"location":"features/serialization/serialization/#performance-comparison","title":"Performance Comparison","text":"C#<pre><code>// \ud83d\udc0c Normal (most compatible, slightly slower)\nbyte[] normal = Serializer.JsonSerialize(data);\n\n// \ud83d\ude80 Fast (stricter, faster parsing/writing)\nbyte[] fast = Serializer.JsonSerializeFast(data);\n\n// \ud83d\ude80\ud83d\ude80 Fast + buffer reuse (zero-allocation after first call)\nbyte[] buffer = null;\nint len = Serializer.JsonSerializeFast(data, ref buffer);\n\n// \ud83d\ude80\ud83d\ude80\ud83d\ude80 Fast POCO (pure C# objects, no Unity types)\nJsonSerializerOptions pocoOpts = Serializer.CreateFastPocoJsonOptions();\nbyte[] fastest = Serializer.JsonSerialize(pureCSharpData, pocoOpts);\n</code></pre>"},{"location":"features/serialization/serialization/#protobuf-examples-compact-evolvable","title":"Protobuf Examples (Compact + Evolvable)","text":"<ul> <li>Basic usage</li> </ul> C#<pre><code>using ProtoBuf; // protobuf-net\nusing WallstopStudios.UnityHelpers.Core.Serialization;\n\n[ProtoContract]\npublic class PlayerInfo\n{\n    [ProtoMember(1)] public int id;\n    [ProtoMember(2)] public string name;\n}\n\nvar info = new PlayerInfo { id = 1, name = \"Hero\" };\nbyte[] buf = Serializer.ProtoSerialize(info);\nPlayerInfo again = Serializer.ProtoDeserialize&lt;PlayerInfo&gt;(buf);\n\n// Generic entry points\nbyte[] buf2 = Serializer.Serialize(info, SerializationType.Protobuf);\nPlayerInfo again2 = Serializer.Deserialize&lt;PlayerInfo&gt;(buf2, SerializationType.Protobuf);\n\n// Buffer reuse (reduce GC in hot paths)\nbyte[] buffer = null;\nint len = Serializer.Serialize(info, SerializationType.Protobuf, ref buffer);\nPlayerInfo sliced = Serializer.Deserialize&lt;PlayerInfo&gt;(buffer.AsSpan(0, len).ToArray(), SerializationType.Protobuf);\n</code></pre> <ul> <li>Unity types with Protobuf: built-in surrogates</li> </ul> C#<pre><code>// This package registers protobuf-net surrogates at startup so Unity structs just work in protobuf models.\n// The following Unity types are protobuf-compatible out of the box:\n// - Vector2, Vector3, Vector2Int, Vector3Int\n// - Quaternion\n// - Color, Color32\n// - Rect, RectInt\n// - Bounds, BoundsInt\n// - Resolution\n// Example: use Vector3 directly in a protobuf-annotated model\nusing ProtoBuf;              // protobuf-net\nusing UnityEngine;           // Unity types\nusing WallstopStudios.UnityHelpers.Core.Serialization;\n\n[ProtoContract]\npublic class NetworkMessage\n{\n    [ProtoMember(1)] public int playerId;\n    [ProtoMember(2)] public Vector3 position;   // Works via registered surrogates\n    [ProtoMember(3)] public Quaternion facing;  // Works via registered surrogates\n}\n\n// Serialize/deserialize as usual\nvar msg = new NetworkMessage { playerId = 7, position = new Vector3(1,2,3), facing = Quaternion.identity };\nbyte[] bytes = Serializer.ProtoSerialize(msg);\nNetworkMessage again = Serializer.ProtoDeserialize&lt;NetworkMessage&gt;(bytes);\n</code></pre> <p>Notes</p> <ul> <li>Surrogates are registered in the Serializer static initializer; you don't need to call anything.</li> <li>If you define your own DTOs, they will continue to work; surrogates simply make Unity structs first-class.</li> <li>Keep using [ProtoContract]/[ProtoMember] and stable field numbers for your own types.</li> </ul>"},{"location":"features/serialization/serialization/#il2cpp-and-code-stripping-warning","title":"\u26a0\ufe0f IL2CPP and Code Stripping Warning","text":"<p>Critical for IL2CPP builds (WebGL, iOS, Android, Consoles):</p> <p>Protobuf uses reflection internally to serialize/deserialize types. Unity's IL2CPP managed code stripping may remove types or fields that are only accessed via reflection, causing silent data loss or runtime crashes in release builds.</p> <p>Common symptoms:</p> <ul> <li><code>NullReferenceException</code> or <code>TypeLoadException</code> during Protobuf deserialization</li> <li>Fields mysteriously have default values after loading (data appears to be lost)</li> <li>Works perfectly in Editor/Development builds, fails in Release/IL2CPP builds</li> <li>\"Type not found\" or \"Method not found\" errors at runtime</li> </ul>"},{"location":"features/serialization/serialization/#solution-create-a-linkxml-file","title":"Solution: Create a link.xml file","text":"<p>In your <code>Assets</code> folder (or any subfolder), create <code>link.xml</code> to preserve your Protobuf types:</p> XML<pre><code>&lt;linker&gt;\n  &lt;!-- Preserve all your Protobuf-serialized types --&gt;\n  &lt;assembly fullname=\"Assembly-CSharp\"&gt;\n    &lt;!-- Preserve specific types --&gt;\n    &lt;type fullname=\"MyGame.PlayerSave\" preserve=\"all\"/&gt;\n    &lt;type fullname=\"MyGame.InventoryData\" preserve=\"all\"/&gt;\n    &lt;type fullname=\"MyGame.NetworkMessage\" preserve=\"all\"/&gt;\n\n    &lt;!-- Or preserve entire namespace --&gt;\n    &lt;namespace fullname=\"MyGame.SaveData\" preserve=\"all\"/&gt;\n  &lt;/assembly&gt;\n\n  &lt;!-- If using Protobuf types across assemblies --&gt;\n  &lt;assembly fullname=\"MyGame.Shared\"&gt;\n    &lt;namespace fullname=\"MyGame.Shared.Protocol\" preserve=\"all\"/&gt;\n  &lt;/assembly&gt;\n\n  &lt;!-- Preserve Unity Helpers if needed --&gt;\n  &lt;assembly fullname=\"WallstopStudios.UnityHelpers.Runtime\"&gt;\n    &lt;!-- Usually not needed, but if you see errors: --&gt;\n    &lt;type fullname=\"WallstopStudios.UnityHelpers.Core.Serialization.Serializer\" preserve=\"all\"/&gt;\n  &lt;/assembly&gt;\n&lt;/linker&gt;\n</code></pre> <p>Testing checklist (CRITICAL):</p> <ul> <li>\u2705 Test every IL2CPP build - Development builds don't strip code, so issues only appear in Release</li> <li>\u2705 Test on actual devices - WebGL/Mobile stripping can differ from standalone builds</li> <li>\u2705 Test full save/load cycle - Save in one session, load in another to verify persistence</li> <li>\u2705 Update link.xml when adding new types - Every <code>[ProtoContract]</code> type needs preservation</li> <li>\u2705 Check build logs for stripping warnings - Unity logs which types/methods are stripped</li> <li>\u2705 Test after Unity upgrades - Stripping behavior can change between Unity versions</li> </ul> <p>When you might not need link.xml:</p> <ul> <li>Only using JSON serialization (source-generated, no reflection)</li> <li>Already preserving entire assembly with <code>preserve=\"all\"</code></li> <li>Using a custom IL2CPP link file that preserves everything</li> </ul>"},{"location":"features/serialization/serialization/#advanced-preserve-only-whats-needed","title":"Advanced: Preserve only what's needed","text":"<p>Instead of <code>preserve=\"all\"</code>, you can be more selective:</p> XML<pre><code>&lt;type fullname=\"MyGame.PlayerSave\"&gt;\n  &lt;method signature=\"System.Void .ctor()\" preserve=\"all\"/&gt;\n  &lt;field name=\"playerId\" /&gt;\n  &lt;field name=\"level\" /&gt;\n  &lt;field name=\"inventory\" /&gt;\n&lt;/type&gt;\n</code></pre> <p>However, this is error-prone. Start with <code>preserve=\"all\"</code> and optimize later if build size is critical.</p> <p>Related documentation:</p> <ul> <li>Unity Manual: Managed Code Stripping</li> <li>Protobuf-net IL2CPP Guide</li> <li>Unity Forum: link.xml best practices</li> </ul> Text Only<pre><code>&lt;a id=\"protobuf-schema-evolution-the-killer-feature\"&gt;&lt;/a&gt;\n## Protobuf Schema Evolution: The Killer Feature\n\n**The Problem Protobuf Solves:**\n\nYou ship your game with this save format:\n```csharp\n[ProtoContract]\npublic class PlayerSave\n{\n    [ProtoMember(1)] public int level;\n    [ProtoMember(2)] public string name;\n}\n</code></pre> <p>A month later, you want to add a new feature and change the format:</p> C#<pre><code>[ProtoContract]\npublic class PlayerSave\n{\n    [ProtoMember(1)] public int level;\n    [ProtoMember(2)] public string name;\n    [ProtoMember(3)] public int gold;        // NEW FIELD\n    [ProtoMember(4)] public bool isPremium;  // NEW FIELD\n}\n</code></pre> <p>With JSON or BinaryFormatter: Players' existing saves break. You must write migration code or wipe their progress.</p> <p>With Protobuf: It just works! Old saves load perfectly with <code>gold = 0</code> and <code>isPremium = false</code> defaults.</p>"},{"location":"features/serialization/serialization/#real-world-save-game-evolution-example-intermediate","title":"Real-World Save Game Evolution Example \ud83d\udfe1 Intermediate","text":"<p>Version 1.0 (Launch):</p> C#<pre><code>[ProtoContract]\npublic class PlayerSave\n{\n    [ProtoMember(1)] public string playerId;\n    [ProtoMember(2)] public int level;\n    [ProtoMember(3)] public Vector3DTO position;\n}\n</code></pre> <p>Version 1.5 (Inventory System Added):</p> C#<pre><code>[ProtoContract]\npublic class PlayerSave\n{\n    [ProtoMember(1)] public string playerId;\n    [ProtoMember(2)] public int level;\n    [ProtoMember(3)] public Vector3DTO position;\n    [ProtoMember(4)] public List&lt;string&gt; inventory = new();  // NEW: defaults to empty\n}\n</code></pre> <p>Version 2.0 (Stats Overhaul - level renamed to xp):</p> C#<pre><code>[ProtoContract]\npublic class PlayerSave\n{\n    [ProtoMember(1)] public string playerId;\n    // [ProtoMember(2)] int level - REMOVED, but tag 2 is NEVER reused\n    [ProtoMember(3)] public Vector3DTO position;\n    [ProtoMember(4)] public List&lt;string&gt; inventory = new();\n    [ProtoMember(5)] public int xp;              // NEW: experience points\n    [ProtoMember(6)] public int skillPoints;     // NEW: unspent skill points\n}\n</code></pre> <p>Result: Players who saved in v1.0 can load their save in v2.0:</p> <ul> <li>Old <code>level</code> value (tag 2) is ignored</li> <li>New <code>xp</code> and <code>skillPoints</code> default to 0</li> <li>All existing data (<code>playerId</code>, <code>position</code>, <code>inventory</code>) loads correctly</li> <li>Zero migration code required!</li> </ul>"},{"location":"features/serialization/serialization/#schema-evolution-rules","title":"Schema Evolution Rules","text":"<p>\u2705 Safe Changes (Always Compatible):</p> <ul> <li>Add new fields with new tag numbers</li> <li>Remove fields (but never reuse their tag numbers)</li> <li>Change field names (tags are what matter, not names)</li> <li>Add new message types</li> <li>Change default values (only affects new saves)</li> </ul> <p>\u26a0\ufe0f Requires Care:</p> <ul> <li>Changing field types (e.g., <code>int</code> \u2192 <code>long</code> works, <code>int</code> \u2192 <code>string</code> doesn't)</li> <li>Changing <code>repeated</code> to singular or vice versa (usually breaks)</li> <li>Renumbering existing tags (breaks everything!)</li> </ul> <p>\u274c Never Do This:</p> <ul> <li>Reuse deleted field tag numbers</li> <li>Change the meaning of an existing tag</li> <li>Remove required fields (avoid <code>required</code> entirely - use validation instead)</li> </ul>"},{"location":"features/serialization/serialization/#multi-version-compatibility-pattern-advanced","title":"Multi-Version Compatibility Pattern \ud83d\udd34 Advanced","text":"<p>Handle breaking changes across major versions gracefully:</p> C#<pre><code>[ProtoContract]\npublic class SaveFile\n{\n    [ProtoMember(1)] public int version = 3;  // Track your save version\n\n    // Version 1-3 fields\n    [ProtoMember(2)] public string playerId;\n    [ProtoMember(3)] public Vector3DTO position;\n\n    // Version 2+ fields\n    [ProtoMember(10)] public List&lt;string&gt; inventory;\n\n    // Version 3+ fields\n    [ProtoMember(20)] public PlayerStats stats;\n\n    public void PostDeserialize()\n    {\n        if (version &lt; 2)\n        {\n            // Migrate v1 saves: initialize empty inventory\n            inventory ??= new List&lt;string&gt;();\n        }\n\n        if (version &lt; 3)\n        {\n            // Migrate v2 saves: create default stats\n            stats ??= new PlayerStats { xp = 0, level = 1 };\n        }\n\n        version = 3; // Update to current version\n    }\n}\n</code></pre> <p>\u26a0\ufe0f Common Mistake: Don't put migration logic in the constructor. Use <code>PostDeserialize()</code> or a dedicated method called after loading. Constructors don't run during deserialization.</p>"},{"location":"features/serialization/serialization/#testing-schema-evolution-beginner","title":"Testing Schema Evolution \ud83d\udfe2 Beginner","text":"<p>Recommended Testing Pattern:</p> C#<pre><code>// 1. Save a file with version N:\nvar oldSave = new PlayerSave { level = 10, name = \"Hero\" };\nbyte[] bytes = Serializer.ProtoSerialize(oldSave);\nFile.WriteAllBytes(\"test_v1.save\", bytes);\n\n// 2. Update your schema (add new fields)\n\n// 3. Load the old file with new schema:\nbyte[] oldBytes = File.ReadAllBytes(\"test_v1.save\");\nvar loaded = Serializer.ProtoDeserialize&lt;PlayerSave&gt;(oldBytes);\n\n// New fields have defaults, old fields are preserved\nAssert.AreEqual(10, loaded.level);\nAssert.AreEqual(\"Hero\", loaded.name);\nAssert.AreEqual(0, loaded.gold);  // New field defaults to 0\n</code></pre> <p>Best Practice: Keep regression test files \u2014 Store save files from each version in your test suite.</p>"},{"location":"features/serialization/serialization/#common-save-system-patterns","title":"Common Save System Patterns","text":"<p>Pattern 1: Version-Aware Loading \ud83d\udfe1 Intermediate</p> C#<pre><code>public SaveFile LoadSave(string path)\n{\n    byte[] bytes = File.ReadAllBytes(path);\n    SaveFile save = Serializer.ProtoDeserialize&lt;SaveFile&gt;(bytes);\n\n    // Perform any version-specific migrations\n    save.PostDeserialize();\n\n    return save;\n}\n</code></pre> <p>Pattern 2: Gradual Migration (preserve old format for rollback) \ud83d\udd34 Advanced</p> C#<pre><code>public class SaveManager\n{\n    public void SaveGame(PlayerData data)\n    {\n        var protobuf = ConvertToProtobuf(data);\n        byte[] bytes = Serializer.ProtoSerialize(protobuf);\n\n        // Write both formats during transition period\n        File.WriteAllBytes(\"save.dat\", bytes);\n        Serializer.WriteToJsonFile(data, \"save.json.backup\");\n    }\n}\n</code></pre> <p>Pattern 3: Automatic Backup Before Save \ud83d\udfe1 Intermediate</p> C#<pre><code>public void SaveGame(SaveFile save)\n{\n    string path = \"player.save\";\n    string backup = $\"player.save.backup_{DateTime.Now:yyyyMMdd_HHmmss}\";\n\n    // Backup existing save before overwriting\n    if (File.Exists(path))\n    {\n        File.Copy(path, backup);\n    }\n\n    byte[] bytes = Serializer.ProtoSerialize(save);\n    File.WriteAllBytes(path, bytes);\n\n    // Keep only last 3 backups\n    CleanupOldBackups(\"player.save.backup_*\", keepCount: 3);\n}\n</code></pre>"},{"location":"features/serialization/serialization/#why-this-matters-for-live-games","title":"Why This Matters for Live Games","text":"<p>Without schema evolution (JSON/BinaryFormatter):</p> <ul> <li>\u274c Every update risks breaking player saves</li> <li>\u274c Must write complex migration code for every version</li> <li>\u274c Players lose progress if migration fails</li> <li>\u274c Can't roll back broken updates (saves are corrupted)</li> <li>\u274c Hotfixes that change save format are terrifying</li> </ul> <p>With Protobuf schema evolution:</p> <ul> <li>\u2705 Add features freely without breaking existing saves</li> <li>\u2705 Graceful degradation (old clients ignore new fields)</li> <li>\u2705 Can roll back game versions without data loss</li> <li>\u2705 Hotfixes are safe (just add new optional fields)</li> <li>\u2705 Reduces QA burden (less migration testing needed)</li> </ul>"},{"location":"features/serialization/serialization/#protobuf-compatibility-tips","title":"Protobuf Compatibility Tips","text":"<ul> <li>Add fields with new numbers; old clients ignore unknown fields; new clients default missing fields.</li> <li>Never reuse or renumber existing field tags; reserve removed numbers if needed.</li> <li>Avoid changing scalar types on the same number.</li> <li>Prefer optional/repeated instead of required.</li> <li>Use sensible defaults to minimize payloads.</li> <li>Group field numbers by version (e.g., v1: 1-10, v2: 11-20, v3: 21-30) for clarity.</li> </ul>"},{"location":"features/serialization/serialization/#protobuf-polymorphism-inheritance-interfaces","title":"Protobuf Polymorphism (Inheritance + Interfaces)","text":"<ul> <li>Abstract base with [ProtoInclude] (recommended)</li> <li>Protobuf-net does not infer subtype graphs unless you tell it. The recommended pattern is to put <code>[ProtoContract]</code> on an abstract base and list all concrete subtypes with <code>[ProtoInclude(tag, typeof(Subtype))]</code>.</li> <li>Declare your fields/properties as the abstract base so protobuf can deserialize to the correct subtype.</li> </ul> C#<pre><code>using ProtoBuf;\n\n[ProtoContract]\npublic abstract class Message { }\n\n[ProtoContract]\npublic sealed class Ping : Message { [ProtoMember(1)] public int id; }\n\n[ProtoContract]\n[ProtoInclude(100, typeof(Ping))]\npublic abstract class MessageBase : Message { }\n\n[ProtoContract]\npublic sealed class Envelope { [ProtoMember(1)] public MessageBase payload; }\n\n// round-trip works: Envelope.payload will be Ping at runtime\nbyte[] bytes = Serializer.ProtoSerialize(new Envelope { payload = new Ping { id = 7 } });\nEnvelope again = Serializer.ProtoDeserialize&lt;Envelope&gt;(bytes);\n</code></pre> <ul> <li> <p>Interfaces require a root mapping \u2014 Protobuf cannot deserialize directly to an interface because it needs a concrete root. You have three options:</p> </li> <li> <p>Use an abstract base with <code>[ProtoInclude]</code> and declare fields as that base (preferred).</p> </li> <li> <p>Register a mapping from the interface to a concrete root type at startup:</p> </li> </ul> C#<pre><code>Serializer.RegisterProtobufRoot&lt;IMsg, Ping&gt;();\nIMsg msg = Serializer.ProtoDeserialize&lt;IMsg&gt;(bytes);\n</code></pre> <ol> <li>Specify the concrete type with the overload:</li> </ol> C#<pre><code>IMsg msg = Serializer.ProtoDeserialize&lt;IMsg&gt;(bytes, typeof(Ping));\n</code></pre>"},{"location":"features/serialization/serialization/#random-system-example","title":"Random System Example","text":"<p>All PRNGs derive from <code>AbstractRandom</code>, which is <code>[ProtoContract]</code> and declares each implementation via <code>[ProtoInclude]</code>. Use this pattern in your models:</p> C#<pre><code>[ProtoContract]\npublic class RNGHolder { [ProtoMember(1)] public AbstractRandom rng; }\n\n// Serialize any implementation without surprises\nRNGHolder holder = new RNGHolder { rng = new PcgRandom(seed: 123) };\nbyte[] buf = Serializer.ProtoSerialize(holder);\nRNGHolder rt = Serializer.ProtoDeserialize&lt;RNGHolder&gt;(buf);\n</code></pre> <ul> <li>If you truly need an <code>IRandom</code> field, register a root or pass the concrete type when deserializing:</li> </ul> C#<pre><code>Serializer.RegisterProtobufRoot&lt;IRandom, PcgRandom&gt;();\nIRandom r = Serializer.ProtoDeserialize&lt;IRandom&gt;(bytes);\n// or\nIRandom r2 = Serializer.ProtoDeserialize&lt;IRandom&gt;(bytes, typeof(PcgRandom));\n</code></pre>"},{"location":"features/serialization/serialization/#tag-numbers-are-api-surface","title":"Tag Numbers Are API Surface","text":"<p>Tags in <code>[ProtoInclude(tag, ...)]</code> and <code>[ProtoMember(tag)]</code> are part of your schema. Add new numbers for new types/fields; never reuse or renumber existing tags once shipped.</p>"},{"location":"features/serialization/serialization/#systembinary-examples-legacytrusted-only","title":"SystemBinary Examples (Legacy/Trusted Only)","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Serialization;\n\nvar obj = new SomeSerializableType();\nbyte[] bin = Serializer.BinarySerialize(obj);\nSomeSerializableType roundtrip = Serializer.BinaryDeserialize&lt;SomeSerializableType&gt;(bin);\n\n// Generic\nbyte[] bin2 = Serializer.Serialize(obj, SerializationType.SystemBinary);\nvar round2 = Serializer.Deserialize&lt;SomeSerializableType&gt;(bin2, SerializationType.SystemBinary);\n</code></pre> <p>Watch-outs</p> <ul> <li>BinaryFormatter is obsolete for modern .NET and unsafe for untrusted input.</li> <li>Version changes often break BinaryFormatter payloads; restrict to same-version caches.</li> </ul> <p>Features</p> <ul> <li>Unity converters for JSON: Vector\u2154/4, Color, Matrix4x4, GameObject, Type</li> <li>Protobuf (protobuf-net) integration</li> <li>LZMA compression utilities (<code>Runtime/Utils/LZMA.cs</code>)</li> <li>Pooled buffers/writers to reduce allocations</li> </ul> <p>References</p> <ul> <li>API: <code>Runtime/Core/Serialization/Serializer.cs:1</code></li> <li>LZMA: <code>Runtime/Utils/LZMA.cs:1</code></li> </ul>"},{"location":"features/serialization/serialization/#migration","title":"Migration","text":"<ul> <li>Replace direct <code>System.Text.Json.JsonSerializer</code> calls in app code with <code>Serializer.JsonSerialize/JsonDeserialize/JsonStringify</code>, or with <code>Serializer.Serialize/Deserialize</code> + <code>SerializationType.Json</code> to centralize options and Unity converters.</li> <li>Replace any custom protobuf helpers with <code>Serializer.ProtoSerialize/ProtoDeserialize</code> or the generic <code>Serializer.Serialize/Deserialize</code> APIs. Ensure models are annotated with <code>[ProtoContract]</code> and stable <code>[ProtoMember(n)]</code> tags.</li> <li>For existing binary saves using BinaryFormatter, prefer migrating to Json or Protobuf. If you must keep BinaryFormatter, scope it to trusted, same-version caches only.</li> </ul>"},{"location":"features/serialization/serialization/#20-changes","title":"2.0 changes","text":"<ul> <li>BinaryFormatter (<code>SerializationType.SystemBinary</code>) is deprecated but remains functional for trusted/legacy scenarios. Prefer:</li> <li><code>SerializationType.Json</code> (System.Text.Json with Unity-aware converters) for readable, diffable content.</li> <li><code>SerializationType.Protobuf</code> (protobuf-net) for compact, high-performance binary payloads.</li> </ul>"},{"location":"features/serialization/serialization/#il2cpp-aot-guidance","title":"IL2CPP / AOT guidance","text":"<p>System.Text.Json can require extra care under AOT (e.g., IL2CPP):</p> <ul> <li>Prefer explicit <code>JsonSerializerOptions</code> and concrete generic APIs over <code>object</code>-based serialization to reduce reflection.</li> <li>For hot POCO models, consider adding a source-generated context (JsonSerializerContext) in your game assembly and pass it to <code>JsonSerializer</code> calls.</li> <li>If you rely on many custom converters, ensure they are referenced by code so the linker doesn't strip them. The UnityHelpers converters are referenced via options by default.</li> <li>Avoid deserializing <code>System.Type</code> from untrusted input (see <code>TypeConverter</code>); this is intended for trusted configs/tools.</li> </ul>"},{"location":"features/spatial/hulls/","title":"Hulls (Convex vs Concave)","text":""},{"location":"features/spatial/hulls/#tldr-when-to-use-which","title":"TL;DR \u2014 When To Use Which","text":"<ul> <li>Convex hull: fastest, safe outer bound; great for coarse collisions and visibility.</li> <li>Concave hull: follows shape detail; tunable fidelity vs. stability via k/alpha parameters.</li> </ul> <p>This guide explains convex and concave hulls, when to use each, and how they differ.</p>"},{"location":"features/spatial/hulls/#convex-hull","title":"Convex Hull","text":"<ul> <li>The smallest convex polygon that contains all points.</li> <li>Algorithms: Monotone Chain (a.k.a. Andrew\u2019s), Graham Scan, Jarvis March.</li> <li>Characteristics</li> <li>Always convex; no inward dents.</li> <li>Stable and deterministic for fixed input.</li> <li>Often used for coarse collision proxies, shape bounds, and visibility.</li> </ul> <p>Illustration:</p> <p></p>"},{"location":"features/spatial/hulls/#concave-hull","title":"Concave Hull","text":"<ul> <li>A polygon that can indent to follow the shape of points more closely.</li> <li>Algorithms: k-nearest-neighbor based, alpha-shapes, ball-pivoting variants.</li> <li>Characteristics</li> <li>Can capture shape detail; may exclude sparse outliers.</li> <li>Parameterized by k (neighbors) or alpha (radius) controlling \u201cconcavity\u201d.</li> <li>May create holes or self-intersections if not constrained; validate output.</li> </ul> <p>Illustration:</p> <p></p>"},{"location":"features/spatial/hulls/#choosing-between-them","title":"Choosing Between Them","text":"<ul> <li>Use convex hull when you need a fast, safe, and simple bound with predictable performance.</li> <li>Use concave hull when shape fidelity matters (e.g., silhouette, path enclosure) and you accept a tunable trade-off between detail and stability.</li> </ul>"},{"location":"features/spatial/hulls/#tips","title":"Tips","text":"<ul> <li>Preprocess: remove duplicate points and optionally simplify clusters.</li> <li>Postprocess: enforce clockwise/CCW winding and run self-intersection checks for concave hulls.</li> <li>Numerical stability: add small epsilons for collinear checks; include or exclude boundary points consistently.</li> </ul>"},{"location":"features/spatial/hulls/#api-reference-grid-vs-gridless","title":"API Reference (Grid vs. Gridless)","text":"<p>All hull helpers now offer both grid-aware (<code>Grid</code> + <code>FastVector3Int</code>) and gridless variants so you can work directly with <code>Vector2</code>/<code>FastVector3Int</code> data:</p> <ul> <li>Convex hull</li> <li><code>points.BuildConvexHull(includeColinearPoints: false)</code> for pure <code>Vector2</code>.</li> <li><code>fastPoints.BuildConvexHull(includeColinearPoints: false)</code> for <code>FastVector3Int</code> without a <code>Grid</code>.</li> <li><code>fastPoints.BuildConvexHull(grid, includeColinearPoints: false)</code> when you need <code>Grid.CellToWorld</code> conversions.</li> <li>Algorithm selection via <code>ConvexHullAlgorithm</code> is available for both gridful and gridless overloads.</li> <li>Concave hull</li> <li><code>vectorPoints.BuildConcaveHull(options)</code> / <code>BuildConcaveHullKnn</code> / <code>BuildConcaveHullEdgeSplit</code> for <code>Vector2</code>.</li> <li><code>fastPoints.BuildConcaveHull(options)</code> plus the <code>Knn</code>/<code>EdgeSplit</code> helpers for <code>FastVector3Int</code> without requiring a <code>Grid</code>.</li> <li><code>fastPoints.BuildConcaveHull(grid, options)</code> remains available when your data lives in grid space.</li> <li> <p>\u26a0\ufe0f The legacy line-division overload <code>BuildConcaveHull(IEnumerable&lt;FastVector3Int&gt;, Grid, float scaleFactor, float concavity)</code> has been retired and now throws <code>NotSupportedException</code>. Switch to <code>ConcaveHullStrategy.Knn</code> or <code>ConcaveHullStrategy.EdgeSplit</code> instead.</p> </li> </ul> <p>Because the new overloads reuse the pooled implementations under the hood, behaviour (winding, pruning, GC profile) matches the grid versions\u2014pick whichever signature best matches your data source.</p>"},{"location":"features/spatial/hulls/#gridless-vs-grid-aware-quickstart","title":"Gridless vs. Grid-Aware Quickstart","text":"<ul> <li>Pick the gridless overloads when your points already live in world/local space (<code>Vector2</code>, <code>Vector3</code>, or <code>FastVector3Int</code> without a <code>Grid</code>). This keeps the hull math independent of Unity\u2019s tile conversion layer.</li> <li>Pick the grid-aware overloads when you have cell coordinates tied to a <code>Grid</code> or <code>Tilemap</code> and you want the helper to respect <code>Grid.CellToWorld</code> so you can visualize the hull in scene space.</li> </ul> <p>Gridless example \u2014 pure <code>Vector2</code> data for nav areas or spline fitting:</p> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\n// outlinePoints could come from mouse clicks or a baked spline\nList&lt;Vector2&gt; outlinePoints = CollectOutlineSamples();\nUnityExtensions.ConcaveHullOptions outlineOptions = UnityExtensions.ConcaveHullOptions.Default\n    .WithStrategy(UnityExtensions.ConcaveHullStrategy.EdgeSplit)\n    .WithBucketSize(32)\n    .WithAngleThreshold(70f);\n\nList&lt;Vector2&gt; hull = outlinePoints.BuildConcaveHull(outlineOptions);\n</code></pre> <p>Grid-aware example \u2014 <code>FastVector3Int</code> tiles aligned to a <code>Grid</code> for tilemaps or voxel data:</p> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\nGrid grid = GetComponent&lt;Grid&gt;();\nList&lt;FastVector3Int&gt; tileSamples = CollectTileCoordinates();\nUnityExtensions.ConcaveHullOptions tileOptions = UnityExtensions.ConcaveHullOptions.Default\n    .WithStrategy(UnityExtensions.ConcaveHullStrategy.Knn)\n    .WithNearestNeighbors(5);\n\nList&lt;FastVector3Int&gt; gridHull = tileSamples.BuildConcaveHull(grid, tileOptions);\n</code></pre> <p>See <code>Samples~/Spatial Structures - 2D and 3D/Scripts/HullUsageDemo.cs</code> for a runnable MonoBehaviour that draws both loops (cyan for gridless, yellow for grid-aware) and logs the strategy/neighbor counts so you can copy the pattern directly into your own tooling, or just open <code>Samples~/Spatial Structures - 2D and 3D/Scenes/HullUsageDemo.unity</code> and press Play to watch both flows without extra setup.</p>"},{"location":"features/spatial/hulls/#collinear-points-includecolinearpoints","title":"Collinear Points &amp; includeColinearPoints","text":"<ul> <li>Convex hull helpers prune collinear points by default so only the true corners remain, even after grid-to-world projections introduce float skew.</li> <li>Opt into boundary retention by passing <code>includeColinearPoints: true</code> to <code>BuildConvexHull</code> (gridless) or its grid-aware overloads.</li> <li>Concave hulls inherit the pruned convex frontier; enabling collinear inclusion widens the seed set and can improve fidelity for dense edge sampling.</li> <li>The comprehensive tests <code>UnityExtensionsComprehensiveTests.ConvexHullDenseSamplesOnAllEdgesCollapseToCorners</code> (grid) and <code>.Vector2ConvexHullDenseSamplesCollapseToCorners</code> (gridless) cover both paths so you can trust the deterministic behavior.</li> </ul> C#<pre><code>List&lt;FastVector3Int&gt; cornersOnly = gridPoints.BuildConvexHull(grid, includeColinearPoints: false);\nList&lt;FastVector3Int&gt; withEdges = gridPoints.BuildConvexHull(grid, includeColinearPoints: true);\n</code></pre>"},{"location":"features/spatial/spatial-tree-semantics/","title":"Spatial Tree Semantics","text":""},{"location":"features/spatial/spatial-tree-semantics/#tldr-why-semantics-matter","title":"TL;DR \u2014 Why Semantics Matter","text":"<ul> <li>Different structures make different promises about boundaries and tie\u2011breaks.</li> <li>2D point trees (QuadTree2D, KdTree2D) agree on results; RTree differs by design (bounds\u2011based).</li> <li>In 3D, KdTree3D vs. OctTree3D can diverge at exact boundaries; add small epsilons if edge\u2011cases matter.</li> </ul> <p>This page explains how the 2D and 3D spatial structures compare in terms of correctness, when to use each structure, and why some 3D variants may produce different results for identical inputs and queries.</p>"},{"location":"features/spatial/spatial-tree-semantics/#result-buffers","title":"Result Buffers","text":"<ul> <li>Every range/bounds/nearest-neighbor API accepts an output <code>List&lt;T&gt;</code> and clears it before appending results. Reuse the same buffer between calls to avoid garbage and do not expect prior contents to survive a query.</li> <li>This pattern is consistent across <code>QuadTree2D</code>, <code>KdTree2D/3D</code>, <code>OctTree3D</code>, and the RTree variants, matching the log-friendly \u201cprovide your own buffer\u201d approach used throughout the helpers.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#structures-at-a-glance","title":"Structures At A Glance","text":"<ul> <li>QuadTree2D \u2014 Recursive 4-way partitioning of space. Good general-purpose point queries.</li> <li>KDTree2D \u2014 Alternating axis splits. Strong for nearest neighbor and range queries on points.</li> <li>RTree2D \u2014 Groups rectangles (AABBs) by minimum bounding rectangles (MBRs). Best when items have size.</li> </ul> <p>Illustrations:</p> <p></p> <p></p> <p></p> <p>3D Variants</p> <p></p> <p></p> <p></p> <p>Diagram notes</p> <ul> <li>Octree splits are centered along each axis, evenly dividing space into eight octants.</li> <li>KDTree3D splits are data\u2011dependent and may be off\u2011center; the diagram shows an off\u2011center y\u2011split to emphasize this difference.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#2d-consistent-results-across-quadtree2d-and-kdtree2d","title":"2D: Consistent Results Across QuadTree2D and KdTree2D","text":"<ul> <li>QuadTree2D and KdTree2D (balanced and unbalanced) index points and use equivalent per\u2011point checks for range and bounds queries. For the same input data and the same queries, they return the same results. Differences are limited to construction/query performance and memory layout.</li> <li>RTree2D differs by design: it indexes rectangles (AABBs). If your elements have size, intersection/containment semantics involve those sizes, so results will differ from point\u2011based trees.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#when-to-use-2d","title":"When To Use (2D)","text":"<ul> <li>QuadTree2D</li> <li>Use for broad-phase neighbor checks, visibility, and general spatial buckets where balanced performance and simplicity help.</li> <li>Pros: Simple mental model, predictable, easy to rebuild or update.</li> <li> <p>Cons: Hotspots can create deeper trees; nearest-neighbor not optimal vs KDTree.</p> </li> <li> <p>KDTree2D</p> </li> <li>Use for nearest-neighbor and precise point range queries at scale.</li> <li>Pros: Excellent for NN queries, balanced variant gives consistent query time.</li> <li> <p>Cons: Balanced build costs; dynamic updates more expensive than QuadTree.</p> </li> <li> <p>RTree2D</p> </li> <li>Use for geometry with area (AABBs), e.g., sprites, colliders, map tiles.</li> <li>Pros: Querying by bounds is very fast; items with size are first-class.</li> <li>Cons: Overlap between MBRs may increase query visits; tuned for bounds, not points.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#3d-why-kdtree3d-and-octtree3d-can-differ","title":"3D: Why KdTree3D and OctTree3D Can Differ","text":"<p>While KdTree3D and OctTree3D are both point\u2011based and target equivalent use cases, algorithmic choices can yield different edge\u2011case behavior for identical inputs/queries.</p> <p>Key reasons and scenarios:</p> <ul> <li>Split planes and child assignment</li> <li>KdTree3D splits by alternating axes (x, y, z); balanced builds use median selection, unbalanced builds split at node\u2011center. Points lying exactly on a split plane are deterministically assigned but may end up in different leaves between balanced vs unbalanced trees.</li> <li> <p>OctTree3D partitions space into eight octants at each node. Points on plane boundaries are classified using octant rules; borderline points may be grouped differently than in KdTree3D.</p> </li> <li> <p>Bounds queries: half\u2011open vs closed edges</p> </li> <li>KdTree3D constructs an inclusive half\u2011open query box for per\u2011point checks and uses Unity <code>Bounds</code> for traversal. OctTree3D uses <code>BoundingBox3D</code> with inclusive\u2011max conversion and additional node\u2011level fast paths when a node is fully contained.</li> <li>Minimum node size enforcement keeps node bounds non\u2011degenerate. Near boundary edges this can expand a node just enough to flip a fully\u2011contained check, changing whether the algorithm fast\u2011adds all points in a node or checks them individually.</li> <li> <p>Result impact: points exactly on max edges or at floating\u2011point limits can be included by one structure and excluded by the other in rare cases.</p> </li> <li> <p>Range (sphere) queries</p> </li> <li> <p>Both trees use exact per\u2011point distance checks. However, their traversal pruning and \u201cnode fully contained in sphere\u201d checks differ (sphere vs AABB overlap/containment and different numeric guards). For points close to the query radius, minor numeric differences can alter inclusion.</p> </li> <li> <p>Balanced vs unbalanced KdTree3D</p> </li> <li>Balanced uses median selection; unbalanced uses quick splits by node center. Both apply equivalent per\u2011point checks, but leaf grouping and bounding boxes differ. Near boundary edges, leaf\u2011level fast paths (e.g., when a node is fully contained) can diverge, leading to differences at exact boundaries.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#3d-rtree3d-semantics","title":"3D: RTree3D Semantics","text":"<ul> <li>RTree3D indexes 3D AABBs and aggregates bounding volumes upward. Queries (box/sphere) operate on those volumes rather than points. Expect results to differ from KdTree3D/OctTree3D in scenes where elements have size.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#guidance","title":"Guidance","text":"<ul> <li>Need consistent point semantics in 2D? Use QuadTree2D or KdTree2D interchangeably; choose based on performance.</li> <li>In 3D, prefer KdTree3D for nearest\u2011neighbor point queries and OctTree3D for general\u2011purpose spatial partitioning. Be mindful of edge cases on query boundaries; add small epsilons if needed.</li> <li>Use RTree2D/RTree3D for sized elements where bounds intersection is the primary concern.</li> <li>For many moving objects with broad\u2011phase neighbor checks, prefer SpatialHash3D (stable) or SpatialHash2D.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#boundary-semantics","title":"Boundary Semantics","text":"<p>Tips</p> <ul> <li>Normalize to closed or half\u2011open intervals across your codebase.</li> <li>Add a small epsilon where necessary to handle ties at split planes.</li> </ul>"},{"location":"features/spatial/spatial-tree-semantics/#cheat-sheet","title":"Cheat Sheet","text":"<ul> <li>Many moving points, frequent rebuilds: QuadTree2D</li> <li>Nearest neighbors on static points: KDTree2D (Balanced)</li> <li>Fast builds with okay query performance: KDTree2D (Unbalanced)</li> <li>Objects with size, bounds queries: RTree2D</li> </ul>"},{"location":"features/spatial/spatial-trees-2d-guide/","title":"2D Spatial Trees \u2014 Concepts and Usage","text":"<p>This practical guide complements performance and semantics pages with diagrams and actionable selection advice.</p>"},{"location":"features/spatial/spatial-trees-2d-guide/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>You often need to answer: \"What's near X?\" or \"What's inside this area?\"</li> <li>\u2b50 Naive loops are O(n) \u2014 check every object. Spatial trees are O(log n) \u2014 only check nearby objects.</li> <li>Result: 10-100x faster queries, scaling from dozens to millions of objects.</li> </ul>"},{"location":"features/spatial/spatial-trees-2d-guide/#the-scaling-advantage","title":"The Scaling Advantage","text":"Object Count Naive Approach (checks) Spatial Tree (checks) Speedup 100 100 ~7 14x 1,000 1,000 ~10 100x 10,000 10,000 ~13 769x 100,000 \ud83d\udc80 Unplayable ~17 \u221e <p>Quick picks</p> <ul> <li>Many moving points, frequent rebuilds, broad searches: QuadTree2D</li> <li>Static points, nearest\u2011neighbor/k\u2011NN: KdTree2D (Balanced)</li> <li>Fast builds with good\u2011enough queries on points: KdTree2D (Unbalanced)</li> <li>Objects with size (bounds), intersect/contain queries: RTree2D</li> </ul>"},{"location":"features/spatial/spatial-trees-2d-guide/#quick-start-code","title":"Quick Start (Code)","text":"<p>Points (QuadTree2D / KdTree2D)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\nusing UnityEngine;\nusing System.Collections.Generic;\n\n// Example element with a position\nstruct Enemy { public Vector2 pos; public int id; }\n\nvar enemies = new List&lt;Enemy&gt;(/* fill with positions */);\n\n// Build a tree from points\nvar quad = new QuadTree2D&lt;Enemy&gt;(enemies, e =&gt; e.pos);\nvar kd   = new KdTree2D&lt;Enemy&gt;(enemies, e =&gt; e.pos); // balanced by default\n\n// Range query (circle)\nvar inRange = new List&lt;Enemy&gt;();\nquad.GetElementsInRange(playerPos, 10f, inRange);\n\n// Bounds (box) query\nvar inBox = new List&lt;Enemy&gt;();\nkd.GetElementsInBounds(new Bounds(center, size), inBox);\n\n// Approximate nearest neighbors\nvar neighbors = new List&lt;Enemy&gt;();\nkd.GetApproximateNearestNeighbors(playerPos, count: 10, neighbors);\n</code></pre> <p>Sized objects (RTree2D)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\nusing UnityEngine;\nusing System.Collections.Generic;\n\nstruct Tile { public Bounds bounds; public int kind; }\n\nvar tiles = new List&lt;Tile&gt;(/* fill with bounds */);\n\n// Build from bounds (AABBs)\nvar rtree = new RTree2D&lt;Tile&gt;(tiles, t =&gt; t.bounds);\n\n// Bounds query (fast for large areas)\nvar hits = new List&lt;Tile&gt;();\nrtree.GetElementsInBounds(worldBounds, hits);\n\n// Range query (treats items by their bounds)\nvar near = new List&lt;Tile&gt;();\nrtree.GetElementsInRange(center, radius, near);\n</code></pre> <p>Notes</p> <ul> <li>These trees are immutable: rebuild when positions/bounds change significantly.</li> <li>For lots of moving points, consider <code>SpatialHash2D</code> for broad\u2011phase.</li> <li>See Spatial Tree Semantics for boundary behavior and edge cases.</li> </ul> <p> </p>"},{"location":"features/spatial/spatial-trees-2d-guide/#zero-allocation-queries-the-performance-killer-feature","title":"\u2b50 Zero-Allocation Queries: The Performance Killer Feature","text":"<p>The Problem - GC Spikes Every Frame:</p> C#<pre><code>void Update()\n{\n    // \ud83d\udd34 BAD: Allocates new List every frame\n    List&lt;Enemy&gt; nearby = tree.GetElementsInRange(playerPos, 10f);\n\n    foreach (Enemy e in nearby)\n    {\n        e.ReactToPlayer();\n    }\n    // Result: GC runs frequently = frame drops\n}\n</code></pre> <p>The Solution - Buffering Pattern:</p> C#<pre><code>// Reusable buffer (declare once)\nprivate List&lt;Enemy&gt; nearbyBuffer = new(64);\n\nvoid Update()\n{\n    nearbyBuffer.Clear();\n\n    // \ud83d\udfe2 GOOD: Reuses same List = zero allocations\n    tree.GetElementsInRange(playerPos, 10f, nearbyBuffer);\n\n    foreach (Enemy e in nearbyBuffer)\n    {\n        e.ReactToPlayer();\n    }\n    // Result: No GC, stable 60fps\n}\n</code></pre> <p>Impact:</p> <ul> <li>Before: GC spikes every 2-3 seconds, frame drops to 40fps</li> <li>After: Zero GC from queries, stable 60fps even with 1000s of queries/second</li> </ul> <p>All spatial trees support this pattern:</p> <ul> <li><code>QuadTree2D.GetElementsInRange(pos, radius, buffer)</code></li> <li><code>KdTree2D.GetElementsInBounds(bounds, buffer)</code></li> <li><code>RTree2D.GetElementsInRange(pos, radius, buffer)</code></li> </ul> <p>\ud83d\udca1 Pro Tip: Pre-size your buffers based on expected max results. <code>new List&lt;Enemy&gt;(64)</code> avoids internal resizing for results up to 64 items.</p> <p>Maximum Ergonomics:</p> <p>These APIs return the buffer you pass in, so you can use them directly in <code>foreach</code> loops:</p> C#<pre><code>private List&lt;Enemy&gt; nearbyBuffer = new(64);\n\nvoid Update()\n{\n    // Returns the same buffer - use it directly in foreach!\n    foreach (Enemy e in tree.GetElementsInRange(playerPos, 10f, nearbyBuffer))\n    {\n        e.ReactToPlayer();\n    }\n}\n</code></pre> <p>Using Pooled Buffers:</p> <p>Don't want to manage buffers yourself? Use the built-in pooling utilities:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nvoid Update()\n{\n    // Get pooled buffer - automatically returned when scope exits\n    using var lease = Buffers&lt;Enemy&gt;.List.Get(out List&lt;Enemy&gt; buffer);\n\n    // Use it directly in foreach - combines zero-alloc query + pooled buffer!\n    foreach (Enemy e in tree.GetElementsInRange(playerPos, 10f, buffer))\n    {\n        e.ReactToPlayer();\n    }\n    // buffer automatically returned to pool here\n}\n</code></pre> <p>See Buffering Pattern for the complete guide and Pooling Utilities for more pooling options.</p>"},{"location":"features/spatial/spatial-trees-2d-guide/#structures","title":"Structures","text":""},{"location":"features/spatial/spatial-trees-2d-guide/#quadtree2d","title":"QuadTree2D","text":"<ul> <li>Partition: Recursively splits space into four quadrants.</li> <li>Use for: Broad-phase proximity, view culling, general spatial bucketing.</li> <li>Pros: Simple structure; predictable performance; incremental updates straightforward.</li> <li>Cons: Data hotspots deepen local trees; nearest neighbors slower than KDTree.</li> </ul> <p>Diagram: </p>"},{"location":"features/spatial/spatial-trees-2d-guide/#kdtree2d","title":"KDTree2D","text":"<ul> <li>Partition: Alternating axis-aligned splits (x/y), often median-balanced.</li> <li>Use for: Nearest neighbor, k-NN, range queries on points.</li> <li>Pros: Strong NN performance; balanced variant gives consistent query time.</li> <li>Cons: Costly to maintain under heavy churn; unbalanced variant can degrade.</li> </ul> <p>Diagram: </p>"},{"location":"features/spatial/spatial-trees-2d-guide/#rtree2d","title":"RTree2D","text":"<ul> <li>Partition: Groups items by minimum bounding rectangles (MBRs) with hierarchical MBRs.</li> <li>Use for: Items with size (AABBs): sprites, tiles, colliders; bounds intersection.</li> <li>Pros: Great for large bounds queries; matches bounds semantics.</li> <li>Cons: Overlapping MBRs can increase node visits; not optimal for point NN.</li> </ul> <p>Diagram: </p>"},{"location":"features/spatial/spatial-trees-2d-guide/#choosing-a-structure","title":"Choosing a Structure","text":"<p>Use this decision flowchart to pick the right spatial tree:</p> Text Only<pre><code>START: Do your objects move frequently?\n  \u2502\n  \u251c\u2500 YES \u2192 Consider SpatialHash2D instead (see README)\n  \u2502         (Spatial trees require rebuild on movement)\n  \u2502\n  \u2514\u2500 NO \u2192 Continue to next question\n      \u2502\n      \u2514\u2500 What type of queries do you need?\n          \u2502\n          \u251c\u2500 Primarily nearest neighbor (k-NN)\n          \u2502   \u2502\n          \u2502   \u251c\u2500 Static data, want consistent performance\n          \u2502   \u2502   \u2192 KDTree2D (Balanced) \u2713\n          \u2502   \u2502\n          \u2502   \u2514\u2500 Data changes occasionally, need fast rebuilds\n          \u2502       \u2192 KDTree2D (Unbalanced) \u2713\n          \u2502\n          \u251c\u2500 Do objects have size/bounds (not just points)?\n          \u2502   \u2502\n          \u2502   \u251c\u2500 YES \u2192 Need bounds intersection queries\n          \u2502   \u2502   \u2192 RTree2D \u2713\n          \u2502   \u2502\n          \u2502   \u2514\u2500 NO \u2192 Continue\n          \u2502\n          \u2514\u2500 General range/circular queries, broad-phase\n              \u2192 QuadTree2D \u2713 (best all-around choice)\n</code></pre>"},{"location":"features/spatial/spatial-trees-2d-guide/#quick-reference","title":"Quick Reference","text":"<ul> <li>Many moving points, rebuild or frequent updates: QuadTree2D</li> <li>Nearest neighbors on static points: KDTree2D (Balanced)</li> <li>Fast builds with good-enough queries: KDTree2D (Unbalanced)</li> <li>Objects with area; bounds queries primary: RTree2D</li> <li>Very frequent movement (every frame): SpatialHash2D (see README)</li> </ul>"},{"location":"features/spatial/spatial-trees-2d-guide/#query-semantics","title":"Query Semantics","text":"<ul> <li>Points vs. Bounds: QuadTree2D and KDTree2D are point-based; RTree2D is bounds-based.</li> <li>Boundary inclusion: normalize half-open vs. closed intervals. Add epsilons for edge cases.</li> <li>Numeric stability: prefer consistent ordering for collinear and boundary points.</li> </ul> <p>For deeper details, performance data, and diagrams, see:</p> <ul> <li>2D Performance Benchmarks</li> <li>Spatial Tree Semantics</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/","title":"3D Spatial Trees \u2014 Concepts and Usage","text":"<p>This approachable guide shows when to use OctTree3D, KdTree3D, and RTree3D, with quick code you can copy.</p>"},{"location":"features/spatial/spatial-trees-3d-guide/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>Answer \u201cWhat\u2019s near X?\u201d or \u201cWhat\u2019s inside this volume?\u201d in 3D without scanning everything.</li> <li>Organize your data so queries touch only relevant spatial buckets.</li> <li>Big speedups for range, bounds, and nearest\u2011neighbor queries.</li> </ul> <p>Quick picks</p> <ul> <li>General 3D queries (broad\u2011phase, good locality): OctTree3D</li> <li>Nearest neighbors on static points: KdTree3D (Balanced)</li> <li>Fast builds with good\u2011enough point queries: KdTree3D (Unbalanced)</li> <li>Objects with size (3D bounds), intersect/contain queries: RTree3D</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#quick-start-code","title":"Quick Start (Code)","text":"<p>Points (OctTree3D / KdTree3D)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\nusing UnityEngine;\nusing System.Collections.Generic;\n\nstruct VfxPoint { public Vector3 pos; public int id; }\n\nvar points = new List&lt;VfxPoint&gt;(/* fill with positions */);\n\n// Build trees from points\nvar oct = new OctTree3D&lt;VfxPoint&gt;(points, p =&gt; p.pos);\nvar kd  = new KdTree3D&lt;VfxPoint&gt;(points, p =&gt; p.pos); // balanced by default\n\n// Range query (sphere)\nvar inRange = new List&lt;VfxPoint&gt;();\noct.GetElementsInRange(playerPos, 12f, inRange);\n\n// Bounds (box) query\nvar inBox = new List&lt;VfxPoint&gt;();\nkd.GetElementsInBounds(new Bounds(center, size), inBox);\n\n// Approximate nearest neighbors\nvar neighbors = new List&lt;VfxPoint&gt;();\nkd.GetApproximateNearestNeighbors(playerPos, count: 12, neighbors);\n</code></pre> <p>Sized objects (RTree3D)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\nusing UnityEngine;\nusing System.Collections.Generic;\n\nstruct Volume { public Bounds bounds; public int kind; }\n\nvar volumes = new List&lt;Volume&gt;(/* fill with bounds */);\n\n// Build from 3D bounds (AABBs)\nvar rtree = new RTree3D&lt;Volume&gt;(volumes, v =&gt; v.bounds);\n\n// Bounds query (fast for large volumes)\nvar hits = new List&lt;Volume&gt;();\nrtree.GetElementsInBounds(worldBounds, hits);\n\n// Range query (treats items by their bounds)\nvar near = new List&lt;Volume&gt;();\nrtree.GetElementsInRange(center, radius, near);\n</code></pre> <p>Notes</p> <ul> <li>These trees are immutable: rebuild when positions/bounds change significantly.</li> <li>For lots of moving points, consider <code>SpatialHash3D</code> for broad\u2011phase neighborhood queries.</li> <li>See Spatial Tree Semantics for boundary behavior and edge cases.</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#zero-allocation-queries-the-performance-killer-feature","title":"\u2b50 Zero-Allocation Queries: The Performance Killer Feature","text":"<p>All 3D spatial trees support the same zero-allocation query pattern as their 2D counterparts. Pass a reusable buffer to avoid GC allocations:</p> C#<pre><code>// Reusable buffer (declare once)\nprivate List&lt;VfxPoint&gt; nearbyBuffer = new(128);\n\nvoid Update()\n{\n    nearbyBuffer.Clear();\n\n    // \ud83d\udfe2 GOOD: Reuses same List = zero allocations\n    tree.GetElementsInRange(playerPos, 15f, nearbyBuffer);\n\n    foreach (VfxPoint p in nearbyBuffer)\n    {\n        p.UpdateEffect();\n    }\n}\n</code></pre> <p>All 3D spatial trees support buffered queries:</p> <ul> <li><code>OctTree3D.GetElementsInRange(pos, radius, buffer)</code></li> <li><code>KdTree3D.GetElementsInBounds(bounds, buffer)</code></li> <li><code>RTree3D.GetElementsInRange(pos, radius, buffer)</code></li> </ul> <p>\ud83d\udcd6 For the complete buffering guide including pooled buffers and GC impact analysis, see:</p> <ul> <li>Zero-Allocation Queries (2D Guide) \u2014 detailed examples</li> <li>Buffering Pattern \u2014 project-wide pooling utilities</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#structures","title":"Structures","text":""},{"location":"features/spatial/spatial-trees-3d-guide/#octtree3d","title":"OctTree3D","text":"<ul> <li>Partition: Recursively splits space into eight octants.</li> <li>Use for: General 3D partitioning, broad\u2011phase, visibility culling, spatial audio.</li> <li>Pros: Good spatial locality; intuitive partitioning; balanced performance.</li> <li>Cons: Nearest neighbors slower than KDTree on pure point data.</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#kdtree3d","title":"KDTree3D","text":"<ul> <li>Partition: Alternating axis\u2011aligned splits (x/y/z), often median\u2011balanced.</li> <li>Use for: Nearest neighbor, k\u2011NN, range queries on points.</li> <li>Pros: Strong NN performance; balanced variant gives consistent query time.</li> <li>Cons: Costly to maintain under heavy churn; unbalanced variant can degrade.</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#rtree3d","title":"RTree3D","text":"<ul> <li>Partition: Groups items by minimum bounding boxes with hierarchical bounding.</li> <li>Use for: Items with size (3D AABBs): volumes, colliders; bounds intersection.</li> <li>Pros: Great for large bounds queries; matches volumetric semantics.</li> <li>Cons: Overlapping boxes can increase node visits; not optimal for point NN.</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#choosing-a-structure","title":"Choosing a Structure","text":"<ul> <li>Many moving points, frequent rebuilds: OctTree3D or SpatialHash3D</li> <li>Nearest neighbors on static points: KDTree3D (Balanced)</li> <li>Fast builds with good\u2011enough point queries: KDTree3D (Unbalanced)</li> <li>Objects with volume; bounds queries primary: RTree3D</li> </ul>"},{"location":"features/spatial/spatial-trees-3d-guide/#query-semantics","title":"Query Semantics","text":"<ul> <li>Points vs. Bounds: KDTree3D/OctTree3D are point\u2011based; RTree3D is bounds\u2011based.</li> <li>Boundary inclusion: 3D variants can differ at exact boundaries. Normalize to half\u2011open or add small epsilons.</li> <li>For details and performance data, see:</li> <li>3D Performance Benchmarks</li> <li>Spatial Tree Semantics</li> </ul>"},{"location":"features/utilities/data-structures/","title":"Core Data Structures \u2014 Concepts, Usage, and Trade-offs","text":""},{"location":"features/utilities/data-structures/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>Pick the right container for performance and clarity: ring buffers, deques, heaps, tries, sparse sets, etc.</li> <li>Each section gives a plain\u2011language \u201cUse for / Pros / Cons\u201d and a tiny code snapshot to copy.</li> <li>When in doubt, prefer simple .NET types; use these when you hit performance or ergonomics limits.</li> </ul> <p>This guide covers several foundational data structures used across the library and when to use them.</p>"},{"location":"features/utilities/data-structures/#cyclic-buffer-ring-buffer","title":"Cyclic Buffer (Ring Buffer)","text":"<ul> <li>What it is: Fixed-capacity circular array with head/tail indices that wrap.</li> <li>Use for: Streaming data, fixed-size queues, audio/network buffers.</li> <li>Operations: enqueue/dequeue in O(1); overwriting old data optional.</li> <li>Pros: Constant-time, cache-friendly, no reallocations at steady size.</li> <li>Cons: Fixed capacity unless resized; must handle wrap-around.</li> </ul> <p>When to use vs. DotNET queues</p> <ul> <li>Prefer <code>CyclicBuffer&lt;T&gt;</code> over <code>Queue&lt;T&gt;</code> when you want bounded memory with O(1) push/pop at both ends and predictable behavior under backpressure (drop/overwrite oldest, or pop proactively).</li> <li>Use <code>Queue&lt;T&gt;</code> when you need unbounded growth without wrap semantics.</li> </ul> <p>API snapshot</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar rb = new CyclicBuffer&lt;int&gt;(capacity: 4);\nrb.Add(10); rb.Add(20); rb.Add(30);\n\n// Pop from front/back\nif (rb.TryPopFront(out var first)) { /* first == 10 */ }\nif (rb.TryPopBack(out var last))  { /* last == 30  */ }\n\n// Remove by value / predicate\nrb.Add(40); rb.Add(50);\nrb.Remove(20);                  // O(n) compacting via temp buffer\nrb.RemoveAll(x =&gt; x % 2 == 0);  // remove evens\n\n// Resize in place (may drop tail elements)\nrb.Resize(8);\n</code></pre> <p>Tips and pitfalls</p> <ul> <li>Know your overflow policy. <code>Add</code> will wrap and overwrite the oldest only once capacity is reached; use <code>TryPopFront</code> periodically to keep buffer from evicting data you still need.</li> <li>Iteration enumerates logical order starting at the head, not the underlying storage order.</li> <li><code>Remove</code>/<code>RemoveAll</code> are O(n); keep hot paths to <code>Add</code>/<code>TryPop*</code> when possible.</li> </ul>"},{"location":"features/utilities/data-structures/#deque-double-ended-queue","title":"Deque (Double-Ended Queue)","text":"<ul> <li>What it is: Queue with efficient push/pop at both ends.</li> <li>Use for: Sliding windows, BFS frontiers, task schedulers.</li> <li>Operations: push_front/push_back/pop_front/pop_back in amortized O(1).</li> <li>Pros: Flexible ends; generalizes queue and stack behavior.</li> <li>Cons: Implementation complexity for block-based layouts.</li> </ul> <p>When to use vs <code>Queue&lt;T&gt;</code> / <code>Stack&lt;T&gt;</code></p> <ul> <li>Prefer <code>Deque&lt;T&gt;</code> when you need both <code>push_front</code> and <code>push_back</code> in O(1) amortized.</li> <li>Use <code>Queue&lt;T&gt;</code> for simple FIFO; <code>Stack&lt;T&gt;</code> for LIFO only.</li> </ul> <p></p> <p>API snapshot</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar dq = new Deque&lt;string&gt;(capacity: 8);\ndq.PushFront(\"start\");\ndq.PushBack(\"end\");\n\nif (dq.TryPopFront(out var a)) { /* a == \"start\" */ }\nif (dq.TryPopBack(out var b))  { /* b == \"end\"   */ }\n\n// Peeking without removal\ndq.PushBack(\"x\");\nif (dq.TryPeekFront(out var f)) { /* f == \"x\" */ }\n</code></pre> <p>Tips</p> <ul> <li>Capacity grows geometrically as needed; call <code>TrimExcess()</code> after spikes to return memory.</li> <li>Indexer is in logical order (0 is front, Count-1 is back).</li> </ul>"},{"location":"features/utilities/data-structures/#binary-heap-priority-queue","title":"Binary Heap (Priority Queue)","text":"<ul> <li>What it is: Array-backed binary tree maintaining heap-order (min/max).</li> <li>Use for: Priority queues, Dijkstra/A*, event simulation.</li> <li>Operations: push/pop in O(log n); peek O(1); build-heap O(n).</li> <li>Pros: Simple; great constant factors; contiguous memory.</li> <li>Cons: Not ideal for decrease-key unless augmented.</li> </ul> <p>When to use vs <code>SortedSet&lt;T&gt;</code></p> <ul> <li>Prefer <code>Heap&lt;T&gt;</code>/<code>PriorityQueue&lt;T&gt;</code> for frequent push/pop top in O(log n) with low overhead.</li> <li>Use <code>SortedSet&lt;T&gt;</code> for ordered iteration and fast remove arbitrary item (by key), at higher constants.</li> </ul> <p>API snapshot (Heap)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar minHeap = new Heap&lt;int&gt;();         // default comparer =&gt; min-heap\nminHeap.Add(5); minHeap.Add(2); minHeap.Add(9);\n\nif (minHeap.TryPop(out var top)) { /* top == 2 */ }\nif (minHeap.TryPeek(out var peek)) { /* peek == 5 */ }\n</code></pre> <p>API snapshot (PriorityQueue)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar pq = PriorityQueue&lt;(int priority, string job)&gt;.CreateMin(\n    capacity: 32\n);\npq.Enqueue((1, \"emergency\"));\npq.Enqueue((5, \"later\"));\npq.TryDequeue(out var item); // (1, \"emergency\")\n</code></pre> <p>Tips</p> <ul> <li>Use <code>PriorityQueue&lt;T&gt;.CreateMax()</code> to flip ordering without writing a custom comparer.</li> <li>Heaps don\u2019t support efficient decrease-key out of the box; reinsert updated items instead.</li> </ul>"},{"location":"features/utilities/data-structures/#disjoint-set-union-find","title":"Disjoint Set (Union-Find)","text":"<ul> <li>What it is: Structure tracking partition of elements into sets.</li> <li>Use for: Connectivity, Kruskal\u2019s MST, percolation, clustering.</li> <li>Operations: union/find in amortized near O(1) with path compression + union by rank.</li> <li>Pros: Extremely fast for bulk unions/finds; minimal memory.</li> <li>Cons: Not suited for deletions or enumerating members without extra indexes.</li> </ul> <p>When to use</p> <ul> <li>Batch connectivity queries where the graph mutates only via unions (no deletions): MST (Kruskal), island labeling, clustering, grouping by equivalence.</li> </ul> <p>API snapshot (int-based)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar uf = new DisjointSet(n: 6);        // elements 0..5\nuf.TryUnion(0, 1);\nuf.TryUnion(2, 3);\nuf.TryIsConnected(0, 3, out var conn); // false\nuf.TryUnion(1, 3);\nuf.TryIsConnected(0, 3, out conn);     // true\n</code></pre> <p>API snapshot (generic)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar people = new[] { \"Ana\", \"Bo\", \"Cy\" };\nvar uf = new DisjointSet&lt;string&gt;(people);\nuf.TryUnion(\"Ana\", \"Bo\");\nuf.TryIsConnected(\"Ana\", \"Cy\", out var conn); // false\n</code></pre> <p>Tips</p> <ul> <li>Use the generic variant to work with domain objects; internally it maps to indices.</li> <li>No deletions: rebuild if you need dynamic splits.</li> </ul>"},{"location":"features/utilities/data-structures/#sparse-set","title":"Sparse Set","text":"<ul> <li>What it is: Two arrays (sparse and dense) enabling O(1) membership checks and iteration over active items.</li> <li>Use for: ECS entity sets, fast presence checks with dense iteration.</li> <li>Operations: insert/remove/contains in O(1); iterate dense in O(n_active).</li> <li>Pros: Very fast, cache-friendly on dense array; stable indices optional.</li> <li>Cons: Requires ID space for indices; sparse array sized by max ID.</li> </ul> <p>When to use vs <code>HashSet&lt;T&gt;</code></p> <ul> <li>Prefer <code>SparseSet</code> when your IDs are small integers (0..N) and you need O(1) contains with dense, cache-friendly iteration over active items.</li> <li>Use <code>HashSet&lt;T&gt;</code> for arbitrary keys, very large/unbounded key spaces, or when memory for <code>sparse</code> cannot scale to the max ID.</li> </ul> <p>API snapshot (int IDs)</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar set = new SparseSet(capacity: 1000); // supports IDs in [0,1000)\nset.TryAdd(42);                   // returns bool indicating success\nbool has42 = set.Contains(42);    // true\nset.TryRemove(42);                // returns bool indicating success\n\n// Dense iteration order over active IDs\nforeach (int id in set) { /* ... */ }\n</code></pre> <p>API snapshot (generic values)</p> C#<pre><code>var set = new SparseSet&lt;MyComponent&gt;(capacity: 1024);\nset.TryAdd(100, new MyComponent()); // key 100 -&gt; value, returns bool\nvar comp = set[0];               // dense index 0 value\n</code></pre> <p>Tips</p> <ul> <li>Capacity equals the universe size for keys; do not set capacity larger than your maximum possible ID.</li> <li>Deletions swap-with-last in dense array; dense order is not stable.</li> </ul>"},{"location":"features/utilities/data-structures/#trie-prefix-tree","title":"Trie (Prefix Tree)","text":"<ul> <li>What it is: Tree keyed by characters for efficient prefix-based lookup.</li> <li>Use for: Autocomplete, spell-checking, dictionary prefix queries.</li> <li>Operations: insert/search O(m) where m is key length.</li> <li>Pros: Predictable per-character traversal; supports prefix enumeration.</li> <li>Cons: Memory overhead vs hash tables; compact with radix/compressed tries.</li> </ul> <p>When to use vs dictionaries</p> <ul> <li>Prefer <code>Trie</code> for lots of prefix queries and auto-complete where per-character traversal beats repeated hashing.</li> <li>Use <code>Dictionary&lt;string, T&gt;</code> when you rarely do prefix scans and primarily need exact lookup.</li> </ul> <p>API snapshot</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Words only\nvar words = new Trie(new[] { \"cat\", \"car\", \"dog\" });\nbool hasDog = words.Contains(\"dog\");          // true\nList&lt;string&gt; outWords = new();\nwords.GetWordsWithPrefix(\"ca\", outWords);     // outWords = [\"cat\",\"car\"]\n\n// Key -&gt; Value\nvar items = new Dictionary&lt;string, int&gt; { [\"apple\"] = 1, [\"apricot\"] = 2 };\nvar trie = new Trie&lt;int&gt;(items);\nif (trie.TryGetValue(\"apricot\", out var v)) { /* 2 */ }\nList&lt;int&gt; values = new();\ntrie.GetValuesWithPrefix(\"ap\", values);       // values = [1,2]\n</code></pre> <p>Tips</p> <ul> <li>Build once with full vocabulary; Tries here are immutable post-construction (no public insert) to stay compact.</li> <li>Memory scales with total characters; very large alphabets or long keys benefit from compressed/radix tries (not included here).</li> </ul>"},{"location":"features/utilities/data-structures/#bitset","title":"Bitset","text":"<ul> <li>What it is: Packed array of bits for boolean sets and flags.</li> <li>Use for: Fast membership bitmaps, masks, filters, small Bloom filters.</li> <li>Operations: set/clear/test O(1); bitwise ops on words are vectorizable.</li> <li>Pros: Extremely compact; very fast bitwise operations.</li> <li>Cons: Fixed maximum size unless dynamically extended; needs index mapping.</li> </ul> <p>When to use vs <code>bool[]</code> / <code>HashSet&lt;int&gt;</code></p> <ul> <li>Prefer <code>BitSet</code> for dense boolean sets with fast bitwise ops (masks, layers, filters) and compact storage.</li> <li>Use <code>bool[]</code> for tiny, fixed schemas you manipulate rarely; use <code>HashSet&lt;int&gt;</code> for sparse, very large universes.</li> </ul> <p>API snapshot</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nvar bits = new BitSet(initialCapacity: 128);\nbits[3] = true;                 // indexer calls TrySet/TryClear\nbits.TrySet(64);\nbool any = bits.Any();          // any bit set?\nint count = bits.CountSetBits();\n\n// Bitwise ops\nbits.Not();        // invert\nbits.LeftShift(2); // multiply-by-4 mask window\nbits.RightShift(1);\n</code></pre> <p>Tips</p> <ul> <li>Capacity grows automatically when setting beyond bounds; prefer sizing appropriately upfront for fewer resizes.</li> <li>Left/Right shift drop/zero-fill at the edges; use with care if capacity is small.</li> </ul>"},{"location":"features/utilities/data-structures/#quick-selection-guide","title":"Quick Selection Guide","text":"<ul> <li>Need O(1) membership and dense iteration: Sparse Set</li> <li>Need priority scheduling: Binary Heap</li> <li>Need two-ended queueing: Deque</li> <li>Need circular fixed-capacity buffer: Cyclic Buffer</li> <li>Need prefix search: Trie</li> <li>Need compact boolean set: Bitset</li> <li>Need dynamic connectivity: Disjoint Set</li> <li>Need auto-evicting key-value store: Cache</li> </ul> <p>Common pitfalls</p> <ul> <li>Sparse Set capacity equals the max key + 1; allocating for huge key spaces is memory-heavy.</li> <li>Heaps don\u2019t give you sorted iteration; popping yields order, but enumerating the heap array is not sorted.</li> <li>Cyclic Buffer <code>Remove</code>/<code>RemoveAll</code> are O(n); keep hot paths to TryPop/Add.</li> </ul>"},{"location":"features/utilities/data-structures/#complexity-summary","title":"Complexity Summary","text":"<ul> <li>Cyclic Buffer: enqueue/dequeue O(1)</li> <li>Deque: push/pop ends amortized O(1)</li> <li>Heap: push/pop O(log n), peek O(1)</li> <li>Disjoint Set: union/find ~O(1) amortized (with heuristics)</li> <li>Sparse Set: insert/remove/contains O(1), iterate dense O(n_active)</li> <li>Trie: insert/search O(m)</li> <li>Bitset: set/test O(1), bitwise ops O(n/word_size)</li> <li>Cache: get/set/remove O(1), expiration scan O(n)</li> </ul> <p>Notes on constants</p> <ul> <li>All structures are allocation-aware (enumerators avoid boxing; internal buffers reuse pools where applicable). Real-world throughput is often more important than asymptotic notation; these implementations are tuned for Unity/IL2CPP.</li> </ul>"},{"location":"features/utilities/data-structures/#cache-lrulfuslrufiforandom","title":"Cache (LRU/LFU/SLRU/FIFO/Random)","text":"<ul> <li>What it is: High-performance key-value cache with configurable eviction policies and time-based expiration.</li> <li>Use for: Memoization, asset lookups, network response caching, session data.</li> <li>Operations: get/set/remove in O(1); supports weighted entries, auto-loading, and statistics.</li> <li>Pros: Multiple eviction strategies; fluent builder API; jitter for thundering herd prevention.</li> <li>Cons: Memory overhead for tracking access patterns; requires configuration for optimal performance.</li> </ul> <pre><code>flowchart LR\n    subgraph Cache Operations\n        Get[Get] --&gt; Hit{Hit?}\n        Hit --&gt;|Yes| Return[Return Value]\n        Hit --&gt;|No| Load[Load/Miss]\n        Set[Set] --&gt; Evict{At Capacity?}\n        Evict --&gt;|Yes| Policy[Apply Eviction Policy]\n        Evict --&gt;|No| Store[Store Entry]\n        Policy --&gt; Store\n    end</code></pre>"},{"location":"features/utilities/data-structures/#when-to-use","title":"When to use","text":"<ul> <li>Use <code>Cache&lt;TKey, TValue&gt;</code> when you need automatic eviction, expiration, or access tracking.</li> <li>Use <code>Dictionary&lt;TKey, TValue&gt;</code> for simple lookups without size limits or expiration.</li> <li>Use <code>ConcurrentDictionary&lt;TKey, TValue&gt;</code> for thread-safe access without cache semantics.</li> </ul>"},{"location":"features/utilities/data-structures/#eviction-policies","title":"Eviction Policies","text":"Policy Description Best For LRU Evicts least recently used entry General purpose, most common SLRU Segmented LRU with probation/protected segments High-throughput with scan resistance LFU Evicts least frequently used entry Stable access patterns FIFO First in, first out eviction Render caches, predictable eviction Random Random eviction Low overhead, uniform access"},{"location":"features/utilities/data-structures/#api-snapshot-basic-usage","title":"API snapshot (Basic usage)","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Simple LRU cache with fluent builder\nCache&lt;string, UserData&gt; cache = CacheBuilder&lt;string, UserData&gt;.NewBuilder()\n    .MaximumSize(1000)\n    .ExpireAfterWrite(TimeSpan.FromMinutes(5))\n    .EvictionPolicy(EvictionPolicy.Lru)\n    .Build();\n\n// Basic operations\ncache.Set(\"user1\", userData);\nif (cache.TryGet(\"user1\", out UserData data))\n{\n    // Use cached data\n}\n\ncache.TryRemove(\"user1\");\ncache.Clear();\n</code></pre>"},{"location":"features/utilities/data-structures/#api-snapshot-loading-cache-with-auto-compute","title":"API snapshot (Loading cache with auto-compute)","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Loading cache - auto-computes missing values\nCache&lt;int, ExpensiveResult&gt; loadingCache = CacheBuilder&lt;int, ExpensiveResult&gt;.NewBuilder()\n    .MaximumSize(100)\n    .Build(key =&gt; ComputeExpensiveResult(key));\n\n// GetOrAdd uses the loader when key is missing\nExpensiveResult result = loadingCache.GetOrAdd(42, null);\n</code></pre>"},{"location":"features/utilities/data-structures/#api-snapshot-advanced-configuration","title":"API snapshot (Advanced configuration)","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\nCache&lt;string, PathResult&gt; pathCache = CacheBuilder&lt;string, PathResult&gt;.NewBuilder()\n    .MaximumSize(2000)\n    .InitialCapacity(64)                    // Start small, grow as needed\n    .ExpireAfterWrite(300f)                 // 5 minutes TTL\n    .WithJitter(12f)                        // Prevent thundering herd\n    .EvictionPolicy(EvictionPolicy.Slru)    // Scan-resistant eviction\n    .ProtectedRatio(0.8f)                   // 80% protected segment for SLRU\n    .AllowGrowth(1.5f, 4000)                // Grow 1.5x up to 4000 when thrashing\n    .RecordStatistics()                     // Enable hit/miss tracking\n    .OnEviction((key, value, reason) =&gt; Debug.Log($\"Evicted {key}: {reason}\"))\n    .OnGet((key, value) =&gt; Debug.Log($\"Cache hit: {key}\"))\n    .OnSet((key, value) =&gt; Debug.Log($\"Cache set: {key}\"))\n    .Build();\n\n// Access statistics\nCacheStatistics stats = pathCache.GetStatistics();\nDebug.Log($\"Hit rate: {stats.HitRate:P1}, Evictions: {stats.EvictionCount}\");\n</code></pre>"},{"location":"features/utilities/data-structures/#api-snapshot-weighted-caching","title":"API snapshot (Weighted caching)","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Weight-based eviction (e.g., by byte size)\nCache&lt;string, Texture2D&gt; textureCache = CacheBuilder&lt;string, Texture2D&gt;.NewBuilder()\n    .MaximumWeight(100_000_000)  // 100 MB total\n    .Weigher((key, tex) =&gt; tex.width * tex.height * 4)  // Bytes per texture\n    .ExpireAfterAccess(TimeSpan.FromMinutes(10))        // Sliding window\n    .Build();\n</code></pre>"},{"location":"features/utilities/data-structures/#cachepresets-ready-to-use-configurations","title":"CachePresets (Ready-to-use configurations)","text":"<p>Use <code>CachePresets</code> for common scenarios:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\n\n// Short-lived cache: 100 entries, 60s TTL, LRU\nCache&lt;int, Vector3&gt; tempCache = CachePresets.ShortLived&lt;int, Vector3&gt;().Build();\n\n// Long-lived cache: 1000 entries, no TTL, LRU\nCache&lt;string, GameObject&gt; prefabCache = CachePresets.LongLived&lt;string, GameObject&gt;()\n    .Build(key =&gt; Resources.Load&lt;GameObject&gt;($\"Prefabs/{key}\"));\n\n// Session cache: 500 entries, 30 min sliding window, LRU\nCache&lt;string, InventoryData&gt; sessionCache = CachePresets.SessionCache&lt;string, InventoryData&gt;().Build();\n\n// High-throughput: 2000 entries, 5 min TTL, SLRU, growth enabled\nCache&lt;(Vector3, Vector3), NavMeshPath&gt; pathCache = CachePresets.HighThroughput&lt;(Vector3, Vector3), NavMeshPath&gt;().Build();\n\n// Render cache: 200 entries, 30s TTL, FIFO\nCache&lt;int, MaterialPropertyBlock&gt; renderCache = CachePresets.RenderCache&lt;int, MaterialPropertyBlock&gt;().Build();\n\n// Network cache: 100 entries, 2 min TTL with jitter, LRU\nCache&lt;string, JsonResponse&gt; apiCache = CachePresets.NetworkCache&lt;string, JsonResponse&gt;().Build();\n</code></pre>"},{"location":"features/utilities/data-structures/#cache-properties-and-methods","title":"Cache Properties and Methods","text":"Member Description <code>Count</code> Current number of entries <code>Size</code> Total weight (weighted caching) or count <code>MaximumSize</code> Configured maximum entries <code>Capacity</code> Current internal capacity (may be &lt; MaximumSize) <code>Keys</code> Enumerable of all keys (allocates state machine) <code>TryGet(key, out value)</code> Returns true if key exists and not expired <code>Set(key, value)</code> Adds or updates an entry <code>GetOrAdd(key, factory)</code> Gets existing or computes and caches new value (factory optional with loader) <code>TryRemove(key)</code> Removes entry if present, returns bool <code>TryRemove(key, out value)</code> Removes entry and returns the removed value <code>ContainsKey(key)</code> Checks if key exists <code>Clear()</code> Removes all entries <code>CleanUp()</code> Forces expiration scan <code>Compact(ratio)</code> Evicts percentage of entries <code>Resize(newSize)</code> Changes maximum size <code>GetStatistics()</code> Returns hit/miss/eviction stats (if enabled) <code>GetAll(keys, dict)</code> Batch get into provided dictionary <code>SetAll(items)</code> Batch set from collection <code>GetKeys(list)</code> Zero-allocation key enumeration into provided list"},{"location":"features/utilities/data-structures/#tips-and-pitfalls","title":"Tips and pitfalls","text":"<ul> <li>Choose the right preset: <code>CachePresets</code> provides optimized defaults for common scenarios.</li> <li>Enable statistics sparingly: Recording stats adds overhead; enable only when debugging.</li> <li>Use jitter for network caches: Prevents thundering herd when many entries expire together.</li> <li>Consider SLRU for high-throughput: Better scan resistance than plain LRU.</li> <li>Watch InitialCapacity: The cache clamps initial capacity to prevent OutOfMemoryException. Don't set it larger than needed.</li> <li>Weighted caches: Use <code>MaximumWeight</code> + <code>Weigher</code> for size-based eviction (e.g., texture bytes).</li> <li>Callbacks are synchronous: <code>OnEviction</code>, <code>OnGet</code>, <code>OnSet</code> run on the calling thread.</li> </ul>"},{"location":"features/utilities/helper-utilities/","title":"Helper Utilities Guide","text":""},{"location":"features/utilities/helper-utilities/#tldr-why-use-these","title":"TL;DR \u2014 Why Use These","text":"<p>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.</p>"},{"location":"features/utilities/helper-utilities/#contents","title":"Contents","text":"<ul> <li>Gameplay Helpers \u2014 Predictive aiming, spatial sampling, rotation</li> <li>GameObject &amp; Component Helpers \u2014 Component discovery, hierarchy manipulation</li> <li>Transform Helpers \u2014 Hierarchy traversal</li> <li>Coroutine Wait Pools \u2014 Configure <code>Buffers.GetWaitForSeconds*</code> caching</li> <li>Threading \u2014 Main thread dispatcher</li> <li>Path &amp; File Helpers \u2014 Path resolution, file operations</li> <li>Scene Helpers \u2014 Scene queries and loading</li> <li>Advanced Utilities \u2014 Null checks, hashing, formatting</li> <li>Environment Detection \u2014 CI, batch mode, and runtime environment</li> </ul>"},{"location":"features/utilities/helper-utilities/#coroutine-wait-pools","title":"Coroutine Wait Pools","text":"<p>Unity allocates a new <code>WaitForSeconds</code>/<code>WaitForSecondsRealtime</code> every time you yield with a literal. <code>Buffers.GetWaitForSeconds(...)</code> and <code>Buffers.GetWaitForSecondsRealTime(...)</code> 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.</p> <p>New pooling policy knobs (Runtime 2.2.1+):</p> Setting Default Purpose <code>Buffers.WaitInstructionMaxDistinctEntries</code> <code>512</code> Upper bound on distinct cached durations. Set to <code>0</code> 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). <code>Buffers.WaitInstructionQuantizationStepSeconds</code> <code>0</code> (off) Rounds requested durations to the nearest step before caching. Useful when you can tolerate millisecond snapping (e.g., <code>.005f</code> \u2192 <code>.01f</code>). <code>Buffers.WaitInstructionUseLruEviction</code> <code>false</code> 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. <code>Buffers.TryGetWaitForSecondsPooled(float seconds)</code> / <code>TryGetWaitForSecondsRealtimePooled</code> n/a Returns the cached instruction or <code>null</code> if the request would exceed the cap. Use this when you want to detect \u201cunsafe\u201d usages and allocate manually instead. <code>Buffers.WaitForSecondsCacheDiagnostics</code> / <code>.WaitForSecondsRealtimeCacheDiagnostics</code> snapshot Exposes <code>DistinctEntries</code>, <code>MaxDistinctEntries</code>, <code>LimitRefusals</code>, and whether quantization is active so you can surface metrics in your own tooling. <p>\u2699\ufe0f Project-wide defaults: Open the Coroutine Wait Instruction Buffers foldout under Project Settings \u25b8 Wallstop Studios \u25b8 Unity Helpers to edit these knobs. The settings asset lives at <code>Resources/Wallstop Studios/Unity Helpers/UnityHelpersBufferSettings.asset</code>, 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 <code>Buffers</code> is using in play mode.</p> <p>\ud83d\udd12 Persistence Behavior: When you click Apply Defaults Now, the settings are immediately:</p> <ol> <li>Saved to disk \u2014 The asset is marked dirty and saved via <code>AssetDatabase.SaveAssets()</code></li> <li>Applied to the runtime \u2014 <code>Buffers.WaitInstruction*</code> properties are updated immediately</li> </ol> <p>This ensures settings persist across:</p> <ul> <li>Domain reloads (script recompilation, entering/exiting play mode) \u2014 Via <code>[InitializeOnLoadMethod]</code></li> <li>Editor restarts \u2014 The asset is saved to disk and reloads automatically</li> <li>Standalone builds \u2014 The asset ships under <code>Resources/</code> and auto-applies via <code>[RuntimeInitializeOnLoadMethod]</code></li> </ul> <p>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 <code>asset.ApplyToBuffers()</code> manually.</p> C#<pre><code>// Clamp the cache to 128 distinct waits, quantize to milliseconds, and reuse LRU entries.\nBuffers.WaitInstructionMaxDistinctEntries = 128;\nBuffers.WaitInstructionQuantizationStepSeconds = 0.001f;\nBuffers.WaitInstructionUseLruEviction = true;\n\nIEnumerator WeaponCooldown(Func&lt;float&gt; cooldownSeconds)\n{\n    float waitSeconds = cooldownSeconds();\n\n    // Prefer pooled waits, but fall back to a fresh instance if the cache refuses it.\n    WaitForSeconds pooled = Buffers.TryGetWaitForSecondsPooled(waitSeconds)\n        ?? new WaitForSeconds(waitSeconds);\n\n    yield return pooled;\n}\n\nvoid OnGUI()\n{\n    WaitInstructionCacheDiagnostics stats = Buffers.WaitForSecondsCacheDiagnostics;\n    GUILayout.Label(\n        $\"Wait cache: {stats.DistinctEntries}/{stats.MaxDistinctEntries} (refusals={stats.LimitRefusals}, evictions={stats.Evictions})\"\n    );\n}\n</code></pre> <p>\u26a0\ufe0f Limit warnings: In Editor and Development builds the first limit hit (and every 25<sup>th</sup> after) emits a warning so you can spot misuses quickly. Production builds skip the log to avoid noise.</p> <p>\u2705 Deterministic fallback: When the cache refuses a duration, <code>Buffers.GetWaitForSeconds*</code> still returns a valid instruction\u2014it just isn\u2019t cached, so highly variable waits no longer lead to unbounded memory growth.</p> <p></p>"},{"location":"features/utilities/helper-utilities/#gameplay-helpers","title":"Gameplay Helpers","text":""},{"location":"features/utilities/helper-utilities/#predictive-aiming","title":"Predictive Aiming","text":"<p>What it does: Calculates where to aim when shooting at a moving target, accounting for projectile travel time.</p> <p>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.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nVector2 enemyPos = enemy.transform.position;\nVector2 enemyVelocity = enemy.GetComponent&lt;Rigidbody2D&gt;().velocity;\nVector2 turretPos = turret.transform.position;\nfloat bulletSpeed = 20f;\n\nVector2? aimPosition = Helpers.PredictCurrentTarget(\n    enemyPos,\n    enemyVelocity,\n    turretPos,\n    bulletSpeed\n);\n\nif (aimPosition.HasValue)\n{\n    // Aim at aimPosition to hit the moving target\n    Vector2 aimDirection = (aimPosition.Value - turretPos).normalized;\n    FireProjectile(aimDirection, bulletSpeed);\n}\nelse\n{\n    // Target is too fast, can't hit\n}\n</code></pre> <p>When to use:</p> <ul> <li>Turrets shooting at moving enemies</li> <li>AI aiming at moving players</li> <li>Predictive targeting systems</li> <li>Guided missiles</li> </ul> <p>When NOT to use:</p> <ul> <li>Homing projectiles (use steering behaviors)</li> <li>Instant-hit weapons (use raycasts)</li> <li>Slow-moving or stationary targets (just aim directly)</li> </ul>"},{"location":"features/utilities/helper-utilities/#spatial-sampling","title":"Spatial Sampling","text":"<p>Get random points in circles/spheres:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Random point inside circle (uniform distribution)\nVector2 spawnPoint = Helpers.GetRandomPointInCircle(center, radius);\n\n// Random point inside sphere (uniform distribution)\nVector3 explosionPoint = Helpers.GetRandomPointInSphere(center, radius);\n</code></pre> <p>Use for:</p> <ul> <li>Spawn points (enemies, pickups, particles)</li> <li>Explosion damage distribution</li> <li>Random movement destinations</li> <li>Scatter patterns</li> </ul>"},{"location":"features/utilities/helper-utilities/#smooth-rotation-helpers","title":"Smooth Rotation Helpers","text":"<p>Get rotation speed for smooth turning:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Calculate how much to rotate this frame toward target\nfloat currentAngle = transform.eulerAngles.z;\nfloat targetAngle = GetTargetAngle();\nfloat maxDegreesPerSecond = 180f;\n\nfloat newAngle = Helpers.GetAngleWithSpeed(\n    currentAngle,\n    targetAngle,\n    maxDegreesPerSecond,\n    Time.deltaTime\n);\n\ntransform.eulerAngles = new Vector3(0, 0, newAngle);\n</code></pre> <p>Handles:</p> <ul> <li>Frame-rate independence</li> <li>Shortest rotation path (doesn't spin 270\u00b0 when 90\u00b0 is shorter)</li> <li>Angle wrapping (0-360\u00b0)</li> </ul>"},{"location":"features/utilities/helper-utilities/#delayed-execution","title":"Delayed Execution","text":"<p>Execute code after delay or next frame:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Execute after 2 seconds\nHelpers.ExecuteFunctionAfterDelay(\n    monoBehaviour,\n    () =&gt; Debug.Log(\"Delayed!\"),\n    delayInSeconds: 2f\n);\n\n// Execute next frame\nHelpers.ExecuteFunctionNextFrame(\n    monoBehaviour,\n    () =&gt; Debug.Log(\"Next frame!\")\n);\n</code></pre> <p>Uses coroutines under the hood.</p>"},{"location":"features/utilities/helper-utilities/#repeating-execution-with-jitter","title":"Repeating Execution with Jitter","text":"<p>Run function repeatedly with random timing variance:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Spawn enemy every 5-8 seconds\nHelpers.StartFunctionAsCoroutine(\n    gameManager,\n    SpawnEnemy,\n    baseInterval: 5f,\n    intervalJitter: 3f  // Random \u00b13 seconds\n);\n\nvoid SpawnEnemy()\n{\n    Instantiate(enemyPrefab, spawnPoint.position, Quaternion.identity);\n}\n</code></pre> <p>Use for:</p> <ul> <li>Enemy spawning with variability</li> <li>Random event triggers</li> <li>Staggered updates to spread CPU load</li> <li>Natural-feeling timing</li> </ul>"},{"location":"features/utilities/helper-utilities/#layer-label-queries","title":"Layer &amp; Label Queries","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Get all layer names (cached after first call)\nstring[] allLayers = Helpers.GetAllLayerNames();\n\n// Get all sprite label names (editor only, cached)\nstring[] labels = Helpers.GetAllSpriteLabelNames();\n</code></pre> <p>Use for:</p> <ul> <li>Populating dropdowns in editor tools</li> <li>Runtime layer/label validation</li> <li>Configuration systems</li> </ul>"},{"location":"features/utilities/helper-utilities/#collider-syncing","title":"Collider Syncing","text":"<p>Update PolygonCollider2D to match sprite:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nSpriteRenderer renderer = GetComponent&lt;SpriteRenderer&gt;();\nPolygonCollider2D collider = GetComponent&lt;PolygonCollider2D&gt;();\n\nHelpers.UpdateShapeToSprite(renderer, collider);\n// Collider now matches sprite's physics shape\n</code></pre> <p></p>"},{"location":"features/utilities/helper-utilities/#gameobject-component-helpers","title":"GameObject &amp; Component Helpers","text":""},{"location":"features/utilities/helper-utilities/#cached-component-lookup","title":"Cached Component Lookup","text":"<p>Tag-based component finding with caching:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// First call searches scene, subsequent calls use cache\nPlayer player = Helpers.Find&lt;Player&gt;(\"Player\");\n\n// Clear cache manually if needed\nHelpers.ClearInstance&lt;Player&gt;();\n\n// Set cache manually (for dependency injection scenarios)\nHelpers.SetInstance(playerInstance);\n</code></pre> <p>Performance: First call searches the scene using GameObject.FindWithTag; subsequent calls use a cached O(1) dictionary lookup. The cache persists until manually cleared.</p>"},{"location":"features/utilities/helper-utilities/#component-existence-checks","title":"Component Existence Checks","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Check if component exists without allocating\nbool hasRigidbody = Helpers.HasComponent&lt;Rigidbody2D&gt;(gameObject);\n\n// Better than:\nbool hasRigidbody = GetComponent&lt;Rigidbody2D&gt;() != null; // Allocates\n</code></pre>"},{"location":"features/utilities/helper-utilities/#get-or-add-pattern","title":"Get-or-Add Pattern","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Get existing component or add if missing\nRigidbody2D rb = Helpers.GetOrAddComponent&lt;Rigidbody2D&gt;(gameObject);\n</code></pre>"},{"location":"features/utilities/helper-utilities/#hierarchical-enabledisable","title":"Hierarchical Enable/Disable","text":"<p>Recursively enable/disable components:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Enable all Collider2D components in children\nHelpers.EnableRecursively&lt;Collider2D&gt;(rootObject, enable: true);\n\n// Disable all renderers in hierarchy\nHelpers.EnableRendererRecursively&lt;SpriteRenderer&gt;(rootObject, enable: false);\n</code></pre> <p>Use for:</p> <ul> <li>Toggling collision for entire character rigs</li> <li>Hiding/showing complex prefabs</li> <li>Debug visualization toggles</li> </ul>"},{"location":"features/utilities/helper-utilities/#bulk-child-destruction","title":"Bulk Child Destruction","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Destroy all children (useful for clearing containers)\nHelpers.DestroyAllChildrenGameObjects(parentTransform);\n</code></pre> <p>Use for:</p> <ul> <li>Clearing inventory UI</li> <li>Resetting spawn containers</li> <li>Cleanup before repopulating</li> </ul>"},{"location":"features/utilities/helper-utilities/#smart-destruction","title":"Smart Destruction","text":"<p>Editor/runtime aware destruction:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Uses DestroyImmediate in editor, Destroy in play mode\nHelpers.SmartDestroy(gameObject);\n\n// Also handles assets correctly (won't destroy project assets)\n</code></pre> <p>Use in editor tools to avoid \"Destroying assets is not permitted\" errors.</p>"},{"location":"features/utilities/helper-utilities/#prefab-utilities","title":"Prefab Utilities","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Check if GameObject is a prefab asset or instance\nbool isPrefab = Helpers.IsPrefab(gameObject);\n\n// Safely modify prefab (editor only)\n#if UNITY_EDITOR\nHelpers.ModifyAndSavePrefab(prefabAssetPath, prefab =&gt;\n{\n    // Modify prefab here\n    var component = prefab.AddComponent&lt;MyComponent&gt;();\n    component.value = 42;\n    // Changes saved automatically\n});\n#endif\n</code></pre>"},{"location":"features/utilities/helper-utilities/#transform-helpers","title":"Transform Helpers","text":""},{"location":"features/utilities/helper-utilities/#hierarchy-traversal-depth-first","title":"Hierarchy Traversal (Depth-First)","text":"<p>Visit all children recursively:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Depth-first traversal (visits deepest children first)\nHelpers.IterateOverAllChildrenRecursively&lt;SpriteRenderer&gt;(rootTransform, renderer =&gt;\n{\n    renderer.color = Color.red;\n});\n\n// Buffered version (reduces allocations)\nusing (var buffer = Buffers&lt;Transform&gt;.List.Get())\n{\n    Helpers.IterateOverAllChildrenRecursively(rootTransform, buffer.Value);\n    foreach (Transform child in buffer.Value)\n    {\n        // Process children\n    }\n}\n</code></pre>"},{"location":"features/utilities/helper-utilities/#hierarchy-traversal-breadth-first","title":"Hierarchy Traversal (Breadth-First)","text":"<p>Visit by depth level:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Breadth-first traversal with depth limit\nHelpers.IterateOverAllChildrenRecursivelyBreadthFirst(\n    rootTransform,\n    transform =&gt; Debug.Log(transform.name),\n    maxDepth: 3  // Only visit 3 levels deep\n);\n</code></pre> <p>Use for:</p> <ul> <li>Finding immediate area (not entire tree)</li> <li>Level-based operations</li> <li>Performance-sensitive searches</li> </ul>"},{"location":"features/utilities/helper-utilities/#parent-traversal","title":"Parent Traversal","text":"<p>Walk up the hierarchy:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Find component in parents\nHelpers.IterateOverAllParentComponentsRecursively&lt;Canvas&gt;(transform, canvas =&gt;\n{\n    Debug.Log($\"Found canvas: {canvas.name}\");\n});\n\n// Get all parents (no component filter)\nusing (var buffer = Buffers&lt;Transform&gt;.List.Get())\n{\n    Helpers.IterateOverAllParents(transform, buffer.Value);\n    // buffer contains all parent transforms up to root\n}\n</code></pre> <p>Use for:</p> <ul> <li>Finding UI Canvas parents</li> <li>Inheritance checking (is this under X?)</li> <li>Walking to root of hierarchy</li> </ul>"},{"location":"features/utilities/helper-utilities/#direct-childrenparents","title":"Direct Children/Parents","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Get immediate children (non-recursive)\nusing (var buffer = Buffers&lt;Transform&gt;.List.Get())\n{\n    Helpers.IterateOverAllChildren(transform, buffer.Value);\n    // Only direct children, no grandchildren\n}\n</code></pre>"},{"location":"features/utilities/helper-utilities/#threading","title":"Threading","text":""},{"location":"features/utilities/helper-utilities/#unitymainthreaddispatcher","title":"UnityMainThreadDispatcher","text":"<p>Execute code on Unity's main thread from background threads:</p> <p>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.</p> <p>See the dedicated Unity Main Thread Dispatcher guide for details about auto-creation, queue limits, the <code>AutoCreationScope</code> helper, and the <code>CreateTestScope(...)</code> convenience method that packages can use in their own test fixtures.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\nusing System.Threading.Tasks;\n\nasync Task LoadDataInBackground()\n{\n    // Background thread work\n    await Task.Run(() =&gt;\n    {\n        // Expensive computation\n        var data = LoadFromDatabase();\n\n        // Need to update UI - marshal back to main thread\n        UnityMainThreadDispatcher.Instance.RunOnMainThread(() =&gt;\n        {\n            // Safe to call Unity APIs here\n            uiText.text = data.ToString();\n        });\n    });\n}\n</code></pre> <p>Async version with result:</p> C#<pre><code>async Task&lt;string&gt; GetTextFromMainThread()\n{\n    // Called from background thread, executes on main thread\n    string text = await UnityMainThreadDispatcher.Instance.Post(() =&gt;\n    {\n        return uiText.text; // Safe to access Unity objects\n    });\n\n    return text;\n}\n</code></pre>"},{"location":"features/utilities/helper-utilities/#logging","title":"Logging","text":"<p>Use the Logging Extensions guide for:</p> <ul> <li>Rich text tags applied directly inside interpolated strings (<code>$\"{value:b,color=red}\"</code>)</li> <li>Thread-aware logging helpers (<code>this.Log</code>, <code>this.LogWarn</code>, <code>this.LogError</code>, <code>this.LogDebug</code>)</li> <li>Tips for registering custom decorations and gating logs per-object or globally</li> </ul> <p>These helpers rely on the same dispatcher utilities above, so logging from jobs/background threads stays safe.</p> <p>Fire-and-forget on main thread:</p> C#<pre><code>// From background thread\nUnityMainThreadDispatcher.Instance.RunOnMainThread(() =&gt;\n{\n    Instantiate(prefab, position, rotation);\n});\n</code></pre> <p>When to use:</p> <ul> <li>Async file loading callbacks</li> <li>Network request callbacks</li> <li>Database query results</li> <li>Background computation results that update UI</li> </ul> <p>Important:</p> <ul> <li>Works in both edit mode and play mode</li> <li>Actions queued during edit mode execute in next editor update</li> <li>Don't block the main thread with long operations</li> </ul> <p></p>"},{"location":"features/utilities/helper-utilities/#path-file-helpers","title":"Path &amp; File Helpers","text":""},{"location":"features/utilities/helper-utilities/#path-sanitization","title":"Path Sanitization","text":"<p>Normalize path separators:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nstring windowsPath = @\"Assets\\Sprites\\Player.png\";\nstring unityPath = PathHelper.Sanitize(windowsPath);\n// Result: \"Assets/Sprites/Player.png\"\n</code></pre> <p>Unity prefers forward slashes. Use this for cross-platform paths.</p>"},{"location":"features/utilities/helper-utilities/#directory-utilities","title":"Directory Utilities","text":"<p>Create directories safely:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n#if UNITY_EDITOR\n// Creates directory and updates AssetDatabase\nDirectoryHelper.EnsureDirectoryExists(\"Assets/Generated/Data\");\n#endif\n</code></pre> <p>Find package root:</p> C#<pre><code>// Walk hierarchy to find package.json\nstring packageRoot = DirectoryHelper.FindPackageRootPath();\n// Returns path to package containing calling script\n</code></pre> <p>Use for:</p> <ul> <li>Editor tools generating assets</li> <li>Finding package-relative paths</li> <li>Build scripts creating folders</li> </ul>"},{"location":"features/utilities/helper-utilities/#path-conversion","title":"Path Conversion","text":"<p>Convert between absolute and Unity-relative paths:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nstring absolute = \"C:/Projects/MyGame/Assets/Textures/player.png\";\nstring relative = DirectoryHelper.AbsoluteToUnityRelativePath(absolute);\n// Result: \"Assets/Textures/player.png\"\n</code></pre> <p>Get calling script's directory:</p> C#<pre><code>// Uses [CallerFilePath] magic\nstring scriptDir = DirectoryHelper.GetCallerScriptDirectory();\n// Returns directory containing the calling .cs file\n</code></pre>"},{"location":"features/utilities/helper-utilities/#file-operations","title":"File Operations","text":"<p>Initialize file if missing:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Create config.json with default contents if it doesn't exist\nFileHelper.InitializePath(\n    \"Assets/config.json\",\n    \"{ \\\"version\\\": 1 }\"\n);\n</code></pre> <p>Async file copy:</p> C#<pre><code>using System.Threading;\n\nCancellationTokenSource cts = new CancellationTokenSource();\n\nawait FileHelper.CopyFileAsync(\n    \"source.txt\",\n    \"destination.txt\",\n    bufferSize: 81920,  // 80KB buffer\n    cts.Token\n);\n</code></pre> <p>Use for:</p> <ul> <li>Large file operations without blocking</li> <li>Cancellable copy operations</li> <li>Streaming file operations</li> </ul> <p></p>"},{"location":"features/utilities/helper-utilities/#scene-helpers","title":"Scene Helpers","text":""},{"location":"features/utilities/helper-utilities/#scene-queries","title":"Scene Queries","text":"<p>Check if scene is loaded:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nbool loaded = SceneHelper.IsSceneLoaded(\"GameLevel\");\n// Checks by scene name or path\n</code></pre> <p>Get all scene paths (editor):</p> C#<pre><code>#if UNITY_EDITOR\nstring[] allScenes = SceneHelper.GetAllScenePaths();\n// Returns all .unity files in project\n\nstring[] buildScenes = SceneHelper.GetScenesInBuild();\n// Returns only scenes in Build Settings\n#endif\n</code></pre>"},{"location":"features/utilities/helper-utilities/#temporary-scene-loading","title":"Temporary Scene Loading","text":"<p>Load scene, extract data, auto-unload:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// RAII pattern - scene unloaded when disposed\nusing (var scope = SceneHelper.GetObjectOfTypeInScene&lt;LevelConfig&gt;(\"Scenes/LevelData\"))\n{\n    if (scope.HasObject)\n    {\n        LevelConfig config = scope.Object;\n        // Use config data\n    }\n    // Scene automatically unloaded here\n}\n</code></pre> <p>Use for:</p> <ul> <li>Extracting data from data-only scenes</li> <li>Editor tools reading scene contents</li> <li>Validation scripts</li> <li>Testing scene contents</li> </ul> <p></p>"},{"location":"features/utilities/helper-utilities/#advanced-utilities","title":"Advanced Utilities","text":""},{"location":"features/utilities/helper-utilities/#unity-aware-null-checks","title":"Unity-Aware Null Checks","text":"<p>The problem: Unity's <code>==</code> operator overload can be slow, and destroyed UnityEngine.Objects return <code>true</code> for <code>== null</code> but <code>false</code> for <code>is null</code>.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nGameObject obj = GetMaybeDestroyedObject();\n\n// Proper Unity null check\nbool isNull = Objects.Null(obj);\nbool notNull = Objects.NotNull(obj);\n</code></pre> <p>Handles:</p> <ul> <li>Destroyed UnityEngine.Objects</li> <li>Actual null references</li> <li>Optimized checks for non-Unity types</li> </ul>"},{"location":"features/utilities/helper-utilities/#deterministic-hashing","title":"Deterministic Hashing","text":"<p>Combine hash codes correctly:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\npublic class CompositeKey\n{\n    public string Name;\n    public int Level;\n    public Vector2 Position;\n\n    public override int GetHashCode()\n    {\n        // FNV-1a based hash combination\n        return Objects.HashCode(Name, Level, Position);\n    }\n}\n</code></pre> <p>Supports up to 11 parameters. Uses FNV-1a algorithm for good distribution.</p> <p>Hash entire collections:</p> C#<pre><code>List&lt;int&gt; numbers = new List&lt;int&gt; { 1, 2, 3, 4, 5 };\nint hash = Objects.EnumerableHashCode(numbers);\n</code></pre> <p>Use for:</p> <ul> <li>Custom GetHashCode implementations</li> <li>Dictionary keys with multiple fields</li> <li>Networking determinism</li> <li>Save file hashing</li> </ul>"},{"location":"features/utilities/helper-utilities/#formatting","title":"Formatting","text":"<p>Human-readable byte counts:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nlong bytes = 1536000;\nstring formatted = FormattingHelpers.FormatBytes(bytes);\n// Result: \"1.46 MB\"\n</code></pre> <p>Auto-scales to B, KB, MB, GB, TB.</p> <p>Use for:</p> <ul> <li>File size displays</li> <li>Memory usage UI</li> <li>Profiling output</li> <li>Download progress</li> </ul>"},{"location":"features/utilities/helper-utilities/#multi-dimensional-array-iteration","title":"Multi-Dimensional Array Iteration","text":"<p>Enumerate 2D/3D array indices:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nint[,] grid = new int[10, 10];\n\n// Get all indices as tuples\nforeach (var (x, y) in IterationHelpers.IndexOver(grid))\n{\n    grid[x, y] = x + y;\n}\n\n// Buffered (reduces allocations)\nusing (var buffer = Buffers&lt;(int, int)&gt;.List.Get())\n{\n    IterationHelpers.IndexOver(grid, buffer.Value);\n    foreach (var (x, y) in buffer.Value)\n    {\n        // Process\n    }\n}\n</code></pre> <p>Also supports 3D arrays with <code>(int, int, int)</code> tuples.</p>"},{"location":"features/utilities/helper-utilities/#binary-array-conversion","title":"Binary Array Conversion","text":"<p>Marshalling between int[] and byte[]:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nint[] ints = { 1, 2, 3, 4, 5 };\n\n// Convert to bytes (uses Buffer.BlockCopy)\nbyte[] bytes = ArrayConverter.IntArrayToByteArrayBlockCopy(ints);\n\n// Convert back\nint[] restored = ArrayConverter.ByteArrayToIntArrayBlockCopy(bytes);\n</code></pre> <p>Use for:</p> <ul> <li>Network serialization</li> <li>Binary file formats</li> <li>Save game data</li> <li>High-performance data conversion</li> </ul> <p>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).</p>"},{"location":"features/utilities/helper-utilities/#custom-comparers","title":"Custom Comparers","text":"<p>Create IComparer from lambda:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nvar enemies = new List&lt;Enemy&gt;();\n\n// Sort by health descending\nenemies.Sort(new FuncBasedComparer&lt;Enemy&gt;((a, b) =&gt;\n    b.health.CompareTo(a.health) // Descending\n));\n</code></pre> <p>Reverse any comparer:</p> C#<pre><code>var comparer = Comparer&lt;int&gt;.Default;\nvar reversed = new ReverseComparer&lt;int&gt;(comparer);\n\n// Now sorts descending\nlist.Sort(reversed);\n</code></pre> <p></p>"},{"location":"features/utilities/helper-utilities/#environment-detection","title":"Environment Detection","text":""},{"location":"features/utilities/helper-utilities/#cicd-detection","title":"CI/CD Detection","text":"<p>Detect if running in a CI environment:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nif (Helpers.IsRunningInContinuousIntegration)\n{\n    // Skip interactive dialogs, use defaults\n}\n\nif (Helpers.IsRunningInBatchMode)\n{\n    // Running headless (no graphics device)\n}\n</code></pre> <p>Supported CI systems (checked via environment variables):</p> CI System Environment Variable Generic CI <code>CI</code> GitHub Actions <code>GITHUB_ACTIONS</code> GitLab CI <code>GITLAB_CI</code> Jenkins <code>JENKINS_URL</code> Travis CI <code>TRAVIS</code> CircleCI <code>CIRCLECI</code> Azure Pipelines <code>TF_BUILD</code> TeamCity <code>TEAMCITY_VERSION</code> Buildkite <code>BUILDKITE</code> AWS CodeBuild <code>CODEBUILD_BUILD_ID</code> Bitbucket Pipelines <code>BITBUCKET_BUILD_NUMBER</code> AppVeyor <code>APPVEYOR</code> Drone CI <code>DRONE</code> Unity CI <code>UNITY_CI</code> Unity Tests <code>UNITY_TESTS</code> <p>Check specific environment variables:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\n// Check if a specific environment variable is set (non-empty, non-whitespace)\nbool onGitHub = Helpers.IsEnvironmentVariableSet(\n    Helpers.CiEnvironmentVariables.GitHubActions\n);\n\nbool onJenkins = Helpers.IsEnvironmentVariableSet(\n    Helpers.CiEnvironmentVariables.JenkinsUrl\n);\n\n// Access all known CI variable names\nforeach (string varName in Helpers.CiEnvironmentVariables.All)\n{\n    if (Helpers.IsEnvironmentVariableSet(varName))\n    {\n        Debug.Log($\"CI detected via: {varName}\");\n    }\n}\n</code></pre> <p>Use for:</p> <ul> <li>Skipping interactive dialogs in CI</li> <li>Disabling expensive editor visualizations</li> <li>Conditional test behavior</li> <li>Build automation scripts</li> <li>Asset processors that shouldn't run headless</li> </ul>"},{"location":"features/utilities/helper-utilities/#best-practices","title":"Best Practices","text":""},{"location":"features/utilities/helper-utilities/#performance","title":"Performance","text":"<ul> <li>Cache lookups: <code>Helpers.Find&lt;T&gt;()</code> caches, but don't call every frame anyway</li> <li>Use buffered variants: <code>IterateOverAllChildrenRecursively</code> with buffers for hot paths</li> <li>Main thread dispatch: Don't send hundreds of tiny tasks, batch work</li> <li>Hierarchy traversal: Use breadth-first with depth limits for large hierarchies</li> </ul>"},{"location":"features/utilities/helper-utilities/#threading_1","title":"Threading","text":"<ul> <li>Main thread rule: Only Unity APIs need main thread, pure C# can stay on background threads</li> <li>Avoid blocking: Don't wait for main thread results in tight loops</li> <li>CancellationToken: Support cancellation for long operations</li> </ul>"},{"location":"features/utilities/helper-utilities/#architecture","title":"Architecture","text":"<ul> <li>Component vs Helper: Components (MonoBehaviours) for per-object state, Helpers for stateless operations</li> <li>Static method smell: If you need instance state, use a component instead</li> <li>Editor/Runtime split: Use <code>#if UNITY_EDITOR</code> guards for editor-only helpers</li> </ul>"},{"location":"features/utilities/helper-utilities/#code-organization","title":"Code Organization","text":"<ul> <li>Namespace imports: Use <code>using WallstopStudios.UnityHelpers.Core.Helper;</code> at top of file</li> <li>Don't extend helpers: These are sealed utility classes, not inheritance hierarchies</li> <li>Prefer composition: Use helpers from components, don't try to combine them</li> </ul>"},{"location":"features/utilities/helper-utilities/#related-documentation","title":"Related Documentation","text":"<ul> <li>Intelligent Pooling System - Advanced object pooling with auto-purging</li> <li>Math &amp; Extensions - Extension methods on built-in types</li> <li>Utility Components - MonoBehaviour-based utilities</li> <li>Reflection Helpers - High-performance reflection utilities</li> <li>Singletons - RuntimeSingleton and ScriptableObjectSingleton</li> <li>Data Structures - Cache, spatial trees, and other collections</li> </ul>"},{"location":"features/utilities/math-and-extensions/","title":"Core Math &amp; Extensions","text":""},{"location":"features/utilities/math-and-extensions/#tldr-why-use-these","title":"TL;DR \u2014 Why Use These","text":"<ul> <li>Small helpers that fix everyday math and Unity annoyances: safe modulo, wrapped indices, approximate equality, bounds math, color utilities, and more.</li> <li>Copy/paste examples and diagrams show intent; use as building blocks in hot paths.</li> </ul> <p>This guide summarizes the math primitives and extension helpers in this package and shows how to apply them effectively, with examples, performance notes, and practical scenarios.</p> <p>Contents</p> <ul> <li>Numeric helpers \u2014 Positive modulo, wrapped arithmetic, approximate equality, clamping</li> <li>Geometry \u2014 Lines, ranges, parabolas, point-in-polygon, polyline simplification</li> <li>Unity extensions \u2014 Rect/Bounds conversions, RectTransform bounds, camera bounds, bounds aggregation</li> <li>Color utilities \u2014 Averaging (LAB/HSV/Weighted/Dominant), hex conversion</li> <li>Collections \u2014 IEnumerable helpers, buffering, infinite sequences</li> <li>Strings \u2014 Casing, encoding/decoding, distance</li> <li>Direction helpers \u2014 Enum conversions and operations</li> <li>Enum helpers \u2014 Zero-allocation flag checks, cached names, display names</li> <li>Random generators \u2014 Weighted selection, vector generation, subset sampling</li> <li>Async/Coroutine interop \u2014 Bridge Unity AsyncOperation with async/await</li> <li>Best Practices</li> </ul> <p></p>"},{"location":"features/utilities/math-and-extensions/#numeric-helpers","title":"Numeric Helpers","text":"<ul> <li>Positive modulo and wrap-around arithmetic</li> <li>Use <code>PositiveMod</code> to ensure non-negative modulo results for indices and cyclic counters.</li> <li>Use <code>WrappedAdd</code>/<code>WrappedIncrement</code> for ring buffer indexes and cursor navigation.</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\n\nint i = -1;\ni = i.PositiveMod(5); // 4\ni = i.WrappedAdd(2, 5); // 1\n\nfloat angle = -30f;\nfloat normalized = angle.PositiveMod(360f); // 330f\n</code></pre> <p>Diagram (wrap-around on a ring of size 5):</p> Text Only<pre><code>Index:   0   1   2   3   4\n           \u2196           \u2199\n            \\  +2 from 4  =&gt; 1\n\nStart at 4, add 2 \u2192 6 \u2192 6 % 5 = 1\n</code></pre> <ul> <li>Approximate equality</li> <li><code>float.Approximately(rhs, tolerance)</code> and <code>double.Approximately</code> add a magnitude-scaled fudge factor.</li> </ul> <p>Example:</p> C#<pre><code>bool close = 0.1f.Approximately(0.10001f, 0.0001f); // true\n</code></pre> <ul> <li>Generic <code>Clamp</code></li> <li><code>Clamp&lt;T&gt;(min, max)</code> works for any <code>IComparable&lt;T&gt;</code>.</li> </ul> <p></p>"},{"location":"features/utilities/math-and-extensions/#geometry","title":"Geometry","text":""},{"location":"features/utilities/math-and-extensions/#line2d-2d-line-segment-operations","title":"Line2D \u2014 2D line segment operations","text":"<p>Why it exists: Provides 2D line segment math for collision detection, ray-casting, and geometric queries.</p> <p>When to use:</p> <ul> <li>Ray-casting for bullets, lasers, or line-of-sight checks</li> <li>Detecting if paths cross obstacles</li> <li>Click detection near edges or borders</li> <li>Finding closest points on paths or walls</li> </ul> <p>When NOT to use:</p> <ul> <li>For 3D geometry (use Line3D instead)</li> <li>For curves or arcs (lines are always straight)</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Math;\nvar a = new Line2D(new Vector2(0,0), new Vector2(2,0));\nvar b = new Line2D(new Vector2(1,-1), new Vector2(1,1));\nbool hit = a.Intersects(b); // true\n</code></pre> <p>Diagram (segment intersection):</p> Text Only<pre><code>y\u2191           b.to (1,1)\n |             \u2502\n |             \u2502  b\n |   a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 x\n |         (1,0)\u00d7  \u2190 intersection\n |             \u2502\n |             \u2502\n +\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n               b.from (1,-1)\n</code></pre> <p>Getting the exact intersection point:</p> C#<pre><code>var wall = new Line2D(new Vector2(0, 0), new Vector2(10, 0));\nvar ray = new Line2D(playerPos, targetPos);\n\nif (wall.TryGetIntersectionPoint(ray, out Vector2 hitPoint))\n{\n    // Spawn bullet impact effect at exact hitPoint\n    Instantiate(sparksPrefab, hitPoint, Quaternion.identity);\n}\n</code></pre> <p>Circle intersection (bullets hitting circular enemies):</p> C#<pre><code>var bulletPath = new Line2D(bulletStart, bulletEnd);\nvar enemy = new Circle(enemyPosition, enemyRadius);\n\nif (bulletPath.Intersects(enemy))\n{\n    // Bullet hit the enemy\n    enemy.TakeDamage(bulletDamage);\n}\n</code></pre> <p>Closest point on line (snapping to paths):</p> C#<pre><code>var path = new Line2D(pathStart, pathEnd);\nVector2 snappedPosition = path.ClosestPointOnLine(mouseWorldPos);\n// Use for UI snapping, path following, or grid alignment\n</code></pre> <p>Performance tip: Use <code>DistanceSquaredToPoint</code> instead of <code>DistanceToPoint</code> when comparing distances (avoids expensive square root):</p> C#<pre><code>// Fast distance comparison (no sqrt)\nfloat distSq = line.DistanceSquaredToPoint(point);\nif (distSq &lt; thresholdSquared)\n{\n    // Point is within threshold\n}\n</code></pre>"},{"location":"features/utilities/math-and-extensions/#line3d-3d-line-segment-operations","title":"Line3D \u2014 3D line segment operations","text":"<p>Why it exists: Extends Line2D concepts to 3D space for sphere intersection, bounding box clipping, and skew line distance.</p> <p>When to use:</p> <ul> <li>3D ray-casting for weapons, lasers, or grappling hooks</li> <li>Visibility checks between 3D objects</li> <li>Cable/rope collision detection</li> <li>Finding closest approach between moving objects</li> </ul> <p>When NOT to use:</p> <ul> <li>For 2D games (use Line2D instead)</li> <li>For complex curved paths (lines are always straight)</li> </ul> <p>Basic operations:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Math;\n\nvar ray = new Line3D(gunBarrel.position, hitPoint);\nvar enemyBounds = new BoundingBox3D(enemy.bounds);\n\n// Check if ray hits enemy bounding box\nif (ray.Intersects(enemyBounds))\n{\n    enemy.TakeDamage(bulletDamage);\n}\n</code></pre> <p>Closest points between two 3D lines (skew lines):</p> <p>Problem: In 3D, two lines might not actually intersect (imagine two pipes that pass by each other). This finds the closest approach.</p> C#<pre><code>var ropeA = new Line3D(ropeAStart, ropeAEnd);\nvar ropeB = new Line3D(ropeBStart, ropeBEnd);\n\nif (ropeA.TryGetClosestPoints(ropeB, out Vector3 pointOnA, out Vector3 pointOnB))\n{\n    float separation = Vector3.Distance(pointOnA, pointOnB);\n    if (separation &lt; 0.1f)\n    {\n        // Ropes are touching or tangled\n    }\n}\n</code></pre> <p>Sphere intersection (force fields, explosions):</p> C#<pre><code>var laserBeam = new Line3D(laserStart, laserEnd);\nvar shield = new Sphere(shieldCenter, shieldRadius);\n\nif (laserBeam.Intersects(shield))\n{\n    float distance = laserBeam.DistanceToSphere(shield);\n    // distance == 0 means line passes through sphere\n    // distance &gt; 0 means line misses sphere\n}\n</code></pre>"},{"location":"features/utilities/math-and-extensions/#range-numeric-ranges-with-flexible-boundaries","title":"Range \u2014 Numeric ranges with flexible boundaries <p>Why it exists: Solves the \"is this value in a valid range\" problem with clear, readable code and support for different boundary conditions.</p> <p>When to use:</p> <ul> <li>Validating user input (is health between 0-100?)</li> <li>Time windows (is this event during business hours?)</li> <li>Array bounds checking with custom inclusivity</li> <li>Overlap detection (do these time slots conflict?)</li> </ul> <p>When NOT to use:</p> <ul> <li>For single comparisons (just use <code>if (x &gt;= min &amp;&amp; x &lt;= max)</code>)</li> <li>When you don't care about boundary inclusivity</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Math;\nvar r = Range&lt;int&gt;.Inclusive(0, 10);\nbool inside = r.Contains(10); // true (10 is included)\n</code></pre> <p>Choosing the right inclusivity:</p> C#<pre><code>// [0, 10] - both endpoints included (closed interval)\nvar healthRange = Range&lt;int&gt;.Inclusive(0, 10);\nhealthRange.Contains(0);  // true\nhealthRange.Contains(10); // true\n\n// [0, 10) - start included, end excluded (common for indices)\nvar arrayRange = Range&lt;int&gt;.InclusiveExclusive(0, 10);\narrayRange.Contains(0);  // true\narrayRange.Contains(10); // false (typical for array[0..10))\n\n// (0, 1) - neither endpoint included (open interval)\nvar normalized = Range&lt;float&gt;.Exclusive(0f, 1f);\nnormalized.Contains(0f); // false\nnormalized.Contains(0.5f); // true\nnormalized.Contains(1f); // false\n</code></pre> <p>Overlap detection:</p> C#<pre><code>var morningShift = Range&lt;int&gt;.Inclusive(9, 13);  // 9am-1pm\nvar afternoonShift = Range&lt;int&gt;.Inclusive(13, 17); // 1pm-5pm\n\nbool conflict = morningShift.Overlaps(afternoonShift); // true (overlap at 1pm)\n</code></pre> <p>Date ranges:</p> C#<pre><code>var january = Range&lt;DateTime&gt;.Inclusive(\n    new DateTime(2025, 1, 1),\n    new DateTime(2025, 1, 31)\n);\n\nif (january.Contains(someDate))\n{\n    // Event happened in January\n}\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#parabola-projectile-trajectories-and-smooth-curves","title":"Parabola \u2014 Projectile trajectories and smooth curves <p>Why it exists: Provides parabolic math for projectile motion, jump arcs, and smooth animation curves without writing quadratic equations by hand.</p> <p>When to use:</p> <ul> <li>Throwing/shooting projectiles (grenades, arrows, basketballs)</li> <li>Character jump arcs</li> <li>Camera dolly movements along smooth paths</li> <li>Particle fountain effects</li> </ul> <p>When NOT to use:</p> <ul> <li>For straight-line motion (use Vector3.Lerp)</li> <li>For complex curves with multiple peaks (parabola has only one peak)</li> <li>When gravity/physics simulation is already handling it</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Math;\n\nvar p = new Parabola(maxHeight: 5f, length: 10f);\nif (p.TryGetValueAtNormalized(0.5f, out float y))\n{\n    // y == 5 (at the peak)\n}\n</code></pre> <p>Diagram (normalized parabola):</p> Text Only<pre><code>y\u2191          * vertex (0.5, 5)\n |        *\n |      *\n |    *\n |  *\n |*           *\n +\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500*\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 x (t from 0..1)\n 0        0.5       1\n</code></pre> <p>Custom coefficients (when you have a specific equation):</p> C#<pre><code>// Create parabola from equation y = -0.5x\u00b2 + 5x\nvar p = Parabola.FromCoefficients(a: -0.5f, b: 5f, length: 10f);\n</code></pre> <p>Performance tip: Use <code>GetValueAtUnchecked</code> when you know the input is in range (skips bounds checking):</p> C#<pre><code>// In a tight loop updating many projectiles\nfor (int i = 0; i &lt; projectiles.Length; i++)\n{\n    float x = projectiles[i].distanceTraveled;\n    if (x &gt;= 0 &amp;&amp; x &lt;= parabola.Length)\n    {\n        float y = parabola.GetValueAtUnchecked(x); // No bounds check\n        projectiles[i].position.y = y;\n    }\n}\n</code></pre> <p>Normalized vs Absolute coordinates:</p> C#<pre><code>// Normalized: Use when working with 0-1 interpolation (animations)\nfloat t = animationTime / totalDuration; // 0-1\nparabola.TryGetValueAtNormalized(t, out float y);\n\n// Absolute: Use when working with world-space coordinates\nfloat worldX = transform.position.x;\nparabola.TryGetValueAt(worldX, out float worldY);\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#point-in-polygon-test-if-points-are-inside-shapes","title":"Point-in-Polygon \u2014 Test if points are inside shapes <p>Why it exists: Detects whether a point lies inside an irregular polygon, solving the \"did the player click this shape\" problem.</p> <p>When to use:</p> <ul> <li>Click detection in irregular UI shapes or game zones</li> <li>Testing if characters are inside territory boundaries</li> <li>Checking if waypoints are in walkable areas</li> <li>Testing if 3D points project inside mesh faces</li> </ul> <p>When NOT to use:</p> <ul> <li>For circles (use <code>Vector2.Distance(point, center) &lt;= radius</code>)</li> <li>For rectangles (use <code>Rect.Contains</code>)</li> <li>For complex 3D volumes (use Collider.bounds or raycasts)</li> </ul> <p>Important: This uses the ray-casting algorithm \u2014 it counts how many times a ray from the point crosses polygon edges. Odd count = inside, even count = outside.</p> <p>2D polygon test:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Math;\n\nVector2[] zoneShape = new Vector2[]\n{\n    new(0, 0), new(10, 0), new(10, 5), new(5, 10), new(0, 5)\n};\n\nVector2 clickPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);\n\nif (PointPolygonCheck.IsPointInsidePolygon(clickPos, zoneShape))\n{\n    Debug.Log(\"Clicked inside the zone!\");\n}\n</code></pre> <p>3D polygon with plane projection:</p> C#<pre><code>// Test if 3D point is inside a 3D triangle (projects onto plane)\nVector3[] triangleFace = new Vector3[]\n{\n    new(0, 0, 0), new(5, 0, 0), new(2.5f, 5, 0)\n};\nVector3 faceNormal = Vector3.forward; // Must be normalized\n\nVector3 testPoint = new Vector3(2.5f, 2f, 1f); // Will project onto z=0 plane\n\nif (PointPolygonCheck.IsPointInsidePolygon(testPoint, triangleFace, faceNormal))\n{\n    Debug.Log(\"Point projects inside triangle\");\n}\n</code></pre> <p>Zero-allocation version for hot paths:</p> C#<pre><code>// Use ReadOnlySpan to avoid heap allocations\nSpan&lt;Vector2&gt; vertices = stackalloc Vector2[4]\n{\n    new(0, 0), new(1, 0), new(1, 1), new(0, 1)\n};\n\nbool inside = PointPolygonCheck.IsPointInsidePolygon(clickPos, vertices);\n// No GC allocations when using ReadOnlySpan\n</code></pre> <p>Edge cases to know:</p> <ul> <li>Points exactly on polygon edges may return inconsistent results (floating-point precision issues)</li> <li>Assumes simple (non-self-intersecting) polygons</li> <li>Winding order (clockwise vs counter-clockwise) doesn't matter</li> <li>For 3D: all polygon vertices must be coplanar for accurate results</li> </ul>  <ul> <li>Polyline simplification (Douglas\u2013Peucker)</li> <li><code>Simplify</code> (float epsilon) and <code>SimplifyPrecise</code> (double tolerance) reduce vertex count while preserving shape.</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Helper;\nList&lt;Vector2&gt; simplified = LineHelper.Simplify(points, epsilon: 0.1f);\n</code></pre> <p>Diagram (original vs simplified):</p> Text Only<pre><code>Original:     *----*--*---*--*-----*\nSimplified:   *-----------*--------*\n\nFewer vertices within epsilon of the original polyline.\n</code></pre> <p>Visual: </p> <p>Convex hull (monotone chain / Jarvis examples used by helpers):</p> Text Only<pre><code>Points:     \u00b7  \u00b7   \u00b7\n          \u00b7      \u00b7   \u00b7\n            \u00b7  \u00b7\n\nHull:     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n          \u2502           \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2518\n                  \u2514\u2500\u2510\n</code></pre> <p>Visual: </p> <p>Edge Cases Gallery</p> <p></p> <p></p>","text":""},{"location":"features/utilities/math-and-extensions/#unity-extensions","title":"Unity Extensions","text":"<ul> <li>Rect/Bounds conversions, RectTransform world bounds</li> <li>Camera <code>OrthographicBounds</code></li> <li>Bounds aggregation from collections</li> </ul> <p>Example:</p> C#<pre><code>Rect r = rectTransform.GetWorldRect();\nBounds view = Camera.main.OrthographicBounds();\n</code></pre> <p>Diagrams:</p> <ul> <li>RectTransform world rect (axis-aligned bounds of rotated UI):</li> </ul> Text Only<pre><code>   \u2022 corner         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n      \u2572             \u2502   AABB (r)    \u2502\n       \u2572  rotated   \u2502   \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u2502\n        \u2572 rectangle \u2502  \u2571\u2502 UI  \u2571\u2502    \u2502\n         \u2022          \u2502 \u2571 \u2514\u2500\u2500\u2500\u2500\u2571\u2500\u2518    \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre> <ul> <li>Orthographic camera bounds (centered on camera):</li> </ul> Text Only<pre><code>            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 view (Bounds) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502           height=2*size      \u2502\n            \u2502         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u2502\n   near \u2500\u2500\u2500\u25b6\u2502         \u2502   camera FOV   \u2502    \u2502\u25c0\u2500\u2500 far\n            \u2502         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre> <p></p>"},{"location":"features/utilities/math-and-extensions/#color-utilities","title":"Color Utilities","text":"<ul> <li>Averaging methods:</li> <li>LAB: perceptually accurate</li> <li>HSV: preserves vibrancy</li> <li>Weighted: luminance-aware</li> <li>Dominant: bucket-based mode</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\nColor avg = sprite.GetAverageColor(ColorAveragingMethod.LAB);\nstring html = avg.ToHex();\n</code></pre> <p>Dominant color example (bucket-based):</p> C#<pre><code>// Emphasize palette extraction (posterized sprites, UI swatches)\nvar dominant = pixels.GetAverageColor(ColorAveragingMethod.Dominant, alphaCutoff: 0.05f);\n</code></pre> <p>Diagram (dominant buckets):</p> Text Only<pre><code>RGB space buckets \u2192 counts\n [R][G][B] \u2026  [R+\u0394][G][B]  \u2026  [R][G+\u0394][B]  \u2026\n          \u2191 pick max bucket centroid as dominant\n</code></pre> <p></p>"},{"location":"features/utilities/math-and-extensions/#collections","title":"Collections","text":""},{"location":"features/utilities/math-and-extensions/#ienumerable-helpers","title":"IEnumerable Helpers <p>Infinite cycling:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\n// Cycle through elements endlessly for repeating patterns\nvar colors = new[] { Color.red, Color.blue, Color.green };\nforeach (var color in colors.Infinite())\n{\n    // Loops forever: red, blue, green, red, blue, green...\n    if (shouldStop) break;\n}\n</code></pre> <p>Partition into chunks:</p> C#<pre><code>// Split large collections into fixed-size batches\nvar items = Enumerable.Range(0, 100);\nforeach (var batch in items.Partition(10))\n{\n    // Process 10 items at a time\n    ProcessBatch(batch); // batch is a List&lt;int&gt; of size 10\n}\n\n// Zero-allocation version for hot paths\nusing (var batchBuffer = items.PartitionPooled(10))\n{\n    foreach (var batch in batchBuffer)\n    {\n        // batch is reused from pool, no allocations\n    }\n} // Automatically returns buffer to pool\n</code></pre> <p>Shuffled (non-destructive):</p> C#<pre><code>// Get shuffled copy without modifying original\nvar shuffled = items.Shuffled();\n// Original list unchanged\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#ilist-operations","title":"IList Operations <p>Remove O(1) by swapping with last element:</p> C#<pre><code>// Fast removal when order doesn't matter (particle systems, entity lists)\nList&lt;Enemy&gt; enemies = GetActiveEnemies();\nenemies.RemoveAtSwapBack(3); // Swaps enemy[3] with last enemy, then removes\n// Avoids O(n) shift operation of List.RemoveAt by swapping with last element\n</code></pre> <p>Partition (split by predicate):</p> C#<pre><code>var numbers = new List&lt;int&gt; { 1, 2, 3, 4, 5, 6 };\nvar (evens, odds) = numbers.Partition(n =&gt; n % 2 == 0);\n// evens: [2, 4, 6]\n// odds: [1, 3, 5]\n</code></pre> <p>Custom sorting:</p> C#<pre><code>// GhostSort: Hybrid sort algorithm for medium-sized lists\nlargeList.GhostSort(); // Uses IComparable&lt;T&gt;\n\n// Custom comparison function\nlist.Sort((a, b) =&gt; a.priority.CompareTo(b.priority));\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#dictionary-helpers","title":"Dictionary Helpers <p>Thread-safe get-or-create:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\n// Thread-safe for ConcurrentDictionary\nvar value = dict.GetOrAdd(key, () =&gt; new ExpensiveObject());\n\n// Read-only version (doesn't modify dict)\nvar value = readOnlyDict.GetOrElse(key, defaultValue);\n</code></pre> <p>Merge dictionaries:</p> C#<pre><code>var defaults = new Dictionary&lt;string, int&gt; { [\"health\"] = 100, [\"mana\"] = 50 };\nvar overrides = new Dictionary&lt;string, int&gt; { [\"health\"] = 150 };\n\nvar merged = defaults.Merge(overrides);\n// Result: { [\"health\"] = 150, [\"mana\"] = 50 }\n</code></pre> <p>Deep equality:</p> C#<pre><code>// Compare dictionary contents (not just references)\nbool same = dict1.ContentEquals(dict2); // Compares all key-value pairs\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#bounds-from-collections","title":"Bounds from Collections <p>Bounds from points example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\n// Compute BoundsInt for occupied grid cells\nVector3Int[] positions = GetOccupiedCells();\nBoundsInt? area = positions.GetBounds(inclusive: false);\nif (area is BoundsInt b)\n{\n    // b contains all positions\n}\n</code></pre> <p>Bounds aggregation example:</p> C#<pre><code>// Merge many Bounds (e.g., from Renderers)\nRenderer[] renderers = GetComponentsInChildren&lt;Renderer&gt;();\nBounds? merged = renderers.Select(r =&gt; r.bounds).GetBounds();\nif (merged is Bounds totalBounds)\n{\n    // totalBounds encompasses all renderers\n}\n</code></pre> <p></p>","text":""},{"location":"features/utilities/math-and-extensions/#strings","title":"Strings","text":""},{"location":"features/utilities/math-and-extensions/#case-conversions","title":"Case Conversions <p>Why it exists: Automatically convert between common programming case styles without writing regex or manual parsing.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\nstring input = \"XMLHttpRequest\";\n\ninput.ToPascalCase();  // \"XmlHttpRequest\"\ninput.ToCamelCase();   // \"xmlHttpRequest\"\ninput.ToSnakeCase();   // \"xml_http_request\"\ninput.ToKebabCase();   // \"xml-http-request\"\ninput.ToTitleCase();   // \"Xml Http Request\"\n</code></pre> <p>Smart tokenization handles mixed cases intelligently.</p>","text":""},{"location":"features/utilities/math-and-extensions/#string-utilities","title":"String Utilities <p>Levenshtein Distance (edit distance):</p> C#<pre><code>// Calculate how many edits to transform one string into another\nstring a = \"kitten\";\nstring b = \"sitting\";\nint distance = a.LevenshteinDistance(b); // 3 edits\n// Use for: fuzzy matching, spell correction, search suggestions\n</code></pre> <p>Base64 encoding:</p> C#<pre><code>string text = \"Hello, World!\";\nstring encoded = text.ToBase64();       // \"SGVsbG8sIFdvcmxkIQ==\"\nstring decoded = encoded.FromBase64();  // \"Hello, World!\"\n</code></pre> <p>String analysis:</p> C#<pre><code>bool isNum = \"12345\".IsNumeric();         // true\nbool isAlpha = \"Hello\".IsAlphabetic();    // true\nbool isAlphaNum = \"Hello123\".IsAlphanumeric(); // true\n</code></pre> <p>Truncate with ellipsis:</p> C#<pre><code>string long = \"This is a very long string\";\nstring short = long.Truncate(10); // \"This is a...\"\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#encoding-helpers","title":"Encoding Helpers C#<pre><code>// UTF-8 conversions\nbyte[] bytes = \"Hello\".GetBytes();\nstring text = bytes.GetString();\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#directions","title":"Directions","text":"<ul> <li>Conversions between enum and vectors; splitting flag sets; combining</li> </ul> <p>Example:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\nVector2Int v = Direction.NorthWest.AsVector2Int(); // (-1, 1)\n</code></pre> <p></p>"},{"location":"features/utilities/math-and-extensions/#enum-helpers","title":"Enum Helpers","text":"<p>Why it exists: Standard C# enum operations cause boxing allocations and are slow in hot paths. These helpers solve performance problems.</p>"},{"location":"features/utilities/math-and-extensions/#zero-allocation-flag-checking","title":"Zero-Allocation Flag Checking <p>The problem: Standard <code>HasFlag()</code> boxes both enums, causing GC pressure.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\n[Flags]\npublic enum Permissions\n{\n    None = 0,\n    Read = 1,\n    Write = 2,\n    Execute = 4\n}\n\nPermissions userPerms = Permissions.Read | Permissions.Write;\n\n// \u274c BAD: Causes boxing allocations\nif (userPerms.HasFlag(Permissions.Write)) { }\n\n// \u2705 GOOD: Zero allocations\nif (userPerms.HasFlagNoAlloc(Permissions.Write)) { }\n</code></pre> <p>Use <code>HasFlagNoAlloc</code> in:</p> <ul> <li>Per-frame checks</li> <li>Hot loops</li> <li>Frequently-called methods</li> <li>Performance-critical code paths</li> </ul>","text":""},{"location":"features/utilities/math-and-extensions/#fast-enum-to-string-conversion","title":"Fast Enum-to-String Conversion <p>The problem: <code>enum.ToString()</code> is slow (reflection) and allocates every call.</p> C#<pre><code>public enum GameState { MainMenu, Playing, Paused, GameOver }\n\nGameState state = GameState.Playing;\n\n// \u274c SLOW: Uses reflection every time\nstring name = state.ToString();\n\n// \u2705 FAST: Cached in array/dictionary after first call\nstring cached = state.ToCachedName();\n// Subsequent calls are O(1) lookups with zero allocation\n</code></pre> <p>Performance: ToCachedName uses cached lookups to avoid repeated allocations and string conversions after the first call.</p>","text":""},{"location":"features/utilities/math-and-extensions/#display-names-for-ui","title":"Display Names for UI <p>The problem: Enum values often need different names in UI than in code.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Attribute;\n\npublic enum Difficulty\n{\n    [EnumDisplayName(\"Easy Mode\")]\n    Easy,\n\n    [EnumDisplayName(\"Normal\")]\n    Medium,\n\n    [EnumDisplayName(\"NIGHTMARE MODE!!!\")]\n    Hard\n}\n\nDifficulty current = Difficulty.Hard;\nstring displayName = current.ToDisplayName(); // \"NIGHTMARE MODE!!!\"\n// Falls back to enum name if attribute not present\n</code></pre> <p>Use for:</p> <ul> <li>Dropdown labels in UI</li> <li>Localization keys</li> <li>User-facing text that doesn't match code names</li> </ul> <p></p>","text":""},{"location":"features/utilities/math-and-extensions/#random-generators","title":"Random Generators","text":"<p>Why it exists: Unity's <code>Random</code> class is limited and not suitable for all scenarios. These extensions provide additional random generation capabilities.</p> <p></p>"},{"location":"features/utilities/math-and-extensions/#weighted-random-selection","title":"Weighted Random Selection <p>The problem: Selecting items based on probability weights (loot tables, spawn chances).</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\n// Items with different drop chances\nvar loot = new[]\n{\n    (item: \"Common Sword\", weight: 50),\n    (item: \"Rare Shield\", weight: 30),\n    (item: \"Epic Helmet\", weight: 15),\n    (item: \"Legendary Ring\", weight: 5)\n};\n\nIRandom rng = PRNG.Instance;\nstring drop = rng.NextWeighted(loot); // More likely to get Common Sword\n\n// Get index instead of value\nint dropIndex = rng.NextWeightedIndex(loot.Select(x =&gt; x.weight));\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#vector-and-quaternion-generation","title":"Vector and Quaternion Generation <p>Uniform random vectors:</p> C#<pre><code>// Random point in rectangle\nVector2 point = rng.NextVector2(minX, maxX, minY, maxY);\n\n// Random point inside circle\nVector2 inCircle = rng.NextVector2InRange(radius);\n\n// Random point ON sphere surface (uniform distribution)\nVector3 onSphere = rng.NextVector3OnSphere(radius);\n// Uses Marsaglia's method for true uniform distribution\n\n// Random rotation (uniform distribution)\nQuaternion rotation = rng.NextQuaternion();\n// Uses Shoemake's algorithm\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#color-generation","title":"Color Generation C#<pre><code>// Random opaque color\nColor color = rng.NextColor();\n\n// Random color in HSV range (for similar hues)\nColor tint = rng.NextColorInRange(\n    baseColor: Color.red,\n    hueVariance: 0.1f,\n    saturationVariance: 0.2f,\n    valueVariance: 0.2f\n);\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#subset-sampling","title":"Subset Sampling <p>Reservoir sampling \u2014 Pick k random items from a large collection without loading it all into memory:</p> C#<pre><code>// Select 5 random enemies from potentially huge list\nIEnumerable&lt;Enemy&gt; allEnemies = GetAllEnemiesInWorld();\nList&lt;Enemy&gt; randomFive = rng.NextSubset(allEnemies, k: 5);\n// O(n) time, uses reservoir sampling for uniform probability\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#random-utilities","title":"Random Utilities C#<pre><code>bool coinFlip = rng.NextBool();              // 50/50\nbool biasedFlip = rng.NextBool(0.7f);        // 70% true\nint sign = rng.NextSign();                   // Randomly -1 or +1\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#asynccoroutine-interop","title":"Async/Coroutine Interop","text":"<p>Why it exists: Unity's <code>AsyncOperation</code> and coroutines don't natively support modern async/await patterns. This bridges the gap.</p>"},{"location":"features/utilities/math-and-extensions/#await-asyncoperation-unity-20231","title":"Await AsyncOperation (Unity &lt; 2023.1) <p>The problem: Unity's AsyncOperations (scene loading, asset loading) don't support <code>await</code>.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\nusing UnityEngine.SceneManagement;\n\n// \u2705 Now you can await scene loading\nasync Task LoadGameScene()\n{\n    var operation = SceneManager.LoadSceneAsync(\"GameLevel\");\n    await operation;\n\n    Debug.Log(\"Scene loaded!\");\n}\n</code></pre> <p>Note: Unity 2023.1+ has built-in await support, but this works in older versions.</p>","text":""},{"location":"features/utilities/math-and-extensions/#convert-asyncoperation-to-task","title":"Convert AsyncOperation to Task C#<pre><code>// As Task\nTask task = asyncOperation.AsTask();\nawait task;\n\n// As ValueTask (reduces allocations for short operations)\nValueTask valueTask = asyncOperation.AsValueTask();\nawait valueTask;\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#run-task-as-coroutine","title":"Run Task as Coroutine <p>The problem: You have async/await code (from a library, or your own), but need to run it in a Unity coroutine context.</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Extension;\n\nasync Task&lt;string&gt; DownloadDataAsync()\n{\n    // Some async operation (HttpClient, database, etc.)\n    await Task.Delay(1000);\n    return \"Downloaded data\";\n}\n\n// In MonoBehaviour\nIEnumerator Start()\n{\n    // \u2705 Convert Task to IEnumerator\n    return DownloadDataAsync().AsCoroutine();\n}\n</code></pre>","text":""},{"location":"features/utilities/math-and-extensions/#chain-continuations","title":"Chain Continuations C#<pre><code>// Chain operations on ValueTask\nawait myValueTask.WithContinuation(() =&gt; Debug.Log(\"Done!\"));\n</code></pre> <p>When to use:</p> <ul> <li>Integrating third-party async libraries with Unity</li> <li>Mixing async/await code with existing coroutine systems</li> <li>Background operations that need to update Unity objects on completion</li> <li>Modernizing legacy coroutine code</li> </ul> <p>When NOT to use:</p> <ul> <li>Unity 2023.1+ (use built-in await support)</li> <li>Simple fire-and-forget operations (just use coroutines)</li> <li>When you have control over both ends (just use all-async or all-coroutines)</li> </ul>","text":""},{"location":"features/utilities/math-and-extensions/#best-practices","title":"Best Practices","text":"<ul> <li>Use <code>PositiveMod</code> instead of <code>%</code> for indices and angles when negatives are possible.</li> <li>Prefer <code>SimplifyPrecise</code> for offline tooling; use <code>Simplify</code> during gameplay for speed.</li> <li>Choose color averaging method per goal: LAB for perceptual palette, Weighted for speed, Dominant for swatches.</li> <li>Favor IReadOnlyList/HashSet specializations to minimize allocations; pooled buffers are used where applicable.</li> <li>Run Unity-dependent extensions (e.g., <code>RectTransform</code>, <code>Camera</code>, <code>Grid</code>) on the main thread.</li> </ul>"},{"location":"features/utilities/math-and-extensions/#related-docs","title":"Related Docs","text":"<ul> <li>Random performance details \u2014 Random Performance</li> <li>Serialization formats \u2014 Serialization Guide</li> <li>Effects system \u2014 Effects System</li> <li>Relational Components \u2014 Relational Components</li> </ul>"},{"location":"features/utilities/pooling-guide/","title":"Intelligent Pooling System","text":""},{"location":"features/utilities/pooling-guide/#tldr-why-use-this","title":"TL;DR \u2014 Why Use This","text":"<ul> <li>Automatic memory management with intelligent purging that adapts to usage patterns.</li> <li>Avoid GC spikes by spreading purges across frames and responding to memory pressure.</li> <li>Type-specific policies for different object lifetimes (short-lived lists vs long-lived audio sources).</li> <li>Zero-configuration defaults that \"just work\" with opt-in customization.</li> </ul>"},{"location":"features/utilities/pooling-guide/#contents","title":"Contents","text":"<ul> <li>Overview</li> <li>Quick Start</li> <li>PoolOptions Configuration</li> <li>Global Settings (PoolPurgeSettings)</li> <li>Eviction Policies</li> <li>Memory Pressure Detection</li> <li>Size-Aware Policies</li> <li>Access Frequency Tracking</li> <li>Application Lifecycle Hooks</li> <li>Global Pool Registry</li> <li>Best Practices</li> </ul>"},{"location":"features/utilities/pooling-guide/#overview","title":"Overview","text":"<p>The intelligent pooling system provides automatic memory management for <code>WallstopGenericPool&lt;T&gt;</code> instances. Instead of pools growing unbounded or requiring manual purge calls, the system:</p> <ol> <li>Tracks usage patterns - Monitors high-water marks and access frequency</li> <li>Purges intelligently - Only removes items unlikely to be needed soon</li> <li>Spreads work - Limits purges per operation to avoid GC spikes</li> <li>Responds to pressure - Aggressive cleanup when memory is low</li> <li>Respects object size - Large objects get stricter policies</li> </ol> <pre><code>flowchart TB\n    subgraph \"Intelligent Purging Flow\"\n        Access[Pool Access] --&gt; Track[Track Usage]\n        Track --&gt; Check{Purge Trigger?}\n        Check --&gt;|Yes| Eligible{Items Eligible?}\n        Eligible --&gt;|Idle Timeout Exceeded| Purge[Purge Items]\n        Eligible --&gt;|No| Skip[Skip Purge]\n        Purge --&gt; Limit{Max Purges/Op?}\n        Limit --&gt;|Reached| Pending[Mark Pending]\n        Limit --&gt;|Not Reached| Continue[Continue]\n    end</code></pre>"},{"location":"features/utilities/pooling-guide/#quick-start","title":"Quick Start","text":""},{"location":"features/utilities/pooling-guide/#basic-usage-zero-configuration","title":"Basic Usage (Zero Configuration)","text":"<p>By default, intelligent purging is enabled with conservative settings:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Pools automatically use intelligent purging\nvar pool = new WallstopGenericPool&lt;List&lt;int&gt;&gt;(\n    createFunc: () =&gt; new List&lt;int&gt;(),\n    actionOnGet: list =&gt; list.Clear()\n);\n\n// Rent and return as usual - purging happens automatically\nvar list = pool.Get();\nlist.Add(1);\nlist.Add(2);\npool.Release(list);\n</code></pre>"},{"location":"features/utilities/pooling-guide/#disable-globally-one-liner-opt-out","title":"Disable Globally (One-Liner Opt-Out)","text":"C#<pre><code>// Disable all intelligent purging\nPoolPurgeSettings.DisableGlobally();\n</code></pre>"},{"location":"features/utilities/pooling-guide/#per-type-configuration","title":"Per-Type Configuration","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Configure specific type behavior\nPoolPurgeSettings.Configure&lt;ExpensiveObject&gt;(options =&gt;\n{\n    options.IdleTimeoutSeconds = 600f;  // 10 minutes\n    options.MinRetainCount = 5;         // Always keep 5\n    options.WarmRetainCount = 10;       // Keep 10 when active\n});\n\n// Configure all List&lt;T&gt; variants\nPoolPurgeSettings.ConfigureGeneric(typeof(List&lt;&gt;), options =&gt;\n{\n    options.IdleTimeoutSeconds = 120f;  // 2 minutes\n    options.BufferMultiplier = 1.5f;    // 50% buffer\n});\n\n// Disable purging for specific types\nPoolPurgeSettings.Disable&lt;CriticalResource&gt;();\n</code></pre>"},{"location":"features/utilities/pooling-guide/#pooloptions-configuration","title":"PoolOptions Configuration","text":"<p><code>PoolOptions&lt;T&gt;</code> provides per-pool configuration:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\nvar options = new PoolOptions&lt;MyObject&gt;\n{\n    // Size limits\n    MaxPoolSize = 100,           // Hard cap on pool size\n    MinRetainCount = 0,          // Absolute minimum to keep\n    WarmRetainCount = 2,         // Keep 2 when active\n\n    // Timing\n    IdleTimeoutSeconds = 300f,   // 5 minutes before eligible\n    PurgeIntervalSeconds = 60f,  // Periodic check interval\n\n    // Intelligent purging\n    UseIntelligentPurging = true,\n    BufferMultiplier = 2.0f,     // 2x peak usage buffer\n    RollingWindowSeconds = 300f, // 5 minute window\n    HysteresisSeconds = 120f,    // 2 minute spike cooldown\n    SpikeThresholdMultiplier = 2.5f, // 2.5x average = spike\n    MaxPurgesPerOperation = 10,  // Spread large purges\n\n    // Triggers\n    Triggers = PurgeTrigger.OnRent | PurgeTrigger.OnReturn,\n\n    // Callbacks\n    OnPurge = (item, reason) =&gt; Debug.Log($\"Purged: {reason}\")\n};\n\nvar pool = new WallstopGenericPool&lt;MyObject&gt;(\n    createFunc: () =&gt; new MyObject(),\n    options: options\n);\n</code></pre>"},{"location":"features/utilities/pooling-guide/#purgetrigger-flags","title":"PurgeTrigger Flags","text":"Trigger Description <code>OnRent</code> Check when item is rented (lazy cleanup) <code>OnReturn</code> Check when item is returned <code>Periodic</code> Timer-based checks at <code>PurgeIntervalSeconds</code> <code>Explicit</code> Only purge when <code>Purge()</code> is called manually"},{"location":"features/utilities/pooling-guide/#purgereason-values","title":"PurgeReason Values","text":"Reason Description <code>IdleTimeout</code> Item was idle longer than <code>IdleTimeoutSeconds</code> <code>CapacityExceeded</code> Pool exceeded <code>MaxPoolSize</code> <code>MemoryPressure</code> System memory pressure detected <code>AppBackgrounded</code> Application went to background <code>SceneUnloaded</code> Scene was unloaded <code>Explicit</code> Manual <code>Purge()</code> call <code>BudgetExceeded</code> Global pool budget exceeded"},{"location":"features/utilities/pooling-guide/#global-settings-poolpurgesettings","title":"Global Settings (PoolPurgeSettings)","text":"<p>Configure system-wide defaults:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Enable/disable globally\nPoolPurgeSettings.GlobalEnabled = true;\n\n// Configure defaults\nPoolPurgeSettings.DefaultGlobalIdleTimeoutSeconds = 300f;\nPoolPurgeSettings.DefaultGlobalMinRetainCount = 0;\nPoolPurgeSettings.DefaultGlobalWarmRetainCount = 2;\nPoolPurgeSettings.DefaultGlobalBufferMultiplier = 2.0f;\nPoolPurgeSettings.DefaultGlobalRollingWindowSeconds = 300f;\nPoolPurgeSettings.DefaultGlobalHysteresisSeconds = 120f;\nPoolPurgeSettings.DefaultGlobalSpikeThresholdMultiplier = 2.5f;\nPoolPurgeSettings.DefaultGlobalMaxPurgesPerOperation = 10;\n\n// Lifecycle hooks\nPoolPurgeSettings.PurgeOnLowMemory = true;       // Application.lowMemory\nPoolPurgeSettings.PurgeOnAppBackground = true;   // Application.focusChanged\nPoolPurgeSettings.PurgeOnSceneUnload = true;     // SceneManager.sceneUnloaded\n</code></pre>"},{"location":"features/utilities/pooling-guide/#retention-model","title":"Retention Model","text":"<p>The system uses a two-tier retention model:</p> <ul> <li>MinRetainCount: Absolute floor. Pool never purges below this, even when completely idle.</li> <li>WarmRetainCount: Floor for \"active\" pools (accessed within IdleTimeoutSeconds). Prevents cold-start allocations.</li> </ul> Text Only<pre><code>Effective Floor = max(MinRetainCount, isActive ? WarmRetainCount : 0)\n</code></pre> <p>Example:</p> <ul> <li><code>MinRetainCount = 0</code>, <code>WarmRetainCount = 2</code></li> <li>Active pool: keeps at least 2 items warm</li> <li>Idle pool (no access for IdleTimeoutSeconds): can purge to 0</li> </ul>"},{"location":"features/utilities/pooling-guide/#eviction-policies","title":"Eviction Policies","text":""},{"location":"features/utilities/pooling-guide/#comfortable-size-calculation","title":"Comfortable Size Calculation","text":"<p>The \"comfortable size\" determines when purging is needed:</p> Text Only<pre><code>ComfortableSize = max(EffectiveMinRetain, RollingHighWaterMark * BufferMultiplier)\n</code></pre> <p>Items that have been idle longer than <code>IdleTimeoutSeconds</code> are purged regardless of comfortable size. The comfortable size primarily influences the target retention during non-idle purges and memory pressure events.</p>"},{"location":"features/utilities/pooling-guide/#hysteresis-protection","title":"Hysteresis Protection","text":"<p>After a usage spike, purging is suppressed for <code>HysteresisSeconds</code> to prevent purge-allocate cycles:</p> <pre><code>sequenceDiagram\n    participant App as Application\n    participant Pool as Pool\n\n    Note over App,Pool: Normal usage period\n    App-&gt;&gt;Pool: Get items (low volume)\n    Pool-&gt;&gt;Pool: Track high-water mark\n\n    Note over App,Pool: Usage spike detected\n    App-&gt;&gt;Pool: Get many items rapidly\n    Pool-&gt;&gt;Pool: Spike! Start hysteresis\n\n    Note over App,Pool: Hysteresis period (2 min default)\n    Pool-&gt;&gt;Pool: Purging suppressed\n\n    Note over App,Pool: After hysteresis\n    Pool-&gt;&gt;Pool: Resume normal purging</code></pre>"},{"location":"features/utilities/pooling-guide/#gradual-purging","title":"Gradual Purging","text":"<p>Large purge operations are spread across multiple calls:</p> C#<pre><code>// Configure max items purged per operation\noptions.MaxPurgesPerOperation = 10;\n\n// Pool tracks pending purges\nif (pool.HasPendingPurges)\n{\n    // More items to purge on next trigger\n}\n\n// Force immediate full purge (bypasses limit)\npool.ForceFullPurge();\n</code></pre>"},{"location":"features/utilities/pooling-guide/#memory-pressure-detection","title":"Memory Pressure Detection","text":"<p>The system monitors memory pressure and adjusts purging aggressiveness:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Check current pressure level\nMemoryPressureLevel level = MemoryPressureMonitor.CurrentPressure;\n\nswitch (level)\n{\n    case MemoryPressureLevel.None:\n        // Normal operation\n        break;\n    case MemoryPressureLevel.Low:\n        // Minor pressure, slightly more aggressive\n        break;\n    case MemoryPressureLevel.Medium:\n        // Moderate pressure, reduced buffers\n        break;\n    case MemoryPressureLevel.High:\n        // Significant pressure, aggressive purging\n        break;\n    case MemoryPressureLevel.Critical:\n        // Emergency cleanup, bypass limits\n        break;\n}\n</code></pre>"},{"location":"features/utilities/pooling-guide/#pressure-detection-sources","title":"Pressure Detection Sources","text":"Metric Threshold Absolute Memory Managed heap exceeds threshold GC Collection Rate Frequent GC collections detected Memory Growth Rate Rapid memory increase Application.lowMemory Unity's low memory callback"},{"location":"features/utilities/pooling-guide/#size-aware-policies","title":"Size-Aware Policies","text":"<p>Large objects (allocated on the Large Object Heap) get stricter policies:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Enable size-aware policies\nPoolPurgeSettings.SizeAwarePoliciesEnabled = true;\n\n// Configure thresholds\nPoolPurgeSettings.LargeObjectThresholdBytes = 85000;  // .NET LOH threshold\nPoolPurgeSettings.LargeObjectBufferMultiplier = 1.0f; // No buffer (vs 2.0x)\nPoolPurgeSettings.LargeObjectIdleTimeoutMultiplier = 0.5f; // 50% shorter\nPoolPurgeSettings.LargeObjectWarmRetainCount = 1;     // Keep 1 (vs 2)\n</code></pre>"},{"location":"features/utilities/pooling-guide/#poolsizeestimator","title":"PoolSizeEstimator","text":"<p>Estimate object sizes for policy decisions:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Estimate single item size\nlong size = PoolSizeEstimator.EstimateItemSizeBytes&lt;MyLargeObject&gt;();\n\n// Estimate array size\nlong arraySize = PoolSizeEstimator.EstimateArraySizeBytes&lt;byte&gt;(length: 100000);\n\n// Check if on LOH\nbool isLargeObject = size &gt;= PoolPurgeSettings.LargeObjectThresholdBytes;\n</code></pre>"},{"location":"features/utilities/pooling-guide/#access-frequency-tracking","title":"Access Frequency Tracking","text":"<p>Pools track access patterns for intelligent decisions:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Get frequency statistics\nPoolFrequencyStatistics stats = pool.FrequencyStatistics;\n\n// Access metrics\nfloat rentalsPerMinute = stats.RentalsPerMinute;\nfloat avgInterRentalTime = stats.AverageInterRentalTimeSeconds;\nfloat lastAccess = stats.LastAccessTime;\n\n// Helper properties\nbool isHighFrequency = stats.IsHighFrequency;  // &gt; 60 rentals/min\nbool isLowFrequency = stats.IsLowFrequency;    // &lt;= 1 rental/min\nbool isUnused = stats.IsUnused;                // No recent access\n</code></pre>"},{"location":"features/utilities/pooling-guide/#application-lifecycle-hooks","title":"Application Lifecycle Hooks","text":"<p>The system responds to application lifecycle events:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Configure lifecycle responses\nPoolPurgeSettings.PurgeOnLowMemory = true;     // Application.lowMemory\nPoolPurgeSettings.PurgeOnAppBackground = true; // Application loses focus\nPoolPurgeSettings.PurgeOnSceneUnload = true;   // Scene unloaded\n</code></pre>"},{"location":"features/utilities/pooling-guide/#mobile-considerations","title":"Mobile Considerations","text":"<p>On mobile platforms:</p> <ul> <li>App backgrounded: Aggressive purge to reduce memory footprint</li> <li>Low memory: Emergency purge, bypasses gradual limits</li> <li>Scene unload: Clean up scene-specific pools</li> </ul>"},{"location":"features/utilities/pooling-guide/#global-pool-registry","title":"Global Pool Registry","text":"<p>Track and manage all pools system-wide:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\n\n// Configure global budget\nPoolPurgeSettings.GlobalMaxPooledItems = 50000;\n\n// Get global statistics\nGlobalPoolStatistics globalStats = GlobalPoolRegistry.GetStatistics();\nint totalPooled = globalStats.TotalPooledItems;\nfloat budgetUtilization = globalStats.BudgetUtilization;\nint registeredPools = globalStats.RegisteredPoolCount;\n\n// Force budget enforcement\nGlobalPoolRegistry.EnforceBudget();\n\n// Try non-blocking budget check\nif (GlobalPoolRegistry.TryEnforceBudgetIfNeeded())\n{\n    // Budget was over, items purged\n}\n</code></pre>"},{"location":"features/utilities/pooling-guide/#lru-cross-pool-eviction","title":"LRU Cross-Pool Eviction","text":"<p>When the global budget is exceeded, items are evicted across all pools using LRU ordering based on pool access times.</p>"},{"location":"features/utilities/pooling-guide/#best-practices","title":"Best Practices","text":""},{"location":"features/utilities/pooling-guide/#configuration-hierarchy","title":"Configuration Hierarchy","text":"<p>Settings are resolved in priority order:</p> <ol> <li>Per-instance PoolOptions (highest priority)</li> <li>Programmatic type configuration (<code>PoolPurgeSettings.Configure&lt;T&gt;</code>)</li> <li>Generic type pattern (<code>PoolPurgeSettings.ConfigureGeneric</code>)</li> <li>Attribute-based (<code>[PoolPurgePolicy]</code> on type)</li> <li>Settings asset configuration</li> <li>Built-in type defaults</li> <li>Global defaults (lowest priority)</li> </ol>"},{"location":"features/utilities/pooling-guide/#type-specific-recommendations","title":"Type-Specific Recommendations","text":"C#<pre><code>// Short-lived temporary collections\nPoolPurgeSettings.Configure&lt;List&lt;int&gt;&gt;(o =&gt;\n{\n    o.IdleTimeoutSeconds = 60f;\n    o.WarmRetainCount = 5;\n});\n\n// Long-lived expensive objects\nPoolPurgeSettings.Configure&lt;AudioSource&gt;(o =&gt;\n{\n    o.IdleTimeoutSeconds = 600f;\n    o.MinRetainCount = 2;\n    o.WarmRetainCount = 4;\n});\n\n// Large buffers (be aggressive)\nPoolPurgeSettings.Configure&lt;byte[]&gt;(o =&gt;\n{\n    o.IdleTimeoutSeconds = 30f;\n    o.BufferMultiplier = 1.0f;\n    o.WarmRetainCount = 1;\n});\n</code></pre>"},{"location":"features/utilities/pooling-guide/#performance-tips","title":"Performance Tips","text":"<ol> <li>Use gradual purging - Default <code>MaxPurgesPerOperation = 10</code> prevents GC spikes</li> <li>Size buffers appropriately - 2x buffer is conservative, 1.5x for memory-constrained</li> <li>Monitor frequency stats - Use <code>FrequencyStatistics</code> to tune per-type settings</li> <li>Enable size-aware policies - Large objects need stricter handling</li> <li>Use lifecycle hooks - Let the system handle mobile backgrounding</li> </ol>"},{"location":"features/utilities/pooling-guide/#debugging","title":"Debugging","text":"C#<pre><code>// Log purge events\nvar options = new PoolOptions&lt;MyObject&gt;\n{\n    OnPurge = (item, reason) =&gt;\n    {\n        Debug.Log($\"[Pool] Purged {typeof(MyObject).Name}: {reason}\");\n    }\n};\n\n// Check global stats periodically\nvoid OnGUI()\n{\n    var stats = GlobalPoolRegistry.Statistics;\n    GUILayout.Label($\"Pools: {stats.RegisteredPoolCount}\");\n    GUILayout.Label($\"Items: {stats.TotalPooledItems}/{PoolPurgeSettings.GlobalMaxPooledItems}\");\n    GUILayout.Label($\"Budget: {stats.BudgetUtilization:P0}\");\n}\n</code></pre>"},{"location":"features/utilities/pooling-guide/#related-documentation","title":"Related Documentation","text":"<ul> <li>Data Structures - Cache and other collections</li> <li>Helper Utilities - Coroutine wait pools (Buffers)</li> <li>Editor Tools Guide - Project settings</li> </ul>"},{"location":"features/utilities/random-generators/","title":"Random Number Generators","text":"<p>TL;DR: Use <code>PRNG.Instance</code> for 10-15x faster random generation than <code>UnityEngine.Random</code>, with a rich API for vectors, colors, weighted selection, and more.</p>"},{"location":"features/utilities/random-generators/#overview","title":"Overview","text":"<p>Unity Helpers provides 15+ high-performance pseudo-random number generators (PRNGs) through a unified <code>IRandom</code> interface. All generators pass standard statistical tests and are optimized for game development workloads.</p>"},{"location":"features/utilities/random-generators/#key-features","title":"Key Features","text":"<ul> <li>10-15x faster than <code>UnityEngine.Random</code> (see benchmarks)</li> <li>Thread-safe access via <code>PRNG.Instance</code> (thread-local)</li> <li>Rich API \u2014 vectors, colors, Gaussian distributions, weighted selection, subset sampling</li> <li>Seedable \u2014 reproducible results for replays and testing</li> <li>IL2CPP compatible \u2014 no reflection, AOT-safe</li> </ul>"},{"location":"features/utilities/random-generators/#quick-start-60-seconds","title":"Quick Start (60 Seconds)","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Core.Random;\n\n// Use the thread-local default (fastest)\nIRandom random = PRNG.Instance;\n\n// Basic generation\nint number = random.Next(0, 100);           // [0, 100)\nfloat value = random.NextFloat();            // [0.0, 1.0)\nbool coinFlip = random.NextBool();\nuint bits = random.NextUint();\n\n// Unity vectors\nVector2 point2D = random.NextVector2(-10f, 10f);\n\n// Colors\nColor randomColor = random.NextColor();\n\n// Weighted selection\nstring[] items = { \"Common\", \"Rare\", \"Epic\" };\nfloat[] weights = { 70f, 25f, 5f };\nstring selected = random.NextWeighted(items.Zip(weights, (x, y) =&gt; (x, y)));\n\n// Gaussian distribution\nfloat normalValue = random.NextGaussian(mean: 0f, stdDev: 1f);\n</code></pre>"},{"location":"features/utilities/random-generators/#choosing-a-generator","title":"Choosing a Generator","text":"Use Case Recommended Generator Why General gameplay <code>PRNG.Instance</code> Thread-local default, excellent quality Procedural generation <code>PcgRandom</code> Reproducible, excellent statistical properties High-throughput effects <code>SplitMix64</code> Fastest with good quality Cryptographic seeding N/A Use <code>System.Security.Cryptography</code> instead Legacy compatibility <code>UnityRandom</code> Matches <code>UnityEngine.Random</code> behavior"},{"location":"features/utilities/random-generators/#available-generators","title":"Available Generators","text":"<p>All generators implement the <code>IRandom</code> interface:</p> Generator Speed Quality Best For <code>LinearCongruentialGenerator</code> Fastest Poor Non-critical effects only <code>SplitMix64</code> Very Fast Very Good High-throughput generation <code>PcgRandom</code> Fast Excellent General purpose, seeded generation <code>IllusionFlow</code> Fast Excellent Balanced speed and quality <code>XoroShiroRandom</code> Fast Very Good Game logic, physics <code>RomuDuo</code> Fast Very Good Alternative to PCG <code>XorShiftRandom</code> Moderate Fair Legacy compatibility <code>WyRandom</code> Moderate Very Good Hash-based scenarios <code>SquirrelRandom</code> Moderate Good Noise-based generation <code>PhotonSpinRandom</code> Slow Excellent Maximum quality needed <code>UnityRandom</code> Slow Fair Match Unity behavior <code>SystemRandom</code> Very Slow Poor .NET compatibility <p>For detailed benchmarks, see Random Performance.</p>"},{"location":"features/utilities/random-generators/#creating-seeded-generators","title":"Creating Seeded Generators","text":"<p>For reproducible sequences (replays, procedural generation, testing):</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Random;\n\n// Create with specific seed\nPcgRandom seeded = new PcgRandom(seed: 12345);\n\n// Generate reproducible sequence\nfor (int i = 0; i &lt; 10; i++)\n{\n    Debug.Log(seeded.Next(0, 100)); // Same values every run\n}\n\n// Different seed = different sequence\nPcgRandom different = new PcgRandom(seed: 67890);\n</code></pre>"},{"location":"features/utilities/random-generators/#api-reference","title":"API Reference","text":""},{"location":"features/utilities/random-generators/#basic-generation","title":"Basic Generation","text":"C#<pre><code>IRandom random = PRNG.Instance;\n\n// Integers\nint value = random.Next();                    // [int.MinValue, int.MaxValue]\nint bounded = random.Next(100);               // [0, 100)\nint ranged = random.Next(10, 50);             // [10, 50)\n\n// Unsigned integers\nuint bits = random.NextUint();\nuint boundedUint = random.NextUint(1000u);\n\n// Floating point\nfloat f = random.NextFloat();                 // [0.0, 1.0)\nfloat rangedF = random.NextFloat(-1f, 1f);    // [-1.0, 1.0)\ndouble d = random.NextDouble();               // [0.0, 1.0)\n\n// Boolean\nbool b = random.NextBool();                   // 50% true/false\nbool weighted = random.NextBool(0.75f);       // 75% true\n</code></pre>"},{"location":"features/utilities/random-generators/#vector-generation","title":"Vector Generation","text":"C#<pre><code>// 2D vectors\nVector2 v2 = random.NextVector2();                      // Each component [0, 1)\nVector2 ranged2 = random.NextVector2(-10f, 10f);        // Each component [-10, 10)\n\n// 3D vectors\nVector3 v3 = random.NextVector3();\nVector3 ranged3 = random.NextVector3(-5f, 5f);\n</code></pre>"},{"location":"features/utilities/random-generators/#color-generation","title":"Color Generation","text":"C#<pre><code>// Random colors\nColor c = random.NextColor();                           // Random RGBA\n</code></pre>"},{"location":"features/utilities/random-generators/#distributions","title":"Distributions","text":"C#<pre><code>// Gaussian (normal) distribution\nfloat gaussian = random.NextGaussian(mean: 0f, stdDev: 1f);\n\n// Weighted selection\nstring[] items = { \"Common\", \"Rare\", \"Epic\", \"Legendary\" };\nfloat[] weights = { 60f, 25f, 12f, 3f };\nstring drop = random.NextWeighted(items.Zip(weights, (x, y) =&gt; (x, y)));\n</code></pre>"},{"location":"features/utilities/random-generators/#collection-operations","title":"Collection Operations","text":"C#<pre><code>// Shuffle in place\nmyList.Shuffle(random);\n\n// Random element\nT element = random.NextOf(array);\nT element2 = random.NextOf(list);\n\n// Random index\nint index = random.Next(collection.Count);\n</code></pre>"},{"location":"features/utilities/random-generators/#thread-safety","title":"Thread Safety","text":"<p><code>PRNG.Instance</code> provides thread-local instances, making it safe for multithreaded code without locks:</p> C#<pre><code>// Safe - each thread gets its own instance\nParallel.For(0, 1000, i =&gt;\n{\n    int value = PRNG.Instance.Next(0, 100);\n    // No race conditions\n});\n</code></pre> <p>For explicit thread-local control:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Random;\n\n// Create thread-local wrapper around any generator\nThreadLocalRandom&lt;PcgRandom&gt; threadLocal = new();\nIRandom random = threadLocal.Value; // Per-thread instance\n</code></pre>"},{"location":"features/utilities/random-generators/#perlin-noise","title":"Perlin Noise","text":"<p>For procedural generation, use the seedable Perlin noise generator:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Random;\n\nPerlinNoise noise = new PerlinNoise(seed: 42);\n\n// 2D noise (terrain, textures)\nfloat value2D = noise.Noise(x, y);\n\n// Octave noise for more detail\nfloat octaves = noise.OctaveNoise(x, y, octaves: 4, persistence: 0.5f);\n</code></pre>"},{"location":"features/utilities/random-generators/#best-practices","title":"Best Practices","text":"<ol> <li>Use <code>PRNG.Instance</code> for most cases \u2014 it's fast, thread-safe, and well-tested</li> <li>Seed generators explicitly when reproducibility matters (replays, tests)</li> <li>Avoid <code>new</code> in hot paths \u2014 cache generator instances</li> <li>Don't use for security \u2014 these are PRNGs, not CSPRNGs</li> </ol> C#<pre><code>// \u2705 Good - cache the reference\nprivate IRandom _random = PRNG.Instance;\n\nvoid Update()\n{\n    float value = _random.NextFloat();\n}\n\n// \u274c Bad - creates new instance every frame\nvoid Update()\n{\n    PcgRandom random = new PcgRandom(); // Allocation!\n    float value = random.NextFloat();\n}\n</code></pre>"},{"location":"features/utilities/random-generators/#see-also","title":"See Also","text":"<ul> <li>Random Performance Benchmarks</li> <li>Math &amp; Extensions</li> <li>README - Random Generators</li> </ul>"},{"location":"features/utilities/reflection-helpers/","title":"Reflection Helpers","text":""},{"location":"features/utilities/reflection-helpers/#reflectionhelpers-fast-safe-reflection-for-hot-paths","title":"ReflectionHelpers \u2014 Fast, Safe Reflection for Hot Paths","text":""},{"location":"features/utilities/reflection-helpers/#tldr-when-to-use","title":"TL;DR \u2014 When To Use","text":"<ul> <li>You need reflection in performance\u2011sensitive code paths but want to avoid allocations and security pitfalls.</li> <li>These helpers cache lookups, avoid boxing where possible, and expose safe, typed APIs.</li> </ul> <p>Visual</p> <p></p> <p>ReflectionHelpers is a set of utilities for high\u2011performance reflection in Unity projects. It generates and caches delegates to access fields and properties, call methods and constructors, and quickly create common collections \u2014 with safe fallbacks when dynamic IL isn\u2019t available.</p> <p>Why it exists</p> <ul> <li>Reflection is flexible but slow when used repeatedly (per\u2011frame, per\u2011object, per\u2011element).</li> <li>Standard reflection allocates (boxing, object[] argument arrays) and repeats costly lookups.</li> <li>ReflectionHelpers compiles or emits delegates once, caches them, then reuses them to remove ongoing overhead.</li> </ul> <p>What it solves</p> <ul> <li>Field/property access without per\u2011call reflection.</li> <li>Fast instance/static method invocation (boxed or strongly typed variants).</li> <li>Allocation\u2011free typed static invokers for common cases (e.g., two parameters).</li> <li>Zero\u2011allocation collection creation helpers (array/list/hash set creators, cached by element type).</li> <li>Resilient type/attribute scanning that swallows loader errors safely.</li> </ul> <p>When to use it</p> <ul> <li>Hot paths: serialization, (de)hydration, UI/inspector tooling, ECS\u2011style systems, property grids.</li> <li>Repeated reflective operations over the same members or types.</li> <li>When you can cache and reuse delegates across many calls.</li> </ul> <p>When not to use it</p> <ul> <li>One-off reflection (e.g., editor button pressed infrequently). Simpler <code>GetValue/SetValue</code> is fine.</li> <li>If you need full runtime codegen in IL2CPP/WebGL: IL emit isn\u2019t available there. ReflectionHelpers still works, but uses expression compilation or reflection fallback \u2014 benefits remain for caching and reduced allocations.</li> <li>Setting struct instance fields using boxed setters: prefer the generic ref setter to mutate the original struct (see \u201cStruct note\u201d below).</li> </ul>"},{"location":"features/utilities/reflection-helpers/#caching-strategy-overview","title":"Caching Strategy Overview","text":"<p>ReflectionHelpers now partitions cached delegates by capability strategy so that expression, dynamic-IL, and reflection fallbacks never overwrite each other. Key points:</p> <ul> <li>Strategy fingerprinting: every delegate cache entry is keyed by <code>CapabilityKey&lt;TMember&gt;</code> (member metadata + <code>ReflectionDelegateStrategy</code>). This applies to fields, properties, indexers, methods, and constructors (boxed + typed variants).</li> <li>Per-strategy blocklists: when a strategy cannot produce a delegate (e.g., IL emit disabled on IL2CPP), we record the failure in a per-cache blocklist so later calls skip unnecessary work.</li> <li>Delegate provenance: created delegates are tracked in a <code>ConditionalWeakTable&lt;Delegate, StrategyHolder&gt;</code> so diagnostics and tests can assert the producing strategy via <code>ReflectionHelpers.TryGetDelegateStrategy</code>.</li> <li>Capability overrides: <code>ReflectionHelpers.OverrideReflectionCapabilities(expressions, dynamicIl)</code> temporarily toggles expression/IL support, letting tests (or runtime feature detection) confirm that caches store independent delegates per strategy.</li> <li>Test hooks: <code>ClearFieldGetterCache</code>, <code>ClearPropertyCache</code>, <code>ClearMethodCache</code>, and <code>ClearConstructorCache</code> flush the relevant cache groups to keep unit tests deterministic.</li> <li>Fallback behaviour: if neither expressions nor dynamic IL are available, the reflection-path delegates still benefit from caching and avoid repeated argument validation/boxing.</li> </ul>"},{"location":"features/utilities/reflection-helpers/#current-implementation-summary","title":"Current Implementation Summary","text":"API Group Representative methods Primary strategy (Mono/Editor) Fallbacks (IL2CPP/WebGL/AOT) Caching Notes Field access (boxed) <code>GetFieldGetter(FieldInfo)</code>, <code>GetFieldSetter(FieldInfo)</code> Emit <code>DynamicMethod</code> IL (<code>BuildFieldGetter/SetterIL</code>) to cast/unbox target and box return <code>CreateCompiled*</code> builds expression delegates; otherwise wraps <code>FieldInfo.GetValue/SetValue</code> <code>FieldGetterCache</code>, <code>FieldSetterCache</code>, static equivalents Supports static + instance fields; struct writes box when IL emit unavailable (IL2CPP/WebGL) Field access (typed) <code>GetFieldGetter&lt;TInstance,TValue&gt;</code>, <code>GetFieldSetter&lt;TInstance,TValue&gt;</code> Emit typed <code>DynamicMethod</code> (setters use by-ref) to avoid boxing Falls back to <code>GetValue/SetValue</code> wrappers; setter fallback boxes then copies back None (callers must hold returned delegate) <code>TInstance</code> must match declaring type; fastest only where IL emit allowed Property access (boxed) <code>GetPropertyGetter(PropertyInfo)</code>, <code>GetPropertySetter(PropertyInfo)</code> Emit <code>DynamicMethod</code> (<code>Call</code>/<code>Callvirt</code>) and box value types Expression-compiled wrapper; else <code>PropertyInfo.GetValue/SetValue</code> <code>PropertyGetterCache</code>, <code>PropertySetterCache</code>, static equivalents Handles non-public accessors; fallback reintroduces boxing/allocations Property access (typed) <code>GetPropertyGetter&lt;TInstance,TValue&gt;</code>, <code>GetPropertySetter&lt;TInstance,TValue&gt;</code> Emit typed <code>DynamicMethod</code> with cast/unbox guards Direct reflection wrappers casting to <code>TValue</code> None Avoids boxing only on IL paths; static typed getter limited to static properties Method invokers (boxed) <code>GetMethodInvoker</code>, <code>GetStaticMethodInvoker</code>, <code>InvokeMethod</code> Emit <code>DynamicMethod</code> to unpack <code>object[]</code> args and box return Expression wrappers; otherwise call <code>MethodInfo.Invoke</code> directly <code>MethodInvokers</code>, <code>StaticMethodInvokers</code> Works with private members; fallback incurs reflection cost per call Method invokers (typed static) <code>GetStaticMethodInvoker&lt;\u2026&gt;</code>, <code>GetStaticActionInvoker&lt;\u2026&gt;</code> Emit <code>DynamicMethod</code> per arity (0\u20134) for direct call Try <code>MethodInfo.CreateDelegate</code>; else expression compile <code>TypedStaticInvoker0-4</code>, <code>TypedStaticAction0-4</code> Signature-checked upfront; limited to four parameters today Method invokers (typed instance) <code>GetInstanceMethodInvoker&lt;TInstance,\u2026&gt;</code>, <code>GetInstanceActionInvoker&lt;TInstance,\u2026&gt;</code> Emit <code>DynamicMethod</code> using <code>ldarga</code> for structs and <code>Callvirt</code> for refs Falls back to <code>Delegate.CreateDelegate</code> / expression lambdas <code>TypedInstanceInvoker0-4</code>, <code>TypedInstanceAction0-4</code> Requires <code>TInstance</code> assignable to declaring type; fallback boxes structs Constructors &amp; factories <code>GetConstructor</code>, <code>CreateInstance</code>, <code>GetParameterlessConstructor&lt;T&gt;</code>, <code>GetParameterlessConstructor</code> Delegate factory prefers expression lambdas, falls back to dynamic IL <code>newobj</code> and finally reflection (<code>ConstructorInfo.Invoke</code> / <code>Activator.CreateInstance</code>) Reflection invoke (no emit) <code>Constructors</code>, <code>ParameterlessConstructors</code>, <code>TypedParameterlessConstructors</code> Works across Editor/IL2CPP; capability overrides let tests force fallback paths Indexer helpers <code>GetIndexerGetter</code>, <code>GetIndexerSetter</code> Expression lambdas or dynamic IL to handle struct receivers and value conversions Reflection <code>PropertyInfo.Get/SetValue</code> with argument validation <code>IndexerGetters</code>, <code>IndexerSetters</code> Throws <code>IndexOutOfRangeException</code>/<code>InvalidCastException</code> when indices mismatch; respects capability overrides Collection creators <code>CreateArray</code>, <code>GetListCreator(Type)</code>, <code>GetDictionaryWithCapacityCreator</code> Emit <code>DynamicMethod</code> for <code>newarr</code>/<code>newobj</code>, plus <code>HashSet.Add</code> wrappers Use <code>Array.CreateInstance</code>, <code>Activator.CreateInstance</code>, or reflection <code>Invoke</code> <code>ArrayCreators</code>, <code>ListCreators</code>, <code>ListWithCapacityCreators</code>, <code>HashSetWithCapacityCreators</code>, adders <code>Create*</code> APIs cache by element type; fallback still functional but allocates Type/attribute scanning <code>GetAllLoadedAssemblies</code>, <code>GetTypesDerivedFrom&lt;T&gt;</code>, <code>HasAttributeSafe</code> Direct reflection with guarded iteration; Editor uses <code>UnityEditor.TypeCache</code> shortcuts Gracefully skips assemblies/types on error; no IL emit needed <code>TypeResolutionCache</code>, <code>FieldLookup</code>, <code>PropertyLookup</code>, <code>MethodLookup</code> Depends on link.xml or addressables to keep members under IL2CPP stripping"},{"location":"features/utilities/reflection-helpers/#current-consumers-snapshot","title":"Current Consumers Snapshot","text":"<ul> <li><code>Runtime/Core/Serialization/Serializer.cs</code> and <code>Runtime/Core/Serialization/JsonConverters/TypeConverter.cs</code> lean on static method invokers and type resolution to integrate ProtoBuf and JSON pipelines.</li> <li><code>Runtime/Core/Attributes</code> (<code>BaseRelationalComponentAttribute</code>, <code>RelationalComponentInitializer</code>, <code>WNotNullAttribute</code>) depend on field getters/setters and collection factories for relational wiring.</li> <li><code>Runtime/Tags</code> (<code>AttributeMetadataCache</code>, <code>AttributeUtilities</code>, <code>AttributeMetadataFilters</code>) use attribute scanning plus cached getters/setters to hydrate metadata tables at startup.</li> <li><code>Runtime/Core/Helper/StringInList.cs</code> and <code>Runtime/Core/Helper/Logging/UnityLogTagFormatter.cs</code> use helper invokers for dynamic lookups during logging and formatting.</li> <li><code>Editor/AnimationEventEditor.cs</code>, <code>Editor/Tags/AttributeMetadataCacheGenerator.cs</code>, and <code>Editor/Utils/ScriptableObjectSingletonCreator.cs</code> call into the helpers for TypeCache-driven discovery and editor automation.</li> <li><code>Runtime/Utils/ScriptableObjectSingleton.cs</code> relies on safe attribute retrieval to locate singleton assets without repeating reflection calls.</li> </ul>"},{"location":"features/utilities/reflection-helpers/#platform-capability-matrix","title":"Platform Capability Matrix","text":"Target Environment Unity Backend <code>DynamicMethod</code> IL Emit <code>Expression.Compile</code> ReflectionHelpers Behaviour Notes Editor (Windows/macOS/Linux) Mono / JIT \u2705 Enabled (<code>EMIT_DYNAMIC_IL</code>) \u2705 Enabled (<code>SUPPORT_EXPRESSION_COMPILE</code>) Uses IL-generated delegates for getters/setters/invokers; expression compile is a fallback if IL creation fails at runtime Same behaviour for play mode in editor; fastest path used during authoring tools and tests. Standalone Player (Mono scripting backend) Mono / JIT \u2705 Enabled \u2705 Enabled Matches editor experience; cached IL delegates provide best throughput Applies to legacy desktop Mono builds (Windows/Mac/Linux) where JIT is available. Standalone / Mobile / Console (IL2CPP) IL2CPP / AOT \u274c Disabled at compile time (<code>ENABLE_IL2CPP</code> blocks <code>EMIT_DYNAMIC_IL</code>) \u26a0\ufe0f Disabled (<code>SUPPORT_EXPRESSION_COMPILE</code> undefined; <code>CheckExpressionCompilationSupport</code> returns false) Falls back to pre-built delegate wrappers or direct <code>Invoke</code>/<code>GetValue</code> with caching; still avoids repeated reflection lookups Covers Windows/macOS/iOS/Android/Consoles when built with IL2CPP. Requires link.xml (or addressables) to preserve reflected members. WebGL Player IL2CPP / AOT (wasm) \u274c Disabled (<code>UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</code>) \u26a0\ufe0f Disabled Uses expression-free reflection paths identical to IL2CPP builds; object boxing unavoidable for struct setters/invokers WebGL disallows runtime codegen; helpers rely on cached reflection only. Burst-compiled jobs Burst \u274c Not permitted \u274c Not permitted ReflectionHelpers should not be called from Burst jobs; wrap calls on main thread or use precomputed data Burst forbids managed reflection; guard usage with <code>Unity.Burst.NoAlias</code> patterns or pre-bake data. Server builds / headless (Mono) Mono / JIT \u2705 Enabled \u2705 Enabled Same as desktop Mono path; suitable for dedicated servers running on JIT Confirm <code>EMIT_DYNAMIC_IL</code> stays enabled unless IL2CPP server build is selected. Continuous Integration Any Depends on selected backend Depends on backend Benchmarks skip doc writes when <code>Helpers.IsRunningInContinuousIntegration</code> is true, but helpers themselves behave per backend Use automated tests to validate both IL2CPP fallback and Mono fast paths. <ul> <li><code>DynamicMethod</code> support is controlled at compile time by <code>#if !((UNITY_WEBGL &amp;&amp; !UNITY_EDITOR) || ENABLE_IL2CPP)</code> in <code>ReflectionHelpers.cs</code>.</li> <li><code>Expression.Compile</code> support is gated by the same define; the runtime guard <code>CheckExpressionCompilationSupport()</code> prevents usage when the platform forbids JIT compilation even if the symbols are present.</li> <li><code>SINGLE_THREADED</code> builds remove <code>System.Collections.Concurrent</code> usage and swap to simple dictionaries; this is rarely needed but remains AOT-friendly for constrained platforms.</li> </ul> <p>Key APIs at a glance</p> <ul> <li>Fields</li> <li><code>GetFieldGetter(FieldInfo)</code> \u2192 <code>Func&lt;object, object&gt;</code></li> <li><code>GetFieldSetter(FieldInfo)</code> \u2192 <code>Action&lt;object, object&gt;</code></li> <li><code>GetFieldGetter&lt;TInstance, TValue&gt;(FieldInfo)</code> \u2192 <code>Func&lt;TInstance, TValue&gt;</code></li> <li><code>GetFieldSetter&lt;TInstance, TValue&gt;(FieldInfo)</code> \u2192 <code>FieldSetter&lt;TInstance, TValue&gt;</code> (ref setter)</li> <li><code>GetStaticFieldGetter&lt;T&gt;(FieldInfo)</code> / <code>GetStaticFieldSetter&lt;T&gt;(FieldInfo)</code></li> <li>Properties</li> <li><code>GetPropertyGetter(PropertyInfo)</code> / <code>GetPropertySetter(PropertyInfo)</code> (boxed)</li> <li><code>GetPropertyGetter&lt;TInstance, TValue&gt;(PropertyInfo)</code> (typed)</li> <li><code>GetStaticPropertyGetter&lt;T&gt;(PropertyInfo)</code></li> <li>Methods and constructors</li> <li><code>GetMethodInvoker(MethodInfo)</code> / <code>GetStaticMethodInvoker(MethodInfo)</code> (boxed)</li> <li><code>GetStaticMethodInvoker&lt;TReturn&gt;(MethodInfo)</code>, <code>GetStaticMethodInvoker&lt;T1, TReturn&gt;(MethodInfo)</code>, <code>GetStaticMethodInvoker&lt;T1, T2, TReturn&gt;(MethodInfo)</code>, <code>GetStaticMethodInvoker&lt;T1, T2, T3, TReturn&gt;(MethodInfo)</code>, <code>GetStaticMethodInvoker&lt;T1, T2, T3, T4, TReturn&gt;(MethodInfo)</code> (typed)</li> <li><code>GetStaticActionInvoker(...)</code> arities 0\u20134 (typed, void return)</li> <li><code>GetInstanceMethodInvoker&lt;TInstance, ...&gt;(MethodInfo)</code> and <code>GetInstanceActionInvoker&lt;TInstance, ...&gt;(MethodInfo)</code> arities 0\u20134</li> <li><code>GetConstructor(ConstructorInfo)</code> (boxed) and <code>GetParameterlessConstructor&lt;T&gt;()</code></li> <li><code>CreateInstance&lt;T&gt;(params object[])</code> and generic type construction helpers</li> <li>Collections</li> <li><code>CreateArray(Type, int)</code>; <code>GetArrayCreator(Type)</code></li> <li>Typed creators: <code>GetArrayCreator&lt;T&gt;()</code>, <code>GetListCreator&lt;T&gt;()</code>, <code>GetListWithCapacityCreator&lt;T&gt;()</code>, <code>GetHashSetWithCapacityCreator&lt;T&gt;()</code></li> <li><code>CreateList(Type)</code> / <code>CreateList(Type, int)</code>; <code>GetListCreator(Type)</code>; <code>GetListWithCapacityCreator(Type)</code></li> <li><code>CreateHashSet(Type, int)</code>; <code>GetHashSetWithCapacityCreator(Type)</code>; <code>GetHashSetAdder(Type)</code>; typed adder <code>GetHashSetAdder&lt;T&gt;()</code></li> <li><code>CreateDictionary(Type, Type, int)</code>; <code>GetDictionaryWithCapacityCreator(Type, Type)</code>; <code>GetDictionaryCreator&lt;TKey, TValue&gt;()</code></li> <li>Scanning and attributes</li> <li><code>GetAllLoadedAssemblies()</code> / <code>GetAllLoadedTypes()</code></li> <li>Safe attribute helpers: <code>HasAttributeSafe</code>, <code>GetAttributeSafe</code>, <code>GetAllAttributesSafe</code>, etc.</li> <li>Indexers</li> <li><code>GetIndexerGetter(PropertyInfo)</code> and <code>GetIndexerSetter(PropertyInfo)</code></li> <li>Unity</li> <li><code>IsComponentEnabled&lt;T&gt;(T)</code> and <code>IsActiveAndEnabled&lt;T&gt;(T)</code></li> </ul> <p>Usage examples</p> <ol> <li>Fast field get/set (boxed)</li> </ol> C#<pre><code>public sealed class Player { public int Score; }\n\nFieldInfo score = typeof(Player).GetField(\"Score\");\nvar getScore = ReflectionHelpers.GetFieldGetter(score);     // object -&gt; object\nvar setScore = ReflectionHelpers.GetFieldSetter(score);     // (object, object) -&gt; void\n\nvar p = new Player();\nsetScore(p, 42);\nUnityEngine.Debug.Log((int)getScore(p)); // 42\n</code></pre> <ol> <li>Struct note: use typed ref setter</li> </ol> C#<pre><code>public struct Stat { public int Value; }\nFieldInfo valueField = typeof(Stat).GetField(\"Value\");\n\n// Prefer typed ref setter for structs\nvar setValue = ReflectionHelpers.GetFieldSetter&lt;Stat, int&gt;(valueField);\nStat s = default;\nsetValue(ref s, 100);\n// s.Value == 100\n</code></pre> <ol> <li>Typed property getter</li> </ol> C#<pre><code>var prop = typeof(Camera).GetProperty(\"orthographicSize\");\nvar getSize = ReflectionHelpers.GetPropertyGetter&lt;Camera, float&gt;(prop);\nfloat size = getSize(UnityEngine.Camera.main);\n</code></pre> <ol> <li>Typed property setter (variant)</li> </ol> C#<pre><code>var prop = typeof(TestPropertyClass).GetProperty(\"InstanceProperty\");\nvar set = ReflectionHelpers.GetPropertySetter&lt;TestPropertyClass, int&gt;(prop);\nvar obj = new TestPropertyClass();\nset(obj, 10);\n</code></pre> <ol> <li>Fast static method invoker (two params, typed)</li> </ol> C#<pre><code>MethodInfo concat = typeof(string).GetMethod(\n    nameof(string.Concat), new[] { typeof(string), typeof(string) }\n);\nvar concat2 = ReflectionHelpers.GetStaticMethodInvoker&lt;string, string, string&gt;(concat);\nstring joined = concat2(\"Hello \", \"World\");\n</code></pre> <ol> <li>Low\u2011allocation constructors</li> </ol> C#<pre><code>// Parameterless constructor\nvar newList = ReflectionHelpers.GetParameterlessConstructor&lt;List&lt;int&gt;&gt;();\nList&lt;int&gt; list = newList();\n\n// Constructor via ConstructorInfo\nConstructorInfo ci = typeof(Dictionary&lt;string, int&gt;)\n    .GetConstructor(new[] { typeof(int) });\nvar ctor = ReflectionHelpers.GetConstructor(ci);\nvar dict = (Dictionary&lt;string, int&gt;)ctor(new object[] { 128 });\n</code></pre> <ol> <li>Collection creators and HashSet adder</li> </ol> C#<pre><code>var makeArray = ReflectionHelpers.GetArrayCreator(typeof(Vector3));\nArray positions = makeArray(256); // Vector3[256]\n\nIList names = ReflectionHelpers.CreateList(typeof(string), 64); // List&lt;string&gt;\n\nobject set = ReflectionHelpers.CreateHashSet(typeof(int), 0); // HashSet&lt;int&gt;\nvar add = ReflectionHelpers.GetHashSetAdder(typeof(int));\nadd(set, 1);\nadd(set, 1);\nadd(set, 2);\n// set contains {1, 2}\n</code></pre> <ol> <li>Typed collection creators</li> </ol> C#<pre><code>var makeArrayT = ReflectionHelpers.GetArrayCreator&lt;int&gt;();\nint[] ints = makeArrayT(128);\n\nvar makeListT = ReflectionHelpers.GetListCreator&lt;string&gt;();\nIList strings = makeListT();\n\nvar makeSetT = ReflectionHelpers.GetHashSetWithCapacityCreator&lt;int&gt;();\nHashSet&lt;int&gt; intsSet = makeSetT(64);\nvar addT = ReflectionHelpers.GetHashSetAdder&lt;int&gt;();\naddT(intsSet, 5);\n</code></pre> <ol> <li>Safe attribute scanning</li> </ol> C#<pre><code>bool hasObsolete = ReflectionHelpers.HasAttributeSafe&lt;ObsoleteAttribute&gt;(typeof(MyComponent));\nvar values = ReflectionHelpers.GetAllAttributeValuesSafe(typeof(MyComponent));\n// e.g., values[\"Obsolete\"] -&gt; ObsoleteAttribute instance\n</code></pre> <p>Performance tips</p> <ul> <li>Cache delegates (getters/setters/invokers) once and reuse them.</li> <li>Prefer typed APIs (<code>GetFieldGetter&lt;TInstance, TValue&gt;</code>, typed static invokers) to avoid boxing and object[] allocations.</li> <li>Use creators (<code>GetListCreator</code>, <code>GetArrayCreator</code>) in loops to avoid reflection/Activator costs.</li> </ul>"},{"location":"features/utilities/reflection-helpers/#benchmarking-verification","title":"Benchmarking &amp; Verification","text":"<ul> <li>Unit coverage: <code>ReflectionHelperCapabilityMatrixTests</code> resets caches and toggles capabilities around each helper. Run these suites in both expression-enabled and expression-disabled modes when changing caching internals.</li> <li>Micro-benchmarks: Use <code>Tests/Runtime/Performance/ReflectionPerformanceTests</code> to capture before/after numbers for getters, setters, method invokers, and constructors (now including expression vs. dynamic IL comparisons). Record results with each <code>ReflectionDelegateStrategy</code> forced via <code>OverrideReflectionCapabilities</code> so regressions are easy to spot.</li> <li>Cache hygiene: when adding new delegate families, update the appropriate <code>Clear*Cache</code> helper and call it from tests to keep scenarios isolated.</li> <li>Documentation updates: note the Unity version, scripting backend, and OS whenever you refresh timing data, and sync any tables in the Reflection Performance docs so contributors can compare against baseline numbers.</li> <li>Execution recipe:</li> <li>Run <code>Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests</code> twice\u2014once normally and once with <code>REFLECTION_HELPERS_FORCE_REFLECTION=1</code> (or by wrapping the suite in <code>OverrideReflectionCapabilities(false, false)</code>) to cover accelerated and fallback paths.</li> <li>Export raw benchmark data by running the <code>ReflectionPerformanceTests</code> category inside the Unity Test Runner with <code>LogFullResults</code> enabled; copy the markdown summary into the Reflection Performance benchmarks.</li> <li>Validate editor/runtime builds (Mono + IL2CPP) to ensure blocklists behave consistently across backends.</li> </ul>"},{"location":"features/utilities/reflection-helpers/#testing-fallback-behaviour","title":"Testing fallback behaviour","text":"<p>When you need to validate the pure-reflection paths (for example, to mimic IL2CPP/WebGL behaviour), override the runtime capability probes inside a <code>using</code> scope:</p> C#<pre><code>using (ReflectionHelpers.OverrideReflectionCapabilities(expressions: false, dynamicIl: false))\n{\n    // Force expression + IL emit to be unavailable\n    Func&lt;TestConstructorClass&gt; ctor = ReflectionHelpers.GetParameterlessConstructor&lt;TestConstructorClass&gt;();\n    TestConstructorClass instance = ctor(); // Uses reflection fallback\n\n    PropertyInfo indexer = typeof(IndexerClass).GetProperty(\"Item\");\n    var getter = ReflectionHelpers.GetIndexerGetter(indexer);\n    var setter = ReflectionHelpers.GetIndexerSetter(indexer);\n    setter(new IndexerClass(), 42, new object[] { 0 }); // reflection-based path\n}\n</code></pre> <p>The helper restores the original capability state when disposed, so nested overrides remain safe. Runtime regression tests now cover constructors and indexers in both accelerated and fallback modes.</p>"},{"location":"features/utilities/reflection-helpers/#il2cppwebgl-notes","title":"IL2CPP/WebGL notes","text":"<ul> <li>Dynamic IL emit is disabled on IL2CPP/WebGL; ReflectionHelpers automatically falls back to expression compilation or direct reflection where necessary.</li> <li>Caching still reduces overhead significantly, even without IL emit.</li> </ul>"},{"location":"features/utilities/reflection-helpers/#il2cpp-code-stripping-considerations","title":"\u26a0\ufe0f IL2CPP Code Stripping Considerations","text":"<p>Important for IL2CPP builds (WebGL, iOS, Android, Consoles):</p> <p>While ReflectionHelpers itself is IL2CPP-safe, Unity's managed code stripping may remove types or members you're trying to access via reflection. This affects any reflection-based code, not just ReflectionHelpers.</p> <p>Symptoms of stripping issues:</p> <ul> <li><code>TypeLoadException</code> or <code>NullReferenceException</code> when calling <code>Type.GetType()</code></li> <li><code>FieldInfo</code> or <code>MethodInfo</code> returns null for members that exist in the Editor</li> <li>\"Type not found\" or \"Member not found\" errors in IL2CPP builds</li> <li>Works in Editor/Development, fails in Release builds</li> </ul>"},{"location":"features/utilities/reflection-helpers/#solution-use-linkxml-to-preserve-reflected-types","title":"Solution: Use link.xml to preserve reflected types","text":"<p>Create a <code>link.xml</code> file in your <code>Assets</code> folder:</p> XML<pre><code>&lt;linker&gt;\n  &lt;!-- Preserve types you access via reflection --&gt;\n  &lt;assembly fullname=\"Assembly-CSharp\"&gt;\n    &lt;!-- Preserve entire type and all members --&gt;\n    &lt;type fullname=\"MyNamespace.MyReflectedClass\" preserve=\"all\"/&gt;\n\n    &lt;!-- Or preserve specific members --&gt;\n    &lt;type fullname=\"MyNamespace.AnotherClass\"&gt;\n      &lt;method signature=\"System.Void DoSomething()\" /&gt;\n      &lt;field name=\"importantField\" /&gt;\n      &lt;property name=\"ImportantProperty\" /&gt;\n    &lt;/type&gt;\n\n    &lt;!-- Preserve all types in a namespace --&gt;\n    &lt;namespace fullname=\"MyNamespace.ReflectedTypes\" preserve=\"all\"/&gt;\n  &lt;/assembly&gt;\n&lt;/linker&gt;\n</code></pre> <p>Best practices:</p> <ul> <li>\u2705 Test IL2CPP builds regularly - Stripping only occurs in Release builds</li> <li>\u2705 Preserve all types accessed via string names - <code>Type.GetType(\"MyType\")</code> requires link.xml</li> <li>\u2705 Check build logs - Unity logs which types are stripped during the build</li> <li>\u2705 Use <code>typeof()</code> when possible - Direct type references prevent stripping without link.xml</li> <li>\u2705 Test on target platform - Stripping behavior differs across platforms</li> </ul> <p>Examples of code that needs link.xml:</p> C#<pre><code>// \u274c Requires link.xml: Type accessed by name\nType t = Type.GetType(\"MyNamespace.MyClass\");\n\n// \u2705 Safer: Direct type reference\nType t = typeof(MyClass);\n\n// \u274c Requires link.xml: Field accessed by name\nFieldInfo field = typeof(MyClass).GetField(\"myField\", BindingFlags.NonPublic);\n\n// \u2705 Safer: If field is definitely there, link.xml ensures it won't be stripped\n</code></pre> <p>When ReflectionHelpers doesn't need link.xml:</p> <ul> <li>Accessing Unity built-in types (they're never stripped)</li> <li>Using generic type parameters (<code>GetFieldGetter&lt;MyClass, int&gt;()</code> prevents stripping of MyClass)</li> <li>Accessing types that are directly referenced elsewhere in code</li> </ul> <p>Thread\u2011safety</p> <ul> <li>Caches use thread\u2011safe dictionaries by default. A <code>SINGLE_THREADED</code> build flag switches to regular dictionaries for very constrained environments.</li> </ul> <p>Common pitfalls</p> <ul> <li>Passing a non\u2011static <code>FieldInfo</code>/<code>PropertyInfo</code> to static getters/setters will throw clear <code>ArgumentException</code>s.</li> <li>Read\u2011only properties do not have setters; using <code>GetPropertySetter</code> on those throws.</li> <li>Struct instance field writes require the generic ref setter (<code>FieldSetter&lt;TInstance, TValue&gt;</code>) to mutate the original struct.</li> <li>Typed method invokers do not support <code>ref</code>/<code>out</code> parameters and throw <code>NotSupportedException</code> for such signatures.</li> </ul> <p>See also</p> <ul> <li>Runtime/Core/Helper/ReflectionHelpers.cs for full XML docs and additional examples.</li> </ul>"},{"location":"features/utilities/singletons/","title":"Singleton Utilities (Runtime + ScriptableObject)","text":"<p>Visual</p> <p></p> <p>This package includes two lightweight, production\u2011ready singleton helpers that make global access patterns safe, consistent, and testable:</p> <ul> <li><code>RuntimeSingleton&lt;T&gt;</code> \u2014 a component singleton that ensures one instance exists in play mode, optionally persists across scenes, and self\u2011initializes when first accessed.</li> <li><code>ScriptableObjectSingleton&lt;T&gt;</code> \u2014 a configuration/data singleton backed by a single asset under <code>Resources/</code>, with an editor auto\u2011creator to keep assets present and correctly placed.</li> </ul> <p>Odin compatibility: When Odin Inspector is present (<code>ODIN_INSPECTOR</code> defined), these types derive from <code>SerializedMonoBehaviour</code> / <code>SerializedScriptableObject</code> for richer serialization. Without Odin, they fall back to Unity base types. No code changes required.</p>"},{"location":"features/utilities/singletons/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>Stop hand\u2011rolling global access. Get a single, safe instance you can call from anywhere.</li> <li>Choose between a scene\u2011resident component or a project asset for settings/data.</li> <li>No manual setup: instances auto\u2011create on first use; ScriptableObject assets auto\u2011create/move under <code>Resources/</code> in the Editor.</li> </ul>"},{"location":"features/utilities/singletons/#auto-loading-singletons","title":"Auto-loading singletons","text":"<ul> <li>Add <code>[AutoLoadSingleton]</code> to any <code>RuntimeSingleton&lt;T&gt;</code> or <code>ScriptableObjectSingleton&lt;T&gt;</code> to have it instantiated automatically.</li> <li>The editor\u2019s Attribute Metadata generator discovers those attributes (via <code>TypeCache</code>) and serializes the type name + load phase into <code>AttributeMetadataCache</code>. No manual registration or code-generation is required.</li> <li>At runtime (play mode only), <code>SingletonAutoLoader</code> reads the serialized entries and uses reflection to touch each singleton\u2019s <code>Instance</code> during the configured <code>RuntimeInitializeLoadType</code> (default <code>BeforeSplashScreen</code>).</li> <li>Prefer auto-loading only for global services/data that every scene requires; optional or level-specific systems should still call <code>Instance</code> manually.</li> <li>Example:</li> </ul> C#<pre><code>[AutoLoadSingleton(RuntimeInitializeLoadType.BeforeSceneLoad)]\npublic sealed class GlobalAudioSettings : ScriptableObjectSingleton&lt;GlobalAudioSettings&gt;\n{\n    public float masterVolume = 0.8f;\n}\n</code></pre> <p>Quick decision guide</p> <ul> <li>Need a behaviour that runs (Update, events, coroutines) and may persist across scenes? Use <code>RuntimeSingleton&lt;T&gt;</code>.</li> <li>Need global config/data you edit in the Inspector and load from any scene? Use <code>ScriptableObjectSingleton&lt;T&gt;</code>.</li> </ul>"},{"location":"features/utilities/singletons/#quick-start-1-minute","title":"Quick Start (1 minute)","text":"<p>RuntimeSingleton</p> C#<pre><code>public sealed class GameServices : RuntimeSingleton&lt;GameServices&gt;\n{\n    // Optional: keep across scene loads\n    protected override bool Preserve =&gt; true;\n}\n\n// Use anywhere\nGameServices.Instance.DoThing();\n</code></pre> <p>ScriptableObjectSingleton</p> C#<pre><code>[CreateAssetMenu(menuName = \"Game/Audio Settings\")]\n[ScriptableSingletonPath(\"Settings/Audio\")] // Assets/Resources/Settings/Audio/AudioSettings.asset\npublic sealed class AudioSettings : ScriptableObjectSingleton&lt;AudioSettings&gt;\n{\n    public float masterVolume = 0.8f;\n}\n\n// Use anywhere (asset auto\u2011created/moved by the editor tool)\nfloat vol = AudioSettings.Instance.masterVolume;\n</code></pre> <p>Contents</p> <ul> <li>Odin Compatibility</li> <li>When To Use / Not To Use</li> <li>RuntimeSingleton <li>Lifecycle diagram, examples, pitfalls</li> <li>ScriptableObjectSingleton <li>Lookup + auto\u2011creator diagrams, examples, tips</li> <li>Scenarios &amp; Guidance</li> <li>Troubleshooting</li> <p></p>"},{"location":"features/utilities/singletons/#odin-compatibility","title":"Odin Compatibility","text":"<ul> <li>With Odin installed (symbol <code>ODIN_INSPECTOR</code>), base classes inherit from <code>SerializedMonoBehaviour</code> and <code>SerializedScriptableObject</code> to enable serialization of complex types (dictionaries, polymorphic fields) with Odin drawers.</li> <li>Without Odin, bases inherit from Unity\u2019s <code>MonoBehaviour</code>/<code>ScriptableObject</code> with no behavior change.</li> </ul>"},{"location":"features/utilities/singletons/#when-to-use","title":"When To Use","text":"<ul> <li><code>RuntimeSingleton&lt;T&gt;</code></li> <li>Cross\u2011scene services (thread dispatcher, audio router, global managers).</li> <li>Utility components that should always be available via <code>T.Instance</code>.</li> <li> <p>Creating the instance on demand when not found in the scene.</p> </li> <li> <p><code>ScriptableObjectSingleton&lt;T&gt;</code></p> </li> <li>Global settings/configuration (graphics, audio, feature flags).</li> <li>Data that should be edited as an asset and loaded via <code>Resources</code>.</li> <li>Consistent project setup for teams (auto\u2011created asset on editor load).</li> </ul> <p></p>"},{"location":"features/utilities/singletons/#when-not-to-use","title":"When Not To Use","text":"<ul> <li>Prefer DI/service locators for heavily decoupled architectures requiring multiple implementations per environment, or for test seams where global state is undesirable.</li> <li>Avoid <code>RuntimeSingleton&lt;T&gt;</code> for ephemeral, per\u2011scene logic or objects that should be duplicated in additive scenes.</li> <li>Avoid <code>ScriptableObjectSingleton&lt;T&gt;</code> for save data or level\u2011specific data that should not live in Resources or should have multiple instances.</li> </ul>"},{"location":"features/utilities/singletons/#runtimesingletont-overview","title":"<code>RuntimeSingleton&lt;T&gt;</code> Overview","text":"<ul> <li>Access via <code>T.Instance</code> (creates a new <code>GameObject</code> named <code>\"&lt;Type&gt;-Singleton\"</code> and adds <code>T</code> if none exists; otherwise finds an existing active instance).</li> <li><code>HasInstance</code> lets you check for an existing instance without creating one.</li> <li><code>Preserve</code> (virtual, default <code>true</code>) controls <code>DontDestroyOnLoad</code>.</li> <li>Handles duplicate detection and cleans up instance reference on destroy. Instance is cleared on domain reload before scene load.</li> </ul> <p>Example: Simple service</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Utils;\n\npublic sealed class GameServices : RuntimeSingleton&lt;GameServices&gt;\n{\n    // Disable cross\u2011scene persistence if desired\n    protected override bool Preserve =&gt; false;\n\n    public void Log(string message)\n    {\n        Debug.Log($\"[GameServices] {message}\");\n    }\n}\n\n// Usage from anywhere\nGameServices.Instance.Log(\"Hello world\");\n</code></pre> <p>Odin note: With Odin installed, the class inherits <code>SerializedMonoBehaviour</code>, enabling dictionaries and other complex serialized types.</p> <p>Common pitfalls:</p> <ul> <li>If an inactive instance exists in the scene, <code>Instance</code> won\u2019t find it (search excludes inactive objects) and will create a new one.</li> <li>If two active instances exist, the newer one logs an error and destroys itself.</li> <li>If <code>Preserve</code> is <code>true</code>, the instance is detached and marked <code>DontDestroyOnLoad</code>.</li> </ul> <p>Lifecycle diagram:</p> Text Only<pre><code>T.Instance \u2500\u252c\u2500 Has _instance? \u2500\u2500\u25b6 return\n            \u2502\n            \u251c\u2500 Find active T in scene? \u2500\u2500\u25b6 set _instance, return\n            \u2502\n            \u2514\u2500 Create GameObject(\"T-Singleton\") + Add T\n                 \u2514\u2500 Awake(): assign _instance, if Preserve: DontDestroyOnLoad\n                         \u2514\u2500 Start(): if duplicate, log + destroy self\n</code></pre> <p>Notes:</p> <ul> <li>To avoid creation during a sensitive frame, place a pre\u2011made instance in your bootstrap scene.</li> <li>For scene\u2011local managers, override <code>Preserve =&gt; false</code>.</li> </ul> <p></p>"},{"location":"features/utilities/singletons/#scriptableobjectsingletont-overview","title":"<code>ScriptableObjectSingleton&lt;T&gt;</code> Overview","text":"<ul> <li>Access via <code>T.Instance</code> (lazy\u2011loads from <code>Resources/</code> using either a custom path or the type name; warns if multiple assets found and chooses the first by name).</li> <li><code>HasInstance</code> indicates whether the lazy value exists and is not null.</li> <li>Optional <code>[ScriptableSingletonPath(\"Sub/Folder\")]</code> to control the <code>Resources</code> subfolder.</li> <li>Editor utility auto\u2011creates and relocates assets: see the \u201cScriptableObject Singleton Creator\u201d in the Editor Tools Guide.</li> </ul> <p>Example: Settings asset</p> C#<pre><code>using WallstopStudios.UnityHelpers.Utils;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n[ScriptableSingletonPath(\"Settings/Audio\")]\npublic sealed class AudioSettings : ScriptableObjectSingleton&lt;AudioSettings&gt;\n{\n    public float musicVolume = 0.8f;\n    public bool enableSpatialAudio = true;\n}\n\n// Usage at runtime\nfloat vol = AudioSettings.Instance.musicVolume;\n</code></pre> <p>Odin note: With Odin installed, the class inherits <code>SerializedScriptableObject</code>, so you can safely serialize complex collections without custom drawers.</p> <p>Asset management tips:</p> <ul> <li>Place the asset under <code>Assets/Resources/</code> (or under the path from <code>[ScriptableSingletonPath]</code>).</li> <li>The Editor\u2019s \u201cScriptableObject Singleton Creator\u201d runs on load to create missing assets and move misplaced ones. It also supports a test\u2011assembly toggle used by our test suite.</li> </ul> <p>Lookup order diagram:</p> Text Only<pre><code>Instance access:\n  [1] Resources.LoadAll&lt;T&gt;(custom path from [ScriptableSingletonPath])\n  [2] if none: Resources.Load&lt;T&gt;(type name)\n  [3] if none: Resources.LoadAll&lt;T&gt;(root)\n  [4] if multiple: warn + pick first by name (sorted)\n</code></pre> <p>Auto\u2011creator flow (Editor):</p> Text Only<pre><code>On editor load:\n  - Scan all ScriptableObjectSingleton&lt;T&gt; types\n  - For each non-abstract type:\n      - Determine Resources path (attribute or type name)\n      - Ensure folder under Assets/Resources\n      - If asset exists elsewhere: move to target path\n      - Else: create new asset at target path\n  - Save &amp; Refresh if changes\n</code></pre> <p>Asset structure diagram:</p> Text Only<pre><code>Default (no attribute):\nAssets/\n  Resources/\n    AudioSettings.asset         // type name\n\nWith [ScriptableSingletonPath(\"Settings/Audio\")]:\nAssets/\n  Resources/\n    Settings/\n      Audio/\n        AudioSettings.asset\n</code></pre> <p></p>"},{"location":"features/utilities/singletons/#scenarios-guidance","title":"Scenarios &amp; Guidance","text":"<ul> <li>Global dispatcher: See <code>UnityMainThreadDispatcher</code> which derives from <code>RuntimeSingleton&lt;UnityMainThreadDispatcher&gt;</code>.</li> <li>Global data caches or registries: Use <code>ScriptableObjectSingleton&lt;T&gt;</code> so data lives in a single editable asset and loads fast.</li> <li>Cross\u2011scene managers: Keep <code>Preserve = true</code> to avoid duplicates across scene loads.</li> </ul>"},{"location":"features/utilities/singletons/#data-registries-lookups-single-source-of-truth","title":"Data Registries &amp; Lookups (Single Source of Truth)","text":"<p>ScriptableObject singletons excel as in\u2011project \u201cdatabases\u201d for content/config:</p> <ul> <li>Centralize definitions (items, abilities, buffs, NPCs, localization) in one asset.</li> <li>Build fast lookup indices (by ID/tag/category) at load or validation time.</li> <li>Keep workflows simple: edit in Inspector, no runtime bootstrapping needed.</li> </ul> <p>Example: Items DB with indices</p> C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Utils;\n\n[CreateAssetMenu(menuName = \"Game/Items DB\")]\n[ScriptableSingletonPath(\"DB\")] // Assets/Resources/DB/ItemsDb.asset\npublic sealed class ItemsDb : ScriptableObjectSingleton&lt;ItemsDb&gt;\n{\n    [System.Serializable]\n    public sealed class ItemDef { public int id; public string name; public Sprite icon; }\n\n    public List&lt;ItemDef&gt; items = new();\n\n    // Non-serialized runtime indices\n    private readonly Dictionary&lt;int, ItemDef&gt; _byId = new();\n    private readonly Dictionary&lt;string, List&lt;ItemDef&gt;&gt; _byName = new();\n\n    private void OnEnable() =&gt; RebuildIndices();\n    private void OnValidate() =&gt; RebuildIndices();\n\n    private void RebuildIndices()\n    {\n        _byId.Clear();\n        _byName.Clear();\n        foreach (var it in items)\n        {\n            if (it == null) continue;\n            _byId[it.id] = it;\n            (_byName.TryGetValue(it.name, out var list) ? list : (_byName[it.name] = new())).Add(it);\n        }\n    }\n\n    public static bool TryGetById(int id, out ItemDef def) =&gt; Instance._byId.TryGetValue(id, out def);\n}\n\n// Usage\nif (ItemsDb.TryGetById(42, out var sword)) { /* equip sword */ }\n</code></pre> <p>Tips</p> <ul> <li>The auto-creator now maintains <code>Assets/Resources/Wallstop Studios/Unity Helpers/ScriptableObjectSingletonMetadata.asset</code>, which records the exact <code>Resources</code> load path + GUID for every singleton asset. At runtime, <code>ScriptableObjectSingleton&lt;T&gt;</code> consults this metadata so it can call <code>Resources.Load(\"Folder/MySingleton\")</code> directly (or <code>Resources.LoadAll</code> scoped to <code>Folder/</code> when it needs to detect duplicates) and never falls back to <code>Resources.LoadAll(string.Empty)</code>.</li> <li>When metadata is missing or stale (e.g., if you deleted the metadata asset in a test project), the runtime logs a warning once per type and falls back to a bounded search (<code>Resources.Load&lt;T&gt;(typeName)</code> + editor-only <code>AssetDatabase</code> lookups) instead of scanning the entire <code>Resources</code> tree.</li> <li>Keep serialized lists as your source of truth; build dictionaries at load/validate.</li> <li>Use <code>[ScriptableSingletonPath]</code> to place the asset predictably under <code>Resources/</code>.</li> <li>Split huge DBs into themed sub\u2011assets and cross\u2011reference via indices.</li> <li>Consider GUIDs or string IDs for modding; validate uniqueness in <code>OnValidate</code>.</li> </ul>"},{"location":"features/utilities/singletons/#example-content-db-with-tags-categories-guids-addressables","title":"Example: Content DB with tags, categories, GUIDs (Addressables)","text":"C#<pre><code>using System.Collections.Generic;\nusing UnityEngine;\nusing WallstopStudios.UnityHelpers.Utils;\n#if UNITY_EDITOR\nusing UnityEditor;\nusing UnityEditor.AddressableAssets;\nusing UnityEditor.AddressableAssets.Settings;\n#endif\nusing UnityEngine.AddressableAssets;\n\npublic enum ContentCategory { Weapon, Armor, Consumable, Quest }\n\n[CreateAssetMenu(menuName = \"Game/Content DB\")]\n[ScriptableSingletonPath(\"DB\")] // Assets/Resources/DB/ContentDb.asset\npublic sealed class ContentDb : ScriptableObjectSingleton&lt;ContentDb&gt;\n{\n    [System.Serializable]\n    public sealed class ContentDef\n    {\n        public string guid;                 // stable ID for saves/mods\n        public string displayName;\n        public ContentCategory category;\n        public string[] tags;               // e.g., \"fire\", \"ranged\"\n        public AssetReferenceGameObject prefab; // addressable ref (optional)\n    }\n\n    public List&lt;ContentDef&gt; entries = new();\n\n    // Indices (runtime only)\n    private readonly Dictionary&lt;string, ContentDef&gt; _byGuid = new();\n    private readonly Dictionary&lt;ContentCategory, List&lt;ContentDef&gt;&gt; _byCategory = new();\n    private readonly Dictionary&lt;string, List&lt;ContentDef&gt;&gt; _byTag = new();\n\n    private void OnEnable() =&gt; RebuildIndices();\n    private void OnValidate() { RebuildIndices(); ValidateEditor(); }\n\n    private void RebuildIndices()\n    {\n        _byGuid.Clear(); _byCategory.Clear(); _byTag.Clear();\n        foreach (var e in entries)\n        {\n            if (e == null || string.IsNullOrEmpty(e.guid)) continue;\n            _byGuid[e.guid] = e;\n            (_byCategory.TryGetValue(e.category, out var listCat) ? listCat : (_byCategory[e.category] = new())).Add(e);\n            if (e.tags != null)\n                foreach (var t in e.tags)\n                    if (!string.IsNullOrEmpty(t))\n                        (_byTag.TryGetValue(t, out var listTag) ? listTag : (_byTag[t] = new())).Add(e);\n        }\n    }\n\n    public static bool TryGetByGuid(string guid, out ContentDef def) =&gt; Instance._byGuid.TryGetValue(guid, out def);\n    public static IReadOnlyList&lt;ContentDef&gt; GetByCategory(ContentCategory cat) =&gt;\n        Instance._byCategory.TryGetValue(cat, out var list) ? list : (IReadOnlyList&lt;ContentDef&gt;)System.Array.Empty&lt;ContentDef&gt;();\n    public static IReadOnlyList&lt;ContentDef&gt; GetByTag(string tag) =&gt;\n        Instance._byTag.TryGetValue(tag, out var list) ? list : (IReadOnlyList&lt;ContentDef&gt;)System.Array.Empty&lt;ContentDef&gt;();\n\n#if UNITY_EDITOR\n    private void ValidateEditor()\n    {\n        // Validate GUID uniqueness\n        var seen = new HashSet&lt;string&gt;();\n        foreach (var e in entries)\n        {\n            if (e == null) continue;\n            if (string.IsNullOrEmpty(e.guid))\n                Debug.LogWarning($\"[ContentDb] Entry '{e?.displayName}' has empty GUID\", this);\n            else if (!seen.Add(e.guid))\n                Debug.LogError($\"[ContentDb] Duplicate GUID '{e.guid}' detected\", this);\n        }\n\n        // Validate Addressables (if package installed and editor context)\n        var settings = AddressableAssetSettingsDefaultObject.Settings;\n        if (settings != null)\n        {\n            foreach (var e in entries)\n            {\n                if (e?.prefab == null) continue;\n                var guid = e.prefab.AssetGUID;\n                if (string.IsNullOrEmpty(guid) || settings.FindAssetEntry(guid) == null)\n                    Debug.LogWarning($\"[ContentDb] Prefab for '{e.displayName}' is not marked Addressable\", this);\n            }\n        }\n    }\n#endif\n}\n</code></pre> <p>Why this works well</p> <ul> <li>One authoritative asset; code reads through stable APIs.</li> <li>Deterministic load path via Resources; Addressables used only for content references.</li> <li>Indices rebuilt automatically to keep lookups fast and in sync while editing.</li> </ul>"},{"location":"features/utilities/singletons/#when-not-to-use-scriptableobject-singletons-as-dbs","title":"When Not To Use ScriptableObject Singletons as DBs","text":"<p>Use alternatives when one or more of these apply:</p> <ul> <li>Very large datasets (tens of thousands of records or &gt;10\u201320 MB serialized)</li> <li>Prefer Addressables catalogs, binary blobs, streaming assets, or an external store; load by page or on demand.</li> <li>Frequent live updates/patches without app updates</li> <li>External data sources (remote JSON/Protobuf), Addressables content updates, or platform DBs are better suited.</li> <li>Strong need for async/background loading or partial paging</li> <li>Addressables + async APIs give finer loading control versus a monolithic Resources asset.</li> <li>Cross\u2011team/content pipelines that generate data at build time</li> <li>Import raw data into Addressables or assets at build; consider codegen for IDs and indices.</li> <li>Complex versioning/migrations of data formats</li> <li>Store version tags and migrate on load, or keep data outside Resources where migrations are simpler to stage.</li> <li>Sensitive/untrusted inputs</li> <li>Don\u2019t deserialize untrusted data into SOs; use validated formats and sandboxed loaders.</li> <li>Save data</li> <li>Keep save/progression separate from the content DB; reference content by GUID/ID in saves.</li> </ul>"},{"location":"features/utilities/singletons/#choosing-a-data-distribution-strategy","title":"Choosing a Data Distribution Strategy","text":"<p>Use this chart to pick an approach based on constraints:</p> <p></p> <p></p>"},{"location":"features/utilities/singletons/#testing-patterns","title":"Testing Patterns","text":""},{"location":"features/utilities/singletons/#testing-with-runtimesingletont","title":"Testing with <code>RuntimeSingleton&lt;T&gt;</code>","text":"<p>Runtime singletons require special handling in tests to avoid leaked GameObjects and unpredictable state across test runs. The recommended pattern uses <code>CommonTestBase</code> which handles cleanup automatically.</p>"},{"location":"features/utilities/singletons/#pattern-1-extend-commontestbase","title":"Pattern 1: Extend CommonTestBase","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Tests.Core;\n\npublic sealed class MyServiceTests : CommonTestBase\n{\n    [Test]\n    public void MyServiceInitializesCorrectly()\n    {\n        // CommonTestBase automatically manages dispatcher lifecycle\n        // and cleans up any spawned GameObjects after each test\n        var service = MyService.Instance;\n        Assert.That(service != null);\n    }\n}\n</code></pre>"},{"location":"features/utilities/singletons/#pattern-2-manual-scope-management","title":"Pattern 2: Manual Scope Management","text":"<p>For tests that need finer control over singleton lifecycle:</p> C#<pre><code>using UnityMainThreadDispatcher = WallstopStudios.UnityHelpers.Core.Helper.UnityMainThreadDispatcher;\n\npublic sealed class CustomSingletonTests\n{\n    private UnityMainThreadDispatcher.AutoCreationScope _scope;\n\n    [SetUp]\n    public void SetUp()\n    {\n        // Disable auto-creation, destroy any existing instance, then re-enable\n        _scope = UnityMainThreadDispatcher.CreateTestScope(destroyImmediate: true);\n    }\n\n    [TearDown]\n    public void TearDown()\n    {\n        // Restores previous auto-creation state and destroys test-created instances\n        _scope?.Dispose();\n        _scope = null;\n    }\n\n    [Test]\n    public void DispatcherIsAvailableInTest()\n    {\n        var dispatcher = UnityMainThreadDispatcher.Instance;\n        Assert.That(dispatcher != null);\n    }\n}\n</code></pre>"},{"location":"features/utilities/singletons/#pattern-3-temporarily-disable-auto-creation","title":"Pattern 3: Temporarily Disable Auto-Creation","text":"<p>For specific tests that need to verify behavior when the singleton doesn't exist:</p> C#<pre><code>[Test]\npublic void CodeHandlesMissingDispatcherGracefully()\n{\n    using (UnityMainThreadDispatcher.AutoCreationScope.Disabled(\n        destroyExistingInstanceOnEnter: true,\n        destroyInstancesOnDispose: true,\n        destroyImmediate: true))\n    {\n        // Inside this scope, accessing Instance won't auto-create\n        bool hasInstance = UnityMainThreadDispatcher.HasInstance;\n        Assert.That(hasInstance, Is.False);\n    }\n    // Auto-creation restored after scope exits\n}\n</code></pre>"},{"location":"features/utilities/singletons/#testing-with-scriptableobjectsingletont","title":"Testing with <code>ScriptableObjectSingleton&lt;T&gt;</code>","text":"<p>ScriptableObject singletons load from <code>Resources/</code> and are typically tested in Editor tests where asset manipulation is possible.</p>"},{"location":"features/utilities/singletons/#pattern-1-use-editor-test-fixtures","title":"Pattern 1: Use Editor Test Fixtures","text":"C#<pre><code>using WallstopStudios.UnityHelpers.Tests.Core;\n#if UNITY_EDITOR\nusing UnityEditor;\n#endif\n\npublic sealed class AudioSettingsTests : CommonTestBase\n{\n    [Test]\n    public void AudioSettingsLoadsFromResources()\n    {\n        // ScriptableObjectSingleton loads lazily from Resources\n        var settings = AudioSettings.Instance;\n        Assert.That(settings != null);\n        Assert.That(settings.masterVolume, Is.InRange(0f, 1f));\n    }\n}\n</code></pre>"},{"location":"features/utilities/singletons/#pattern-2-create-test-specific-assets","title":"Pattern 2: Create Test-Specific Assets","text":"<p>For tests that need controlled data:</p> C#<pre><code>#if UNITY_EDITOR\n[Test]\npublic void SettingsWithCustomValuesWork()\n{\n    // Create a test instance (tracked by CommonTestBase for cleanup)\n    var testSettings = CreateScriptableObject&lt;AudioSettings&gt;();\n    testSettings.masterVolume = 0.5f;\n\n    // Test logic using the instance directly (not via Instance property)\n    Assert.That(testSettings.masterVolume, Is.EqualTo(0.5f));\n}\n#endif\n</code></pre>"},{"location":"features/utilities/singletons/#key-testing-guidelines","title":"Key Testing Guidelines","text":"<ol> <li> <p>Inherit from <code>CommonTestBase</code>: This handles most singleton cleanup automatically, including dispatcher scope management.</p> </li> <li> <p>Use <code>CreateTestScope</code> for dispatcher: The <code>UnityMainThreadDispatcher.CreateTestScope()</code> method packages the common test setup pattern: disable auto-creation \u2192 destroy existing \u2192 re-enable auto-creation.</p> </li> <li> <p>Prefer <code>destroyImmediate: true</code> in EditMode: EditMode tests should use <code>DestroyImmediate</code> to ensure synchronous cleanup without Unity's delayed destruction.</p> </li> <li> <p>Track created objects: Use <code>Track&lt;T&gt;()</code> or <code>TrackDisposable&lt;T&gt;()</code> to ensure objects are cleaned up after tests.</p> </li> <li> <p>Clear singleton state between tests: Domain reloads clear singleton instances, but within a test run you may need explicit cleanup.</p> </li> </ol> <p></p>"},{"location":"features/utilities/singletons/#troubleshooting","title":"Troubleshooting","text":""},{"location":"features/utilities/singletons/#best-practices","title":"Best Practices","text":"<ul> <li>Prefer placing a pre-made runtime singleton in a bootstrap scene when construction order matters; avoid first-access implicit creation during critical frames.</li> <li>For scene-local managers, override <code>Preserve =&gt; false</code> to prevent cross-scene persistence.</li> <li>Keep exactly one singleton asset under <code>Resources/</code> for each <code>ScriptableObjectSingleton&lt;T&gt;</code>; let the auto-creator relocate any strays.</li> <li>Use <code>[ScriptableSingletonPath]</code> to group related settings; avoid deep nesting that hurts discoverability.</li> <li> <p>With Odin installed, take advantage of <code>Serialized*</code> bases for complex serialized fields; without Odin, keep fields Unity-serializable.</p> </li> <li> <p>Multiple ScriptableObject assets found: a warning is logged and the first by name is used. Resolve by keeping only one asset in Resources or by letting the auto\u2011creator relocate the correct one.</p> </li> <li><code>Instance</code> returns null for ScriptableObject: Ensure the asset exists under <code>Resources/</code> and the type name or custom path matches.</li> <li>Domain reloads: Both singletons clear cached instances before scene load.</li> <li>Leaked GameObjects in tests: Use <code>CommonTestBase</code> or wrap test code with <code>AutoCreationScope.Disabled()</code> to ensure cleanup.</li> </ul>"},{"location":"features/utilities/singletons/#related-docs","title":"Related Docs","text":"<ul> <li>Editor tool: ScriptableObject Singleton Creator.</li> <li>Tests: <code>Tests/Runtime/Utils/RuntimeSingletonTests.cs</code> and <code>Tests/Editor/Utils/ScriptableObjectSingletonTests.cs</code>.</li> <li>Dispatcher testing: Unity Main Thread Dispatcher Guide.</li> </ul>"},{"location":"guides/odin-migration-guide/","title":"Odin Inspector to Unity Helpers Migration Guide","text":"<p>A practical guide for migrating from Odin Inspector to Unity Helpers. Examples are verified against the actual source code.</p>"},{"location":"guides/odin-migration-guide/#quick-reference-table","title":"Quick Reference Table","text":"Odin Feature Unity Helpers Equivalent <code>[Button]</code> <code>[WButton]</code> <code>[ReadOnly]</code> <code>[WReadOnly]</code> <code>[ShowIf]</code> / <code>[HideIf]</code> <code>[WShowIf]</code> <code>[EnumToggleButtons]</code> <code>[WEnumToggleButtons]</code> <code>[ValueDropdown]</code> <code>[WValueDropDown]</code>, <code>[IntDropDown]</code>, <code>[StringInList]</code> <code>[BoxGroup]</code> <code>[WGroup]</code> <code>[FoldoutGroup]</code> <code>[WGroup(collapsible: true)]</code> <code>[InlineEditor]</code> <code>[WInLineEditor]</code> <code>[Required]</code> <code>[WNotNull]</code>, <code>[ValidateAssignment]</code> <code>SerializedMonoBehaviour</code> Standard <code>MonoBehaviour</code> <code>SerializedDictionary</code> <code>SerializableDictionary&lt;K,V&gt;</code> N/A (paid feature) <code>SerializableHashSet&lt;T&gt;</code>"},{"location":"guides/odin-migration-guide/#1-serializable-collections","title":"1. Serializable Collections","text":""},{"location":"guides/odin-migration-guide/#dictionary","title":"Dictionary","text":"<p>Odin:</p> C#<pre><code>using Sirenix.OdinInspector;\nusing Sirenix.Serialization;\n\npublic class Example : SerializedMonoBehaviour\n{\n    public Dictionary&lt;string, int&gt; scores;\n}\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class Example : MonoBehaviour\n{\n    [SerializeField]\n    private SerializableDictionary&lt;string, int&gt; scores = new SerializableDictionary&lt;string, int&gt;();\n}\n</code></pre> <p>Key differences:</p> <ul> <li>No special base class required (use standard <code>MonoBehaviour</code>)</li> <li>Must use <code>[SerializeField]</code> or <code>public</code></li> <li>Initialize with <code>new</code> to avoid null references (good practice, Unity will initialize this like it does List and arrays)"},{"location":"guides/odin-migration-guide/#hashset","title":"HashSet","text":"<p>Odin:</p> C#<pre><code>using Sirenix.OdinInspector;\nusing Sirenix.Serialization;\n\npublic class Example : SerializedMonoBehaviour\n{\n    public HashSet&lt;string&gt; unlockedItems;\n}\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class Example : MonoBehaviour\n{\n    [SerializeField]\n    private SerializableHashSet&lt;string&gt; unlockedItems = new SerializableHashSet&lt;string&gt;();\n}\n</code></pre>"},{"location":"guides/odin-migration-guide/#2-inspector-buttons","title":"2. Inspector Buttons","text":"<p>Odin:</p> C#<pre><code>[Button(\"Regenerate\")]\nprivate void RegenerateLevel() { }\n\n[Button, ButtonGroup(\"Actions\")]\nprivate void Save() { }\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WButton(\"Regenerate\")]\nprivate void RegenerateLevel() { }\n\n[WButton(groupName: \"Actions\")]\nprivate void Save() { }\n</code></pre> <p>Additional options:</p> C#<pre><code>// Control button order within a group\n[WButton(drawOrder: 1, groupName: \"Debug\")]\nprivate void PrintDebugInfo() { }\n\n// Control group placement (top or bottom of inspector)\n[WButton(groupName: \"Authoring\", groupPlacement: WButtonGroupPlacement.Top)]\nprivate void GenerateIds() { }\n</code></pre> <p>Odin Inspector Support:</p> <p>WButton works with Odin's <code>SerializedMonoBehaviour</code> and <code>SerializedScriptableObject</code> without additional setup. Use <code>[WButton]</code> on your methods.</p> <p>Manual integration may be needed if:</p> <ul> <li>You create a custom <code>OdinEditor</code> for a specific type</li> <li>See Inspector Buttons - Custom Editors for details</li> </ul>"},{"location":"guides/odin-migration-guide/#3-conditional-display","title":"3. Conditional Display","text":""},{"location":"guides/odin-migration-guide/#basic-boolean-condition","title":"Basic Boolean Condition","text":"<p>Odin:</p> C#<pre><code>public bool showAdvanced;\n\n[ShowIf(\"showAdvanced\")]\npublic float advancedSetting;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>public bool showAdvanced;\n\n[WShowIf(nameof(showAdvanced))]\npublic float advancedSetting;\n</code></pre>"},{"location":"guides/odin-migration-guide/#hide-if-inverse","title":"Hide If (Inverse)","text":"<p>Odin:</p> C#<pre><code>[HideIf(\"isDisabled\")]\npublic float value;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WShowIf(nameof(isDisabled), inverse: true)]\npublic float value;\n</code></pre>"},{"location":"guides/odin-migration-guide/#enum-value-comparison","title":"Enum Value Comparison","text":"<p>Odin:</p> C#<pre><code>public AttackType attackType;\n\n[ShowIf(\"attackType\", AttackType.Ranged)]\npublic float range;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>public AttackType attackType;\n\n[WShowIf(nameof(attackType), AttackType.Ranged)]\npublic float range;\n</code></pre>"},{"location":"guides/odin-migration-guide/#numeric-comparisons","title":"Numeric Comparisons","text":"<p>Odin:</p> C#<pre><code>[ShowIf(\"@level &gt;= 5\")]\npublic Ability ultimateAbility;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WShowIf(nameof(level), WShowIfComparison.GreaterThanOrEqual, 5)]\npublic Ability ultimateAbility;\n</code></pre> <p>Available comparisons: <code>Equal</code>, <code>NotEqual</code>, <code>GreaterThan</code>, <code>GreaterThanOrEqual</code>, <code>LessThan</code>, <code>LessThanOrEqual</code>, <code>IsNull</code>, <code>IsNotNull</code>, <code>IsNullOrEmpty</code>, <code>IsNotNullOrEmpty</code></p>"},{"location":"guides/odin-migration-guide/#4-enum-toggle-buttons","title":"4. Enum Toggle Buttons","text":"<p>Odin:</p> C#<pre><code>[EnumToggleButtons]\npublic Direction direction;\n\n[EnumToggleButtons]\npublic MovementFlags flags; // [Flags] enum\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WEnumToggleButtons]\npublic Direction direction;\n\n[WEnumToggleButtons(showSelectAll: true, showSelectNone: true)]\npublic MovementFlags flags; // [Flags] enum\n</code></pre> <p>Control buttons per row:</p> C#<pre><code>[WEnumToggleButtons(buttonsPerRow: 4)]\npublic DamageType damageTypes;\n</code></pre>"},{"location":"guides/odin-migration-guide/#5-value-dropdowns","title":"5. Value Dropdowns","text":""},{"location":"guides/odin-migration-guide/#integer-dropdown","title":"Integer Dropdown","text":"<p>Odin:</p> C#<pre><code>[ValueDropdown(\"GetFrameRates\")]\npublic int targetFrameRate;\n\nprivate int[] GetFrameRates() =&gt; new[] { 30, 60, 120 };\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>// Inline values (simplest)\n[IntDropDown(30, 60, 120)]\npublic int targetFrameRate;\n\n// Or with provider method\n[IntDropDown(nameof(GetFrameRates))]\npublic int targetFrameRate;\n\nprivate IEnumerable&lt;int&gt; GetFrameRates() =&gt; new[] { 30, 60, 120 };\n</code></pre>"},{"location":"guides/odin-migration-guide/#string-dropdown","title":"String Dropdown","text":"<p>Odin:</p> C#<pre><code>[ValueDropdown(\"GetDifficulties\")]\npublic string difficulty;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>// Inline values\n[StringInList(\"Easy\", \"Normal\", \"Hard\")]\npublic string difficulty;\n\n// Or with provider\n[StringInList(nameof(GetDifficulties))]\npublic string difficulty;\n</code></pre>"},{"location":"guides/odin-migration-guide/#generic-value-dropdown","title":"Generic Value Dropdown","text":"<p>Unity Helpers:</p> C#<pre><code>// Static provider from another class\n[WValueDropDown(typeof(AudioManager), nameof(AudioManager.GetSoundNames))]\npublic string soundEffect;\n\n// Instance provider (method on same class)\n[WValueDropDown(nameof(GetAvailableWeapons), typeof(WeaponData))]\npublic WeaponData selectedWeapon;\n</code></pre>"},{"location":"guides/odin-migration-guide/#6-grouping-fields","title":"6. Grouping Fields","text":"<p>Odin:</p> C#<pre><code>[BoxGroup(\"Movement\")]\npublic float speed;\n\n[BoxGroup(\"Movement\")]\npublic float jumpHeight;\n\n[FoldoutGroup(\"Advanced\")]\npublic float acceleration;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>// Auto-include next N fields\n[WGroup(\"Movement\", autoIncludeCount: 2)]\npublic float speed;        // Field 1: in group\npublic float jumpHeight;   // Field 2: in group (auto-included, last by count)\n\n// Or explicit end marker\n[WGroup(\"Movement\")]\npublic float speed;        // In group\npublic float jumpHeight;   // In group (auto-included)\n[WGroupEnd]                // friction IS included, then group closes\npublic float friction;     // In group (last field)\n\n// Collapsible (foldout)\n[WGroup(\"Advanced\", collapsible: true, startCollapsed: true)]\npublic float acceleration;\n</code></pre>"},{"location":"guides/odin-migration-guide/#nested-groups","title":"Nested Groups","text":"<p>Unity Helpers:</p> C#<pre><code>[WGroup(\"Character\", displayName: \"Character Settings\")]\npublic string characterName;\n\n[WGroup(\"Stats\", parentGroup: \"Character\")]\npublic int health;\npublic int mana;\n</code></pre>"},{"location":"guides/odin-migration-guide/#7-inline-editors","title":"7. Inline Editors","text":"<p>Odin:</p> C#<pre><code>[InlineEditor]\npublic EnemyConfig config;\n\n[InlineEditor(InlineEditorModes.GUIOnly)]\npublic ItemData item;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WInLineEditor]\npublic EnemyConfig config;\n\n[WInLineEditor(WInLineEditorMode.FoldoutExpanded, inspectorHeight: 200f)]\npublic ItemData item;\n</code></pre> <p>Available modes: <code>AlwaysExpanded</code>, <code>FoldoutExpanded</code>, <code>FoldoutCollapsed</code></p>"},{"location":"guides/odin-migration-guide/#8-requirednotnull-validation","title":"8. Required/NotNull Validation","text":"<p>Odin:</p> C#<pre><code>[Required]\npublic GameObject prefab;\n\n[Required(\"Player reference is required!\")]\npublic Transform player;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WNotNull]\npublic GameObject prefab;\n\n[WNotNull(WNotNullMessageType.Error, \"Player reference is required!\")]\npublic Transform player;\n\n// Runtime validation in Awake/Start\nprivate void Awake()\n{\n    this.CheckForNulls(); // Extension method\n}\n</code></pre>"},{"location":"guides/odin-migration-guide/#collection-validation","title":"Collection Validation","text":"<p>For validating that collections aren't empty:</p> C#<pre><code>[ValidateAssignment]\npublic List&lt;Transform&gt; spawnPoints; // Warns if null or empty\n\n[ValidateAssignment(ValidateAssignmentMessageType.Error, \"Need at least one enemy type\")]\npublic List&lt;EnemyData&gt; enemyTypes;\n\n// Runtime check\nprivate void Start()\n{\n    this.ValidateAssignments();\n}\n</code></pre>"},{"location":"guides/odin-migration-guide/#9-read-only-fields","title":"9. Read-Only Fields","text":"<p>Odin:</p> C#<pre><code>[ReadOnly]\npublic string generatedId;\n</code></pre> <p>Unity Helpers:</p> C#<pre><code>[WReadOnly]\npublic string generatedId;\n</code></pre>"},{"location":"guides/odin-migration-guide/#10-complete-migration-example","title":"10. Complete Migration Example","text":"<p>Before (Odin):</p> C#<pre><code>using Sirenix.OdinInspector;\nusing Sirenix.Serialization;\nusing UnityEngine;\nusing System.Collections.Generic;\n\npublic class EnemySpawner : SerializedMonoBehaviour\n{\n    [BoxGroup(\"Settings\")]\n    [Required]\n    public GameObject enemyPrefab;\n\n    [BoxGroup(\"Settings\")]\n    [ShowIf(\"useWaves\")]\n    public int wavesCount = 3;\n\n    public bool useWaves;\n\n    [EnumToggleButtons]\n    public SpawnPattern pattern;\n\n    [ValueDropdown(\"GetSpawnRates\")]\n    public float spawnRate;\n\n    public Dictionary&lt;string, int&gt; enemyWeights;\n\n    [Button(\"Spawn Wave\")]\n    private void SpawnWave() { }\n\n    private float[] GetSpawnRates() =&gt; new[] { 0.5f, 1f, 2f };\n}\n</code></pre> <p>After (Unity Helpers):</p> C#<pre><code>using UnityEngine;\nusing System.Collections.Generic;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n\npublic class EnemySpawner : MonoBehaviour\n{\n    [WGroup(\"Settings\", autoIncludeCount: 2)]\n    [WNotNull]\n    [SerializeField]\n    private GameObject enemyPrefab;\n\n    [WShowIf(nameof(useWaves))]\n    [SerializeField]\n    private int wavesCount = 3;\n\n    [SerializeField]\n    private bool useWaves;\n\n    [WEnumToggleButtons]\n    [SerializeField]\n    private SpawnPattern pattern;\n\n    [WValueDropDown(0.5f, 1f, 2f)]\n    [SerializeField]\n    private float spawnRate;\n\n    [SerializeField]\n    private SerializableDictionary&lt;string, int&gt; enemyWeights =\n        new SerializableDictionary&lt;string, int&gt;();\n\n    [WButton(\"Spawn Wave\")]\n    private void SpawnWave() { }\n}\n</code></pre>"},{"location":"guides/odin-migration-guide/#namespace-reference","title":"Namespace Reference","text":"C#<pre><code>// Attributes\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\n// Serializable collections\nusing WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;\n</code></pre>"},{"location":"guides/odin-migration-guide/#key-differences-summary","title":"Key Differences Summary","text":"<ol> <li>No special base class - Use standard <code>MonoBehaviour</code> / <code>ScriptableObject</code></li> <li>Use <code>nameof()</code> - Unity Helpers uses <code>nameof()</code> for condition fields (type-safe)</li> <li>Initialize collections - Initialize <code>new SerializableDictionary&lt;K,V&gt;()</code> etc. to avoid null references</li> <li>[HideIf] becomes inverse - Use <code>[WShowIf(..., inverse: true)]</code> instead of <code>[HideIf]</code></li> <li>Numeric conditions - Use <code>WShowIfComparison</code> enum instead of expression strings</li> <li>Groups auto-include - <code>[WGroup]</code> can auto-include subsequent fields with <code>autoIncludeCount</code></li> </ol>"},{"location":"guides/odin-migration-guide/#see-also","title":"See Also","text":"<ul> <li>Inspector Overview - Complete inspector features guide</li> <li>Serialization Types - All serializable types</li> <li>Inspector Buttons - WButton detailed guide</li> <li>Inspector Conditional Display - WShowIf detailed guide</li> <li>Inspector Selection Attributes - Dropdowns and toggles</li> </ul>"},{"location":"includes/abbreviations/","title":"Abbreviations","text":""},{"location":"overview/","title":"Feature Index","text":"<p>Alphabetical index of all Unity Helpers features with quick links to documentation.</p> <p>Quick Navigation: A | B | C | D | E | F | G | H | I | K | L | M | N | O | P | Q | R | S | T | U | V | W | X</p>"},{"location":"overview/#a","title":"A","text":"<p>Animation Copier - Sync AnimationClips between folders \u2192 Editor Tools Guide</p> <p>Animation Creator - Bulk-create clips from sprite naming patterns \u2192 Editor Tools Guide</p> <p>Animation Event Editor - Visual event editing with sprite preview \u2192 Editor Tools Guide</p> <p>AnimatedSpriteLayer - Data structure for sprite animation layers \u2192 Visual Components</p> <p>AnimatorEnumStateMachine - Type-safe enum-based animator control \u2192 Utility Components</p> <p>Async Extensions - Await AsyncOperation with Task/ValueTask \u2192 Math &amp; Extensions</p> <p>Attribute - Dynamic numeric value with modifications \u2192 Effects System | Glossary</p> <p>Attribute Metadata Cache - Pre-computed attribute reflection data \u2192 Editor Tools Guide</p> <p>AttributeEffect - ScriptableObject for data-driven gameplay effects \u2192 Effects System</p> <p>AttributesComponent - Base class for modifiable attributes \u2192 Effects System | README</p>"},{"location":"overview/#b","title":"B","text":"<p>Binary Heap - Priority queue with O(log n) operations \u2192 Data Structures</p> <p>BitSet - Compact boolean storage with bitwise operations \u2192 Data Structures | README</p> <p>Buffering Pattern - Reusable collections for zero-allocation queries \u2192 README - Buffering Pattern | Glossary</p> <p>Buffers - Pooled collections (List/Stack/Queue/HashSet) \u2192 README - Buffering Pattern"},{"location":"overview/#c","title":"C","text":"<p>Camera Extensions - OrthographicBounds and helpers \u2192 Math &amp; Extensions</p> <p>CenterPointOffset - Define logical center points separate from transform pivot \u2192 Utility Components</p> <p>ChildComponent - Auto-wire components from children \u2192 Relational Components | README</p> <p>ChildSpawner - Conditional prefab instantiation with environment filtering \u2192 Utility Components</p> <p>CircleLineRenderer - Dynamic circle visualization synced to CircleCollider2D \u2192 Utility Components</p> <p>CollisionProxy - Event-based 2D collision detection without inheritance \u2192 Utility Components</p> <p>Color Utilities - Averaging (LAB/HSV/Weighted/Dominant), hex conversion \u2192 Math &amp; Extensions</p> <p>CoroutineHandler - Singleton MonoBehaviour for coroutine hosting \u2192 Utility Components</p> <p>CosmeticEffectData - Presenters for effect cosmetics \u2192 Effects System</p> <p>Cyclic Buffer - Fixed-capacity ring buffer \u2192 Data Structures | README</p>"},{"location":"overview/#d","title":"D","text":"<p>Data Structures - Heaps, tries, sparse sets, and more \u2192 Data Structures Guide | README</p> <p>Deque - Double-ended queue \u2192 Data Structures | README</p> <p>Dictionary Extensions - GetOrAdd, GetOrElse, Merge, ContentEquals \u2192 Math &amp; Extensions</p> <p>Disjoint Set - Union-find for connectivity \u2192 Data Structures | README</p> <p>Douglas-Peucker - Polyline simplification algorithm \u2192 Math &amp; Extensions | Glossary</p> <p>WReadOnly - Read-only inspector display attribute \u2192 Editor Tools Guide | README</p>"},{"location":"overview/#e","title":"E","text":"<p>Editor Tools - 20+ tools for sprites, animations, validation \u2192 Editor Tools Guide | README</p> <p>EffectHandle - Identifier for effect application instances \u2192 Effects System | Glossary</p> <p>EffectHandler - Component managing effect lifecycle \u2192 Effects System</p> <p>Effects System - Data-driven buffs/debuffs/status effects \u2192 Effects System Guide | README</p> <p>EnhancedImage - Unity Image with HDR color and shape masks \u2192 Visual Components | Editor Tools Guide</p> <p>Enum Extensions - Zero-allocation flag checks, cached names, display names \u2192 Math &amp; Extensions</p>"},{"location":"overview/#f","title":"F","text":"<p>Failed Tests Exporter - Capture and export failed test results to timestamped files \u2192 Failed Tests Exporter</p> <p>Fit Texture Size - Auto-adjust texture max size to source dimensions \u2192 Editor Tools Guide</p> <p>FlurryBurstRandom - Six-word ARX generator (FlurryBurst32 port) \u2192 README - Random Generators | Random Performance</p>"},{"location":"overview/#g","title":"G","text":"<p>Gaussian Distribution - Normal distribution random values \u2192 README - Random Generators</p> <p>Geometry Helpers - Lines, ranges, parabolas, convex hulls \u2192 Math &amp; Extensions</p> <p>Glossary - Term definitions \u2192 Glossary</p>"},{"location":"overview/#h","title":"H","text":"<p>Heap - Binary heap for priority queues \u2192 Data Structures | README</p> <p>Helpers Class - General utilities (layers, sprites, components) \u2192 Helper Utilities | README</p> <p>Hulls - Convex vs concave hull algorithms \u2192 Hulls Guide</p>"},{"location":"overview/#i","title":"I","text":"<p>IllusionFlow - Default recommended PRNG \u2192 README - Random Generators | Random Performance</p> <p>Image Blur Tool - Gaussian blur for textures \u2192 Editor Tools Guide</p> <p>Immutable Trees - Spatial trees requiring rebuild on changes \u2192 Spatial Trees 2D | Glossary</p> <p>Inspector Settings - Project-wide configuration for inspector features (pagination, colors, animations) \u2192 Inspector Settings</p> <p>Inspector Tooling Overview - Complete guide to inspector attributes and serialization types \u2192 Inspector Overview</p> <p>IntDropdown - Integer dropdown property drawer \u2192 Editor Tools Guide | Inspector Selection Attributes</p> <p>IRandom Interface - Common interface for all RNGs \u2192 README - Random Generators</p>"},{"location":"overview/#k","title":"K","text":"<p>KdTree2D - 2D k-dimensional tree for nearest neighbors \u2192 2D Spatial Trees | 2D Performance</p> <p>KdTree3D - 3D k-dimensional tree for nearest neighbors \u2192 3D Spatial Trees | 3D Performance</p>"},{"location":"overview/#l","title":"L","text":"<p>LayeredImage - UI Toolkit element for composited sprite animations \u2192 Visual Components</p> <p>Line2D / Line3D - Line segment operations \u2192 Math &amp; Extensions</p> <p>LineHelper - Douglas-Peucker simplification \u2192 Math &amp; Extensions | README</p> <p>llms.txt - LLM-friendly documentation for AI assistants \u2192 llms.txt</p> <p>LoggingExtensions - Color-coded, thread-safe logging utilities \u2192 Logging Extensions</p> <p>LZMA Compression - Compression utilities \u2192 README - Serialization</p>"},{"location":"overview/#m","title":"M","text":"<p>Math Helpers - Positive modulo, wrapped arithmetic, geometry \u2192 Math &amp; Extensions Guide | README</p> <p>MatchColliderToSprite - Sync collider shape to sprite \u2192 Utility Components | Editor Tools Guide</p> <p>MatchTransform - Follow another transform with offset and timing control \u2192 Utility Components</p>"},{"location":"overview/#n","title":"N","text":"<p>Noise Maps - Perlin noise generation \u2192 README - Random Generators</p> <p>WNotNull Attribute - Inspector validation attribute \u2192 README</p> <p>Numeric Helpers - PositiveMod, Clamp, Approximately \u2192 Math &amp; Extensions</p>"},{"location":"overview/#o","title":"O","text":"<p>Oscillator - Automatic circular/elliptical motion component \u2192 Utility Components</p> <p>OctTree3D - 3D spatial tree (octree) \u2192 3D Spatial Trees | 3D Performance</p> <p>Odin Inspector Migration - Step-by-step guide for migrating from Odin Inspector \u2192 Migration Guide</p> <p>Odin Compatibility - Automatic Odin Inspector integration \u2192 Singletons - Odin | Glossary</p>"},{"location":"overview/#p","title":"P","text":"<p>Parabola - Parabolic trajectory helper \u2192 Math &amp; Extensions</p> <p>ParentComponent - Auto-wire components from parents \u2192 Relational Components | README</p> <p>PcgRandom - High-quality PCG random generator \u2192 README - Random Generators | Random Performance</p> <p>PhotonSpinRandom - SHISHUA-inspired bulk generator \u2192 README - Random Generators | Random Performance</p> <p>Point-in-Polygon - 2D/3D containment tests \u2192 Math &amp; Extensions</p> <p>PolygonCollider2DOptimizer - Simplify collider points \u2192 Utility Components | Editor Tools Guide</p> <p>Predictive Aiming - Calculate where to aim at moving targets \u2192 Helper Utilities</p> <p>Pooled Buffers - Reusable memory allocations \u2192 README - Buffering Pattern | Glossary</p> <p>Positive Modulo - Non-negative modulo operation \u2192 Math &amp; Extensions | Glossary</p> <p>Prefab Checker - Comprehensive prefab validation \u2192 Editor Tools Guide</p> <p>PriorityQueue - Min/max heap-based priority queue \u2192 Data Structures | README</p> <p>PRNG.Instance - Thread-local default random generator \u2192 README - Random Generators</p> <p>Property Drawers - Custom inspector rendering \u2192 Editor Tools Guide</p> <p>Protobuf Serialization - Compact binary serialization \u2192 Serialization Guide | Glossary</p>"},{"location":"overview/#q","title":"Q","text":"<p>QuadTree2D - 2D spatial tree (quadtree) \u2192 2D Spatial Trees | 2D Performance</p>"},{"location":"overview/#r","title":"R","text":"<p>Random Extensions - Random vectors, colors, weighted selection, subset sampling \u2192 Random Generators Guide</p> <p>Random Generators - 15 high-performance PRNG implementations \u2192 Random Generators Guide | Random Performance</p> <p>Range - Inclusive/exclusive range helper \u2192 Math &amp; Extensions <p>Rect/Bounds Extensions - Conversions and aggregation \u2192 Math &amp; Extensions</p> <p>RectTransform Extensions - GetWorldRect and helpers \u2192 Math &amp; Extensions</p> <p>Reflection Helpers - High-performance cached reflection \u2192 Reflection Helpers</p> <p>Relational Components - Auto-wire hierarchy components \u2192 Relational Components Guide | Relational Component Performance Benchmarks</p> <p>RTree2D - 2D R-tree for bounding boxes \u2192 2D Spatial Trees | 2D Performance</p> <p>RTree3D - 3D R-tree for bounding volumes \u2192 3D Spatial Trees | 3D Performance</p> <p>RuntimeSingleton - Component singleton pattern \u2192 Singletons Guide | Testing Patterns | README"},{"location":"overview/#s","title":"S","text":"<p>ScriptableObject Singleton - Settings/data singleton pattern \u2192 Singletons Guide | Testing Patterns | README</p> <p>ScriptableObject Singleton Creator - Auto-create singleton assets \u2192 Editor Tools Guide</p> <p>Serialization - JSON, Protobuf, BinaryFormatter support \u2192 Serialization Guide | README</p> <p>SiblingComponent - Auto-wire components on same GameObject \u2192 Relational Components | README</p> <p>SerializableDictionary - Unity-friendly dictionary with key/value serialization \u2192 Serialization Types</p> <p>SerializableHashSet / SerializableSortedSet - Unity-friendly set collections \u2192 Serialization Types</p> <p>SerializableNullable - Unity-friendly nullable value wrapper \u2192 Serialization Types</p> <p>SerializableType - Type reference that survives refactoring \u2192 Serialization Types</p> <p>Singletons - Runtime and ScriptableObject singleton patterns \u2192 Singletons Guide | README</p> <p>StormDropRandom - Large-buffer ARX generator \u2192 README - Random Generators | Random Performance</p> <p>Sparse Set - O(1) membership with dense iteration \u2192 Data Structures | README</p> <p>Spatial Hash 2D/3D - Grid-based spatial structure \u2192 2D Spatial Trees | 3D Spatial Trees</p> <p>Spatial Trees - Fast spatial queries (QuadTree, KdTree, RTree, OctTree) \u2192 2D Guide | 3D Guide</p> <p>Spatial Tree Semantics - Boundary behavior and edge cases \u2192 Spatial Tree Semantics</p> <p>Sprite Animation Editor - Visual animation editing with preview \u2192 Editor Tools Guide</p> <p>Sprite Atlas Generator - Regex/label-based atlas creation \u2192 Editor Tools Guide</p> <p>Sprite Cropper - Remove transparent padding \u2192 Editor Tools Guide</p> <p>Sprite Label Processor - Auto-cache sprite labels \u2192 Editor Tools Guide</p> <p>Sprite Pivot Adjuster - Alpha-weighted pivot adjustment \u2192 Editor Tools Guide</p> <p>Sprite Settings Applier - Batch sprite import settings \u2192 Editor Tools Guide</p> <p>Sprite Sheet Animation Creator - Convert sprite sheets to clips \u2192 Editor Tools Guide</p> <p>SpriteRendererMetadata - Stack-based color and material management \u2192 Utility Components</p> <p>SpriteRendererSync - Mirror SpriteRenderer properties to another renderer \u2192 Utility Components</p> <p>StartTracker - Track MonoBehaviour Start() lifecycle event \u2192 Utility Components</p> <p>String Extensions - Casing, encoding, Levenshtein distance, Base64, analysis \u2192 Math &amp; Extensions</p> <p>StringInList - String dropdown property drawer with search and pagination \u2192 Inspector Selection Attributes | Editor Tools Guide</p>"},{"location":"overview/#t","title":"T","text":"<p>Tag Handler - Reference-counted string tags \u2192 Effects System | Glossary</p> <p>Texture Resizer - Batch resize with bilinear/point filtering \u2192 Editor Tools Guide</p> <p>Texture Settings Applier - Batch texture import settings \u2192 Editor Tools Guide</p> <p>Trie - Prefix tree for autocomplete \u2192 Data Structures | README</p>"},{"location":"overview/#u","title":"U","text":"<p>Unity Extensions - Rect/Bounds, Camera, Rigidbody2D, Grid helpers \u2192 Math &amp; Extensions</p> <p>UnityMainThreadDispatcher - Execute work on main thread from background threads \u2192 Threading Guide | Helper Utilities</p>"},{"location":"overview/#v","title":"V","text":"<p>ValidateAssignment - Inspector validation attribute \u2192 Relational Components</p> <p>Vector Extensions - Random vectors, noise detection \u2192 Math &amp; Extensions</p>"},{"location":"overview/#w","title":"W","text":"<p>WallMath - Positive modulo, wrapped arithmetic \u2192 Math &amp; Extensions</p> <p>WallstopArrayPool - Pooled array rental \u2192 README - Buffering Pattern <p>WallstopFastArrayPool - Fast array pool for short-lived arrays (<code>T : unmanaged</code>) \u2192 README - Buffering Pattern <p>WButton - Inspector method buttons with history, async support, cancellation \u2192 Inspector Buttons | Inspector Overview</p> <p>WEnumToggleButtons - Enum and flag enum toggle button toolbars \u2192 Inspector Selection Attributes</p> <p>WGuid - Immutable version-4 GUID using two longs for fast Unity serialization \u2192 Serialization Types</p> <p>WGroup / WGroupEnd - Boxed inspector sections with auto-inclusion, palette-driven styling, and optional collapsible headers \u2192 Inspector Grouping Attributes</p> <p>Weighted Random - Weighted random selection \u2192 README - Random Generators</p> <p>WInLineEditor - Inline inspector for object references \u2192 Editor Tools Guide | README - Relational Components</p> <p>WShowIf - Conditional field display attribute with comparison operators \u2192 Inspector Conditional Display | Editor Tools Guide</p> <p>WValueDropDown - Generic dropdown for any type with fixed values or providers \u2192 Inspector Selection Attributes</p>"},{"location":"overview/#x","title":"X","text":"<p>XorShift Random - Fast XorShift PRNG \u2192 README - Random Generators | Random Performance</p> <p>XoroShiro Random - Fast XoroShiro PRNG \u2192 README - Random Generators | Random Performance</p> <p>See Also:</p> <ul> <li>Glossary - Term definitions</li> <li>Getting Started Guide - Quick start guide</li> <li>Main Documentation - Main documentation</li> </ul>"},{"location":"overview/getting-started/","title":"Getting Started with Unity Helpers","text":"<p>This guide introduces key features that can help reduce repetitive coding patterns.</p> <p>Unity Helpers is a toolkit used in commercial games that reduces common boilerplate patterns in Unity development. This guide covers the top features and basic usage patterns, whether you're a beginner or a senior engineer.</p>"},{"location":"overview/getting-started/#core-features","title":"Core Features","text":"<p>Three core principles:</p>"},{"location":"overview/getting-started/#1-reduced-boilerplate","title":"1. \ud83c\udfaf Reduced Boilerplate","text":"<p>Common APIs:</p> <ul> <li>Random selection with weights? \u2192 <code>random.NextWeightedIndex(weights)</code></li> <li>Auto-wire components? \u2192 <code>[SiblingComponent] private Animator animator;</code></li> <li>Gaussian distribution? Perlin noise? \u2192 Built-in, one method call</li> </ul> <p>Self-documenting code:</p> C#<pre><code>[SiblingComponent] private Animator animator;                      // Clear intent\n[ParentComponent(OnlyAncestors = true)] private Rigidbody2D rb;  // Explicit search\n[ChildComponent(MaxDepth = 1)] private Collider2D[] colliders;   // Limited scope\n</code></pre> <p>Error messages:</p> <ul> <li>Missing components? \u2192 Full GameObject path + component type</li> <li>Invalid queries? \u2192 Explanation of what went wrong + how to fix it</li> <li>Schema issues? \u2192 Specific guidance for your serialization problem</li> </ul>"},{"location":"overview/getting-started/#2-performance-characteristics","title":"2. \u26a1 Performance Characteristics","text":"<p>Speed improvements measured in benchmarks:</p> <ul> <li>10-15x faster in benchmarks random generation (benchmark details)</li> <li>10-100x faster in benchmarks reflection (varies by operation; see benchmark details)</li> <li>O(log n) spatial queries tested with millions of objects (benchmark details)</li> <li>Zero GC with buffering pattern</li> </ul> <p>Benchmark Results:</p> <ul> <li>Stable 60 FPS with 1000+ AI agents (benchmark details)</li> <li>No allocation spikes from pooled collections</li> <li>Deterministic replays with seedable RNG</li> </ul>"},{"location":"overview/getting-started/#3-testing-compatibility","title":"3. \u2705 Testing &amp; Compatibility","text":"<ul> <li>\u2705 8,000+ automated tests - Edge cases are handled through test coverage</li> <li>\u2705 Shipped in commercial games - Used at scale in production</li> <li>\u2705 IL2CPP/WebGL compatible - Works with aggressive compilers</li> <li>\u2705 Schema evolution - Player saves maintain compatibility across updates</li> <li>\u2705 SINGLE_THREADED optimized - Reduced overhead on WebGL</li> </ul> <p>Key capabilities:</p> <ul> <li>Edge cases are handled through test coverage</li> <li>Consistent behavior in editor and builds</li> <li>Player data compatibility maintained across updates</li> </ul>"},{"location":"overview/getting-started/#choose-your-path","title":"Choose Your Path","text":""},{"location":"overview/getting-started/#path-1-i-have-a-specific-problem","title":"\ud83c\udfaf Path 1: \"I Have a Specific Problem\"","text":"<p>Jump directly to the solution you need:</p> <p>Performance Issues?</p> <ul> <li>Slow random number generation \u2192 Random Generators</li> <li>Too many objects to search \u2192 Spatial Queries</li> <li>Frame drops from allocations \u2192 Buffering Pattern</li> </ul> <p>Workflow Issues?</p> <ul> <li>Writing too much GetComponent \u2192 Auto Component Wiring</li> <li>Manual sprite animation setup \u2192 Editor Tools</li> <li>Prefab validation problems \u2192 Prefab Checker</li> </ul> <p>Architecture Issues?</p> <ul> <li>Need global settings \u2192 Singletons</li> <li>Need buff/debuff system \u2192 Effects System</li> <li>Need save/load system \u2192 Serialization</li> <li>Migrating from Odin Inspector \u2192 Odin Migration Guide</li> </ul>"},{"location":"overview/getting-started/#path-2-i-want-to-understand-the-full-picture","title":"\ud83d\udcda Path 2: \"I Want to Understand the Full Picture\"","text":"<p>Full documentation overview (best for team leads and senior developers):</p> <ol> <li>Read Main Documentation - Full feature overview</li> <li>Review Features Documentation - Detailed API documentation</li> <li>Explore category-specific guides as needed</li> </ol>"},{"location":"overview/getting-started/#path-3-i-learn-best-from-examples","title":"\ud83d\udca1 Path 3: \"I Learn Best from Examples\"","text":"<p>See it working first, understand the theory later:</p> <ol> <li>Follow the 3 Quick Examples below</li> <li>Explore the Samples~ folder (see sample README files in the repo) for DI integration examples</li> <li>Modify examples for your specific needs</li> <li>Read the detailed guides when you need to go deeper</li> </ol>"},{"location":"overview/getting-started/#installation","title":"Installation","text":"<p>See the Installation section in the main README for detailed installation instructions using:</p> <ul> <li>OpenUPM (Recommended) \u2014 Easy version management via Package Manager or CLI</li> <li>Git URL \u2014 Direct from GitHub, great for CI/CD pipelines</li> <li>NPM Registry \u2014 For teams already using NPM scoped registries</li> <li>Source \u2014 Import <code>.unitypackage</code> from releases, or clone the repository</li> </ul> <p>After installation, verify the package appears in Window \u2192 Package Manager under \"My Registries\" or \"In Project\".</p>"},{"location":"overview/getting-started/#three-quick-examples","title":"Three Quick Examples","text":""},{"location":"overview/getting-started/#example-1-random-generation-beginner","title":"Example 1: Random Generation (Beginner)","text":"<p>Problem: Unity's <code>UnityEngine.Random</code> is slow and not seedable.</p> <p>Solution:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.Random;\nusing WallstopStudios.UnityHelpers.Core.Extension;\n\npublic class LootDrop : MonoBehaviour\n{\n    void Start()\n    {\n        // Performance comparison available in benchmarks\n        IRandom rng = PRNG.Instance;\n\n        // Basic usage\n        int damage = rng.Next(10, 20);\n        float chance = rng.NextFloat();\n\n        // Weighted random selection\n        string[] loot = { \"Common\", \"Rare\", \"Epic\", \"Legendary\" };\n        float[] weights = { 0.6f, 0.25f, 0.10f, 0.05f };\n        int index = rng.NextWeightedIndex(weights);\n        Debug.Log($\"Dropped: {loot[index]}\");\n    }\n}\n</code></pre> <p>\u26a0\ufe0f Common Mistake: Don't use <code>UnityEngine.Random</code> and <code>PRNG.Instance</code> together in the same class - pick one and stick with it for consistent results.</p> <p>Learn More: Random Performance</p>"},{"location":"overview/getting-started/#example-2-component-wiring-beginner","title":"Example 2: Component Wiring (Beginner)","text":"<p>Problem: Writing <code>GetComponent</code> calls everywhere is tedious and error-prone.</p> <p>Solution:</p> C#<pre><code>using UnityEngine;\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n\npublic class Player : MonoBehaviour\n{\n    // Auto-finds SpriteRenderer on same GameObject\n    [SiblingComponent]\n    private SpriteRenderer spriteRenderer;\n\n    // Auto-finds Rigidbody2D in parent hierarchy\n    [ParentComponent]\n    private Rigidbody2D rigidbody;\n\n    // Auto-finds all Collider2D in immediate children only\n    [ChildComponent(OnlyDescendants = true, MaxDepth = 1)]\n    private Collider2D[] childColliders;\n\n    void Awake()\n    {\n        // One call wires all attributed components\n        this.AssignRelationalComponents();\n\n        // Now use them\n        spriteRenderer.color = Color.red;\n        rigidbody.velocity = Vector2.up * 5f;\n        Debug.Log($\"Found {childColliders.Length} child colliders\");\n    }\n}\n</code></pre> <p>\u26a0\ufe0f Common Mistake: Don't call <code>AssignRelationalComponents()</code> in <code>Update()</code> - it should only run once during initialization (Awake/Start).</p> <p>Learn More: Relational Components</p>"},{"location":"overview/getting-started/#using-with-di-containers-vcontainerzenjectreflex","title":"Using With DI Containers (VContainer/Zenject/Reflex)","text":"<ul> <li>If you use dependency injection, you can auto-populate relational fields right after DI injection.</li> <li>Quick setup:</li> <li>VContainer: in <code>LifetimeScope.Configure</code>, call <code>builder.RegisterRelationalComponents()</code>.</li> <li>Zenject/Extenject: add <code>RelationalComponentsInstaller</code> to your <code>SceneContext</code> and (optionally) enable the scene scan on initialize.</li> <li>Reflex: attach <code>RelationalComponentsInstaller</code> alongside your <code>SceneScope</code>. The installer binds the assigner, hydrates the active scene, and can listen for additive scenes. Use <code>ContainerRelationalExtensions</code> helpers (<code>InjectWithRelations</code>, <code>InstantiateGameObjectWithRelations</code>, etc.) when spawning objects through the container.</li> <li>Samples: See sample folders in the repository for VContainer, Zenject, and Reflex integration examples</li> <li>Full guide with scenarios and testing tips: Dependency Injection Integrations</li> </ul>"},{"location":"overview/getting-started/#example-3-spatial-queries-intermediate","title":"Example 3: Spatial Queries (Intermediate)","text":"<p>Problem: Finding nearby objects with <code>FindObjectsOfType</code> and distance checks is O(n) and slow.</p> <p>Solution:</p> C#<pre><code>using WallstopStudios.UnityHelpers.Core.DataStructure;\nusing UnityEngine;\nusing System.Collections.Generic;\n\npublic class EnemyManager : MonoBehaviour\n{\n    private QuadTree2D&lt;Enemy&gt; enemyTree;\n    private List&lt;Enemy&gt; nearbyBuffer = new(64); // Reusable buffer\n\n    void Start()\n    {\n        // Build tree once (O(n log n))\n        Enemy[] enemies = FindObjectsOfType&lt;Enemy&gt;();\n        enemyTree = new QuadTree2D&lt;Enemy&gt;(enemies, e =&gt; e.transform.position);\n    }\n\n    public List&lt;Enemy&gt; GetEnemiesNearPlayer(Vector2 playerPos, float radius)\n    {\n        nearbyBuffer.Clear();\n\n        // Fast query: O(log n) instead of O(n)\n        enemyTree.GetElementsInRange(playerPos, radius, nearbyBuffer);\n\n        return nearbyBuffer;\n    }\n}\n</code></pre> <p>\u26a0\ufe0f Common Mistake: Spatial trees are immutable - you must rebuild the tree when enemy positions change. For frequently moving objects, use <code>SpatialHash2D</code> instead.</p> <p>Learn More:</p> <ul> <li>2D Spatial Trees Guide</li> <li>Performance Benchmarks</li> </ul>"},{"location":"overview/getting-started/#what-should-i-learn-next","title":"What Should I Learn Next?","text":"<p>Based on your needs:</p>"},{"location":"overview/getting-started/#for-gameplay-programmers","title":"For Gameplay Programmers","text":"<ol> <li>Master the Effects System - Data-driven buffs/debuffs</li> <li>Start: Effects System TL;DR</li> <li> <p>Why: Build status effects without writing repetitive code</p> </li> <li> <p>Use Spatial Trees for AI - Efficient awareness systems</p> </li> <li>Start: Spatial Trees 2D Guide</li> <li> <p>Why: Make enemy AI scale to hundreds of units</p> </li> <li> <p>Learn Serialization - Save systems and networking</p> </li> <li>Start: Serialization Guide</li> <li>Why: Save/load with Unity types supported</li> </ol>"},{"location":"overview/getting-started/#for-toolseditor-programmers","title":"For Tools/Editor Programmers","text":"<ol> <li>Explore Editor Tools - Automate your asset pipeline</li> <li>Start: Editor Tools Guide</li> <li> <p>Why: 20+ tools for sprites, animations, validation, and more</p> </li> <li> <p>Use ScriptableObject Singletons - Global settings management</p> </li> <li>Start: Singletons Guide</li> <li> <p>Why: Auto-created, Odin-compatible config assets</p> </li> <li> <p>Master Property Drawers - Better inspector workflows</p> </li> <li>Start: Property Drawers</li> <li>Why: Conditional fields, dropdowns, validation</li> </ol>"},{"location":"overview/getting-started/#for-performance-focused-developers","title":"For Performance-Focused Developers","text":"<ol> <li>Study Data Structures - Choose the right container</li> <li>Start: Data Structures Guide</li> <li> <p>Why: Heaps, tries, sparse sets, and more with clear trade-offs</p> </li> <li> <p>Use Math Helpers - Avoid common pitfalls</p> </li> <li>Start: Math &amp; Extensions</li> <li> <p>Why: Modulo, geometry, color averaging, and more</p> </li> <li> <p>Adopt the Buffering Pattern - Zero-allocation queries</p> </li> <li>Start: Buffering Pattern</li> <li>Why: Stable GC even under load</li> </ol>"},{"location":"overview/getting-started/#common-questions","title":"Common Questions","text":""},{"location":"overview/getting-started/#is-this-production-ready","title":"\"Is this production-ready?\"","text":"<p>Yes! Unity Helpers is:</p> <ul> <li>\u2705 Used in shipped commercial games</li> <li>\u2705 8,000+ automated test cases</li> <li>\u2705 Compatible with Unity 2022, 2023, and Unity 6</li> <li>\u2705 Zero external dependencies \u2014 protobuf-net is bundled</li> <li>\u2705 Fully WebGL/IL2CPP compatible with optimized SINGLE_THREADED hot paths</li> <li>\u2705 Multiplatform support - Desktop, Mobile, Web, and Consoles</li> <li>\u26a0\ufe0f Requires .NET Standard 2.1</li> </ul>"},{"location":"overview/getting-started/#will-this-conflict-with-my-existing-code","title":"\"Will this conflict with my existing code?\"","text":"<p>No! Unity Helpers:</p> <ul> <li>\u2705 Uses namespaces (<code>WallstopStudios.UnityHelpers.*</code>)</li> <li>\u2705 Doesn't modify Unity types or global state</li> <li>\u2705 Opt-in design - use what you need</li> </ul>"},{"location":"overview/getting-started/#how-do-i-get-help","title":"\"How do I get help?\"","text":"<ol> <li>Check the Troubleshooting section    in the relevant guide</li> <li>Search the GitHub Issues</li> <li>Open a new issue with code examples and error messages</li> </ol>"},{"location":"overview/getting-started/#can-i-use-this-in-commercial-projects","title":"\"Can I use this in commercial projects?\"","text":"<p>Yes! Unity Helpers is released under the MIT License - use it freely in commercial projects.</p>"},{"location":"overview/getting-started/#next-steps","title":"Next Steps","text":"<p>Pick one feature that solves your immediate problem:</p> Your Need Start Here Time to Learn Faster random numbers Random Performance ~5 min Auto-wire components Relational Components ~10 min Spatial queries 2D Spatial Trees ~15 min Buff/debuff system Effects System ~20 min Save/load data Serialization ~20 min Editor automation Editor Tools ~30 min Global settings Singletons ~10 min <p>Ready to dive deeper? Return to the main README for the complete feature list.</p> <p>Building something cool? We'd love to hear about it! Share your experience by opening an issue.</p>"},{"location":"overview/getting-started/#related-documentation","title":"\ud83d\udcda Related Documentation","text":"<p>Core Guides:</p> <ul> <li>Main README - Complete feature overview</li> <li>Feature Index - Alphabetical reference</li> <li>Glossary - Term definitions</li> <li>Odin Migration Guide - Migrate from Odin Inspector</li> </ul> <p>Deep Dives:</p> <ul> <li>Relational Components - Auto-wiring guide</li> <li>Effects System - Buff/debuff system</li> <li>Spatial Trees 2D - Fast spatial queries</li> <li>Serialization - Save systems and networking</li> <li>Editor Tools - Asset pipeline automation</li> </ul> <p>DI Integration:</p> <ul> <li>VContainer Sample - VContainer integration guide (see Samples~ folder in repo)</li> <li>Zenject Sample - Zenject integration guide (see Samples~ folder in repo)</li> </ul> <p>Need help? Open an issue or check Troubleshooting</p>"},{"location":"overview/glossary/","title":"Glossary","text":"<p>Quick reference for terms used throughout Unity Helpers documentation.</p>"},{"location":"overview/glossary/#core-concepts","title":"Core Concepts","text":""},{"location":"overview/glossary/#attribute","title":"Attribute","text":"<ul> <li>A dynamic numeric value with a base and calculated current value</li> <li>Current value applies all active modifications from effects</li> <li>Used in the Effects System for stats like Health, Speed, Defense</li> <li>See: Effects System</li> </ul>"},{"location":"overview/glossary/#buffering-pattern","title":"Buffering Pattern","text":"<ul> <li>Reusing pre-allocated collections (List, arrays) to minimize GC allocations</li> <li>Pass a buffer to API methods that clear and fill it with results</li> <li>Critical for performance in hot paths (per-frame queries)</li> <li>See: Buffering Pattern</li> </ul>"},{"location":"overview/glossary/#immutable-tree","title":"Immutable Tree","text":"<ul> <li>Spatial data structure that cannot be modified after creation</li> <li>Must be rebuilt when underlying data changes</li> <li>Provides consistent query performance but requires full reconstruction</li> <li>Examples: QuadTree2D, KdTree2D, RTree2D</li> <li>See: Spatial Trees</li> </ul>"},{"location":"overview/glossary/#odin-compatibility","title":"Odin Compatibility","text":"<ul> <li>Automatic integration with Odin Inspector when installed</li> <li>Base classes switch from MonoBehaviour \u2192 SerializedMonoBehaviour</li> <li>Enables serialization of dictionaries, polymorphic fields, etc.</li> <li>No code changes required - works automatically via #if ODIN_INSPECTOR</li> <li>See: Singletons - Odin Compatibility</li> </ul>"},{"location":"overview/glossary/#pooled-buffers","title":"Pooled Buffers","text":"<ul> <li>Reusable memory allocations managed by <code>Buffers&lt;T&gt;</code> or <code>WallstopArrayPool&lt;T&gt;</code></li> <li>Reduces GC pressure by recycling collections instead of allocating new ones</li> <li>Use with <code>using</code> statements for automatic cleanup</li> <li>See: Buffering Pattern</li> </ul>"},{"location":"overview/glossary/#relational-components","title":"Relational Components","text":"<ul> <li>Attributes that auto-wire component references via hierarchy traversal</li> <li>Includes: <code>[SiblingComponent]</code>, <code>[ParentComponent]</code>, <code>[ChildComponent]</code></li> <li>Eliminates manual GetComponent calls</li> <li>See: Relational Components</li> </ul>"},{"location":"overview/glossary/#seedable-random","title":"Seedable Random","text":"<ul> <li>Random number generator that accepts a seed value for deterministic output</li> <li>Same seed = same sequence of random numbers</li> <li>Essential for replay systems, networked games, procedural generation</li> <li>See: Random Performance</li> </ul>"},{"location":"overview/glossary/#data-structures","title":"Data Structures","text":""},{"location":"overview/glossary/#binary-heap","title":"Binary Heap","text":"<ul> <li>Array-backed binary tree maintaining min/max heap property</li> <li>O(log n) push/pop, O(1) peek</li> <li>Used for priority queues, pathfinding, event scheduling</li> <li>See: Data Structures - Heap</li> </ul>"},{"location":"overview/glossary/#cyclic-buffer-ring-buffer","title":"Cyclic Buffer (Ring Buffer)","text":"<ul> <li>Fixed-capacity circular array with wrapping head/tail pointers</li> <li>O(1) enqueue/dequeue at both ends</li> <li>Overwrites oldest data when full</li> <li>See: Data Structures - Cyclic Buffer</li> </ul>"},{"location":"overview/glossary/#disjoint-set-union-find","title":"Disjoint Set (Union-Find)","text":"<ul> <li>Data structure tracking partitions of elements into sets</li> <li>Near O(1) union/find operations with path compression</li> <li>Used for connectivity, clustering, MST algorithms</li> <li>See: Data Structures - Disjoint Set</li> </ul>"},{"location":"overview/glossary/#kdtree-k-dimensional-tree","title":"KdTree (K-Dimensional Tree)","text":"<ul> <li>Binary tree partitioning space along alternating axes</li> <li>Excellent for nearest neighbor queries on points</li> <li>Balanced variant: consistent query time; Unbalanced: faster builds</li> <li>See: 2D Spatial Trees</li> </ul>"},{"location":"overview/glossary/#quadtree","title":"QuadTree","text":"<ul> <li>Tree recursively splitting 2D space into four quadrants</li> <li>General-purpose spatial structure for points</li> <li>Good for range queries, broad-phase collision detection</li> <li>See: 2D Spatial Trees</li> </ul>"},{"location":"overview/glossary/#rtree","title":"RTree","text":"<ul> <li>Tree grouping items by minimum bounding rectangles (MBRs)</li> <li>Optimized for objects with size/bounds</li> <li>Excellent for bounds intersection queries</li> <li>See: 2D Spatial Trees</li> </ul>"},{"location":"overview/glossary/#sparse-set","title":"Sparse Set","text":"<ul> <li>Two arrays (sparse + dense) enabling O(1) membership checks</li> <li>O(1) insert/remove/contains with cache-friendly dense iteration</li> <li>Requires contiguous ID space for indices</li> <li>See: Data Structures - Sparse Set</li> </ul>"},{"location":"overview/glossary/#spatial-hash","title":"Spatial Hash","text":"<ul> <li>Grid-based spatial structure with fixed cell size</li> <li>Excellent for many moving objects uniformly distributed</li> <li>O(1) insertion with fast approximate queries</li> <li>See: README - When to Use Spatial Trees</li> </ul>"},{"location":"overview/glossary/#trie-prefix-tree","title":"Trie (Prefix Tree)","text":"<ul> <li>Tree keyed by characters for efficient prefix lookups</li> <li>O(m) search where m = key length</li> <li>Used for autocomplete, spell-checking, dictionary queries</li> <li>See: Data Structures - Trie</li> </ul>"},{"location":"overview/glossary/#editor-tools","title":"Editor &amp; Tools","text":""},{"location":"overview/glossary/#attribute-metadata-cache","title":"Attribute Metadata Cache","text":"<ul> <li>Pre-generated metadata for Effects System attributes</li> <li>Eliminates runtime reflection overhead</li> <li>Powers editor dropdowns for attribute names</li> <li>Auto-generated on editor load</li> <li>See: Editor Tools - Attribute Metadata Cache</li> </ul>"},{"location":"overview/glossary/#property-drawer","title":"Property Drawer","text":"<ul> <li>Custom inspector rendering for serialized fields</li> <li>Examples: <code>[WShowIf]</code>, <code>[StringInList]</code>, <code>[WReadOnly]</code></li> <li>Improves editor workflows with conditional display, validation, etc.</li> <li>See: Property Drawers</li> </ul>"},{"location":"overview/glossary/#scriptableobject-singleton","title":"ScriptableObject Singleton","text":"<ul> <li>Global settings/data singleton backed by a Resources asset</li> <li>Auto-created by editor tool with <code>[ScriptableSingletonPath]</code> attribute</li> <li>Accessed via <code>T.Instance</code> pattern</li> <li>See: Singletons</li> </ul>"},{"location":"overview/glossary/#patterns-techniques","title":"Patterns &amp; Techniques","text":""},{"location":"overview/glossary/#douglas-peucker-algorithm","title":"Douglas-Peucker Algorithm","text":"<ul> <li>Polyline simplification algorithm that reduces vertex count</li> <li>Preserves shape within epsilon tolerance</li> <li>Used by <code>LineHelper.Simplify</code> and <code>SimplifyPrecise</code></li> <li>See: Math &amp; Extensions - Geometry</li> </ul>"},{"location":"overview/glossary/#effects-pipeline","title":"Effects Pipeline","text":"<ul> <li>Data-driven gameplay modification system</li> <li>Flow: Author AttributeEffect \u2192 Apply to GameObject \u2192 Modifications + Tags + Cosmetics</li> <li>Handles stacking, duration, removal automatically</li> <li>See: Effects System</li> </ul>"},{"location":"overview/glossary/#handle-effect-handle","title":"Handle (Effect Handle)","text":"<ul> <li>Opaque identifier for a specific effect application instance</li> <li>Used to remove one stack of an effect</li> <li>Only returned for Duration/Infinite effects (Instant returns null)</li> <li>See: Effects System</li> </ul>"},{"location":"overview/glossary/#positive-modulo","title":"Positive Modulo","text":"<ul> <li>Modulo operation that always returns non-negative results</li> <li>Essential for array indices and angle normalization</li> <li>Use <code>WallMath.PositiveMod</code> instead of <code>%</code> operator</li> <li>See: Math &amp; Extensions - Numeric Helpers</li> </ul>"},{"location":"overview/glossary/#tag-handler","title":"Tag Handler","text":"<ul> <li>Component managing string tags with reference counting</li> <li>Multiple sources can apply same tag; removed when count reaches 0</li> <li>Used for categorical states (Stunned, Poisoned, Invulnerable)</li> <li>See: Effects System</li> </ul>"},{"location":"overview/glossary/#serialization","title":"Serialization","text":""},{"location":"overview/glossary/#protobuf-protocol-buffers","title":"Protobuf (Protocol Buffers)","text":"<ul> <li>Compact binary serialization format from Google</li> <li>Forward/backward compatible with schema evolution</li> <li>Requires <code>[ProtoContract]</code> and <code>[ProtoMember(n)]</code> annotations</li> <li>See: Serialization - Protobuf</li> </ul>"},{"location":"overview/glossary/#systemtextjson","title":"System.Text.Json","text":"<ul> <li>Modern .NET JSON serialization library</li> <li>Unity Helpers provides custom converters for Unity types</li> <li>Profiles: Normal, Pretty, Fast, FastPOCO</li> <li>See: Serialization - JSON</li> </ul>"},{"location":"overview/glossary/#unity-converters","title":"Unity Converters","text":"<ul> <li>Custom JSON converters for Unity engine types</li> <li>Supports: Vector\u2154/4, Vector2Int/3Int, Color/Color32/ColorBlock, Quaternion, Matrix4x4, Pose, Plane, SphericalHarmonicsL2, Bounds/BoundsInt, Rect/RectInt/RectOffset, RangeInt, Ray/Ray2D/RaycastHit, BoundingSphere, Resolution, RenderTextureDescriptor, LayerMask, Hash128, Scene, AnimationCurve, Gradient, Touch, GameObject, ParticleSystem.MinMaxCurve, ParticleSystem.MinMaxGradient, System.Type</li> <li>Automatically included in Unity Helpers JSON options</li> <li>See: Serialization</li> </ul>"},{"location":"overview/glossary/#performance-terms","title":"Performance Terms","text":""},{"location":"overview/glossary/#amortized-complexity","title":"Amortized Complexity","text":"<ul> <li>Average complexity over many operations</li> <li>Example: Deque push is O(1) amortized (occasional O(n) resize)</li> <li>Smooths out occasional expensive operations</li> </ul>"},{"location":"overview/glossary/#big-o-notation","title":"Big-O Notation","text":"<ul> <li>Describes algorithm scaling behavior</li> <li>O(1) = constant time, O(log n) = logarithmic, O(n) = linear, O(n\u00b2) = quadratic</li> <li>Smaller is better; focus on dominant term</li> </ul>"},{"location":"overview/glossary/#cache-friendly","title":"Cache-Friendly","text":"<ul> <li>Data layout that maximizes CPU cache hits</li> <li>Contiguous memory access patterns (arrays) are cache-friendly</li> <li>Random memory jumps (linked lists) are cache-unfriendly</li> </ul>"},{"location":"overview/glossary/#gc-pressure","title":"GC Pressure","text":"<ul> <li>Frequency and volume of garbage collection required</li> <li>High pressure = frequent allocations = more GC pauses</li> <li>Reduce with object pooling, reusable buffers, value types</li> </ul>"},{"location":"overview/glossary/#hot-path","title":"Hot Path","text":"<ul> <li>Code executed very frequently (per-frame, per-update)</li> <li>Performance critical; avoid allocations and expensive operations</li> <li>Profile to identify actual hot paths</li> </ul>"},{"location":"overview/glossary/#il2cpp","title":"IL2CPP","text":"<ul> <li>Unity's ahead-of-time (AOT) compiler for mobile/console</li> <li>Reflection is expensive; metadata caching becomes critical</li> <li>Some reflection patterns may not work; prefer cached delegates</li> </ul>"},{"location":"overview/glossary/#abbreviations","title":"Abbreviations","text":""},{"location":"overview/glossary/#aabb","title":"AABB","text":"<p>Axis-Aligned Bounding Box</p>"},{"location":"overview/glossary/#aot","title":"AOT","text":"<p>Ahead-Of-Time (compilation)</p>"},{"location":"overview/glossary/#dto","title":"DTO","text":"<p>Data Transfer Object (simple data container for serialization)</p>"},{"location":"overview/glossary/#fifo","title":"FIFO","text":"<p>First-In-First-Out (queue behavior)</p>"},{"location":"overview/glossary/#gc","title":"GC","text":"<p>Garbage Collector/Garbage Collection</p>"},{"location":"overview/glossary/#hdrp","title":"HDRP","text":"<p>High Definition Render Pipeline</p>"},{"location":"overview/glossary/#knn","title":"kNN","text":"<p>k-Nearest Neighbors</p>"},{"location":"overview/glossary/#lifo","title":"LIFO","text":"<p>Last-In-First-Out (stack behavior)</p>"},{"location":"overview/glossary/#mbr","title":"MBR","text":"<p>Minimum Bounding Rectangle</p>"},{"location":"overview/glossary/#mst","title":"MST","text":"<p>Minimum Spanning Tree</p>"},{"location":"overview/glossary/#poco","title":"POCO","text":"<p>Plain Old CLR Object (simple class with no framework dependencies)</p>"},{"location":"overview/glossary/#ppu","title":"PPU","text":"<p>Pixels Per Unit (sprite import setting)</p>"},{"location":"overview/glossary/#prng","title":"PRNG","text":"<p>Pseudo-Random Number Generator</p>"},{"location":"overview/glossary/#rng","title":"RNG","text":"<p>Random Number Generator</p>"},{"location":"overview/glossary/#urp","title":"URP","text":"<p>Universal Render Pipeline</p>"},{"location":"overview/glossary/#see-also","title":"See Also","text":"<ul> <li>Feature Index - Alphabetical feature index</li> <li>Getting Started Guide - Quick start guide</li> <li>Main Documentation - Main documentation</li> </ul>"},{"location":"overview/roadmap/","title":"Unity Helpers Roadmap","text":"<p>This roadmap outlines planned enhancements to Unity Helpers. All \"Currently shipping\" features are production-ready and available now. See the main README for current capabilities.</p>"},{"location":"overview/roadmap/#1-comprehensive-inspector-tooling","title":"1. Comprehensive Inspector Tooling","text":"<p>Currently shipping: Attribute and property drawer suite covering enum toggles, dropdowns, conditional display, grouping, buttons, and validation. Key attributes: <code>WEnumToggleButtons</code>, <code>WShowIf</code>, <code>WValueDropDown</code>, <code>WGroup</code>, <code>WButton</code>, <code>WNotNull</code>, <code>ValidateAssignment</code>.</p> <p>Next up:</p> <ul> <li>Odin Inspector migration tooling</li> <li>Investigate color themes (had a version of this for WGroup, had to scrap it due to complexity)</li> <li>Tabbed/section navigation with persistent layout bookmarks</li> <li>Visual instrumentation (progress bars, warning badges, inline state telemetry)</li> <li>Additional attributes, serializable types</li> </ul>"},{"location":"overview/roadmap/#2-expanded-editor-tooling","title":"2. Expanded Editor Tooling","text":"<p>Currently shipping: Animation Creator, Sprite Sheet Animation Creator, Animation Event Editor, plus 20+ sprite/texture/prefab utilities. See Editor Tools Guide for full list.</p> <p>Next up:</p> <ul> <li>Animation Creator enhancements</li> <li>Sprite Sheet Animation Creator enhancements</li> <li>Animation Event Editor refinements</li> <li>Additional automation surfaces</li> </ul>"},{"location":"overview/roadmap/#3-advanced-random-statistical-testing","title":"3. Advanced Random &amp; Statistical Testing","text":"<p>Currently shipping: 15+ high-quality RNG implementations (IllusionFlow, PcgRandom, XoroShiro, SplitMix64, RomuDuo, FlurryBurst, PhotonSpin, etc.) with extensive <code>IRandom</code> API. See Random Performance.</p> <p>Next up:</p> <ul> <li>CI-friendly statistical harness: PractRand/TestU01 suites with automated pass/fail artifacts</li> <li>Automated quality reports: histograms, percentile deltas, change detection for PR gates</li> <li>Higher-level sampling: Poisson disk, stratified sampling, correlated noise, shuffled streams, deterministic scenario builders</li> <li>Investigation of Job/Burst-aware stream schedulers: seed pools, jump-ahead APIs, reservoir/permutation helpers with property-based tests</li> </ul>"},{"location":"overview/roadmap/#4-enhanced-spatial-trees","title":"4. Enhanced Spatial Trees","text":"<p>Currently shipping: Production 2D trees (QuadTree2D, KdTree2D, RTree2D, SpatialHash2D) and experimental 3D variants (OctTree3D, KdTree3D, RTree3D, SpatialHash3D). See 2D Performance and 3D Performance.</p> <p>Next up:</p> <ul> <li>Graduate 3D trees to production: profiling data, comprehensive docs, parity with 2D APIs</li> <li>Mutable/incremental updates/variants: localized inserts/removals without full rebuilds</li> <li>Unity Physics parity: ray/capsule/sphere casts, overlap tests, PhysicsScene adapter structs</li> <li>Investigate streaming builders: tile-based loading for large worlds, job-based construction</li> </ul>"},{"location":"overview/roadmap/#5-ui-toolkit-enhancements","title":"5. UI Toolkit Enhancements","text":"<p>Currently shipping: LayeredImage and MultiFileSelectorElement custom visual elements with samples and persistence helpers.</p> <p>Next up:</p> <ul> <li>Control pack: dockable panes, inspector tab bars, data tables, curve editors, virtualized multi-column lists</li> <li>Theme/palette system: USS/UXML snippets with runtime/editor parity samples</li> <li>Performance patterns: batched bindings, incremental painters, list virtualization utilities with comprehensive docs</li> <li>Automation dashboards: UI Toolkit-based wizards for workflow automation</li> </ul>"},{"location":"overview/roadmap/#6-utility-expansion","title":"6. Utility Expansion","text":"<p>Currently shipping: Extensive utilities covering pooling (Buffers, array pools), singleton patterns, animation helpers, sprite utilities, compression, math extensions, and more. See Helper Utilities.</p> <p>Next up:</p> <ul> <li>Cross-system bridges: effects \u2194 serialization, pooling \u2194 DI containers, random \u2194 spatial query fuzzers with ready-made samples</li> <li>Math/combinatorics helpers: curve fitting, statistics, interpolation packs, IO/localization conveniences</li> <li>Service patterns: task/tween schedulers, async job orchestrators, gameplay timers with integrated diagnostics</li> </ul>"},{"location":"overview/roadmap/#7-performance-program","title":"7. Performance Program","text":"<p>Currently shipping: Comprehensive benchmarks for random generators, spatial trees, reflection helpers, and IList sorting. See Random Performance, Spatial Tree Performance, and Reflection Performance.</p> <p>Next up:</p> <ul> <li>Automated benchmark harness: CI integration, baseline storage, regression detection per subsystem</li> <li>Investigate burst/Jobs optimizations: hot loop rewrites for spatial queries, pooling, math helpers with analyzer hints</li> <li>Allocation/GC audits: Roslyn analyzers and NUnit tests enforcing zero-allocation guarantees for critical APIs</li> <li>Safety analyzers: custom Roslyn rule that flags <code>SerializableNullable&lt;T&gt;.Value</code> access without a preceding <code>HasValue</code> check (Unity asmdef-friendly package)</li> </ul>"},{"location":"overview/roadmap/#8-attribute-tag-system-evolution","title":"8. Attribute &amp; Tag System Evolution","text":"<p>Currently shipping: Data-driven effects system with attributes, tags, effect stacks, and metadata caches. ScriptableObject-driven effect authoring with cosmetics and duration management. See Effects System.</p> <p>Next up:</p> <ul> <li>Effect visualization: inspector timeline for active effects, stack inspection, debug overlays</li> <li>Attribute graphs: dependency tracking with automatic recalculation when modifiers change</li> <li>Migration tools: schema evolution helpers for effects and attribute definitions</li> </ul>"},{"location":"overview/roadmap/#9-relational-component-enhancements","title":"9. Relational Component Enhancements","text":"<p>Currently shipping: Component auto-wiring attributes (SiblingComponent, ParentComponent, ChildComponent) with DI integrations for VContainer, Zenject, and Reflex. See Relational Components.</p> <p>Next up:</p> <ul> <li>Performance improvements: cached reflection paths, Roslyn source generators for zero-reflection wiring</li> <li>Enhanced validation: editor-time dependency visualization, hierarchy relationship graphs</li> <li>Advanced querying: interface-based resolution, filtered component searches</li> </ul>"},{"location":"performance/baseline-tests-performance/","title":"Performance Baseline Tests","text":"<p>Auto-generated via PerformanceBaselineTests.GeneratePerformanceBaselineReport. Run the test explicitly to refresh these tables.</p> <p>These tests serve as automated CI regression guards. They verify that critical operations complete within acceptable time bounds, detecting performance regressions before they reach production.</p>"},{"location":"performance/baseline-tests-performance/#baseline-philosophy","title":"Baseline Philosophy","text":"<p>Baselines are set generously (2-3x expected typical performance) to account for CI environment variability while still catching significant regressions. A test failure indicates a performance regression that needs investigation.</p>"},{"location":"performance/baseline-tests-performance/#test-categories","title":"Test Categories","text":"<ul> <li>Spatial Trees: QuadTree2D, KdTree2D, KdTree3D, OctTree3D, RTree2D construction and query performance</li> <li>PRNG: Random number generation throughput for PcgRandom, XoroShiroRandom, SplitMix64, RomuDuo</li> <li>Pooling: Collection pool rent/return overhead for List, HashSet, Dictionary, StringBuilder, SystemArrayPool</li> <li>Serialization: JSON and Protobuf serialization/deserialization throughput</li> </ul>"},{"location":"performance/baseline-tests-performance/#performance-baseline-report","title":"Performance Baseline Report","text":"<p>Generated: 2026-01-12 01:36:55 UTC</p>"},{"location":"performance/baseline-tests-performance/#spatial-trees","title":"Spatial Trees","text":"Test Iterations Time (ms) Baseline (ms) % of Baseline Status QuadTree2DRangeQuery1K2720013.5%Pass QuadTree2DBoundsQuery1K2920014.5%Pass KdTree2DRangeQuery1K2720013.5%Pass KdTree2DNearestNeighbor1K3220016.0%Pass RTree2DRangeQuery1K24792001239.5%FAIL OctTree3DRangeQuery1K152007.5%Pass KdTree3DRangeQuery1K3320016.5%Pass QuadTree2DConstruction125000.4%Pass KdTree2DConstruction125000.4%Pass RTree2DConstruction115000.2%Pass"},{"location":"performance/baseline-tests-performance/#prng","title":"PRNG","text":"Test Iterations Time (ms) Baseline (ms) % of Baseline Status PcgRandomNextInt1M15000.2%Pass PcgRandomNextFloat1M55001.0%Pass XoroShiroRandomNextInt1M15000.2%Pass SplitMix64NextInt1M15000.2%Pass RomuDuoNextInt1M15000.2%Pass"},{"location":"performance/baseline-tests-performance/#pooling","title":"Pooling","text":"Test Iterations Time (ms) Baseline (ms) % of Baseline Status ListPooling100K239504200119752.0%FAIL HashSetPooling100K165032008251.5%FAIL DictionaryPooling100K169972008498.5%FAIL SystemArrayPool100K82004.0%Pass StringBuilderPooling100K164562008228.0%FAIL"},{"location":"performance/baseline-tests-performance/#serialization","title":"Serialization","text":"Test Iterations Time (ms) Baseline (ms) % of Baseline Status JsonSerialize10K435008.6%Pass JsonDeserialize10K6450012.8%Pass JsonRoundTrip10K113100011.3%Pass ProtobufSerialize10K1169500233.8%FAIL ProtobufDeserialize10K125002.4%Pass ProtobufRoundTrip10K17281000172.8%FAIL"},{"location":"performance/baseline-tests-performance/#summary","title":"Summary","text":"<p>19 passed, 7 failed out of 26 tests.</p>"},{"location":"performance/baseline-tests-performance/#running-the-tests","title":"Running the Tests","text":"<p>These tests run automatically during CI to catch regressions. To generate fresh benchmark results:</p> <ol> <li>Open Unity Test Runner</li> <li>Navigate to <code>PerformanceBaselineTests</code></li> <li>Run <code>GeneratePerformanceBaselineReport</code> explicitly (it is marked <code>[Explicit]</code>)</li> <li>Results will be output to the console and can be copied to this document</li> </ol>"},{"location":"performance/baseline-tests-performance/#interpreting-results","title":"Interpreting Results","text":"<ul> <li>Time (ms): Actual measured time for the operation</li> <li>Baseline (ms): Maximum allowed time before test failure</li> <li>% of Baseline: How much of the baseline budget was used (lower is better)</li> <li>Status: Pass if within baseline, Fail if exceeded</li> </ul>"},{"location":"performance/ilist-sorting-performance/","title":"IList Sorting Performance Benchmarks","text":"<p>Unity Helpers ships several custom sorting algorithms for <code>IList&lt;T&gt;</code> that cover different trade-offs between adaptability, allocation patterns, and stability. This page gathers context and benchmark snapshots so you can choose the right algorithm for your workload and compare results across operating systems.</p>"},{"location":"performance/ilist-sorting-performance/#algorithm-cheatsheet","title":"Algorithm Cheatsheet","text":"Algorithm Stable? Best For Reference Ghost Sort No Mixed workloads that benefit from adaptive gap sorting and few allocations Upstream project by Will Stafford Parsons (public repository currently offline) Meteor Sort No Almost-sorted data where gap shrinking beats plain insertion sort Upstream project by Will Stafford Parsons (public repository currently offline) Pattern-Defeating QuickSort No General-purpose quicksort with protections against worst-case inputs pdqsort by Orson Peters Grail Sort Yes Large datasets where stability + low allocations matter GrailSort Power Sort Yes Partially ordered data that benefits from adaptive run detection PowerSort (Munro &amp; Wild) Tim Sort Yes General-purpose stable sorting with abundant natural runs Wikipedia - Timsort Jesse Sort No Data with long runs or duplicates where dual patience piles shine JesseSort Green Sort Yes Sustainable stable merges that trim ordered prefixes greeNsort Ska Sort No Branch-friendly partitioning on large unstable datasets Ska Sort Ipn Sort No In-place adaptive quicksort scenarios needing robust pivots ipnsort write-up Smooth Sort No Weak-heap hybrid that approaches O(n) for presorted data Smoothsort - Wikipedia Block Merge Sort Yes Stable merges with \u221an buffer (WikiSort style) WikiSort IPS\u2074o Sort No Cache-aware samplesort with multiway partitioning IPS\u2074o paper Power Sort Plus Yes Enhanced run-priority merges inspired by Wild &amp; Nebel PowerSort paper Glide Sort Yes Stable galloping merges from the Rust glidesort research sort-research-rs Flux Sort No Dual-pivot quicksort tuned for modern CPUs sort-research-rs Insertion Sort Yes Tiny or nearly sorted collections where O(n\u00b2) is acceptable Wikipedia - Insertion sort <p>What does \u201cstable\u201d mean? Stable sorting algorithms preserve the relative order of elements that compare as equal. This matters when items carry secondary keys (e.g., sorting people by last name but keeping first-name order deterministic). Unstable algorithms can reshuffle equal entries, which is usually fine for numeric keys but can break deterministic pipelines.</p> <p>Heads up: The original Ghost Sort repository was formerly hosted on GitHub under <code>wstaffordp/ghostsort</code>, but it currently returns 404. The Unity Helpers implementation remains based on that source; we will relink if/when an official mirror returns.</p>"},{"location":"performance/ilist-sorting-performance/#dataset-scenarios","title":"Dataset Scenarios","text":"<ul> <li>Sorted \u2013 ascending integers, verifying best-case behavior.</li> <li>Nearly Sorted (2% swaps) \u2013 deterministic neighbor swaps introduce light disorder to expose adaptive optimizations.</li> <li>Shuffled (deterministic) \u2013 Fisher\u2013Yates shuffle using a fixed seed for reproducibility across runs and machines.</li> </ul> <p>Each benchmark sorts a fresh copy of the dataset once and reports wall-clock duration. If a cell still shows <code>pending</code>, re-run the benchmark suite to collect fresh data for that algorithm/dataset size.</p> <p>Run the <code>IListSortingPerformanceTests.Benchmark</code> test inside Unity\u2019s Test Runner to refresh the tables below. Results automatically land in the section that matches the current operating system.</p>"},{"location":"performance/ilist-sorting-performance/#windows-editorplayer","title":"Windows (Editor/Player)","text":"<p>Last updated 2026-01-12 01:17 UTC on Windows 11 (10.0.26200) 64bit</p> <p>Times are single-pass measurements in milliseconds (lower is better). <code>n/a</code> indicates the algorithm was skipped for the dataset size.</p>"},{"location":"performance/ilist-sorting-performance/#sorted","title":"Sorted","text":"List Size Ghost Meteor Pattern-Defeating QuickSort Grail Power Insertion Tim Jesse Green Ska Ipn Smooth Block IPS4o Power+ Glide Flux 1000.029 ms0.002 ms0.001 ms0.001 ms0.002 ms0.001 ms0.001 ms0.624 ms0.001 ms0.007 ms0.001 ms0.002 ms0.001 ms0.001 ms0.001 ms0.002 ms0.002 ms 1,0000.023 ms0.027 ms0.008 ms0.007 ms0.007 ms0.006 ms0.006 ms54.3 ms0.007 ms0.112 ms0.009 ms0.018 ms0.005 ms0.040 ms0.007 ms0.007 ms0.029 ms 10,0000.305 ms0.394 ms0.077 ms0.066 ms0.049 ms0.066 ms0.047 ms3.24 s0.071 ms1.45 ms0.083 ms0.175 ms0.056 ms0.571 ms0.048 ms0.048 ms0.386 ms 100,0003.51 ms5.14 ms0.764 ms0.658 ms0.469 msn/a0.459 ms136.86 s0.670 ms18.5 ms0.773 ms1.80 ms0.512 ms7.57 ms0.473 ms0.479 ms4.93 ms"},{"location":"performance/ilist-sorting-performance/#nearly-sorted-2-swaps","title":"Nearly Sorted (2% swaps)","text":"List Size Ghost Meteor Pattern-Defeating QuickSort Grail Power Insertion Tim Jesse Green Ska Ipn Smooth Block IPS4o Power+ Glide Flux 1000.001 ms0.002 ms0.002 ms0.001 ms0.003 ms0.001 ms0.002 ms241 ms0.001 ms0.007 ms0.002 ms0.002 ms0.001 ms0.002 ms0.004 ms0.003 ms0.002 ms 1,0000.023 ms0.031 ms0.033 ms0.007 ms0.018 ms0.007 ms0.014 ms2.43 s0.007 ms0.100 ms0.030 ms0.018 ms0.007 ms0.045 ms0.025 ms0.016 ms0.030 ms 10,0000.293 ms0.395 ms0.443 ms0.073 ms0.232 ms0.063 ms0.173 ms23.44 s0.071 ms1.44 ms0.405 ms0.189 ms0.069 ms0.674 ms0.433 ms0.166 ms0.390 ms 100,0003.60 ms5.19 ms5.30 ms0.843 ms3.53 msn/a2.43 ms137.09 s0.727 ms18.8 ms4.98 ms1.95 ms0.633 ms8.46 ms5.37 ms2.28 ms4.96 ms"},{"location":"performance/ilist-sorting-performance/#shuffled-deterministic","title":"Shuffled (deterministic)","text":"List Size Ghost Meteor Pattern-Defeating QuickSort Grail Power Insertion Tim Jesse Green Ska Ipn Smooth Block IPS4o Power+ Glide Flux 1000.008 ms0.007 ms0.006 ms0.007 ms0.008 ms0.017 ms0.011 ms61.7 ms0.008 ms0.011 ms0.008 ms0.012 ms0.005 ms0.006 ms0.025 ms0.012 ms0.007 ms 1,0000.156 ms0.132 ms0.088 ms0.105 ms0.152 ms1.49 ms0.160 ms216 ms0.121 ms0.133 ms0.097 ms0.200 ms0.089 ms0.107 ms0.434 ms0.159 ms0.098 ms 10,0002.50 ms1.95 ms1.22 ms1.46 ms1.49 ms147 ms1.57 ms669 ms1.38 ms1.78 ms1.33 ms2.76 ms1.20 ms1.57 ms5.98 ms1.80 ms1.44 ms 100,00031.9 ms26.0 ms15.3 ms18.2 ms18.3 msn/a20.4 ms2.21 s18.3 ms23.0 ms15.9 ms35.5 ms15.2 ms20.2 ms77.1 ms23.6 ms17.6 ms"},{"location":"performance/ilist-sorting-performance/#macos","title":"macOS","text":"<p>Pending \u2014 run the IList sorting benchmark suite on macOS to capture results.</p>"},{"location":"performance/ilist-sorting-performance/#linux","title":"Linux","text":"<p>Pending \u2014 run the IList sorting benchmark suite on Linux to capture results.</p>"},{"location":"performance/ilist-sorting-performance/#other-platforms","title":"Other Platforms","text":"<p>Pending \u2014 run the IList sorting benchmark suite on the target platform to capture results.</p>"},{"location":"performance/random-performance/","title":"Random Number Generator Performance Benchmarks","text":"<p>Auto-generated via RandomPerformanceTests.Benchmark. Run the test to refresh these summary and detail tables.</p>"},{"location":"performance/random-performance/#summary-fastest-first","title":"Summary (fastest first)","text":"Random NextUint (ops/s) Speed Quality Notes LinearCongruentialGenerator1,333,700,000FastestPoorMinimal standard LCG; fails spectral tests and exhibits lattice artifacts beyond small dimensions. WaveSplatRandom1,323,500,000FastestExperimentalSingle-word chaotic generator; author notes period 2^64 but provides no formal test results\u2014treat as experimental. BlastCircuitRandom1,068,900,000Very FastGoodEmpirical PractRand testing to 32GB shows strong diffusion; designed as a chaotic ARX mixer rather than a proven statistically optimal generator. SplitMix641,042,200,000Very FastVery GoodWell-known SplitMix64 mixer; passes TestU01 BigCrush and PractRand up to large data sizes in literature. Steele, Lea, Flood 2014 FlurryBurstRandom947,500,000FastExcellentSix-word ARX-style generator tuned for all-around use; passes TestU01 BigCrush per upstream reference implementation. Will Stafford Parsons (wileylooper) PcgRandom916,300,000FastExcellentPCG XSH RR 64/32 variant; passes TestU01 BigCrush and PractRand in published results. O'Neill 2014 IllusionFlow891,400,000FastExcellentHybridized PCG + xorshift design; upstream PractRand 64GB passes with no anomalies per author. XoroShiroRandom762,900,000FastVery Goodxoshiro128** variant; authors recommend for general-purpose use and report clean BigCrush performance with jump functions. Blackman &amp; Vigna 2018 RomuDuo757,900,000FastVery GoodROMU family member (RomuDuo); authors report strong BigCrush results with minor low-bit weaknesses in some rotations. StormDropRandom713,500,000ModerateExcellent20-word ARX generator derived from SHISHUA; author reports excellent PractRand performance and long periods. XorShiftRandom599,800,000ModerateFairClassic 32-bit xorshift; known to fail portions of TestU01 and PractRand, acceptable for lightweight effects only. Marsaglia 2003 WyRandom440,100,000SlowVery GoodWyhash-based generator; published testing shows it clears BigCrush/PractRand with wide seed coverage. Wang Yi 2019 SquirrelRandom409,000,000SlowGoodHash-based generator built on Squirrel3; good equidistribution for table lookups but not extensively tested beyond moderate ranges. Squirrel Eiserloh PhotonSpinRandom260,200,000Very SlowExcellentSHISHUA-inspired generator; independent testing (PractRand 128GB) by author indicates excellent distribution properties. UnityRandom86,800,000Very SlowFairMirrors UnityEngine.Random (Xorshift196 + additive); suitable for legacy compatibility but not high-stakes simulation. Unity Random Internals SystemRandom64,200,000Very SlowPoorThin wrapper over System.Random; inherits same LCG weaknesses and fails modern statistical batteries. System.Random considered harmful DotNetRandom54,300,000Very SlowPoorLinear congruential generator (mod 2^31) with known correlation failures; unsuitable for high-quality simulations. System.Random considered harmful"},{"location":"performance/random-performance/#detailed-metrics","title":"Detailed Metrics","text":"Random NextBool Next NextUint NextFloat NextDouble NextUint (Range) NextInt (Range) LinearCongruentialGenerator812,500,000844,800,0001,333,700,000184,600,000387,000,000580,800,000505,400,000 WaveSplatRandom780,800,000721,900,0001,323,500,000182,500,000400,700,000532,600,000463,400,000 BlastCircuitRandom785,700,000716,200,0001,068,900,000183,300,000353,100,000485,400,000422,600,000 SplitMix64777,700,000774,200,0001,042,200,000183,200,000362,300,000484,000,000436,100,000 FlurryBurstRandom771,200,000678,400,000947,500,000183,500,000311,600,000445,700,000405,200,000 PcgRandom777,500,000673,700,000916,300,000184,200,000328,700,000454,100,000409,400,000 IllusionFlow789,200,000641,300,000891,400,000177,800,000309,100,000445,700,000396,000,000 XoroShiroRandom770,000,000592,300,000762,900,000165,800,000242,400,000427,700,000381,800,000 RomuDuo783,100,000592,000,000757,900,000164,500,000249,800,000443,500,000394,800,000 StormDropRandom776,100,000568,200,000713,500,000181,400,000264,100,000397,600,000364,300,000 XorShiftRandom779,800,000587,300,000599,800,000183,300,000272,400,000432,200,000388,700,000 WyRandom749,900,000380,600,000440,100,000164,700,000186,700,000293,600,000274,300,000 SquirrelRandom759,200,000407,200,000409,000,000169,500,000193,400,000331,400,000311,200,000 PhotonSpinRandom701,900,000246,500,000260,200,000118,000,000117,000,000211,300,000204,300,000 UnityRandom623,500,00084,100,00086,800,00061,200,00041,000,00080,400,00081,400,000 SystemRandom147,100,000148,100,00064,200,000130,900,000137,700,00058,500,00056,900,000 DotNetRandom539,800,00052,500,00054,300,00045,800,00026,700,00053,500,00053,000,000"},{"location":"performance/reflection-performance/","title":"Reflection Performance Benchmarks","text":"<p>Unity Helpers replaces ad-hoc reflection with cached delegates that favour expression lambdas on IL2CPP-safe platforms and fall back to dynamic IL emit or plain reflection where available. These benchmarks compare raw <code>System.Reflection</code> against the helpers for common access patterns.</p> <p>Each run updates the table for the current operating system only. Sections that still show <code>_No benchmark data generated yet._</code> simply have not been executed on that platform.</p>"},{"location":"performance/reflection-performance/#windows","title":"Windows","text":"<p>Generated on 2026-01-12 01:50:03 UTC</p>"},{"location":"performance/reflection-performance/#strategy-default-auto","title":"Strategy: Default (auto)","text":""},{"location":"performance/reflection-performance/#boxed-access-object","title":"Boxed Access (object)","text":"Scenario Helper (ops/sec) System.Reflection (ops/sec) Speedup vs Reflection Instance Field Get (boxed)25.21M6.69M3.77x Instance Field Set (boxed)22.18M5.50M4.03x Static Field Get (boxed)16.53M2.71M6.09x Static Field Set (boxed)18.01M4.92M3.66x Instance Property Get (boxed)28.85M25.19M1.15x Instance Property Set (boxed)24.78M1.49M16.61x Static Property Get (boxed)21.82M24.49M0.89x Static Property Set (boxed)26.46M2.54M10.42x Instance Method Invoke (boxed)20.55M1.71M11.99x Static Method Invoke (boxed)25.30M2.68M9.42x Constructor Invoke (boxed)23.48M2.52M9.33x"},{"location":"performance/reflection-performance/#typed-access-no-boxing","title":"Typed Access (no boxing)","text":"Scenario Helper (ops/sec) Baseline Delegate (ops/sec) System.Reflection (ops/sec) Speedup vs Delegate Speedup vs Reflection Instance Field Get (typed)650.81M661.45M6.69M0.98x97.25x Instance Field Set (typed)653.34M660.94M5.50M0.99x118.80x Static Field Get (typed)663.07M687.03M2.71M0.97x244.39x Static Field Set (typed)681.63M670.11M4.92M1.02x138.44x Instance Property Get (typed)685.44M692.09M25.19M0.99x27.21x Instance Property Set (typed)661.24M701.70M1.49M0.94x443.26x Static Property Get (typed)652.49M685.16M24.49M0.95x26.64x Static Property Set (typed)677.05M657.03M2.54M1.03x266.65x Instance Method Invoke (typed)686.13M685.59M1.71M1.00x400.58x Static Method Invoke (typed)659.66M678.18M2.68M0.97x245.75x"},{"location":"performance/reflection-performance/#strategy-expressions","title":"Strategy: Expressions","text":""},{"location":"performance/reflection-performance/#boxed-access-object_1","title":"Boxed Access (object)","text":"Scenario Helper (ops/sec) System.Reflection (ops/sec) Speedup vs Reflection Instance Field Get (boxed)24.78M6.08M4.07x Instance Field Set (boxed)28.58M5.48M5.21x Static Field Get (boxed)20.95M8.51M2.46x Static Field Set (boxed)27.73M4.57M6.06x Instance Property Get (boxed)21.15M3.49M6.06x Instance Property Set (boxed)12.82M2.02M6.34x Static Property Get (boxed)22.50M20.45M1.10x Static Property Set (boxed)20.66M2.88M7.17x Instance Method Invoke (boxed)23.90M1.98M12.08x Static Method Invoke (boxed)27.12M2.21M12.27x Constructor Invoke (boxed)27.01M2.54M10.65x"},{"location":"performance/reflection-performance/#typed-access-no-boxing_1","title":"Typed Access (no boxing)","text":"Scenario Helper (ops/sec) Baseline Delegate (ops/sec) System.Reflection (ops/sec) Speedup vs Delegate Speedup vs Reflection Instance Field Get (typed)692.43M658.49M6.08M1.05x113.81x Instance Field Set (typed)651.66M657.97M5.48M0.99x118.84x Static Field Get (typed)647.30M683.07M8.51M0.95x76.02x Static Field Set (typed)669.78M656.50M4.57M1.02x146.40x Instance Property Get (typed)676.92M684.18M3.49M0.99x194.03x Instance Property Set (typed)657.90M691.31M2.02M0.95x325.26x Static Property Get (typed)643.98M684.71M20.45M0.94x31.49x Static Property Set (typed)669.72M653.78M2.88M1.02x232.38x Instance Method Invoke (typed)680.33M680.42M1.98M1.00x343.95x Static Method Invoke (typed)653.83M674.42M2.21M0.97x295.69x"},{"location":"performance/reflection-performance/#strategy-dynamic-il","title":"Strategy: Dynamic IL","text":""},{"location":"performance/reflection-performance/#boxed-access-object_2","title":"Boxed Access (object)","text":"Scenario Helper (ops/sec) System.Reflection (ops/sec) Speedup vs Reflection Instance Field Get (boxed)27.70M5.80M4.77x Instance Field Set (boxed)28.24M5.45M5.18x Static Field Get (boxed)21.57M8.41M2.56x Static Field Set (boxed)25.65M6.16M4.17x Instance Property Get (boxed)18.83M22.95M0.82x Instance Property Set (boxed)25.31M1.98M12.80x Static Property Get (boxed)23.51M8.69M2.71x Static Property Set (boxed)2.06M2.86M0.72x Instance Method Invoke (boxed)23.09M2.00M11.56x Static Method Invoke (boxed)27.05M2.12M12.75x Constructor Invoke (boxed)26.10M2.52M10.36x"},{"location":"performance/reflection-performance/#typed-access-no-boxing_2","title":"Typed Access (no boxing)","text":"Scenario Helper (ops/sec) Baseline Delegate (ops/sec) System.Reflection (ops/sec) Speedup vs Delegate Speedup vs Reflection Instance Field Get (typed)662.00M685.29M5.80M0.97x114.09x Instance Field Set (typed)654.77M661.50M5.45M0.99x120.17x Static Field Get (typed)657.70M683.29M8.41M0.96x78.17x Static Field Set (typed)675.09M659.39M6.16M1.02x109.61x Instance Property Get (typed)673.39M690.30M22.95M0.98x29.34x Instance Property Set (typed)652.07M693.53M1.98M0.94x329.72x Static Property Get (typed)648.59M682.82M8.69M0.95x74.63x Static Property Set (typed)671.01M654.32M2.86M1.03x234.23x Instance Method Invoke (typed)690.97M686.98M2.00M1.01x345.91x Static Method Invoke (typed)660.09M679.79M2.12M0.97x311.08x"},{"location":"performance/reflection-performance/#strategy-reflection-fallback","title":"Strategy: Reflection Fallback","text":""},{"location":"performance/reflection-performance/#boxed-access-object_3","title":"Boxed Access (object)","text":"Scenario Helper (ops/sec) System.Reflection (ops/sec) Speedup vs Reflection Instance Field Get (boxed)7.48M5.76M1.30x Instance Field Set (boxed)5.57M5.47M1.02x Static Field Get (boxed)7.71M7.19M1.07x Static Field Set (boxed)6.01M6.13M0.98x Instance Property Get (boxed)21.09M18.38M1.15x Instance Property Set (boxed)2.09M2.06M1.01x Static Property Get (boxed)20.07M23.51M0.85x Static Property Set (boxed)2.88M2.02M1.42x Instance Method Invoke (boxed)1.99M1.98M1.01x Static Method Invoke (boxed)2.69M2.72M0.99x Constructor Invoke (boxed)2.55M2.50M1.02x"},{"location":"performance/reflection-performance/#typed-access-no-boxing_3","title":"Typed Access (no boxing)","text":"Scenario Helper (ops/sec) Baseline Delegate (ops/sec) System.Reflection (ops/sec) Speedup vs Delegate Speedup vs Reflection Instance Field Get (typed)5.24M662.95M5.76M0.01x0.91x Instance Field Set (typed)5.46M668.23M5.47M0.01x1.00x Static Field Get (typed)8.64M683.37M7.19M0.01x1.20x Static Field Set (typed)4.77M661.19M6.13M0.01x0.78x Instance Property Get (typed)675.25M683.53M18.38M0.99x36.75x Instance Property Set (typed)655.22M704.28M2.06M0.93x317.56x Static Property Get (typed)652.21M691.83M23.51M0.94x27.75x Static Property Set (typed)678.92M659.94M2.02M1.03x335.95x Instance Method Invoke (typed)692.36M685.00M1.98M1.01x350.50x Static Method Invoke (typed)661.40M678.90M2.72M0.97x242.98x"},{"location":"performance/reflection-performance/#macos","title":"macOS","text":"<p>No benchmark data generated yet.</p>"},{"location":"performance/reflection-performance/#linux","title":"Linux","text":"<p>No benchmark data generated yet.</p>"},{"location":"performance/reflection-performance/#unknown-other","title":"Unknown / Other","text":"<p>No benchmark data generated yet.</p>"},{"location":"performance/relational-components-performance/","title":"Relational Component Performance Benchmarks","text":"<p>Relational component attributes (<code>[SiblingComponent]</code>, <code>[ParentComponent]</code>, <code>[ChildComponent]</code>) remove repetitive <code>GetComponent*</code> code. These benchmarks quantify the runtime cost of calling <code>Assign*Components</code> for common field shapes (single component, array, <code>List&lt;T&gt;</code>, and <code>HashSet&lt;T&gt;</code>) against hand-written lookups.</p> <p>How to refresh these tables:</p> <ol> <li>Open Unity\u2019s Test Runner (EditMode/PlayMode as appropriate for your setup).</li> <li>Run <code>RelationalComponentBenchmarkTests.Benchmark</code> inside <code>Tests/Runtime/Performance</code>.</li> <li>The test logs the tables to the console and rewrites the section that matches the current operating system.</li> </ol> <p>The script executes the benchmark test in batch mode, captures the markdown tables to <code>BenchmarkLogs/RelationalBenchmark.log</code>, and preserves the raw <code>TestResults.xml</code> when <code>-KeepResults</code> is specified.</p>"},{"location":"performance/relational-components-performance/#windows-editorplayer","title":"Windows (Editor/Player)","text":"<p>Last updated 2026-01-12 01:51 UTC on Windows 11 (10.0.26200) 64bit</p> <p>Numbers capture repeated <code>Assign*Components</code> calls for one second per scenario. Higher operations per second are better.</p>"},{"location":"performance/relational-components-performance/#operations-per-second-higher-is-better","title":"Operations per second (higher is better)","text":"Scenario Relational Ops/s Manual Ops/s Rel/Manual Iterations Parent - Single9,7675,654,1260.00x10,000 Parent - Array2,9163,311,5420.00x10,000 Parent - List2,8994,236,7900.00x10,000 Parent - HashSet2,9342,871,9590.00x10,000 Child - Single2,6723,554,1340.00x10,000 Child - Array1,4522,312,9280.00x10,000 Child - List1,9072,576,9930.00x10,000 Child - HashSet1,9141,705,8800.00x10,000 Sibling - Single3,687,34014,312,5000.26x3,690,000 Sibling - Array5,8312,491,7100.00x10,000 Sibling - List5,7613,383,6400.00x10,000 Sibling - HashSet5,8271,829,9980.00x10,000"},{"location":"performance/relational-components-performance/#macos","title":"macOS","text":"<p>Pending \u2014 run the relational component benchmark suite on macOS to capture results.</p>"},{"location":"performance/relational-components-performance/#linux","title":"Linux","text":"<p>Pending \u2014 run the relational component benchmark suite on Linux to capture results.</p>"},{"location":"performance/relational-components-performance/#other-platforms","title":"Other Platforms","text":"<p>Pending \u2014 run the relational component benchmark suite on the target platform to capture results.</p>"},{"location":"performance/spatial-tree-2d-performance/","title":"2D Spatial Tree Performance Benchmarks","text":""},{"location":"performance/spatial-tree-2d-performance/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>Fast range/bounds/nearest\u2011neighbor queries on 2D data without scanning everything.</li> <li>Quick picks: QuadTree2D for broad\u2011phase; KdTree2D (Balanced) for NN; KdTree2D (Unbalanced) for fast rebuilds; RTree2D for bounds\u2011based data.</li> </ul> <p>This document contains performance benchmarks for the 2D spatial tree implementations in Unity Helpers.</p>"},{"location":"performance/spatial-tree-2d-performance/#available-2d-spatial-trees","title":"Available 2D Spatial Trees","text":"<ul> <li>QuadTree2D - Easiest to use, good all-around performance</li> <li>KdTree2D - Balanced and unbalanced variants available</li> <li>RTree2D - Optimized for bounding box queries</li> </ul>"},{"location":"performance/spatial-tree-2d-performance/#correctness-semantics","title":"Correctness &amp; Semantics","text":"<ul> <li>QuadTree2D and KdTree2D (balanced and unbalanced) guarantee the same results for the same input data and the same queries. They are both point-based trees and differ only in construction/query performance characteristics.</li> <li>RTree2D is bounds-based (stores rectangles/AABBs), not points. Its spatial knowledge and query semantics operate on rectangles, so its results will intentionally differ for sized objects and bounds intersection queries.</li> </ul>"},{"location":"performance/spatial-tree-2d-performance/#performance-benchmarks","title":"Performance Benchmarks","text":""},{"location":"performance/spatial-tree-2d-performance/#datasets","title":"Datasets","text":""},{"location":"performance/spatial-tree-2d-performance/#1000000-entries","title":"1,000,000 entries","text":""},{"location":"performance/spatial-tree-2d-performance/#construction","title":"Construction","text":"Construction KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 1,000,000 entries4 (0.247s)2 (0.346s)4 (0.223s)2 (0.379s)"},{"location":"performance/spatial-tree-2d-performance/#elements-in-range","title":"Elements In Range","text":"Elements In Range KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (~span/2) (r=499.5)5856567 Half (~span/4) (r=249.8)23421521428 Quarter (~span/8) (r=124.9)909795785117 Tiny (~span/1000) (r=1)8,2805,5656,9736,724"},{"location":"performance/spatial-tree-2d-performance/#get-elements-in-bounds","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (size=999.0x999.0)33435531416 Half (size=499.5x499.5)1,3561,3671,00566 Quarter (size=249.8x249.8)3,1483,1832,281322 Unit (size=1)5,6125,6065,5772,834"},{"location":"performance/spatial-tree-2d-performance/#approximate-nearest-neighbors","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 500 neighbors3,0231,7332,1481,855 100 neighbors2,8281,8632,3081,802 10 neighbors1,9581,8991,6411,692 1 neighbor1,9611,9571,4641,465"},{"location":"performance/spatial-tree-2d-performance/#100000-entries","title":"100,000 entries","text":""},{"location":"performance/spatial-tree-2d-performance/#construction_1","title":"Construction","text":"Construction KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 100,000 entries49 (0.020s)77 (0.013s)48 (0.021s)43 (0.023s)"},{"location":"performance/spatial-tree-2d-performance/#elements-in-range_1","title":"Elements In Range","text":"Elements In Range KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (~span/2) (r=199.5)53952753671 Half (~span/4) (r=99.75)1,0651,0731,012171 Quarter (~span/8) (r=49.88)2,4942,7102,448577 Tiny (~span/1000) (r=1)5,6075,6165,6602,872"},{"location":"performance/spatial-tree-2d-performance/#get-elements-in-bounds_1","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (size=399.0x249.0)2,4622,5242,549209 Half (size=199.5x124.5)3,5633,9013,304674 Quarter (size=99.75x62.25)4,7264,9344,5191,550 Unit (size=1)5,6255,6585,6612,850"},{"location":"performance/spatial-tree-2d-performance/#approximate-nearest-neighbors_1","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 500 neighbors1,5621,6121,2361,407 100 neighbors1,8611,8701,3971,439 10 neighbors1,9421,9041,4561,464 1 neighbor1,9111,9121,4611,462"},{"location":"performance/spatial-tree-2d-performance/#10000-entries","title":"10,000 entries","text":""},{"location":"performance/spatial-tree-2d-performance/#construction_2","title":"Construction","text":"Construction KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 10,000 entries541 (0.002s)818 (0.001s)541 (0.002s)322 (0.003s)"},{"location":"performance/spatial-tree-2d-performance/#elements-in-range_2","title":"Elements In Range","text":"Elements In Range KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (~span/2) (r=49.50)2,9502,9512,873579 Half (~span/4) (r=24.75)4,6314,5484,0921,428 Quarter (~span/8) (r=12.38)5,1465,1645,0322,339 Tiny (~span/1000) (r=1)5,5815,6465,6802,865"},{"location":"performance/spatial-tree-2d-performance/#get-elements-in-bounds_2","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (size=99.00x99.00)5,0875,1675,1811,288 Half (size=49.50x49.50)5,6225,6525,0102,185 Quarter (size=24.75x24.75)5,4355,5615,4142,687 Unit (size=1)5,6995,7095,7492,817"},{"location":"performance/spatial-tree-2d-performance/#approximate-nearest-neighbors_2","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 500 neighbors1,6811,6961,3041,369 100 neighbors1,8931,8881,3871,445 10 neighbors1,9611,9551,4131,470 1 neighbor1,9711,9171,4221,471"},{"location":"performance/spatial-tree-2d-performance/#1000-entries","title":"1,000 entries","text":""},{"location":"performance/spatial-tree-2d-performance/#construction_3","title":"Construction","text":"Construction KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 1,000 entries5,376 (0.000s)7,429 (0.000s)4,940 (0.000s)889 (0.001s)"},{"location":"performance/spatial-tree-2d-performance/#elements-in-range_3","title":"Elements In Range","text":"Elements In Range KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (~span/2) (r=24.50)5,3705,3675,3482,032 Half (~span/4) (r=12.25)5,3335,4215,1992,425 Quarter (~span/8) (r=6.13)5,5415,5675,4332,713 Tiny (~span/1000) (r=1)5,7025,6195,7402,874"},{"location":"performance/spatial-tree-2d-performance/#get-elements-in-bounds_3","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (size=49.00x19.00)5,7475,6905,8002,590 Half (size=24.50x9.5)5,5675,7755,6292,806 Quarter (size=12.25x4.75)5,6295,7395,6982,842 Unit (size=1)5,7295,7535,7512,868"},{"location":"performance/spatial-tree-2d-performance/#approximate-nearest-neighbors_3","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 500 neighbors1,8621,8711,3861,412 100 neighbors1,9001,8931,4181,401 10 neighbors1,9591,9651,4681,428 1 neighbor1,9701,9491,4341,434"},{"location":"performance/spatial-tree-2d-performance/#100-entries","title":"100 entries","text":""},{"location":"performance/spatial-tree-2d-performance/#construction_4","title":"Construction","text":"Construction KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 100 entries43,859 (0.000s)40,650 (0.000s)26,954 (0.000s)1,682 (0.001s)"},{"location":"performance/spatial-tree-2d-performance/#elements-in-range_4","title":"Elements In Range","text":"Elements In Range KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (~span/2) (r=4.5)5,8625,8295,8112,816 Half (~span/4) (r=2.25)5,7765,7345,6882,886 Quarter (~span/8) (r=1.13)5,7575,7235,7662,904 Tiny (~span/1000) (r=1)5,7275,5835,7652,904"},{"location":"performance/spatial-tree-2d-performance/#get-elements-in-bounds_4","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D Full (size=9x9)5,8465,8215,9102,817 Half (size=4.5x4.5)5,7425,7915,7092,848 Quarter (size=2.25x2.25)5,7535,6865,7622,907 Unit (size=1)5,6845,7025,8352,898"},{"location":"performance/spatial-tree-2d-performance/#approximate-nearest-neighbors_4","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree2D (Balanced) KDTree2D (Unbalanced) QuadTree2D RTree2D 100 neighbors (max)1,8691,9181,4471,447 10 neighbors1,9501,9601,4761,480 1 neighbor1,9681,9551,4751,478"},{"location":"performance/spatial-tree-2d-performance/#interpreting-the-results","title":"Interpreting the Results","text":"<p>All numbers represent operations per second (higher is better), except for construction times which show operations per second and absolute time.</p>"},{"location":"performance/spatial-tree-2d-performance/#choosing-the-right-tree","title":"Choosing the Right Tree","text":"<p>QuadTree2D:</p> <ul> <li>Best for: General-purpose 2D spatial queries</li> <li>Strengths: Balanced performance across all operation types, simple to use</li> <li>Weaknesses: Slightly slower than KdTree for point queries</li> </ul> <p>KdTree2D (Balanced):</p> <ul> <li>Best for: When you need consistent query performance</li> <li>Strengths: Fast nearest-neighbor queries, good for smaller datasets</li> <li>Weaknesses: Slower construction time</li> </ul> <p>KdTree2D (Unbalanced):</p> <ul> <li>Best for: When you need fast construction and will rebuild frequently</li> <li>Strengths: Fastest construction, similar query performance to balanced</li> <li>Weaknesses: May degrade on pathological data distributions</li> </ul> <p>RTree2D:</p> <ul> <li>Best for: Bounding box queries, especially with large query areas</li> <li>Strengths: Excellent for large bounding box queries, handles overlapping objects well</li> <li>Weaknesses: Slower for point queries and small ranges</li> </ul>"},{"location":"performance/spatial-tree-2d-performance/#important-notes","title":"Important Notes","text":"<ul> <li>All spatial trees assume immutable positional data</li> <li>If positions change, you must reconstruct the tree</li> <li>Spatial queries are O(log n) vs O(n) for linear search</li> <li>Construction cost is amortized over many queries</li> </ul>"},{"location":"performance/spatial-tree-3d-performance/","title":"3D Spatial Tree Performance Benchmarks","text":""},{"location":"performance/spatial-tree-3d-performance/#tldr-what-problem-this-solves","title":"TL;DR \u2014 What Problem This Solves","text":"<ul> <li>Need fast \u201cwhat\u2019s near X?\u201d or \u201cwhat\u2019s inside this volume?\u201d in 3D.</li> <li>These structures avoid scanning every object; queries touch only nearby data.</li> <li>Quick picks: OctTree3D for general 3D queries; KdTree3D for nearest\u2011neighbor on points; RTree3D for volumetric bounds.</li> </ul> <p>Note: KdTree3D, OctTree3D, and RTree3D are under active development and their APIs/performance may evolve. SpatialHash3D is stable and recommended for broad\u2011phase neighbor queries with many moving objects.</p> <p>For boundary and result semantics across structures, see Spatial Tree Semantics</p> <p>This document contains performance benchmarks for the 3D spatial tree implementations in Unity Helpers.</p>"},{"location":"performance/spatial-tree-3d-performance/#available-3d-spatial-trees","title":"Available 3D Spatial Trees","text":"<ul> <li>OctTree3D - Easiest to use, good all-around performance for 3D</li> <li>KdTree3D - Balanced and unbalanced variants available</li> <li>RTree3D - Optimized for 3D bounding box queries</li> <li>SpatialHash3D - Efficient for uniformly distributed moving objects (stable)</li> </ul>"},{"location":"performance/spatial-tree-3d-performance/#performance-benchmarks","title":"Performance Benchmarks","text":""},{"location":"performance/spatial-tree-3d-performance/#datasets","title":"Datasets","text":""},{"location":"performance/spatial-tree-3d-performance/#1000000-entries","title":"1,000,000 entries","text":""},{"location":"performance/spatial-tree-3d-performance/#construction","title":"Construction","text":"Construction KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 1,000,000 entries3 (0.260s)5 (0.168s)1 (0.515s)3 (0.314s)"},{"location":"performance/spatial-tree-3d-performance/#elements-in-range","title":"Elements In Range","text":"Elements In Range KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (~span/2) (r=49.50)20223214 Half (~span/4) (r=24.75)149152250140 Quarter (~span/8) (r=12.38)9751,0961,6151,095 Tiny (~span/1000) (r=1)7,0644,7337,8884,196"},{"location":"performance/spatial-tree-3d-performance/#get-elements-in-bounds","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (size\u224899.00x99.00x99.00)333719920 Half (size\u224849.50x49.50x49.50)44491,078242 Quarter (size\u224824.75x24.75x24.75)47532,1031,303 Unit (size=1)48535,5232,789"},{"location":"performance/spatial-tree-3d-performance/#approximate-nearest-neighbors","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 500 neighbors2,7171,6361,585289 100 neighbors2,9801,8492,9681,958 10 neighbors1,9441,8961,8952,512 1 neighbor1,9521,9461,7701,659"},{"location":"performance/spatial-tree-3d-performance/#100000-entries","title":"100,000 entries","text":""},{"location":"performance/spatial-tree-3d-performance/#construction_1","title":"Construction","text":"Construction KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 100,000 entries49 (0.020s)97 (0.010s)61 (0.016s)42 (0.024s)"},{"location":"performance/spatial-tree-3d-performance/#elements-in-range_1","title":"Elements In Range","text":"Elements In Range KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (~span/2) (r=49.50)358498688173 Half (~span/4) (r=24.75)8921,3201,509573 Quarter (~span/8) (r=12.38)1,8042,5892,9581,466 Tiny (~span/1000) (r=1)4,8434,9535,6732,832"},{"location":"performance/spatial-tree-3d-performance/#get-elements-in-bounds_1","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (size\u224899.00x99.00x9)5456371,908303 Half (size\u224849.50x49.50x4.5)6257343,4931,480 Quarter (size\u224824.75x24.75x2.25)6357505,1062,543 Unit (size=1)6427525,5902,829"},{"location":"performance/spatial-tree-3d-performance/#approximate-nearest-neighbors_1","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 500 neighbors1,4961,666871239 100 neighbors1,8671,8191,6071,044 10 neighbors1,9341,8741,7701,530 1 neighbor1,8931,8991,8301,660"},{"location":"performance/spatial-tree-3d-performance/#10000-entries","title":"10,000 entries","text":""},{"location":"performance/spatial-tree-3d-performance/#construction_2","title":"Construction","text":"Construction KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 10,000 entries602 (0.002s)742 (0.001s)577 (0.002s)447 (0.002s)"},{"location":"performance/spatial-tree-3d-performance/#elements-in-range_2","title":"Elements In Range","text":"Elements In Range KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (~span/2) (r=49.50)2,7522,6823,3421,113 Half (~span/4) (r=24.75)3,0103,0733,4581,576 Quarter (~span/8) (r=12.38)3,0053,2513,7441,993 Tiny (~span/1000) (r=1)5,0395,1345,5282,789"},{"location":"performance/spatial-tree-3d-performance/#get-elements-in-bounds_2","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (size\u224899.00x9x9)2,9122,9884,7991,516 Half (size\u224849.50x4.5x4.5)3,1753,1665,0282,635 Quarter (size\u224824.75x2.25x2.25)3,1703,1865,3572,722 Unit (size=1)3,2143,1975,7022,771"},{"location":"performance/spatial-tree-3d-performance/#approximate-nearest-neighbors_2","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 500 neighbors1,6141,629456161 100 neighbors1,8711,8621,4001,015 10 neighbors1,9171,9001,7211,638 1 neighbor1,9421,8901,8201,751"},{"location":"performance/spatial-tree-3d-performance/#1000-entries","title":"1,000 entries","text":""},{"location":"performance/spatial-tree-3d-performance/#construction_3","title":"Construction","text":"Construction KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 1,000 entries5,192 (0.000s)6,939 (0.000s)2,725 (0.000s)3,868 (0.000s)"},{"location":"performance/spatial-tree-3d-performance/#elements-in-range_3","title":"Elements In Range","text":"Elements In Range KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (~span/2) (r=4.5)4,0084,2044,6872,445 Half (~span/4) (r=2.25)5,1465,3415,4972,805 Quarter (~span/8) (r=1.13)5,3105,3335,6472,851 Tiny (~span/1000) (r=1)5,2165,2455,6942,866"},{"location":"performance/spatial-tree-3d-performance/#get-elements-in-bounds_3","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (size\u22489x9x9)5,2345,2225,6752,649 Half (size\u22484.5x4.5x4.5)5,1355,3445,5842,832 Quarter (size\u22482.25x2.25x2.25)5,1625,3435,7452,877 Unit (size=1)5,2405,3815,7662,879"},{"location":"performance/spatial-tree-3d-performance/#approximate-nearest-neighbors_3","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 500 neighbors1,7081,6971,174462 100 neighbors1,8591,8681,7051,263 10 neighbors1,9191,9241,8621,765 1 neighbor1,9361,9391,8501,829"},{"location":"performance/spatial-tree-3d-performance/#100-entries","title":"100 entries","text":""},{"location":"performance/spatial-tree-3d-performance/#construction_4","title":"Construction","text":"Construction KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 100 entries42,016 (0.000s)37,593 (0.000s)14,662 (0.000s)13,927 (0.000s)"},{"location":"performance/spatial-tree-3d-performance/#elements-in-range_4","title":"Elements In Range","text":"Elements In Range KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (~span/2) (r=4.5)5,5875,6015,7022,837 Half (~span/4) (r=2.25)5,6235,6255,7072,873 Quarter (~span/8) (r=1.13)5,6155,6185,7042,889 Tiny (~span/1000) (r=1)5,5925,6235,7122,886"},{"location":"performance/spatial-tree-3d-performance/#get-elements-in-bounds_4","title":"Get Elements In Bounds","text":"Get Elements In Bounds KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D Full (size\u22489x4x1)5,7655,7815,8122,853 Half (size\u22484.5x2x1)5,7805,7775,6882,868 Quarter (size\u22482.25x1x1)5,7635,7515,7482,911 Unit (size=1)5,7525,7635,7982,916"},{"location":"performance/spatial-tree-3d-performance/#approximate-nearest-neighbors_4","title":"Approximate Nearest Neighbors","text":"Approximate Nearest Neighbors KDTree3D (Balanced) KDTree3D (Unbalanced) OctTree3D RTree3D 100 neighbors (max)1,8661,8831,8651,869 10 neighbors1,9451,9211,9001,880 1 neighbor1,9391,9391,8991,898"},{"location":"performance/spatial-tree-3d-performance/#interpreting-the-results","title":"Interpreting the Results","text":"<p>All numbers represent operations per second (higher is better), except for construction times which show operations per second and absolute time.</p>"},{"location":"performance/spatial-tree-3d-performance/#choosing-the-right-tree","title":"Choosing the Right Tree","text":"<p>OctTree3D:</p> <ul> <li>Best for: General-purpose 3D spatial queries</li> <li>Strengths: Balanced performance, easy to use, good spatial locality</li> <li>Use cases: 3D collision detection, visibility culling, spatial audio</li> </ul> <p>KdTree3D (Balanced):</p> <ul> <li>Best for: Nearest-neighbor queries in 3D space</li> <li>Strengths: Fast point queries, good for smaller datasets</li> <li>Use cases: Pathfinding, AI spatial awareness, particle systems</li> </ul> <p>KdTree3D (Unbalanced):</p> <ul> <li>Best for: When you need fast construction and will rebuild frequently</li> <li>Strengths: Fastest construction, similar query performance to balanced</li> <li>Use cases: Dynamic environments, frequently changing spatial data</li> </ul> <p>RTree3D:</p> <ul> <li>Best for: 3D bounding box queries, especially with volumetric data</li> <li>Strengths: Excellent for large bounding volumes, handles overlapping objects</li> <li>Use cases: Physics engines, frustum culling, volumetric effects</li> </ul>"},{"location":"performance/spatial-tree-3d-performance/#important-notes","title":"Important Notes","text":"<ul> <li>All spatial trees assume immutable positional data</li> <li>If positions change, you must reconstruct the tree</li> <li>Spatial queries are O(log n) vs O(n) for linear search</li> <li>3D trees have higher construction costs than 2D variants due to additional dimension</li> <li>Construction cost is amortized over many queries</li> </ul>"},{"location":"project/contributing/","title":"Contributing","text":"<p>Thanks for helping make Unity Helpers better! This project uses a few automated checks and formatters to keep the codebase consistent and easy to review.</p>"},{"location":"project/contributing/#dev-container-setup-recommended","title":"Dev Container Setup (Recommended)","text":"<p>The easiest way to contribute is using the included dev container, which has all CI/CD tools pre-installed:</p> <ol> <li>Open in VS Code with the Dev Containers extension</li> <li>Click \"Reopen in Container\" when prompted</li> <li>Run <code>npm run verify:tools</code> to confirm all tools are available</li> </ol>"},{"location":"project/contributing/#pre-installed-cicd-tools-container-only","title":"Pre-installed CI/CD Tools (Container Only)","text":"<p>The dev container includes these additional tools that are not required on your host machine. Git hooks gracefully skip them if not present\u2014CI will catch any issues:</p> <ul> <li>actionlint \u2014 GitHub Actions workflow linter</li> <li>shellcheck \u2014 Shell script linter</li> <li>yamllint \u2014 YAML linter</li> <li>lychee \u2014 Fast link checker</li> </ul>"},{"location":"project/contributing/#required-tools-all-environments","title":"Required Tools (All Environments)","text":"<p>These tools are required and installed via npm/dotnet:</p> <ul> <li>markdownlint \u2014 Markdown linter (via npm)</li> <li>prettier \u2014 Markdown/JSON/YAML formatter (via npm)</li> <li>cspell \u2014 Spell checker (via npm)</li> <li>CSharpier \u2014 C# formatter (via .NET tools)</li> </ul>"},{"location":"project/contributing/#formatting-and-linting","title":"Formatting and Linting","text":"<ul> <li>C# formatting: CSharpier (via dotnet tools)</li> <li>Markdown/JSON/YAML formatting: Prettier</li> <li>Markdown linting: markdownlint</li> <li>Link checks: lychee and custom script</li> <li>YAML linting: yamllint</li> <li>Workflow linting: actionlint</li> </ul>"},{"location":"project/contributing/#llm-scratch-artifacts","title":"LLM Scratch Artifacts","text":"<ul> <li>Files or folders starting with <code>_llm_</code> are git-ignored and automatically removed from the Unity package during imports.</li> <li>Keep temporary AI outputs outside the package root (or rename them) to avoid unexpected deletions by the asset cleaner.</li> </ul>"},{"location":"project/contributing/#dependabot-prs","title":"Dependabot PRs","text":"<p>Dependabot PRs are auto-formatted by CI. The bot pushes commits (same\u2011repo PRs) or opens a formatting PR (forked PRs) so they pass formatting gates.</p>"},{"location":"project/contributing/#optin-formatting-for-contributor-prs","title":"Opt\u2011In Formatting for Contributor PRs","text":"<p>If you want the bot to apply formatting to your PR:</p> <ul> <li>Comment on your PR with <code>/format</code> (aliases: <code>/autofix</code>, <code>/lint-fix</code>).</li> <li>If your branch is in this repo, the bot pushes a commit with fixes.</li> <li>If your PR is from a fork, the bot opens a formatting PR targeting the base branch.</li> <li>The commenter must be the PR author or a maintainer/collaborator.</li> <li>Or run manually from the Actions tab: select \"Opt\u2011in Formatting\", click \"Run workflow\", and enter the PR number.</li> </ul> <p>What gets auto\u2011fixed:</p> <ul> <li>C# via CSharpier</li> <li>Markdown/JSON/YAML via Prettier</li> <li>Markdown lint via markdownlint with <code>--fix</code></li> </ul> <p>What does not auto\u2011fix:</p> <ul> <li>Broken links (lychee)</li> <li>YAML issues that require manual edits</li> </ul>"},{"location":"project/contributing/#run-checks-locally","title":"Run Checks Locally","text":"<ul> <li>Install tools once:</li> <li><code>npm ci</code> (or <code>npm i --no-audit --no-fund</code>)</li> <li><code>dotnet tool restore</code></li> <li>Verify all tools: <code>npm run verify:tools</code></li> <li>Format C#: <code>dotnet tool run csharpier format</code></li> <li>Check docs/JSON/YAML: <code>npm run validate:content</code></li> <li>Enforce EOL/encoding: <code>npm run eol:check</code></li> <li>Lint GitHub Actions: <code>actionlint</code></li> <li>Verify Markdown/code links: <code>npm run lint:doc-links</code> (cross-platform wrapper that locates PowerShell automatically)</li> <li>The wrapper lives at <code>scripts/run-doc-link-lint.js</code> so you can also run <code>node ./scripts/run-doc-link-lint.js --verbose</code> if you are not using npm scripts.</li> <li>The underlying PowerShell script validates intra-repo Markdown links and any <code>docs/...</code> references inside source files or scripts. The <code>lint-doc-links</code> GitHub Actions workflow runs it on every PR, so run it locally before pushing large doc updates.</li> </ul>"},{"location":"project/contributing/#style-and-naming","title":"Style and Naming","text":"<p>Please follow the conventions outlined in <code>.editorconfig</code> and the repository guidelines (PascalCase types, camelCase fields, explicit types, braces required, no regions).</p>"},{"location":"project/contributing/#releases-and-versioning","title":"Releases and Versioning","text":"<p>This project follows Semantic Versioning. Key points:</p> <ul> <li>Git tags use the format <code>3.1.5</code> (no <code>v</code> prefix)</li> <li>package.json contains the authoritative version number</li> <li>SVG banner displays the version with a <code>v</code> prefix (e.g., <code>v3.1.5</code>) for visual consistency, synced automatically via pre-commit hook</li> </ul> <p>When installing via Git URL, reference versions without the <code>v</code> prefix:</p> Text Only<pre><code>https://github.com/wallstop/unity-helpers.git#3.1.5\n</code></pre> <p>Releases are drafted automatically via release-drafter. Maintainers review and publish the draft when ready.</p>"},{"location":"project/license/","title":"License","text":"<p>This project is licensed under the MIT License.</p> <p>See the full license text in the repository root: LICENSE</p>"},{"location":"project/llms-txt/","title":"llms.txt - LLM-Friendly Documentation","text":"<p>Unity Helpers includes an <code>llms.txt</code> file following the llmstxt.org specification. This file provides structured, LLM-optimized documentation that enables AI assistants to quickly understand and work with the package.</p>"},{"location":"project/llms-txt/#what-is-llmstxt","title":"What is llms.txt?","text":"<p>The <code>llms.txt</code> specification defines a standard format for providing LLM-friendly content. Unlike full HTML documentation that may be too large for context windows, <code>llms.txt</code> offers:</p> <ul> <li>A concise overview of the project and its key features</li> <li>Structured sections with links to detailed documentation</li> <li>Machine-readable format (Markdown) that LLMs can parse efficiently</li> <li>Curated content focused on what's most useful for development tasks</li> </ul>"},{"location":"project/llms-txt/#location","title":"Location","text":"<p>The file is located at the repository root: <code>/llms.txt</code></p>"},{"location":"project/llms-txt/#contents","title":"Contents","text":"<p>The Unity Helpers <code>llms.txt</code> includes:</p> <ol> <li>Package Overview - Name, version, license, repository links</li> <li>Implementation Notes - Coding style requirements and conventions</li> <li>Assembly Structure - Runtime and Editor assembly organization</li> <li>Core Features - Inspector tooling, relational components, serialization, spatial trees, PRNGs, effects system, pooling, editor tools</li> <li>Documentation Links - Organized by category (Docs, Feature Guides, Performance, Project)</li> <li>Optional Resources - LLM instructions, third-party notices</li> </ol>"},{"location":"project/llms-txt/#usage","title":"Usage","text":"<p>AI assistants and LLM-powered tools can:</p> <ol> <li>Fetch the file directly from <code>https://raw.githubusercontent.com/wallstop/unity-helpers/main/llms.txt</code></li> <li>Use it as context when answering questions about Unity Helpers</li> <li>Follow links to detailed documentation for specific features</li> <li>Understand coding conventions before generating code for this project</li> </ol>"},{"location":"project/llms-txt/#for-ai-agents","title":"For AI Agents","text":"<p>If you're an AI assistant working with this repository, you can also reference:</p> <ul> <li>AI Agent Guidelines - Comprehensive guidelines for AI agents</li> </ul> <p>This file provides additional context about coding style, testing patterns, and repository-specific conventions.</p>"},{"location":"project/llms-txt/#related","title":"Related","text":"<ul> <li>llmstxt.org Specification - The official llms.txt specification</li> <li>Feature Index - Complete A-Z index of Unity Helpers features</li> <li>Getting Started Guide - Quick start for new users</li> </ul>"},{"location":"project/third-party-notices/","title":"Third-Party Notices","text":"<p>This package contains third-party software components governed by the license(s) indicated below.</p>"},{"location":"project/third-party-notices/#serialization-compression","title":"Serialization &amp; Compression","text":""},{"location":"project/third-party-notices/#protobuf-net","title":"protobuf-net","text":"<ul> <li>Description: .NET runtime/library for Protocol Buffers serialization by Marc Gravell.</li> <li>Upstream: GitHub repository</li> <li>License: Apache License 2.0</li> <li>License URL: Apache License 2.0</li> <li>Notes: Uses attributes such as [ProtoContract]/[ProtoMember] and runtime <code>ProtoBuf.Serializer</code>.</li> </ul>"},{"location":"project/third-party-notices/#7-zip-lzma-sdk","title":"7-Zip LZMA SDK","text":"<ul> <li>Description: LZMA compression/decompression implementation (encoder/decoder) used via <code>SevenZip.Compression.LZMA</code>.</li> <li>Upstream: 7-Zip LZMA SDK</li> <li>License: Public Domain (per 7-Zip LZMA SDK)</li> <li>Notes: Integrated sources under <code>Runtime/Utils/SevenZip/Compress/LZMA</code>.</li> </ul>"},{"location":"project/third-party-notices/#editor-tools","title":"Editor Tools","text":""},{"location":"project/third-party-notices/#unity-serializable-dictionary","title":"Unity-Serializable-Dictionary","text":"<ul> <li>Description: Serializable dictionary implementation enabling Unity serialization of generic dictionaries.</li> <li>Upstream: GitHub repository</li> <li>License: MIT License</li> <li>License URL: MIT License</li> <li>Notes: Adapted naming and serialization cache handling to align with Wallstop Studios Unity Helpers conventions.</li> </ul>"},{"location":"project/third-party-notices/#unity-editor-toolbox-inline-editor","title":"Unity Editor Toolbox (Inline Editor)","text":"<ul> <li>Description: Inline inspector drawer inspiration for editing object references in-place.</li> <li>Upstream: GitHub repository</li> <li>License: MIT License</li> <li>License URL: MIT License</li> <li>Notes: Portions of <code>WInLineEditorDrawer</code> build upon concepts from the toolbox's InlineEditor drawer implementation.</li> </ul>"},{"location":"project/third-party-notices/#sorting-algorithms","title":"Sorting Algorithms","text":"<p>The following sorting algorithm implementations in <code>Runtime/Core/Extension/IListExtensions.cs</code> are adapted from or inspired by third-party sources.</p>"},{"location":"project/third-party-notices/#pattern-defeating-quicksort-pdqsort","title":"Pattern-Defeating QuickSort (pdqsort)","text":"<ul> <li>Description: Hybrid sorting algorithm combining quicksort with insertion sort and heapsort fallback.</li> <li>Author: Orson Peters</li> <li>Upstream: GitHub repository</li> <li>License: zlib License</li> <li>Notes: C# adaptation retaining pattern-detection heuristics while operating on <code>IList&lt;T&gt;</code>.</li> </ul>"},{"location":"project/third-party-notices/#grail-sort","title":"Grail Sort","text":"<ul> <li>Description: Block merge sort algorithm achieving stable O(n log n) sorting with O(1) extra space.</li> <li>Author: Mrrl (Andrey Astrelin)</li> <li>Upstream: GitHub repository</li> <li>License: MIT License</li> <li>Notes: Adaptation uses pooled buffers instead of manual block buffers while keeping stability.</li> </ul>"},{"location":"project/third-party-notices/#wikisort-block-merge-sort","title":"WikiSort (Block Merge Sort)","text":"<ul> <li>Description: In-place stable merge sort using block rearrangement.</li> <li>Author: Mike McFadden (BonzaiThePenguin)</li> <li>Upstream: GitHub repository</li> <li>License: Public Domain</li> <li>Notes: Adaptation uses a pooled full-size buffer for simplicity.</li> </ul>"},{"location":"project/third-party-notices/#powersort","title":"PowerSort","text":"<ul> <li>Description: Adaptive mergesort leveraging natural runs with optimal merge scheduling.</li> <li>Authors: J. Ian Munro and Sebastian Wild</li> <li>Upstream: arXiv paper</li> <li>License: CC BY 4.0 (paper); algorithm is public domain</li> <li>Notes: Implementation detects runs and merges them with pooled buffers.</li> </ul>"},{"location":"project/third-party-notices/#sort-research-rs-algorithms-glidesort-fluxsort-ipnsort","title":"sort-research-rs Algorithms (Glidesort, Fluxsort, Ipnsort)","text":"<ul> <li>Description: Modern high-performance sorting algorithms from the sort-research-rs project.</li> <li>Authors: Orson Peters, Lukas Bergdoll (Voultapher)</li> <li>Upstream: GitHub repository</li> <li>License: Apache License 2.0 / MIT License (dual-licensed)</li> <li>Notes: C# adaptations of Glidesort (stable galloping merges), Fluxsort (dual-pivot quicksort), and Ipnsort (introspective quicksort with median-of-medians).</li> </ul>"},{"location":"project/third-party-notices/#ips4o-sort","title":"IPS4o Sort","text":"<ul> <li>Description: In-place parallel super scalar samplesort.</li> <li>Authors: Michael Axtmann, Sascha Witt, Daniel Ferizovic, Peter Sanders</li> <li>Upstream: arXiv paper</li> <li>License: Academic paper; algorithm concepts are freely implementable</li> <li>Notes: Single-threaded C# adaptation with multiway partitioning.</li> </ul>"},{"location":"project/third-party-notices/#random-number-generators","title":"Random Number Generators","text":"<p>The following PRNG implementations in <code>Runtime/Core/Random/</code> are adapted from or inspired by third-party sources.</p>"},{"location":"project/third-party-notices/#pcg-random","title":"PCG Random","text":"<ul> <li>Description: Permuted Congruential Generator family of PRNGs with excellent statistical properties.</li> <li>Author: Melissa O'Neill</li> <li>Upstream: PCG Random website</li> <li>Paper: PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation</li> <li>License: Apache License 2.0</li> <li>Notes: Implementation based on the reference PCG Random.</li> </ul>"},{"location":"project/third-party-notices/#xoroshiro-xoshiro-splitmix64","title":"Xoroshiro / Xoshiro / SplitMix64","text":"<ul> <li>Description: Fast, high-quality PRNGs with small state.</li> <li>Authors: David Blackman, Sebastiano Vigna</li> <li>Upstream: Scrambled Linear PRNGs (xoshiro/xoroshiro); Fast Splittable PRNGs (SplitMix64)</li> <li>License: CC0 1.0 Universal (Public Domain)</li> <li>Notes: Implements xoroshiro128** and SplitMix64 variants.</li> </ul>"},{"location":"project/third-party-notices/#romuduo","title":"RomuDuo","text":"<ul> <li>Description: Rotate-multiply PRNG family optimized for modern CPUs.</li> <li>Authors: Mark A. Overton</li> <li>Upstream: ROMU website (archived)</li> <li>License: CC0 1.0 Universal (Public Domain)</li> <li>Notes: Implements the RomuDuo variant with two 64-bit state words.</li> </ul>"},{"location":"project/third-party-notices/#wyrandom-wyhash","title":"WyRandom (wyhash)","text":"<ul> <li>Description: Fast PRNG based on the wyhash hash function.</li> <li>Author: Wang Yi</li> <li>Upstream: GitHub repository</li> <li>License: The Unlicense (Public Domain)</li> <li>.NET Reference: cocowalla/wyhash-dotnet (MIT License)</li> <li>Notes: Implementation references the cocowalla .NET port.</li> </ul>"},{"location":"project/third-party-notices/#will-stafford-parsons-algorithms","title":"Will Stafford Parsons Algorithms","text":"<p>The following algorithms are by Will Stafford Parsons (wileylooper). Note: The original GitHub repositories are currently offline.</p> <ul> <li>IllusionFlow: Hybridized PCG + xorshift design.</li> <li>FlurryBurst: Six-word ARX-style generator.</li> <li>StormDrop: Large-state ARX generator inspired by SHISHUA.</li> <li>PhotonSpin: 20-word ring-buffer generator.</li> <li>BlastCircuit: Four-word ARX-style generator.</li> <li>WaveSplat: One-word chaotic generator.</li> <li>Meteor Sort: Gap-sequence-based hybrid sorting algorithm.</li> <li>Ghost Sort: Hybrid gap-based sorting algorithm.</li> </ul> <p>License: These implementations are used with attribution to the original author. Please refer to the individual repositories for specific licensing terms if they become available again.</p>"},{"location":"project/third-party-notices/#academic-historical-acknowledgments","title":"Academic &amp; Historical Acknowledgments","text":"<p>The following algorithms are based on well-known academic work and are implemented from published descriptions:</p>"},{"location":"project/third-party-notices/#sorting-algorithms_1","title":"Sorting Algorithms","text":"<ul> <li>TimSort: Hybrid stable sort by Tim Peters (Python) and OpenJDK. Python description</li> <li>SmoothSort: Heap-based adaptive sort by Edsger Dijkstra. Further analysis by Stefan Edelkamp and Armin Wegener.</li> <li>JesseSort: Dual-patience sort hybrid by Jesse Michel. GitHub</li> <li>greeNsort: Symmetric mergesort by Jens Oehlschlegel. Website</li> <li>Ska Sort: Branch-friendly dual-pivot quicksort by Malte Skarupke. Blog post</li> <li>PowerSort+: Enhanced run-priority mergesort by Sebastian Wild and Martin Nebel.</li> </ul>"},{"location":"project/third-party-notices/#random-number-generators_1","title":"Random Number Generators","text":"<ul> <li>XorShift: Classic PRNG by George Marsaglia (2003). Paper</li> <li>Linear Congruential Generator: Park-Miller variant (1988). \"Random Number Generators: Good Ones Are Hard to Find\" Communications of the ACM 31(10):1192-1201</li> <li>Squirrel Noise: Hash-based noise function by Squirrel Eiserloh. GDC Talk</li> </ul>"},{"location":"project/third-party-notices/#additional-notes","title":"Additional Notes","text":"<ul> <li>System.Text.Json and other .NET BCL components are used as part of the .NET runtime and are subject to their respective licenses (e.g., MIT for dotnet/runtime). No vendored sources from these components are included in this repository.</li> </ul>"},{"location":"project/third-party-notices/#full-license-texts","title":"Full License Texts","text":""},{"location":"project/third-party-notices/#mit-license","title":"MIT License","text":"<p>Used by: Unity-Serializable-Dictionary, Unity Editor Toolbox, Grail Sort, cocowalla/wyhash-dotnet</p> Text Only<pre><code>MIT License\n\nCopyright (c) [year] [copyright holders]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n</code></pre>"},{"location":"project/third-party-notices/#apache-license-20","title":"Apache License 2.0","text":"<p>Used by: protobuf-net, PCG Random, sort-research-rs algorithms</p> Text Only<pre><code>Apache License\nVersion 2.0, January 2004\nhttps://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n   \"License\" shall mean the terms and conditions for use, reproduction, and\n   distribution as defined by Sections 1 through 9 of this document.\n   \"Licensor\" shall mean the copyright owner or entity authorized by the\n   copyright owner that is granting the License.\n   \"Legal Entity\" shall mean the union of the acting entity and all other\n   entities that control, are controlled by, or are under common control with\n   that entity.\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\n   permissions granted by this License.\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation source,\n   and configuration files.\n   \"Object\" form shall mean any form resulting from mechanical transformation\n   or translation of a Source form, including but not limited to compiled\n   object code, generated documentation, and conversions to other media types.\n   \"Work\" shall mean the work of authorship, whether in Source or Object form,\n   made available under the License, as indicated by a copyright notice that\n   is included in or attached to the work.\n   \"Derivative Works\" shall mean any work, whether in Source or Object form,\n   that is based on (or derived from) the Work and for which the editorial\n   revisions, annotations, elaborations, or other modifications represent, as\n   a whole, an original work of authorship.\n   \"Contribution\" shall mean any work of authorship, including the original\n   version of the Work and any modifications or additions to that Work or\n   Derivative Works thereof, that is intentionally submitted to Licensor for\n   inclusion in the Work by the copyright owner or by an individual or Legal\n   Entity authorized to submit on behalf of the copyright owner.\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity on\n   behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License.\n   Subject to the terms and conditions of this License, each Contributor\n   hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,\n   royalty-free, irrevocable copyright license to reproduce, prepare\n   Derivative Works of, publicly display, publicly perform, sublicense, and\n   distribute the Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License.\n   Subject to the terms and conditions of this License, each Contributor\n   hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,\n   royalty-free, irrevocable (except as stated in this section) patent license\n   to make, have made, use, offer to sell, sell, import, and otherwise\n   transfer the Work.\n\n4. Redistribution.\n   You may reproduce and distribute copies of the Work or Derivative Works\n   thereof in any medium, with or without modifications, and in Source or\n   Object form, provided that You meet the following conditions:\n   (a) You must give any other recipients of the Work or Derivative Works a\n       copy of this License; and\n   (b) You must cause any modified files to carry prominent notices stating\n       that You changed the files; and\n   (c) You must retain, in the Source form of any Derivative Works that You\n       distribute, all copyright, patent, trademark, and attribution notices\n       from the Source form of the Work; and\n   (d) If the Work includes a \"NOTICE\" text file as part of its distribution,\n       then any Derivative Works that You distribute must include a readable\n       copy of the attribution notices contained within such NOTICE file,\n       excluding those notices that do not pertain to any part of the\n       Derivative Works.\n\n5. Submission of Contributions.\n   Unless You explicitly state otherwise, any Contribution intentionally\n   submitted for inclusion in the Work by You to the Licensor shall be under\n   the terms and conditions of this License, without any additional terms or\n   conditions.\n\n6. Trademarks.\n   This License does not grant permission to use the trade names, trademarks,\n   service marks, or product names of the Licensor, except as required for\n   reasonable and customary use in describing the origin of the Work and\n   reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n   Unless required by applicable law or agreed to in writing, Licensor\n   provides the Work (and each Contributor provides its Contributions) on an\n   \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n   or implied.\n\n8. Limitation of Liability.\n   In no event and under no legal theory, whether in tort (including\n   negligence), contract, or otherwise, unless required by applicable law\n   (such as deliberate and grossly negligent acts) or agreed to in writing,\n   shall any Contributor be liable to You for damages, including any direct,\n   indirect, special, incidental, or consequential damages of any character\n   arising as a result of this License or out of the use or inability to use\n   the Work.\n\n9. Accepting Warranty or Additional Liability.\n   While redistributing the Work or Derivative Works thereof, You may choose\n   to offer, and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this License.\n\nEND OF TERMS AND CONDITIONS\n</code></pre>"},{"location":"project/third-party-notices/#zlib-license","title":"zlib License","text":"<p>Used by: pdqsort</p> Text Only<pre><code>zlib License\n\nCopyright (c) [year] [copyright holders]\n\nThis software is provided 'as-is', without any express or implied warranty.\nIn no event will the authors be held liable for any damages arising from the\nuse of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not claim\n   that you wrote the original software. If you use this software in a\n   product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n3. This notice may not be removed or altered from any source distribution.\n</code></pre>"},{"location":"project/third-party-notices/#cc0-10-universal-public-domain-dedication","title":"CC0 1.0 Universal (Public Domain Dedication)","text":"<p>Used by: Xoroshiro/Xoshiro/SplitMix64, RomuDuo</p> Text Only<pre><code>CC0 1.0 Universal\n\nCREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL\nSERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT\nRELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN \"AS-IS\" BASIS.\nCREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR\nTHE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR\nDAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\nPROVIDED HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator and\nsubsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for the\npurpose of contributing to a commons of creative, cultural and scientific\nworks (\"Commons\") that the public can reliably and without fear of later\nclaims of infringement build upon, modify, incorporate in other works, reuse\nand redistribute as freely as possible in any form whatsoever and for any\npurposes, including without limitation commercial purposes.\n\nThe person associating CC0 with a Work (the \"Affirmer\"), to the extent that\nhe or she is an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the Work\nand the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights.\n   A Work made available under CC0 may be protected by copyright and related\n   or neighboring rights (\"Copyright and Related Rights\").\n\n2. Waiver.\n   To the greatest extent permitted by, but not in contravention of,\n   applicable law, Affirmer hereby overtly, fully, permanently, irrevocably\n   and unconditionally waives, abandons, and surrenders all of Affirmer's\n   Copyright and Related Rights and associated claims and causes of action.\n\n3. Public License Fallback.\n   Should any part of the Waiver for any reason be judged legally invalid or\n   ineffective under applicable law, then the Waiver shall be preserved to\n   the maximum extent permitted. In addition, to the extent the Waiver is so\n   judged Affirmer hereby grants to each affected person a royalty-free, non\n   transferable, non sublicensable, non exclusive, irrevocable and\n   unconditional license to exercise Affirmer's Copyright and Related Rights\n   in the Work.\n\n4. Limitations and Disclaimers.\n   a. No trademark or patent rights held by Affirmer are waived, abandoned,\n      surrendered, licensed or otherwise affected by this document.\n   b. Affirmer offers the Work as-is and makes no representations or\n      warranties of any kind concerning the Work.\n   c. Affirmer disclaims responsibility for clearing rights of other persons\n      that may apply to the Work.\n   d. Affirmer understands and acknowledges that Creative Commons is not a\n      party to this document and has no duty or obligation with respect to\n      this CC0 or use of the Work.\n</code></pre>"},{"location":"project/third-party-notices/#the-unlicense","title":"The Unlicense","text":"<p>Used by: wyhash</p> Text Only<pre><code>This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, compile, sell, or distribute\nthis software, either in source code form or as a compiled binary, for any\npurpose, commercial or non-commercial, and by any means.\n\nIn jurisdictions that recognize copyright laws, the author or authors of this\nsoftware dedicate any and all copyright interest in the software to the\npublic domain. We make this dedication for the benefit of the public at large\nand to the detriment of our heirs and successors. We intend this dedication\nto be an overt act of relinquishment in perpetuity of all present and future\nrights to this software under copyright law.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\nACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\nFor more information, please refer to &lt;https://unlicense.org&gt;\n</code></pre>"}]}