Unity Helpers

Logo

Treasure chest of Unity developer tools. Professional inspector tooling, high-performance utilities, spatial queries, and 20+ editor tools.

Inspector Buttons (WButton)

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.


Table of Contents


Basic Usage

using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

public class PlayerController : MonoBehaviour
{
    public int health = 100;

    [WButton("Heal Player")]
    private void Heal()
    {
        health = 100;
        Debug.Log("Player healed!");
    }

    [WButton("Take Damage")]
    private void TakeDamage()
    {
        health -= 10;
        Debug.Log($"Player took damage! Health: {health}");
    }
}

Parameters

The [WButton] attribute accepts several optional parameters to customize button appearance, behavior, and organization.

Parameter Overview

[WButton(
    string displayName = null,
    int drawOrder = 0,
    int historyCapacity = WButtonAttribute.UseGlobalHistory,
    string colorKey = null,
    string groupName = null,
    int groupPriority = WButtonAttribute.NoGroupPriority,
    WButtonGroupPlacement groupPlacement = WButtonGroupPlacement.UseGlobalSetting
)]

displayName (string, optional)

Controls the text shown on the button in the inspector.

// Without displayName - shows "SpawnEnemy"
[WButton]
private void SpawnEnemy() { }

// With displayName - shows "🔫 Spawn Enemy"
[WButton("🔫 Spawn Enemy")]
private void SpawnEnemy1() { }

// More descriptive labels
[WButton("Reset Player to Checkpoint")]
private void ResetPlayer() { }

[WButton("Clear All Save Data")]
private void ClearSaveData() { }

Side-by-side comparison showing method names vs custom display names


drawOrder (int, optional)

Controls the sort order of buttons within their placement section (top or bottom).

public class PlayerController1 : MonoBehaviour
{
    public int health = 100;
    public float speed = 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)]
    private void Initialize() { }

    [WButton("Validate", drawOrder: 0)]
    private void Validate() { }

    [WButton("Debug Info", drawOrder: 1)]
    private void ShowDebugInfo() { }
}

Visual layout:

Inspector showing buttons sorted by draw order


historyCapacity (int, optional)

Controls how many previous results are stored for methods that return values.

// Use global setting (default: 5 results)
[WButton("Roll Dice")]
private int RollDice() => Random.Range(1, 7);

// Store only last result
[WButton("Get Timestamp", historyCapacity: 1)]
private string GetTimestamp() => DateTime.Now.ToString();

// Store up to 10 results for detailed history
[WButton("Measure Frame Time", historyCapacity: 10)]
private float MeasureFrameTime() => Time.deltaTime * 1000f;

// Disable history completely (1 = minimum)
[WButton("Ping Server", historyCapacity: 1)]
private async Task<string> PingServerAsync(CancellationToken ct)
{
    // ... ping logic
    return "Pong!";
}

History panels showing different capacity settings

Performance tip: Use lower values (1-3) for methods called frequently to reduce memory usage.


colorKey (string, optional)

Applies custom color themes to buttons using predefined color keys.

// Default blue button
[WButton("Standard Action")]
private void StandardAction() { }

// Custom themed button (requires setup in project settings)
[WButton("Dangerous Action", colorKey: "Danger")]
private void DangerousAction() { }

[WButton("Success Action", colorKey: "Success")]
private void SuccessAction() { }

[WButton("Warning Action", colorKey: "Warning")]
private void WarningAction() { }

Buttons with different color themes

Setting up custom colors:

  1. Open Edit → Project Settings → Unity Helpers
  2. Navigate to WButton Color Palettes
  3. Add a new color key (e.g., "Danger")
  4. Set background/text colors
  5. Use the key in your [WButton] attribute

Button color configuration via Unity Helper Settings

Project settings showing color palette configuration


groupName (string, optional)

Organizes buttons under labeled group headers.

