The Spellbound Console System provides a powerful command-line interface for Unity games. It supports three types of commands:
- ICommand Classes - Self-contained command implementations (e.g.,
help,clear,list) - Utility Commands - Static methods that can be called directly (e.g.,
terraform_flatten) - Preset Commands - Methods that operate on ObjectPresets with specific modules (e.g.,
spawn sword)
- ✅ Automatic command discovery via attributes
- ✅ Type-safe parameter parsing (int, float, bool, Vector3, Vector2, enums, etc.)
- ✅ Command aliases
- ✅ Command history navigation (up/down arrows)
- ✅ Extensible architecture
- ✅ Clean separation of concerns
- Create a Canvas with a UI setup for the console
- Add
ConsoleControllercomponent - Assign required references:
inputField- TMP_InputField for user inputoutputText- TextMeshProUGUI for output displayscrollRect- ScrollRect for scrollingcontentContainer- RectTransform for content sizing
// In your input action asset or input manager:
consoleToggleAction.performed += consoleController.OnTogglePerformed;
consoleHistoryUpAction.performed += consoleController.OnHistoryUpPerformed;
consoleHistoryDownAction.performed += consoleController.OnHistoryDownPerformed;Open the console and try:
> help
> list
> clear
Best for: Standalone utility commands that don't need external context.
Examples: help, clear, list, custom help commands
Characteristics:
- Self-contained logic
- Support aliases
- Auto-registered via
[ConsoleCommandClass]attribute - Implement
ICommandinterface
Best for: Static helper methods, debug utilities, system commands.
Examples: Terrain manipulation, stats display, debug toggles
Characteristics:
- Must be static methods
- Decorated with
[ConsoleUtilityCommand] - No preset/target required
- Called directly:
commandname arg1 arg2
Best for: Commands that operate on game objects/presets.
Examples: spawn, delete, modify
Characteristics:
- Can be static or instance methods
- Decorated with
[ConsolePresetCommand] - Require a preset target:
commandname targetname [args] - Only called if preset has required module type
using Spellbound.Core.Console;
[ConsoleCommandClass("mycommand", "mc", "mycmd")] // Name + aliases
public class MyCommand : ICommand {
public string Name => "mycommand";
public string Description => "Does something useful";
public string Usage => "mycommand [arg]";
public CommandResult Execute(string[] args) {
if (args.Length == 0)
return CommandResult.Fail("Missing argument");
var result = DoSomething(args[0]);
return CommandResult.Ok($"Success: {result}");
}
private string DoSomething(string arg) {
return $"Processed {arg}";
}
}Usage in console:
> mycommand test
> mc test (alias)
> mycmd test (alias)
using Spellbound.Core.Console;
using UnityEngine;
public static class MyUtilities {
[ConsoleUtilityCommand("setgravity", "Sets physics gravity")]
public static void SetGravity(float x, float y, float z) {
Physics.gravity = new Vector3(x, y, z);
Debug.Log($"Gravity set to ({x}, {y}, {z})");
}
[ConsoleUtilityCommand("resetgravity", "Resets gravity to default")]
public static void ResetGravity() {
Physics.gravity = new Vector3(0, -9.81f, 0);
Debug.Log("Gravity reset to default");
}
[ConsoleUtilityCommand("timescale", "Sets time scale")]
public static void SetTimeScale(float scale = 1f) {
Time.timeScale = Mathf.Clamp(scale, 0f, 10f);
Debug.Log($"Time scale set to {Time.timeScale}");
}
}Usage in console:
> setgravity 0 -20 0
> resetgravity
> timescale 0.5
> timescale (uses default: 1)
using Spellbound.Core.Console;
using UnityEngine;
public class GameObjectSpawner : MonoBehaviour {
[ConsolePresetCommand("spawn", typeof(ConsoleModule))]
public void SpawnObject(string presetUid, Vector3 position, Quaternion rotation, float scale) {
var preset = presetUid.ResolvePreset();
// Your spawn logic here
var obj = Instantiate(preset.prefab, position, rotation);
obj.transform.localScale = Vector3.one * scale;
Debug.Log($"Spawned {preset.objectName} at {position}");
}
[ConsolePresetCommand("delete", typeof(ConsoleModule))]
public void DeleteObject(string presetUid) {
// Your delete logic here
Debug.Log($"Deleted {presetUid}");
}
}Setup:
- Your ObjectPreset must have a
ConsoleModuleattached - Set
autoRegister = trueon the ConsoleModule - Place the preset in a Resources folder
Usage in console:
> spawn sword
> spawn sword 5 (spawns 5 swords)
> delete sword
using System.Text;
using Spellbound.Core.Console;
[ConsoleCommandClass("terraform", "tf")]
public class TerraformHelpCommand : ICommand {
public string Name => "terraform";
public string Description => "List all terraform commands";
public string Usage => "terraform";
public CommandResult Execute(string[] args) {
// Get all utility commands from a specific class
var commands = AttributeCommandRegistry.GetUtilityCommandsByClass(typeof(TerrainSystem));
var sb = new StringBuilder();
sb.AppendLine("=== Terraform Commands ===");
sb.AppendLine();
foreach (var (commandName, description) in commands)
sb.AppendLine($"{commandName,-25} {description}");
return CommandResult.Ok(sb.ToString());
}
}CommandRegistry
├── Manages ICommand implementations
├── Routes commands to appropriate handlers
└── Provides ExecuteCommand() entry point
AttributeCommandRegistry
├── Discovers methods with [ConsoleUtilityCommand]
├── Discovers methods with [ConsolePresetCommand]
├── Handles method invocation and parameter parsing
└── Caches method instances
PresetResolver
├── Maps preset names to UIDs
├── Scans Resources for ObjectPresets with ConsoleModule
└── Provides TryResolvePresetUid()
ConsoleCommandRouter
├── Routes preset commands to handler methods
├── Checks module type requirements
├── Handles quantity multiplier
└── Provides execution context (position, rotation, etc.)
ConsoleController
├── UI management
├── Input handling
├── Command execution
└── Output display
User Input: "spawn sword 5"
↓
ConsoleController.ExecuteCommand()
↓
CommandRegistry.ExecuteCommand()
↓
[Checks ICommand] → Not found
↓
[Checks Utility Command] → Not found
↓
[Checks Preset Command] → Found!
↓
ConsoleCommandRouter.RouteCommand()
↓
PresetResolver.TryResolvePresetUid("sword")
↓
AttributeCommandRegistry.TryGetPresetHandler("spawn", typeof(ConsoleModule))
↓
Invokes method 5 times
↓
Returns success
Result object returned by all commands.
// Success
return CommandResult.Ok();
return CommandResult.Ok("Success message");
// Failure
return CommandResult.Fail("Error message");Static utility for printing to console from anywhere in your code.
ConsoleLogger.PrintToConsole("Info message");
ConsoleLogger.PrintError("Error message");
if (ConsoleLogger.IsInitialized) {
// Safe to use
}Query utility commands programmatically.
// Get all utility commands from a class
var commands = AttributeCommandRegistry.GetUtilityCommandsByClass(typeof(MyClass));
// Get by class name
var commands = AttributeCommandRegistry.GetUtilityCommandsByClassName("MyClass");
// Check if utility command exists
bool exists = AttributeCommandRegistry.HasUtilityCommand("mycommand");
// Check if preset command exists
bool exists = AttributeCommandRegistry.HasPresetCommand("spawn");Query registered presets.
// Resolve preset name to UID
if (PresetResolver.TryResolvePresetUid("sword", out string uid)) {
var preset = uid.ResolvePreset();
}
// Get all preset names
var names = PresetResolver.GetAllPresetNames();
// Get count
int count = PresetResolver.GetPresetCount();Utility and preset commands automatically parse these types:
stringintfloatdoublebool(supports: true/false, 1/0)byteVector3(consumes 3 args: x y z)Vector2(consumes 2 args: x y)List<T>(consumes all remaining args)- Any
enumtype
Example:
[ConsoleUtilityCommand("moveobject")]
public static void MoveObject(string name, Vector3 position, bool instant = false) {
// Called as: moveobject player 10 5 0 true
// name = "player"
// position = Vector3(10, 5, 0)
// instant = true
}Methods can use default parameter values:
[ConsoleUtilityCommand("damage")]
public static void ApplyDamage(float amount, string damageType = "physical") {
// damage 50 → amount=50, damageType="physical"
// damage 50 fire → amount=50, damageType="fire"
}Preset commands receive context from the system:
[ConsolePresetCommand("spawn", typeof(ConsoleModule))]
public void SpawnObject(string presetUid, Vector3 position, Quaternion rotation, float scale) {
// presetUid - automatically provided
// position - from crosshair raycast (configurable via ConsoleModule.spawnLocation)
// rotation - Quaternion.identity
// scale - 1.0f
}public CommandResult Execute(string[] args) {
try {
DoSomethingRisky();
return CommandResult.Ok("Success!");
}
catch (Exception ex) {
return CommandResult.Fail($"Failed: {ex.Message}");
}
}void Start() {
var console = FindObjectOfType();
console.OnVisibilityChanged += OnConsoleVisibilityChanged;
}
void OnConsoleVisibilityChanged(bool isVisible) {
if (isVisible) {
// Disable player input
playerInput.Disable();
} else {
// Re-enable player input
playerInput.Enable();
}
}// Register a command instance manually
var myCommand = new MyCommand();
CommandRegistry.Instance.Register(myCommand, "alias1", "alias2");
// Unregister
CommandRegistry.Instance.Unregister("mycommand");// Execute from code
var result = CommandRegistry.Instance.ExecuteCommand("help");
if (result.Success) {
Debug.Log(result.Message);
}- ICommand for standalone utilities (help, clear, stats display)
- Utility Commands for static helper methods (debug toggles, system commands)
- Preset Commands for object manipulation (spawn, delete, modify)
// ✅ Good
[ConsoleUtilityCommand("setfov", "Sets camera field of view (30-120)")]
// ❌ Bad
[ConsoleUtilityCommand("setfov", "Sets FOV")]// ✅ Good - allows: spawn sword OR spawn sword 5
public void Spawn(string presetUid, int quantity = 1)
// ❌ Bad - always requires quantity
public void Spawn(string presetUid, int quantity)[ConsoleUtilityCommand("setvolume")]
public static void SetVolume(float volume) {
volume = Mathf.Clamp01(volume); // ✅ Clamp to valid range
AudioListener.volume = volume;
}// ✅ Good - all audio commands in one class
public static class AudioCommands {
[ConsoleUtilityCommand("volume")] public static void SetVolume(float v) { }
[ConsoleUtilityCommand("mute")] public static void Mute() { }
[ConsoleUtilityCommand("unmute")] public static void Unmute() { }
}// ✅ Provide a way to discover related commands
[ConsoleCommandClass("audio")]
public class AudioHelpCommand : ICommand {
public CommandResult Execute(string[] args) {
var commands = AttributeCommandRegistry.GetUtilityCommandsByClass(typeof(AudioCommands));
// Format and return
}
}Problem: Utility command not found
Solution: Ensure method is static and has [ConsoleUtilityCommand] attribute
Problem: Preset command not routing
Solution:
- Check preset has
ConsoleModulewithautoRegister = true - Verify preset is in a Resources folder
- Ensure method has
[ConsolePresetCommand]with correct module type
Problem: Commands don't show up
Solution:
- Ensure
CommandRegistry.Instance.AutoRegisterCommands()is called (done in ConsoleController.Awake) - Check that AttributeCommandRegistry initializes before scene load
- Verify namespace is
Spellbound.Core.Console
Problem: "Invalid arguments" errors
Solution:
- Check parameter types are supported
- Ensure correct number of arguments
- Use correct format for Vector3/Vector2 (space-separated)
For questions, issues, or feature requests, please contact Spellbound Studio or post in the Discord.
Version: 1.0
Unity Version: 2021.3+
Dependencies: TextMeshPro, Unity Input System
Copyright 2025 Spellbound Studio Inc.