In-game debug menu system for Unity that uses attributes and UI Toolkit to create developer-friendly debug commands.
- Attribute-Based: Simple
[DevMenu]attribute to mark debug methods - Hotkey Support: Assign keyboard shortcuts to debug methods
- UI Toolkit Integration: Modern, responsive UI built with UI Toolkit
- Parameter Support: Full support for all parameter types (int, float, bool, enum, string)
- Editor Integration: Works in both Play Mode and Edit Mode
- Project Agnostic: Designed as a Unity package for easy distribution
- Import into Unity via Package Manager (
Add package from diskβ¦and pick this package'spackage.json). - Mark methods with the
[DevMenu]attribute on anyMonoBehaviourin your scene. - Use it:
- In the editor, open
DevMenu > Open Dev Menufrom the menu bar. - In Play Mode, hotkeys work automatically β a listener is injected on start, no setup required.
- For an in-game UI panel, add a
UIDocumentplus theDevMenuUIControllercomponent (the UXML needs aScrollViewnamedmethod-scroll).
- In the editor, open
In the Package Manager, select this package and click Samples > DevMenu Sample > Import. It adds a single DevMenuSample MonoBehaviour you can drop on an empty GameObject to see actions, typed parameters, validation, and change events in action.
using DevMenuTool.Core.Attributes;
using UnityEngine;
public class GameDebugger : MonoBehaviour
{
[DevMenu(KeyCode.G, "Cheats")]
private void AddGold(int amount = 100)
{
Debug.Log($"Added {amount} gold!");
// This method will appear in the DevMenu with a hotkey
}
[DevMenu(KeyCode.H, "Player")]
private void SetPlayerHealth(int health = 100, bool invincible = false)
{
Debug.Log($"Set health to {health}, invincible: {invincible}");
// Parameters are stored and remembered between sessions
}
}- Runtime: Press the configured hotkey or use the UI
- Editor: Go to
DevMenu > Open Dev Menuin the menu bar. The window rescans automatically after script recompiles and when entering or exiting Play Mode; use the toolbar Refresh button after loading scenes or spawning objects at runtime.
Pass only a group name, or KeyCode.None, to list a method in the menu without binding a hotkey:
[DevMenu("World")] // menu-only
private void RespawnEnemies() { }
[DevMenu(KeyCode.None, "World")] // equivalent
private void ClearDebris() { }[DevMenu] methods are discovered on every MonoBehaviour in the loaded scenes, including inactive
objects, DontDestroyOnLoad objects, and objects hidden with HideFlags such as HideAndDontSave.
Prefabs and other assets are not scanned. Only instance methods are supported.
- Parameter Storage: Each
DevMethodstores its own parameter values - Automatic Persistence: Values are saved automatically while playing β to
EditorPrefsin the editor and toPlayerPrefsin player builds (hotkey rebinds use the same store) - Smart Resolution: UI updates stored values, hotkeys use stored values
- Reset Functionality: Easy reset to default values
graph TD
A[DevMethod Created] --> B[Load from prefs store]
B --> C{Values Found?}
C -->|Yes| D[Use Stored Values]
C -->|No| E[Use Default Values]
D --> F[UI Shows Current Values]
E --> F
F --> G[User Modifies Values]
G --> H[Update Stored Parameters]
H --> I[Save to prefs store]
I --> J[Method Invocation]
J --> K[Use Stored Values]
| Action | Parameter Source | Behavior |
|---|---|---|
| UI Button | Current UI field values | Updates stored parameters, then invokes |
| Hotkey | Stored parameter values | Uses last-saved values, no UI interaction |
[DevMenu(KeyCode.F1, "Debug")]
private void LogPlayerPosition()
{
var player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
Debug.Log($"Player position: {player.transform.position}");
}
[DevMenu(KeyCode.F2, "Debug")]
private void ShowGameStats()
{
Debug.Log($"FPS: {1.0f / Time.deltaTime:F1}");
Debug.Log($"Memory: {System.GC.GetTotalMemory(false) / 1024 / 1024}MB");
}[DevMenu(KeyCode.G, "Cheats")]
private void AddGold(int amount = 100)
{
// amount will be stored and remembered
PlayerGold += amount;
Debug.Log($"Added {amount} gold! Total: {PlayerGold}");
}
[DevMenu(KeyCode.H, "Player")]
private void SetPlayerStats(int health = 100, float speed = 5.0f, bool invincible = false)
{
// All parameters are stored independently
playerHealth = health;
playerSpeed = speed;
playerInvincible = invincible;
Debug.Log($"Set stats: Health={health}, Speed={speed}, Invincible={invincible}");
}public enum GameState { Menu, Playing, Paused, GameOver }
[DevMenu(KeyCode.J, "Game")]
private void SetGameState(GameState state = GameState.Playing)
{
currentGameState = state;
Debug.Log($"Game state set to: {state}");
}The DevMenu tool follows SOLID principles and Clean Code practices:
DevMethod: Represents a debug method with parameter storageMethodScanner: Discovers methods with[DevMenu]attributesMethodInvoker: Handles method invocation with parameter resolutionUIBuilder: Creates UI elements for method interactionDevMenuContainer: Composition root that wires and exposes the shared services
- Factory Pattern:
DevMethodFactorycreatesDevMethodinstances and wires their collaborators - Strategy Pattern:
ParameterHandlerRegistryhandles different parameter types - Dependency Injection: collaborators (
IParameterManager,IParameterValidatorManager,IParameterEventManager,IParameterHandlerRegistry) are passed in through constructors;DevMenuContaineris the composition root that wires them together
DevMenuTool(all platforms): core, runtime, UI, and persistence. Contains no unguardedUnityEditorreferences, so it compiles into player builds.DevMenuTool.Editor(Editor platform only): theDevMenu > Open Dev Menuwindow. It is stripped from player builds automatically.
- Interface Segregation:
IDevMethodis composed from focused role interfaces (IDevMethodInfo,IDevMethodExecutor,IParameterManager,IParameterValidatorManager,IParameterChangeNotifier) rather than a single fat interface. - Dependency Inversion:
DevMethoddepends on collaborator abstractions injected via its constructor instead ofnew-ing concretes, and parameter handling is resolved through theIParameterHandlerRegistryabstraction instead of a static lookup.
Tests are not shipped with the package. They live in the development project (DevMenuTool/Assets/Tests/) so consumers stay clean of test code and NUnit references. To run them, open the development project and use Window > General > Test Runner.
com.routaz.devmenu/
βββ Core/ # Core interfaces and implementations
β βββ Interfaces/ # Interface definitions
β βββ DevMethod.cs # Enhanced DevMethod with parameter storage
β βββ MethodScanner.cs # Method discovery
β βββ DevMenuContainer.cs # Composition root
βββ Runtime/ # Runtime functionality
β βββ MethodInvoker.cs # Method invocation
β βββ Handlers/ # Parameter type handlers
β βββ Persistence/ # Prefs-backed parameter & hotkey storage (EditorPrefs / PlayerPrefs)
β βββ DevMenuHotkeyListener.cs
βββ UI/ # UI Toolkit components
β βββ UIBuilder.cs # UI construction
β βββ DevMenuUIController.cs
βββ Editor/ # Editor-only assembly (DevMenuTool.Editor.asmdef)
β βββ DevMenuEditorWindow.cs
βββ Samples~/ # Importable sample (via Package Manager)
βββ DevMenuSample/
βββ DevMenuSample.cs
Parameter parsing/formatting is handled by IParameterHandler strategies. The built-in set
covers string, numeric, bool, and enum types. To support an additional type, implement
IParameterHandler:
public class Vector3ParameterHandler : IParameterHandler
{
public bool CanHandle(Type parameterType) => parameterType == typeof(Vector3);
public object Parse(string input, Type targetType)
{
// Parse "x,y,z" format
var parts = input.Split(',');
return new Vector3(float.Parse(parts[0]), float.Parse(parts[1]), float.Parse(parts[2]));
}
public string ToString(object value)
{
var vector = (Vector3)value;
return $"{vector.x},{vector.y},{vector.z}";
}
}Note: handlers are currently registered in the
ParameterHandlerRegistryconstructor. Add your handler to that list to enable it β there is no runtime registration API yet.
Validation is declarative: annotate parameters with validation attributes and the DevMenu wires them up automatically when the method is discovered. There is no need to hand-write guard clauses inside the method body.
using DevMenuTool.Core.Attributes;
[DevMenu(KeyCode.G, "Cheats")]
private void AddGold([ParameterRange(0, 9999)] int amount = 100)
{
// Only reached with a valid amount; no manual checks required.
PlayerGold += amount;
Debug.Log($"Added {amount} gold!");
}
[DevMenu(KeyCode.N, "Player")]
private void SetPlayerName([StringValidation(minLength: 2, maxLength: 16)] string name = "Hero")
{
playerName = name;
}Built-in attributes:
| Attribute | Applies to | Purpose |
|---|---|---|
[ParameterRange(min, max, inclusive = true)] |
numeric | Value must fall within the range |
[StringValidation(minLength, maxLength, pattern, allowEmpty)] |
string | Length / regex / emptiness rules |
[Required] |
string | Value must be non-null and non-empty |
When an entered value fails validation the edit is rejected and the last valid value is
kept β the parameter is never silently overwritten with the default. Add your own rule by
implementing IParameterValidator and a matching ParameterValidationAttribute.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ for Unity developers