public class GameManager : MonoBehaviour
{
    // "Debug Tools" group - will appear based on groupPlacement or global settings
    [WButton("Log State", groupName: "Debug Tools", groupPlacement: WButtonGroupPlacement.Top)]
    private void LogState() { }

    [WButton("Clear Console", groupName: "Debug Tools")]
    private void ClearConsole() { }

    // Inspector properties here
    public int currentLevel = 1;
    public bool debugMode = false;

    // "Save System" group - explicitly placed at bottom
    [WButton("Save Game", groupName: "Save System", groupPlacement: WButtonGroupPlacement.Bottom)]
    private void SaveGame() { }

    [WButton("Load Game", groupName: "Save System")]
    private void LoadGame() { }

    [WButton("Delete Save", groupName: "Save System")]
    private void DeleteSave() { }
}

Inspector showing grouped buttons with headers

Notes:


groupPriority (int, optional)

Controls the render order of button groups within a placement section.

public class ActionPanel : MonoBehaviour
{
    // This group renders FIRST (priority 0)
    [WButton("Quick Save", groupName: "Primary", groupPriority: 0)]
    private void QuickSave() { }

    [WButton("Quick Load", groupName: "Primary", groupPriority: 0)]
    private void QuickLoad() { }

    // This group renders SECOND (priority 10)
    [WButton("Debug Info", groupName: "Debug", groupPriority: 10)]
    private void ShowDebugInfo() { }

    // This group renders LAST (no explicit priority)
    [WButton("Reset", groupName: "Misc")]
    private void Reset() { }
}

Groups ordered by priority: Primary first, Debug second, Misc last

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.

// ⚠️ WARNING: Conflicting priorities in the same group
[WButton("Action A", groupName: "Tools", groupPriority: 0)]  // This priority is used
private void ActionA() { }

[WButton("Action B", groupName: "Tools", groupPriority: 10)] // Ignored! Warning shown
private void ActionB() { }

groupPlacement (WButtonGroupPlacement, optional)

Controls where a button group renders, overriding the global Unity Helpers setting.

public class MixedPlacementExample : MonoBehaviour
{
    public int health = 100;
    public float speed = 5f;

    // This group ALWAYS renders at the top, regardless of global setting
    [WButton("Initialize", groupName: "Setup", groupPlacement: WButtonGroupPlacement.Top)]
    private void Initialize() { }

    [WButton("Validate", groupName: "Setup", groupPlacement: WButtonGroupPlacement.Top)]
    private void Validate() { }

    // Properties appear here (health, speed)

    // This group ALWAYS renders at the bottom, regardless of global setting
    [WButton("Cleanup", groupName: "Maintenance", groupPlacement: WButtonGroupPlacement.Bottom)]
    private void Cleanup() { }

    [WButton("Reset All", groupName: "Maintenance", groupPlacement: WButtonGroupPlacement.Bottom)]
    private void ResetAll() { }
}

Setup group at top, properties in middle, Maintenance group at bottom

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.


Combining groupPriority and groupPlacement

Use both parameters together for fine-grained control:

using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

[CreateAssetMenu(
    fileName = "AdvancedButtonLayout",
    menuName = "Wallstop Studios/Advanced Button Layout"
)]
public class AdvancedButtonLayout : ScriptableObject
{
    // TOP SECTION - ordered by priority
    [WButton(
        "Validate Data",
        groupName: "Validation",
        groupPriority: 1,
        groupPlacement: WButtonGroupPlacement.Top
    )]
    private void ValidateData() { }

    [WButton(
        "Generate IDs",
        groupName: "Authoring",
        groupPriority: 0,
        groupPlacement: WButtonGroupPlacement.Top
    )]
    private void GenerateIds() { }

    // Properties appear here

    public int property1;
    public string property2;

    // BOTTOM SECTION - ordered by priority

