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.
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.
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassCharacterStatsWGroup:MonoBehaviour{// Simple box with 4 fields[WGroup("combat", "Combat Stats")]publicfloatmaxHealth=100f;publicfloatdefense=10f;publicfloatattackPower=25f;[WGroupEnd("combat")]// criticalChance IS included, then group closespublicfloatcriticalChance=0.15f;publicstringcharacterName;// Not in group (comes after WGroupEnd)}
[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:
[WGroup("items", "Inventory", autoIncludeCount: 3)]publicGameObjectweapon;// Field 1: in grouppublicGameObjectarmor;// Field 2: in group (auto-included)[WGroupEnd("items")]// accessory IS included (field 3), then group closespublicGameObjectaccessory;// Field 3: in group (last field)publicintgold;// NOT in group (comes after WGroupEnd)
[WGroup("settings", "Settings", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]publicboolenableSound;// In grouppublicboolenableMusic;// In group (auto-included)publicfloatvolume;// In group (auto-included)// ... 20 more fields ... // All auto-included[WGroupEnd("settings")]// lastField IS included, then group closespublicboollastField;// In group (last field)publicintoutsideGroup;// NOT in group (comes after WGroupEnd)
// Uses WGroupAutoIncludeRowCount from ProjectSettings/UnityHelpersSettings.asset (default: 4)[WGroup("stats", "Stats")]// autoIncludeCount defaults to UseGlobalAutoIncludepublicintstrength;// Field 1: in grouppublicintintelligence;// Field 2: in group (auto-included)publicintagility;// Field 3: in group (auto-included)[WGroupEnd("stats")]// luck IS included (field 4), then group closespublicintluck;// Field 4: in group (last field)
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassWGroupEndExample:MonoBehaviour{[WGroup("advanced", "Advanced Options", collapsible: true, startCollapsed: true)]publicfloatraycastDistance=100f;// In grouppublicLayerMaskcollisionMask;// In group (auto-included)[WGroupEnd("advanced")]// debugDraw IS included, then group closespublicbooldebugDraw;// In group (last field)publicboolsomeOtherField;// NOT in group (comes after WGroupEnd)}
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 for details.
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassHealthExample:MonoBehaviour{[WGroup("stealth", "", hideHeader: true)]publicfloatopacity=1f;// In group[WGroupEnd("stealth")]// isVisible IS included, then group closespublicboolisVisible=true;// In group (last field)}
Use the parentGroup parameter to nest one group inside another. Nested groups render visually inside their parent's box, with accumulated indentation and padding.
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassNestedGroupExample:MonoBehaviour{[WGroup("outer", "Character")]publicstringcharacterName;// In outer group[WGroup("inner", "Stats", parentGroup: "outer")]publicintlevel;// In inner group (nested in outer)publicintexperience;// In inner group (auto-included)[WGroupEnd("inner")]// faction IS included in BOTH groups[WGroupEnd("outer")]// Then both groups closepublicstringfaction;// In inner AND outer groups (last field)}
How Nesting Works:
Declare the parent group first with [WGroup("outer", ...)]
Declare child group with parentGroup: "outer" parameter
Child groups are rendered recursively inside parent content areas
Indentation and padding accumulate for each nesting level
Each group maintains its own foldout state when collapsible
[WGroup("level1", "Level 1")]publicstringfield1;// In level1 only[WGroup("level2", "Level 2", parentGroup: "level1")]publicstringfield2;// In level2 (nested in level1)[WGroup("level3", "Level 3", parentGroup: "level2")]publicstringfield3;// 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")]publicstringfield4;// In level3, level2, AND level1 (last field)
[WGroup("parent", "Parent")]publicstringparentField;// In parent group[WGroup("child1", "Child 1", parentGroup: "parent")]publicstringchild1Field;// In child1 (nested in parent)[WGroupEnd("child1")]// child2Field starts NEW group, so closes child1 first[WGroup("child2", "Child 2", parentGroup: "parent")]publicstringchild2Field;// In child2 (nested in parent)[WGroupEnd("child2")]// afterParent IS included in child2 AND parent[WGroupEnd("parent")]// Then both groups closepublicstringafterParent;// 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
⚠️ 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.
[WGroup("combat", "Combat Stats")]publicinthealth;// In grouppublicintdefense;// In group (auto-included)[WGroupEnd("combat")]// stamina IS included, then "combat" closespublicintstamina;// In group (last field)publicintunrelatedField;// NOT in group
[WGroup("outer", "Outer")]publicintouterField;// In outer[WGroup("inner", "Inner", parentGroup: "outer")]publicintinnerField;// In inner (nested in outer)[WGroupEnd("inner")]// lastField IS included in both groups[WGroupEnd("outer")]// Then both groups closepublicintlastField;// In inner AND outer (last field)
[WGroup("settings", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]publicboolenableSound;// In grouppublicfloatvolume;// In group (auto-included)[WGroupEnd]// lastSetting IS included, then ALL groups closepublicboollastSetting;// In group (last field)publicintoutsideAllGroups;// NOT in any group
publicclassWGroupAttribute{publicconstintInfiniteAutoInclude=-1;// Include until WGroupEndpublicconstintUseGlobalAutoInclude=-2;// Default: use project setting}
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassWGroupOutOfOrderExample:MonoBehaviour{[WGroup("settings", "Game Settings", autoIncludeCount: 1)]publicfloatmasterVolume;publicfloatmusicVolume;publicintnumChannels;// Later in the same script...[WGroup("settings", autoIncludeCount: 1)]// Reuses "Game Settings" header, included in original grouppublicfloatsfxVolume;[WGroupEnd("settings")]publicboolenableSound;}
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.
// ✅ GOOD: Explicit count for small groups[WGroup("position", "Position", autoIncludeCount: 3)]publicVector3position;// Field 1: in grouppublicQuaternionrotation;// Field 2: in group (auto-included)[WGroupEnd("position")]// scale IS included (field 3), then group closespublicVector3scale;// Field 3: in group (last field)// ✅ GOOD: Infinite for dynamic/long lists[WGroup("inventory", "Items", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]publicList<GameObject>weapons;// In grouppublicList<GameObject>consumables;// In group (auto-included)// ... many more fields ... // All auto-included[WGroupEnd("inventory")]// lastItem IS included, then group closespublicintlastItem;// In group (last field)// ❌ BAD: Infinite without WGroupEnd (includes everything below!)[WGroup("bad", autoIncludeCount: WGroupAttribute.InfiniteAutoInclude)]publicintfield1;publicintfield2;// Oops, forgot [WGroupEnd]!publicstringunrelatedField;// Also included!
// ✅ GOOD: Always-visible for frequently accessed data[WGroup("core", "Core Stats", collapsible: false)]publicfloathealth;// In group[WGroupEnd("core")]// energy IS included, then group closespublicfloatenergy;// In group (last field)// ✅ GOOD: Collapsible for optional/advanced features[WGroup("advanced", "Advanced", collapsible: true, startCollapsed: true)]publicfloatdebugParameter;// In group[WGroupEnd("advanced")]// experimentalFeature IS included, then closespublicboolexperimentalFeature;// In group (last field)// ❌ BAD: Everything collapsible (hides important data)[WGroup("important", "Critical Settings", collapsible: true, startCollapsed: true, autoIncludeCount: 0)]publicfloatmaxHealth;// Why hide this?
usingSystem.Collections.Generic;usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassRPGCharacter:MonoBehaviour{[WGroup("identity", "Identity")]publicstringcharacterName;// In identity grouppublicSpriteportrait;// In identity group (auto-included)publicstringclassName;// In identity group (auto-included)[WGroupEnd("identity")]// strength IS included, then identity closes[WGroup("attributes", "Base Attributes", collapsible: true)]publicintstrength=10;// In identity (last) AND starts attributespublicintagility=10;// In attributes (auto-included)publicintintelligence=10;// In attributes (auto-included)publicintvitality=10;// In attributes (auto-included)[WGroupEnd("attributes")]// maxHealth IS included, then attributes closes[WGroup("combat", "Combat Stats")]publicfloatmaxHealth=100f;// In attributes (last) AND starts combatpublicfloatattackPower=25f;// In combat (auto-included)publicfloatdefense=15f;// In combat (auto-included)[WGroupEnd("combat")]// learnedSkills IS included, then combat closes[WGroup("skills", "Skills", collapsible: true, startCollapsed: true)]publicList<string>learnedSkills=new();// In combat (last) AND starts skills[WGroupEnd("skills")]// skillPoints IS included, then skills closespublicintskillPoints=0;// In skills (last field)}
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicenumDamageType{Physical,Magic,}publicclassWeaponConfig2:MonoBehaviour{[WGroup("basic", "Basic Info", autoIncludeCount: 2)]publicstringweaponName;// Field 1: in basic group[WGroupEnd("basic")]// icon IS included (field 2), then closespublicSpriteicon;// Field 2: in basic (last field)[WGroup("damage", "Damage", collapsible: true)]publicfloatbaseDamage=10f;// In damage grouppublicfloatcriticalMultiplier=2f;// In damage (auto-included)[WGroupEnd("damage")]// damageType IS included, then closespublicDamageTypedamageType;// In damage (last field)[WGroup("effects", "Special Effects", collapsible: true, startCollapsed: true)]publicParticleSystemhitEffect;// In effects grouppublicAudioCliphitSound;// In effects (auto-included)[WGroupEnd("effects")]// effectDuration IS included, then closespublicfloateffectDuration=1f;// In effects (last field)[WGroup("advanced", "Advanced Settings", collapsible: true, startCollapsed: true)]publicfloatprojectileSpeed=20f;// In advanced grouppublicLayerMasktargetLayers;// In advanced (auto-included)[WGroupEnd("advanced")]// debugMode IS included, then closespublicbooldebugMode=false;// In advanced (last field)}
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassLevelSettings:MonoBehaviour{[WGroup("general", "General", autoIncludeCount: 3)]publicstringlevelName;// Field 1: in general grouppublicSpritethumbnail;// Field 2: in general (auto-included)[WGroupEnd("general")]// description IS included (field 3), then closespublicstringdescription;// Field 3: in general (last field)[WGroup( "environment", "Environment", collapsible: true, startCollapsed: true, autoIncludeCount: WGroupAttribute.InfiniteAutoInclude )]publicColorskyColor;// In environment grouppublicColorfogColor;// In environment (auto-included)publicfloatfogDensity;// In environment (auto-included)publicLightdirectionalLight;// In environment (auto-included)publicCubemapskybox;// In environment (auto-included)publicfloatambientIntensity;// In environment (auto-included)[WGroupEnd("environment")]// sunIntensity IS included, then closespublicfloatsunIntensity;// In environment (last field)[WGroup("gameplay", "Gameplay Rules", collapsible: true, startCollapsed: false)]publicintenemyCount=10;// In gameplay grouppublicfloatdifficultyMultiplier=1f;// In gameplay (auto-included)[WGroupEnd("gameplay")]// allowRespawns IS included, then closespublicboolallowRespawns=true;// In gameplay (last field)[WGroup("debug", "Debug Options", collapsible: true, startCollapsed: true)]publicboolgodMode=false;// In debug grouppublicboolunlimitedAmmo=false;// In debug (auto-included)[WGroupEnd("debug")]// showHitboxes IS included, then closespublicboolshowHitboxes=false;// In debug (last field)}
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassAIController:MonoBehaviour{[WGroup("outer", "AI Configuration")][WGroup("detection", "Detection", parentGroup: "outer")]publicfloatsightRange=10f;// In detection (nested in outer)[WGroupEnd("detection")]// hearingRange IS included, then detection closespublicfloathearingRange=5f;// In detection (last) AND outer (auto-included)[WGroup("behavior", "Behavior", parentGroup: "outer")]publicfloataggressionLevel=0.5f;// In behavior (nested in outer)[WGroupEnd("behavior")]// retreatThreshold IS included in both[WGroupEnd("outer")]// Then both groups closepublicfloatretreatThreshold=0.2f;// In behavior (last) AND outer (last)}
// ❌ WRONG: Count too low (only 2 fields included, but intelligence has WGroupEnd)[WGroup("stats", autoIncludeCount: 2)]publicintstrength;// Field 1: in grouppublicintagility;// Field 2: in group (auto-included)[WGroupEnd("stats")]// intelligence would be field 3, but count is only 2!publicintintelligence;// NOT included - auto-include budget exhausted before WGroupEnd// ✅ CORRECT: Increase count to include the WGroupEnd field[WGroup("stats", autoIncludeCount: 3)]publicintstrength;// Field 1: in grouppublicintagility;// Field 2: in group (auto-included)[WGroupEnd("stats")]// intelligence IS included (field 3), then group closespublicintintelligence;// Field 3: in group (last field)
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.