Execute methods from the inspector with one click.
The [WButton] 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.
// Without displayName - shows "SpawnEnemy"[WButton]privatevoidSpawnEnemy(){}// With displayName - shows "🔫 Spawn Enemy"[WButton("🔫 Spawn Enemy")]privatevoidSpawnEnemy1(){}// More descriptive labels[WButton("Reset Player to Checkpoint")]privatevoidResetPlayer(){}[WButton("Clear All Save Data")]privatevoidClearSaveData(){}
publicclassPlayerController1:MonoBehaviour{publicinthealth=100;publicfloatspeed=5f;// These buttons render in order: Initialize, Validate, Debug Info// Placement (above/below properties) is controlled by groupPlacement or global settings[WButton("Initialize", drawOrder: -1)]privatevoidInitialize(){}[WButton("Validate", drawOrder: 0)]privatevoidValidate(){}[WButton("Debug Info", drawOrder: 1)]privatevoidShowDebugInfo(){}}
// Use global setting (default: 5 results)[WButton("Roll Dice")]privateintRollDice()=>Random.Range(1,7);// Store only last result[WButton("Get Timestamp", historyCapacity: 1)]privatestringGetTimestamp()=>DateTime.Now.ToString();// Store up to 10 results for detailed history[WButton("Measure Frame Time", historyCapacity: 10)]privatefloatMeasureFrameTime()=>Time.deltaTime*1000f;// Disable history completely (1 = minimum)[WButton("Ping Server", historyCapacity: 1)]privateasyncTask<string>PingServerAsync(CancellationTokenct){// ... ping logicreturn"Pong!";}
Performance tip: Use lower values (1-3) for methods called frequently to reduce memory usage.
publicclassActionPanel:MonoBehaviour{// This group renders FIRST (priority 0)[WButton("Quick Save", groupName: "Primary", groupPriority: 0)]privatevoidQuickSave(){}[WButton("Quick Load", groupName: "Primary", groupPriority: 0)]privatevoidQuickLoad(){}// This group renders SECOND (priority 10)[WButton("Debug Info", groupName: "Debug", groupPriority: 10)]privatevoidShowDebugInfo(){}// This group renders LAST (no explicit priority)[WButton("Reset", groupName: "Misc")]privatevoidReset(){}}
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.
publicclassMixedPlacementExample:MonoBehaviour{publicinthealth=100;publicfloatspeed=5f;// This group ALWAYS renders at the top, regardless of global setting[WButton("Initialize", groupName: "Setup", groupPlacement: WButtonGroupPlacement.Top)]privatevoidInitialize(){}[WButton("Validate", groupName: "Setup", groupPlacement: WButtonGroupPlacement.Top)]privatevoidValidate(){}// Properties appear here (health, speed)// This group ALWAYS renders at the bottom, regardless of global setting[WButton("Cleanup", groupName: "Maintenance", groupPlacement: WButtonGroupPlacement.Bottom)]privatevoidCleanup(){}[WButton("Reset All", groupName: "Maintenance", groupPlacement: WButtonGroupPlacement.Bottom)]privatevoidResetAll(){}}
Important: Like groupPriority, 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.
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;publicclassLevelManager:MonoBehaviour{publicintcurrentLevel=1;publicbooldebugMode=false;// Setup group - explicitly placed at top, renders first due to groupPriority: 0[WButton("Initialize Level", groupName: "Setup", groupPriority: 0, groupPlacement: WButtonGroupPlacement.Top)]privatevoidInitialize(){Debug.Log("Level initialized!");}[WButton("✓ Validate Configuration", groupName: "Setup")]privatevoidValidateConfig(){Debug.Log("Configuration valid!");}// Debug group - explicitly placed at top, renders second due to groupPriority: 1[WButton("Roll Dice", historyCapacity: 10, groupName: "Debug", groupPriority: 1, groupPlacement: WButtonGroupPlacement.Top)]privateintRollDice()=>Random.Range(1,7);[WButton("🎯 Spawn Test Enemy", colorKey: "Warning", groupName: "Debug")]privatevoidSpawnTestEnemy(){// Spawn logic here}// Properties appear here in the inspector// Actions group - explicitly placed at bottom, renders first in bottom section due to groupPriority: 0[WButton("▶ Start Level", colorKey: "Success", groupName: "Actions", groupPriority: 0, groupPlacement: WButtonGroupPlacement.Bottom)]privatevoidStartLevel(){Debug.Log($"Starting level {currentLevel}...");}[WButton("⏸ Pause Game", groupName: "Actions")]privatevoidPauseGame(){Time.timeScale=0f;}[WButton("🔄 Restart Level", colorKey: "Danger", groupName: "Actions")]privatevoidRestartLevel(){// Restart logic here}// Maintenance group - explicitly placed at bottom, renders last in bottom section due to groupPriority: 10[WButton("Clear Cache", historyCapacity: 1, groupName: "Maintenance", groupPriority: 10, groupPlacement: WButtonGroupPlacement.Bottom)]privatestringClearCache(){return$"Cache cleared at {System.DateTime.Now:HH:mm:ss}";}}
// Use global setting (default: 5, configurable in UnityHelpersSettings)[WButton("Use Global")]privateintUseGlobal()=>Random.Range(1,100);// Custom capacity per method[WButton("Keep 20 Results", historyCapacity: 20)]privatefloatKeepMany()=>Random.value;// Disable history (0 capacity)[WButton("No History", historyCapacity: 0)]privatevoidNoHistory()=>Debug.Log("No history stored");
Global Setting:UnityHelpersSettings.WButtonHistorySize (default: 5, range: 1-10)
// ✅ GOOD: Action-oriented, descriptive[WButton("Heal to Full")]privatevoidHealToFull(){...}[WButton("Spawn 10 Enemies")]privatevoidSpawnEnemies(){...}// ❌ BAD: Vague or technical[WButton("DoStuff")]privatevoidDoStuff(){...}[WButton]// Defaults to method name "HandlePlayerDeath"privatevoidHandlePlayerDeath(){...}
// ✅ GOOD: History helps track random values[WButton("Roll Loot", historyCapacity: 10)]privatestringRollLoot(){returnlootTable[PRNG.Instance.Next(0,lootTable.Length)];}// ✅ GOOD: No history needed for fixed actions[WButton("Reset Position", historyCapacity: 0)]privatevoidResetPosition(){transform.position=Vector3.zero;}
// ✅ GOOD: Accept CancellationToken, check it[WButton("Long Task")]privateasyncTaskLongTaskAsync(CancellationTokenct){for(inti=0;i<100;i++){ct.ThrowIfCancellationRequested();awaitTask.Delay(100,ct);}}// ✅ GOOD: Handle exceptions gracefully[WButton("Risky Operation")]privateasyncTaskRiskyOperationAsync(){try{awaitSomeRiskyApiCall();}catch(Exceptionex){Debug.LogError($"Operation failed: {ex.Message}");}}// ❌ BAD: Long operation with no cancellation support[WButton("Infinite Loop")]privateasyncTaskInfiniteLoopAsync(){while(true)// No way to stop this!{awaitTask.Delay(1000);}}
All Unity Objects (MonoBehaviour, ScriptableObject, etc.)
Odin Inspector's SerializedMonoBehaviour and SerializedScriptableObject (when ODIN_INSPECTOR is defined)
When do you need WButtonEditorHelper?
Only when you create custom Odin editors that override the default behavior. For example, if you create a CustomEditor that inherits from OdinEditor for a specific type, you'll need to integrate WButton manually.
#if UNITY_EDITOR && ODIN_INSPECTORusingSirenix.OdinInspector.Editor;usingUnityEditor;usingWallstopStudios.UnityHelpers.Editor.Utils.WButton;// Custom editor for a specific type[CustomEditor(typeof(MyComponent))]publicclassMyComponentEditor:OdinEditor{privateWButtonEditorHelper_wButtonHelper;protectedoverridevoidOnEnable(){base.OnEnable();_wButtonHelper=newWButtonEditorHelper();}publicoverridevoidOnInspectorGUI(){// Draw WButtons at top (optional - based on your settings)_wButtonHelper.DrawButtonsAtTop(this);// Draw Odin inspectorbase.OnInspectorGUI();// Draw WButtons at bottom and process any invocations_wButtonHelper.DrawButtonsAtBottomAndProcessInvocations(this);}}#endif
#if UNITY_EDITORusingUnityEditor;usingUnityEngine;usingWallstopStudios.UnityHelpers.Editor.Utils.WButton;[CustomEditor(typeof(MyComponent))]publicclassMyComponentEditor:Editor{privateWButtonEditorHelper_wButtonHelper;privatevoidOnEnable(){_wButtonHelper=newWButtonEditorHelper();}publicoverridevoidOnInspectorGUI(){serializedObject.Update();// Draw WButtons at top_wButtonHelper.DrawButtonsAtTop(this);// Draw your custom inspectorDrawDefaultInspector();serializedObject.ApplyModifiedProperties();// Draw WButtons at bottom and process invocations_wButtonHelper.DrawButtonsAtBottomAndProcessInvocations(this);}}#endif
publicoverridevoidOnInspectorGUI(){// Your inspector code hereDrawDefaultInspector();// Draw all buttons at the end_wButtonHelper.DrawAllButtonsAndProcessInvocations(this);}