    [WButton(
        "Submit to Server",
        groupName: "Network",
        groupPriority: 10,
        groupPlacement: WButtonGroupPlacement.Bottom
    )]
    private void Submit() { }

    [WButton(
        "Export",
        groupName: "IO",
        groupPriority: 0,
        groupPlacement: WButtonGroupPlacement.Bottom
    )]
    private void Export() { }

    [WButton(
        "Import",
        groupName: "IO",
        groupPriority: 0,
        groupPlacement: WButtonGroupPlacement.Bottom
    )]
    private void Import() { }
}

Advanced layout: Authoring then Validation at top, IO then Network at bottom

Rendering Order:

  1. Top Section (sorted by groupPriority):
    • Authoring (priority 0)
    • Validation (priority 1)
  2. Default Inspector Properties
  3. Bottom Section (sorted by groupPriority):
    • IO (priority 0)
    • Network (priority 10)

Complete Example

using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

public class LevelManager : MonoBehaviour
{
    public int currentLevel = 1;
    public bool debugMode = false;

    // Setup group - explicitly placed at top, renders first due to groupPriority: 0
    [WButton("Initialize Level", groupName: "Setup", groupPriority: 0, groupPlacement: WButtonGroupPlacement.Top)]
    private void Initialize()
    {
        Debug.Log("Level initialized!");
    }

