UNPKG

com.wallstop-studios.unity-helpers

Version:

Treasure chest of Unity developer tools

659 lines (475 loc) 23.4 kB
# Inspector Grouping Attributes **Organize your inspector without writing custom editors.** 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. --- ## Table of Contents - [WGroup & WGroupEnd](#wgroup--wgroupend) - [Common Features](#common-features) - [Configuration](#configuration) - [Best Practices](#best-practices) - [Examples](#examples) --- <a id="wgroup--wgroupend"></a> <a id="wgroup-wgroupend"></a> ## WGroup & WGroupEnd Creates boxed inspector sections with optional collapsible headers and automatic field inclusion. > ⚠️ **Important:** `[WGroupEnd]` must be placed on the **last field you want included** in the group. The field with `[WGroupEnd]` IS included in the group, and then the group closes for subsequent fields. ### Basic Usage ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class CharacterStatsWGroup : MonoBehaviour { // Simple box with 4 fields [WGroup("combat", "Combat Stats")] public float maxHealth = 100f; public float defense = 10f; public float attackPower = 25f; [WGroupEnd("combat")] // criticalChance IS included, then group closes public float criticalChance = 0.15f; public string characterName; // Not in group (comes after WGroupEnd) } ``` ![WGroup showing boxed combat stats with header](../../images/inspector/wgroup-simple.png) ### Parameters ```csharp [WGroup( string groupName, // Required: Unique identifier string displayName = null, // Optional: Header text (defaults to groupName) int autoIncludeCount = UseGlobalAutoInclude, // Auto-include N fields (or use global setting) bool collapsible = false, // Enable foldout behavior bool startCollapsed = false, // Initial collapsed state bool hideHeader = false, // Draw body without header bar string parentGroup = null // Optional: Nest inside another group )] ``` > 💡 Use the optional `CollapseBehavior` named argument (or `startCollapsed: true`) to override the project-wide default configured under **Project Settings Wallstop Studios Unity Helpers Start WGroups Collapsed**. Example: > > ```csharp > [WGroup( > "advanced", > collapsible: true, > CollapseBehavior = WGroupAttribute.WGroupCollapseBehavior.ForceExpanded > )] > ``` `CollapseBehavior` options: - `UseProjectSetting` (default) – defers to the Unity Helpers project setting. - `ForceExpanded` – always starts expanded. - `ForceCollapsed` – always starts collapsed (equivalent to `startCollapsed: true`). --- ### Auto-Inclusion Modes #### 1. Explicit Count ```csharp [WGroup("items", "Inventory", autoIncludeCount: 3)] public GameObject weapon; // Field 1: in group public GameObject armor; // Field 2: in group (auto-included) [WGroupEnd("items")] // accessory IS included (field 3), then group closes public GameObject accessory; // Field 3: in group (last field) public int gold; // NOT in group (comes after WGroupEnd) ``` #### 2. Infinite Auto-Include ```csharp [WGroup("settings", "Settings", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)] public bool enableSound; // In group public bool enableMusic; // In group (auto-included) public float volume; // In group (auto-included) // ... 20 more fields ... // All auto-included [WGroupEnd("settings")] // lastField IS included, then group closes public bool lastField; // In group (last field) public int outsideGroup; // NOT in group (comes after WGroupEnd) ``` #### 3. Global Default ```csharp // Uses WGroupAutoIncludeRowCount from ProjectSettings/UnityHelpersSettings.asset (default: 4) [WGroup("stats", "Stats")] // autoIncludeCount defaults to UseGlobalAutoInclude public int strength; // Field 1: in group public int intelligence; // Field 2: in group (auto-included) public int agility; // Field 3: in group (auto-included) [WGroupEnd("stats")] // luck IS included (field 4), then group closes public int luck; // Field 4: in group (last field) ``` --- ### Collapsible Groups ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class WGroupEndExample : MonoBehaviour { [WGroup("advanced", "Advanced Options", collapsible: true, startCollapsed: true)] public float raycastDistance = 100f; // In group public LayerMask collisionMask; // In group (auto-included) [WGroupEnd("advanced")] // debugDraw IS included, then group closes public bool debugDraw; // In group (last field) public bool someOtherField; // NOT in group (comes after WGroupEnd) } ``` ![WGroup being collapsed and expanded with smooth animation](../../images/inspector/wgroup-collapsible.gif) **Animation Settings:** - Speed controlled by `UnityHelpersSettings.WGroupFoldoutSpeed` (default: 2.0, range: 2.0-12.0) - Enable/disable via `UnityHelpersSettings.WGroupFoldoutTweenEnabled` (default: enabled) Configure in **Project Settings Unity Helpers** or see [Inspector Settings](./inspector-settings.md#wgroup-settings) for details. --- ### Hiding Headers ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class HealthExample : MonoBehaviour { [WGroup("stealth", "", hideHeader: true)] public float opacity = 1f; // In group [WGroupEnd("stealth")] // isVisible IS included, then group closes public bool isVisible = true; // In group (last field) } ``` ![WGroup with just border and body, no header](../../images/inspector/wgroup-no-header.png) **Use Cases:** - Visual separation without labels - Nested grouping styles - Minimalist inspector layouts --- ### Nested Groups Use the `parentGroup` parameter to nest one group inside another. Nested groups render visually inside their parent's box, with accumulated indentation and padding. ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class NestedGroupExample : MonoBehaviour { [WGroup("outer", "Character")] public string characterName; // In outer group [WGroup("inner", "Stats", parentGroup: "outer")] public int level; // In inner group (nested in outer) public int experience; // In inner group (auto-included) [WGroupEnd("inner")] // faction IS included in BOTH groups [WGroupEnd("outer")] // Then both groups close public string faction; // In inner AND outer groups (last field) } ``` ![Nested WGroup showing inner Stats box rendered inside outer Character box](../../images/inspector/wgroup-nested.png) **How Nesting Works:** 1. Declare the parent group first with `[WGroup("outer", ...)]` 2. Declare child group with `parentGroup: "outer"` parameter 3. Child groups are rendered recursively inside parent content areas 4. Indentation and padding accumulate for each nesting level 5. Each group maintains its own foldout state when collapsible **Multiple Nesting Levels:** ```csharp [WGroup("level1", "Level 1")] public string field1; // In level1 only [WGroup("level2", "Level 2", parentGroup: "level1")] public string field2; // In level2 (nested in level1) [WGroup("level3", "Level 3", parentGroup: "level2")] public string field3; // In level3 (nested in level2 → level1) [WGroupEnd("level3")] // field4 IS included in all three groups [WGroupEnd("level2")] // Then all groups close in order [WGroupEnd("level1")] public string field4; // In level3, level2, AND level1 (last field) ``` **Sibling Nested Groups:** ```csharp [WGroup("parent", "Parent")] public string parentField; // In parent group [WGroup("child1", "Child 1", parentGroup: "parent")] public string child1Field; // In child1 (nested in parent) [WGroupEnd("child1")] // child2Field starts NEW group, so closes child1 first [WGroup("child2", "Child 2", parentGroup: "parent")] public string child2Field; // In child2 (nested in parent) [WGroupEnd("child2")] // afterParent IS included in child2 AND parent [WGroupEnd("parent")] // Then both groups close public string afterParent; // In child2 AND parent groups (last field) ``` **Important Notes:** - Parent group must be declared before or on the same property as the child - Circular references are detected and logged as warnings; affected groups are treated as top-level - If `parentGroup` references a non-existent group, the child is rendered as a top-level group --- ### WGroupEnd Variants > ⚠️ **Key Point:** `[WGroupEnd]` must always be attached to a field. The field with `[WGroupEnd]` IS included in the group (via auto-include), and then the group closes. #### 1. End Specific Group ```csharp [WGroup("combat", "Combat Stats")] public int health; // In group public int defense; // In group (auto-included) [WGroupEnd("combat")] // stamina IS included, then "combat" closes public int stamina; // In group (last field) public int unrelatedField; // NOT in group ``` #### 2. End Multiple Groups When closing nested groups, stack multiple `[WGroupEnd]` attributes on the last field: ```csharp [WGroup("outer", "Outer")] public int outerField; // In outer [WGroup("inner", "Inner", parentGroup: "outer")] public int innerField; // In inner (nested in outer) [WGroupEnd("inner")] // lastField IS included in both groups [WGroupEnd("outer")] // Then both groups close public int lastField; // In inner AND outer (last field) ``` #### 3. Close All Active Groups Omit the group name to close all currently active auto-include groups: ```csharp [WGroup("settings", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)] public bool enableSound; // In group public float volume; // In group (auto-included) [WGroupEnd] // lastSetting IS included, then ALL groups close public bool lastSetting; // In group (last field) public int outsideAllGroups; // NOT in any group ``` --- ## Common Features ### Auto-Include Constants ```csharp public class WGroupAttribute { public const int InfiniteAutoInclude = -1; // Include until WGroupEnd public const int UseGlobalAutoInclude = -2; // Default: use project setting } ``` ### Shared Group Names ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class WGroupOutOfOrderExample : MonoBehaviour { [WGroup("settings", "Game Settings", autoIncludeCount: 1)] public float masterVolume; public float musicVolume; public int numChannels; // Later in the same script... [WGroup("settings", autoIncludeCount: 1)] // Reuses "Game Settings" header, included in original group public float sfxVolume; [WGroupEnd("settings")] public bool enableSound; } ``` ![Two separate WGroup sections with same header styling](../../images/inspector/wgroup-out-of-order.png) **Note:** Multiple `[WGroup]` attributes with the same `groupName` merge into a single group instance. This allows for logical grouping of related fields that may not be contiguous in code. --- ## Configuration ### Global Settings All grouping attributes respect project-wide settings defined in `UnityHelpersSettings`: **Location:** `ProjectSettings/UnityHelpersSettings.asset` **Settings:** - `WGroupAutoIncludeRowCount` (default: 4) - Default fields to auto-include - `WGroupStartCollapsed` (default: true) - Whether collapsible groups start collapsed - `WGroupFoldoutTweenEnabled` (default: true) - Enable expand/collapse animations - `WGroupFoldoutSpeed` (default: 2.0, range: 2-12) - Animation speed ![UnityHelpersSettings asset showing WGroup configuration section](../../images/inspector/wgroup-settings.png) --- ## Best Practices ### 1. Consistent Naming ```csharp // ✅ GOOD: Clear, descriptive group names [WGroup("combat", "Combat Stats")] [WGroup("movement", "Movement Settings")] [WGroup("visuals", "Visual Effects")] // ❌ BAD: Vague or inconsistent [WGroup("group1", "Stuff")] [WGroup("misc", "Things")] ``` ### 2. Auto-Inclusion Strategy ```csharp // ✅ GOOD: Explicit count for small groups [WGroup("position", "Position", autoIncludeCount: 3)] public Vector3 position; // Field 1: in group public Quaternion rotation; // Field 2: in group (auto-included) [WGroupEnd("position")] // scale IS included (field 3), then group closes public Vector3 scale; // Field 3: in group (last field) // ✅ GOOD: Infinite for dynamic/long lists [WGroup("inventory", "Items", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)] public List<GameObject> weapons; // In group public List<GameObject> consumables; // In group (auto-included) // ... many more fields ... // All auto-included [WGroupEnd("inventory")] // lastItem IS included, then group closes public int lastItem; // In group (last field) // ❌ BAD: Infinite without WGroupEnd (includes everything below!) [WGroup("bad", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)] public int field1; public int field2; // Oops, forgot [WGroupEnd]! public string unrelatedField; // Also included! ``` ### 3. Collapsible vs. Always-Open ```csharp // ✅ GOOD: Always-visible for frequently accessed data [WGroup("core", "Core Stats", collapsible: false)] public float health; // In group [WGroupEnd("core")] // energy IS included, then group closes public float energy; // In group (last field) // ✅ GOOD: Collapsible for optional/advanced features [WGroup("advanced", "Advanced", collapsible: true, startCollapsed: true)] public float debugParameter; // In group [WGroupEnd("advanced")] // experimentalFeature IS included, then closes public bool experimentalFeature; // In group (last field) // ❌ BAD: Everything collapsible (hides important data) [WGroup("important", "Critical Settings", collapsible: true, startCollapsed: true, autoIncludeCount: 0)] public float maxHealth; // Why hide this? ``` --- ## Examples ### Example 1: RPG Character Stats ```csharp using System.Collections.Generic; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class RPGCharacter : MonoBehaviour { [WGroup("identity", "Identity")] public string characterName; // In identity group public Sprite portrait; // In identity group (auto-included) public string className; // In identity group (auto-included) [WGroupEnd("identity")] // strength IS included, then identity closes [WGroup("attributes", "Base Attributes", collapsible: true)] public int strength = 10; // In identity (last) AND starts attributes public int agility = 10; // In attributes (auto-included) public int intelligence = 10; // In attributes (auto-included) public int vitality = 10; // In attributes (auto-included) [WGroupEnd("attributes")] // maxHealth IS included, then attributes closes [WGroup("combat", "Combat Stats")] public float maxHealth = 100f; // In attributes (last) AND starts combat public float attackPower = 25f; // In combat (auto-included) public float defense = 15f; // In combat (auto-included) [WGroupEnd("combat")] // learnedSkills IS included, then combat closes [WGroup("skills", "Skills", collapsible: true, startCollapsed: true)] public List<string> learnedSkills = new(); // In combat (last) AND starts skills [WGroupEnd("skills")] // skillPoints IS included, then skills closes public int skillPoints = 0; // In skills (last field) } ``` ![RPGCharacter inspector showing all groups](../../images/inspector/rpg-character.png) --- ### Example 2: Weapon Configuration ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public enum DamageType { Physical, Magic, } public class WeaponConfig2 : MonoBehaviour { [WGroup("basic", "Basic Info", autoIncludeCount: 2)] public string weaponName; // Field 1: in basic group [WGroupEnd("basic")] // icon IS included (field 2), then closes public Sprite icon; // Field 2: in basic (last field) [WGroup("damage", "Damage", collapsible: true)] public float baseDamage = 10f; // In damage group public float criticalMultiplier = 2f; // In damage (auto-included) [WGroupEnd("damage")] // damageType IS included, then closes public DamageType damageType; // In damage (last field) [WGroup("effects", "Special Effects", collapsible: true, startCollapsed: true)] public ParticleSystem hitEffect; // In effects group public AudioClip hitSound; // In effects (auto-included) [WGroupEnd("effects")] // effectDuration IS included, then closes public float effectDuration = 1f; // In effects (last field) [WGroup("advanced", "Advanced Settings", collapsible: true, startCollapsed: true)] public float projectileSpeed = 20f; // In advanced group public LayerMask targetLayers; // In advanced (auto-included) [WGroupEnd("advanced")] // debugMode IS included, then closes public bool debugMode = false; // In advanced (last field) } ``` ![WeaponConfig inspector with mixed open/closed groups](../../images/inspector/wgroup-weapon-config-2.png) --- ### Example 3: Dynamic Form with Many Fields ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class LevelSettings : MonoBehaviour { [WGroup("general", "General", autoIncludeCount: 3)] public string levelName; // Field 1: in general group public Sprite thumbnail; // Field 2: in general (auto-included) [WGroupEnd("general")] // description IS included (field 3), then closes public string description; // Field 3: in general (last field) [WGroup( "environment", "Environment", collapsible: true, startCollapsed: true, autoIncludeCount: WGroupAttribute.InfiniteAutoInclude )] public Color skyColor; // In environment group public Color fogColor; // In environment (auto-included) public float fogDensity; // In environment (auto-included) public Light directionalLight; // In environment (auto-included) public Cubemap skybox; // In environment (auto-included) public float ambientIntensity; // In environment (auto-included) [WGroupEnd("environment")] // sunIntensity IS included, then closes public float sunIntensity; // In environment (last field) [WGroup("gameplay", "Gameplay Rules", collapsible: true, startCollapsed: false)] public int enemyCount = 10; // In gameplay group public float difficultyMultiplier = 1f; // In gameplay (auto-included) [WGroupEnd("gameplay")] // allowRespawns IS included, then closes public bool allowRespawns = true; // In gameplay (last field) [WGroup("debug", "Debug Options", collapsible: true, startCollapsed: true)] public bool godMode = false; // In debug group public bool unlimitedAmmo = false; // In debug (auto-included) [WGroupEnd("debug")] // showHitboxes IS included, then closes public bool showHitboxes = false; // In debug (last field) } ``` ![LevelSettings with multiple collapsible groups being toggled](../../images/inspector/wgroup-collapsing.gif) --- ### Example 4: Nested Configuration ```csharp using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; public class AIController : MonoBehaviour { [WGroup("outer", "AI Configuration")] [WGroup("detection", "Detection", parentGroup: "outer")] public float sightRange = 10f; // In detection (nested in outer) [WGroupEnd("detection")] // hearingRange IS included, then detection closes public float hearingRange = 5f; // In detection (last) AND outer (auto-included) [WGroup("behavior", "Behavior", parentGroup: "outer")] public float aggressionLevel = 0.5f; // In behavior (nested in outer) [WGroupEnd("behavior")] // retreatThreshold IS included in both [WGroupEnd("outer")] // Then both groups close public float retreatThreshold = 0.2f; // In behavior (last) AND outer (last) } ``` ![Nested groups showing visual hierarchy](../../images/inspector/wgroup-nested-2.png) --- ## Troubleshooting ### Group Not Appearing **Problem:** Fields not showing in a group **Solutions:** 1. Check `autoIncludeCount` - make sure it includes all desired fields 2. Verify `WGroupEnd` placement - the field WITH `WGroupEnd` IS included, fields AFTER are excluded 3. Ensure group names match between `WGroup` and `WGroupEnd` ```csharp // ❌ WRONG: Count too low (only 2 fields included, but intelligence has WGroupEnd) [WGroup("stats", autoIncludeCount: 2)] public int strength; // Field 1: in group public int agility; // Field 2: in group (auto-included) [WGroupEnd("stats")] // intelligence would be field 3, but count is only 2! public int intelligence; // NOT included - auto-include budget exhausted before WGroupEnd // ✅ CORRECT: Increase count to include the WGroupEnd field [WGroup("stats", autoIncludeCount: 3)] public int strength; // Field 1: in group public int agility; // Field 2: in group (auto-included) [WGroupEnd("stats")] // intelligence IS included (field 3), then group closes public int intelligence; // Field 3: in group (last field) ``` --- ### Animation Not Working **Problem:** Groups don't animate when collapsed/expanded **Solutions:** 1. Check `UnityHelpersSettings.WGroupFoldoutTweenEnabled` is `true` 2. Ensure `collapsible: true` is set for WGroup 3. Verify `WGroupFoldoutSpeed` isn't set too low (minimum is 2.0) 4. Open **Project Settings Unity Helpers** to review settings --- ## Compatibility 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. --- ## See Also - **[Inspector Overview](./inspector-overview.md)** - Complete inspector features overview - **[Inspector Buttons](./inspector-button.md)** - WButton for method invocation - **[Inspector Settings](./inspector-settings.md)** - Configuration reference - **[Editor Tools Guide](../editor-tools/editor-tools-guide.md)** - Other editor utilities --- **Next Steps:** - Try grouping your existing scripts with `[WGroup]` - Explore `[WButton]` to add method buttons to your groups