    [WButton("âś“ Validate Configuration", groupName: "Setup")]
    private void ValidateConfig()
    {
        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)]
    private int RollDice() => Random.Range(1, 7);

    [WButton("🎯 Spawn Test Enemy", colorKey: "Warning", groupName: "Debug")]
    private void SpawnTestEnemy()
    {
        // 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)]
    private void StartLevel()
    {
        Debug.Log($"Starting level {currentLevel}...");
    }

    [WButton("⏸ Pause Game", groupName: "Actions")]
    private void PauseGame()
    {
        Time.timeScale = 0f;
    }

    [WButton("🔄 Restart Level", colorKey: "Danger", groupName: "Actions")]
    private void RestartLevel()
    {
        // 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)]
    private string ClearCache()
    {
        return $"Cache cleared at {System.DateTime.Now:HH:mm:ss}";
    }
}

Full inspector showing all parameter features working together


Execution Types

WButton supports four method signatures:

1. Void Methods (Immediate)

[WButton("Log Message")]
private void LogMessage()
{
    Debug.Log("Button clicked!");
}

Behavior: Executes immediately, no return value shown


2. Returning Values (With History)

[WButton("Roll Dice", historyCapacity: 10, groupName: "Debug")]
private int RollDice()
{
    return Random.Range(1, 7);
}

[WButton("Get Position")]
private Vector3 GetPlayerPosition()
{
    return transform.position;
}

Button with result history showing multiple dice rolls: 4, 2, 6, 1, 5

Clicking "Roll Dice" and seeing new result added to history

Behavior: Shows return value in a collapsible history panel


3. Coroutines (IEnumerator)

[WButton("Fade Out")]
private IEnumerator FadeOut()
{
    SpriteRenderer sprite = GetComponent<SpriteRenderer>();
    Color color = sprite.color;

    for (float t = 1f; t >= 0f; t -= Time.deltaTime)
    {
        color.a = t;
        sprite.color = color;
        yield return null;
    }

    Debug.Log("Fade complete!");
}

Button showing spinner while coroutine executes, then "Complete" status

Behavior:


4. Async Methods (Task / ValueTask)

using System.Threading;
using System.Threading.Tasks;

[WButton("Load Data")]
private async Task<string> LoadDataAsync(CancellationToken ct)
{
    Debug.Log("Loading...");
    await Task.Delay(2000, ct);  // Simulate async work
    return "Data loaded successfully!";
}

[WButton("Download Asset")]
private async ValueTask<Texture2D> DownloadAssetAsync(CancellationToken ct)
{
    Debug.Log("Downloading...");
    await Task.Delay(1000, ct);
    // Simulate download
    return new Texture2D(256, 256);
}

Clicking button, showing spinner, then result appearing

Behavior:

Cancellation Example:

[WButton("Long Operation")]
private async Task LongOperationAsync(CancellationToken ct)
{
    for (int i = 0; i < 10; i++)
    {
        ct.ThrowIfCancellationRequested();  // Check cancellation
        Debug.Log($"Step {i + 1}/10");
        await Task.Delay(500, ct);
    }
    return Task.CompletedTask;
}

Supported Signatures:


Result History

Automatic History

[WButton("Generate ID", historyCapacity: 10)]
private string GenerateId()
{
    return System.Guid.NewGuid().ToString().Substring(0, 8);
}

History panel showing last 10 generated IDs

Features:


History Capacity Options

// Use global setting (default: 5, configurable in UnityHelpersSettings)
[WButton("Use Global")]
private int UseGlobal() => Random.Range(1, 100);

// Custom capacity per method
[WButton("Keep 20 Results", historyCapacity: 20)]
private float KeepMany() => Random.value;

// Disable history (0 capacity)
[WButton("No History", historyCapacity: 0)]
private void NoHistory() => Debug.Log("No history stored");

Global Setting: UnityHelpersSettings.WButtonHistorySize (default: 5, range: 1-10)


Draw Order & Positioning

Control the sort order of buttons within their placement section:

public class ButtonPositioning : MonoBehaviour
{
    // Buttons are sorted by drawOrder (lower values first)
    [WButton("Top Button", drawOrder: -1)]
    private void TopButton() => Debug.Log("Renders first (lowest drawOrder)");

    [WButton("Middle Button", drawOrder: 0)]
    private void MiddleButton() => Debug.Log("Renders second");

    [WButton("Bottom Button", drawOrder: 1)]
    private void BottomButton() => Debug.Log("Renders last (highest drawOrder)");

    // Inspector fields - buttons appear above or below based on groupPlacement/global settings
    public int someField = 10;
}

Inspector showing button order

Positioning Rules:


Pagination by Draw Order

[WButton(drawOrder: 0)]
private void Action1() {}

[WButton( drawOrder: 0)]
private void Action2() {}

// ... 10 more buttons with drawOrder: 0 ...

[WButton(drawOrder: 0)]
private void Action12() {}

Button pagination controls showing "Page 1 of 2" with navigation buttons

Pagination Settings:


Grouping

Organize buttons into named sections:

[WButton("Spawn Enemy", groupName: "Combat")]
private void SpawnEnemy() => Debug.Log("Enemy spawned");

[WButton("Clear Enemies", groupName: "Combat")]
private void ClearEnemies() => Debug.Log("Enemies cleared");

[WButton("Save Game", groupName: "Persistence")]
private void SaveGame() => Debug.Log("Game saved");

[WButton("Load Game", groupName: "Persistence")]
private void LoadGame() => Debug.Log("Game loaded");

Two button groups with headers "Combat" and "Persistence"

Grouping Behavior:


Foldout Behavior

Global Setting: UnityHelpersSettings.WButtonFoldoutBehavior

Options:

Animation:

Button group folding/unfolding with smooth animation


Color Theming

[WButton("Dangerous Action", colorKey: "Default-Dark")]
private void DangerousAction() => Debug.LogWarning("Dangerous!");

[WButton("Safe Action", colorKey: "Default-Light")]
private void SafeAction() => Debug.Log("Safe operation");

Two buttons with different color themes (default dark/light)

Built-in Priorities (Color Keys):

Define Custom Colors:

  1. Open ProjectSettings/UnityHelpersSettings.asset
  2. Add entry to WButtonCustomColors dictionary
  3. Set a button background, text color, border

UnityHelpersSettings showing WButton custom color configuration


Configuration

Global Settings

All buttons respect project-wide settings defined in UnityHelpersSettings:

Location: ProjectSettings/UnityHelpersSettings.asset

Settings:

UnityHelpersSettings showing all WButton configuration options


Best Practices

1. Clear Button Names

// âś… GOOD: Action-oriented, descriptive
[WButton("Heal to Full")]
private void HealToFull() { ... }

[WButton("Spawn 10 Enemies")]
private void SpawnEnemies() { ... }

// ❌ BAD: Vague or technical
[WButton("DoStuff")]
private void DoStuff() { ... }

[WButton]  // Defaults to method name "HandlePlayerDeath"
private void HandlePlayerDeath() { ... }

// âś… GOOD: Grouped by feature
[WButton("Spawn Enemy", groupName: "Combat Testing")]
private void SpawnEnemy() { ... }

[WButton("Kill All Enemies", groupName: "Combat Testing")]
private void KillAll() { ... }

[WButton("Save Progress", groupName: "Persistence")]
private void Save() { ... }

// ❌ BAD: No grouping, cluttered inspector
[WButton("Spawn Enemy")]
private void SpawnEnemy() { ... }

[WButton("Save Progress")]
private void Save() { ... }

[WButton("Kill All Enemies")]
private void KillAll() { ... }

3. Use History for Random/Variable Results

// âś… GOOD: History helps track random values
[WButton("Roll Loot", historyCapacity: 10)]
private string RollLoot()
{
    return lootTable[PRNG.Instance.Next(0, lootTable.Length)];
}

// âś… GOOD: No history needed for fixed actions
[WButton("Reset Position", historyCapacity: 0)]
private void ResetPosition()
{
    transform.position = Vector3.zero;
}

4. Async Best Practices

// âś… GOOD: Accept CancellationToken, check it
[WButton("Long Task")]
private async Task LongTaskAsync(CancellationToken ct)
{
    for (int i = 0; i < 100; i++)
    {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(100, ct);
    }
}

// âś… GOOD: Handle exceptions gracefully
[WButton("Risky Operation")]
private async Task RiskyOperationAsync()
{
    try
    {
        await SomeRiskyApiCall();
    }
    catch (Exception ex)
    {
        Debug.LogError($"Operation failed: {ex.Message}");
    }
}

// ❌ BAD: Long operation with no cancellation support
[WButton("Infinite Loop")]
private async Task InfiniteLoopAsync()
{
    while (true)  // No way to stop this!
    {
        await Task.Delay(1000);
    }
}

5. Color Usage

// âś… GOOD: Use colors to indicate risk/importance
[WButton("Delete All Data", colorKey: "Default-Dark")]  // Dark = danger
private void DeleteAllData() { ... }

[WButton("Quick Save", colorKey: "Default-Light")]  // Light = safe
private void QuickSave() { ... }

// ❌ BAD: Random colors without meaning
[WButton("Log Message", colorKey: "CustomPurple")]  // Why purple?
private void LogMessage() { ... }

Examples

Example 1: Gameplay Testing

using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

public class PlayerDebug : MonoBehaviour
{
    public int health = 100;
    public int gold = 0;

    [WButton("Heal", groupName: "Health", colorKey: "Default-Light")]
    private void Heal()
    {
        health = 100;
        Debug.Log("Player healed!");
    }

    [WButton("Take Damage", groupName: "Health")]
    private void TakeDamage()
    {
        health -= 25;
        Debug.Log($"Took damage! Health: {health}");
    }

    [WButton("Kill Player", groupName: "Health", colorKey: "Default-Dark")]
    private void Kill()
    {
        health = 0;
        Debug.LogWarning("Player died!");
    }

    [WButton("Add Gold", groupName: "Economy")]
    private void AddGold()
    {
        gold += 100;
        Debug.Log($"Gold: {gold}");
    }

    [WButton("Roll Reward", groupName: "Economy", historyCapacity: 10)]
    private int RollReward()
    {
        int amount = PRNG.Instance.Next(10, 100);
        gold += amount;
        return amount;
    }
}

PlayerDebug inspector with Health and Economy button groups


Example 2: Async Data Loading

using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
using WallstopStudios.UnityHelpers.Core.Attributes;

public class DataManager : MonoBehaviour
{
    [WButton("Load Player Data")]
    private async Task<string> LoadPlayerDataAsync(CancellationToken ct)
    {
        Debug.Log("Loading player data...");

        // Simulate network request
        await Task.Delay(2000, ct);

        string playerName = "TestPlayer_" + Random.Range(1000, 9999);
        Debug.Log($"Loaded: {playerName}");

        return playerName;
    }

    [WButton("Batch Load", historyCapacity: 5)]
    private async Task<int> BatchLoadAsync(CancellationToken ct)
    {
        int count = 0;
        for (int i = 0; i < 10; i++)
        {
            ct.ThrowIfCancellationRequested();
            await Task.Delay(200, ct);
            count++;
            Debug.Log($"Loaded item {count}/10");
        }
        return count;
    }
}

Async button showing loading spinner, then result


Example 3: Procedural Generation Testing

using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

public class LevelGenerator : MonoBehaviour
{
    [WButton("Generate Seed", historyCapacity: 20)]
    private int GenerateSeed()
    {
        return Random.Range(1000, 9999);
    }

    [WButton("Generate Level")]
    private void GenerateLevel()
    {
        int seed = Random.Range(1000, 9999);
        Random.InitState(seed);
        Debug.Log($"Generating level with seed: {seed}");
        // ... generation logic ...
    }

    [WButton("Clear Level", colorKey: "Default-Dark")]
    private void ClearLevel()
    {
        // ... cleanup logic ...
        Debug.Log("Level cleared");
    }

    [WButton("Generate with Seed")]
    private void GenerateWithSeed(int seed)
    {
        Random.InitState(seed);
        Debug.Log($"Generating with seed: {seed}");
        // ... generation logic ...
    }
}

Level Generator script showing that WButton supports parameters


Example 4: Coroutine Animation Testing

using System.Collections;
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

public class AnimationTester : MonoBehaviour
{
    public SpriteRenderer spriteRenderer;

    private void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    [WButton("Fade Out", groupName: "Animations")]
    private IEnumerator FadeOutCoroutine()
    {
        Color color = spriteRenderer.color;
        while (color.a > 0f)
        {
            color.a -= Time.deltaTime;
            spriteRenderer.color = color;
            yield return null;
        }
        Debug.Log("Fade out complete");
    }

    [WButton("Fade In", groupName: "Animations")]
    private IEnumerator FadeInCoroutine()
    {
        Color color = spriteRenderer.color;
        while (color.a < 1f)
        {
            color.a += Time.deltaTime;
            spriteRenderer.color = color;
            yield return null;
        }
        Debug.Log("Fade in complete");
    }

    [WButton("Pulse", groupName: "Animations")]
    private IEnumerator PulseCoroutine()
    {
        Vector3 originalScale = transform.localScale;
        for (int i = 0; i < 3; i++)
        {
            transform.localScale = originalScale * 1.2f;
            yield return new WaitForSeconds(0.2f);
            transform.localScale = originalScale;
            yield return new WaitForSeconds(0.2f);
        }
        Debug.Log("Pulse complete");
    }
}

Animation buttons triggering visual effects


Using WButton with Custom Editors

Overview

WButton works automatically with:

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.


Automatic Integration with Odin Inspector

No setup required! WButton automatically works with Odin’s base types:

#if ODIN_INSPECTOR
using Sirenix.OdinInspector;
#endif
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;

#if ODIN_INSPECTOR
public class MyComponent : SerializedMonoBehaviour
#else
public class MyComponent : MonoBehaviour
#endif
{
    [WButton("Test Button")]
    private void TestMethod()
    {
        Debug.Log("Button clicked!");
    }
}

WButton will appear automatically in the inspector - no additional code needed!


Integration with Custom Odin Editors

If you create a custom Odin editor for your type, you need to manually integrate WButton using WButtonEditorHelper:

#if UNITY_EDITOR && ODIN_INSPECTOR
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using WallstopStudios.UnityHelpers.Editor.Utils.WButton;

// Custom editor for a specific type
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : OdinEditor
{
    private WButtonEditorHelper _wButtonHelper;

    protected override void OnEnable()
    {
        base.OnEnable();
        _wButtonHelper = new WButtonEditorHelper();
    }

    public override void OnInspectorGUI()
    {
        // Draw WButtons at top (optional - based on your settings)
        _wButtonHelper.DrawButtonsAtTop(this);

        // Draw Odin inspector
        base.OnInspectorGUI();

        // Draw WButtons at bottom and process any invocations
        _wButtonHelper.DrawButtonsAtBottomAndProcessInvocations(this);
    }
}
#endif

Integration with Standard Custom Editors

For standard Unity custom editors:

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using WallstopStudios.UnityHelpers.Editor.Utils.WButton;

[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
    private WButtonEditorHelper _wButtonHelper;

    private void OnEnable()
    {
        _wButtonHelper = new WButtonEditorHelper();
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        // Draw WButtons at top
        _wButtonHelper.DrawButtonsAtTop(this);

        // Draw your custom inspector
        DrawDefaultInspector();

        serializedObject.ApplyModifiedProperties();

        // Draw WButtons at bottom and process invocations
        _wButtonHelper.DrawButtonsAtBottomAndProcessInvocations(this);
    }
}
#endif

WButtonEditorHelper API

The WButtonEditorHelper class provides several methods for different use cases:

Method Description
DrawButtonsAtTop(Editor) Draws buttons configured for top placement
DrawButtonsAtBottom(Editor) Draws buttons configured for bottom placement
ProcessInvocations() Processes any triggered button invocations
DrawButtonsAtBottomAndProcessInvocations(Editor) Convenience method combining bottom drawing + processing (most common)
DrawAllButtonsAndProcessInvocations(Editor) Draws all buttons in one location regardless of placement settings

Key Points:

  1. Create one WButtonEditorHelper instance per editor (typically in OnEnable)
  2. Call DrawButtonsAtTop before your inspector content
  3. Call DrawButtonsAtBottomAndProcessInvocations after your inspector content
  4. Always call ProcessInvocations() after all button drawing is complete

Single Location Button Drawing

If you prefer all buttons in one location regardless of placement settings:

public override void OnInspectorGUI()
{
    // Your inspector code here
    DrawDefaultInspector();

    // Draw all buttons at the end
    _wButtonHelper.DrawAllButtonsAndProcessInvocations(this);
}

Troubleshooting

Button Not Appearing

Problem: Method has [WButton] but button doesn’t show

Solutions:

  1. Ensure method is private or protected (public methods may conflict)
  2. Verify the component is enabled and active

History Not Showing

Problem: Method returns a value but no history appears

Solutions:

  1. Check historyCapacity - make sure it’s > 0
  2. Verify that the return type is serializable
  3. Check UnityHelpersSettings.WButtonHistorySize if using global setting

Async Method Not Cancelling

Problem: Cancel button doesn’t stop an async method

Solutions:

  1. Ensure method accepts CancellationToken parameter
  2. Check token periodically: ct.ThrowIfCancellationRequested()
  3. Pass token to async operations: await Task.Delay(1000, ct)
// âś… CORRECT: Cancellable
[WButton("Long Task")]
private async Task LongTaskAsync(CancellationToken ct)
{
    for (int i = 0; i < 100; i++)
    {
        ct.ThrowIfCancellationRequested();  // Check token
        await Task.Delay(100, ct);  // Pass token
    }
}

See Also


Next Steps: