I was working on my game Quadrapus Sumo and I wanted to be able to tweak a setting without having to recompile the game but I also didn’t want to create a user interface for that setting. I needed a developer console finally. What’s a developer console? It’s a way for developers to hide some levers and dials under the rug of their game’s standard user interface. It’s more meant for developers and beta testers than players.

CLI Development Consoles

I looked through the Unity Asset Store for developer consoles, tried what was free, bought a fair number, but none really gave me what I wanted. Special mention for Mike “@mikelovesrobots” Judge’s unity3d-console which was recommended by William Chyr. It exemplifies the Command Line Interface (CLI) approach for Quake-like developer consoles. Had I not decided to start from scratch, unity3d-console is where I would have started.

A Different Kind of Development Console

But what I wanted, dear reader, was not a CLI experience but an Emacs-like experience. I wanted a minibuffer from which I could quickly invoke commands. I wanted tab completion everywhere. I wanted an interface that was inspectable and discoverable. And it didn’t exist, so I decided to try and build it and issued the following tweet.

  • Goals
    • Emacs-like minibuffer
    • C# native
    • Tab completion where possible
    • Can execute commands
      • Solicit user for any parameters
    • Can manipulate variables
    • Create UI by merely decorating code with [Command] and [Prompt]
    • Disappears when not in use
  • Anti-goals
    • Doesn’t do everything
    • Just one window
    • No modes
    • Not an editor!
  • Deviations from Emacs (not strictly goals though)
    • The list of keymaps is fixed but they may be toggled on or off.

Making Minibuffer

Progress went quickly for Minibuffer in the beginning. This is why greenfield projects are so alluring to me; they’re the antithesis of “finishing” instead of diminishing returns you have initially increasing returns. It didn’t take long till I was running commands with tab completion, even for Unity builtin types. I thought I’d be finished in short order! “A couple weeks or so,” I thought. Note the date of these tweets.

Making minibuffer work with methods that took multiple arguments was an order of magnitude more difficult than one argument basically because it exposed a concurrency problem: How do you wait for input from the user without blocking? I was happy to find a C# Promise library that helped me break the problem down, and I imagine I’ll be using promises more in my future projects. Promises are a good pattern for making asynchronous pieces of code composable.

Try Minibuffer

Try a WebGL demo of Minibuffer v0.1.5 here. There’s more information available about how to use Minibuffer in Minibuffer. Just hit ‘Ctrl-h’ then ’t’, or 'C-h t’, for a tutorial. The same tutorial is provided below.

Don’t forget to try the Cubefetti!


Minibuffer Tutorial

This is an early build of Minibuffer, a developer console for Unity. It’s inspired by Emacs but stripped down to little more than, well, a minibuffer, which is a specialized prompt for user input. This tutorial will show you the basics of how to use and extend Minibuffer. To get back to this tutorial, hit 'Ctrl-h’ then ’t’ at any time.

Run a Command

To run a command, hit 'Alt-x’ referred to as ’M-x’ in Emacs and also in Minibuffer. A prompt will appear. Type 'hello’ then hit 'return’. It will ask for a name. You can provide your name or type 'World’ then hit 'return’. You should now see a message in the echo area that says, "Hello, $name!” Congratulations! That’s your first command of many! To dismiss the message, hit 'Ctrl-g’ or 'C-g’. To dismiss minibuffer, hit 'Ctrl-g’ or 'C-g’ again. (Vi users may be happy to know that they can hit ’:’ to bring up a prompt and 'escape’ instead of 'C-g’. You can of course add your own key bindings as well.)

Type in 'describe-command’ and hit 'return’. This will ask for a command to describe. Type 'hello’ and hit return. It will show the following.

hello is not bound to any keys.
Description: "Say hello and show off other completers"
It is defined in class MyCommands.
  Void Hello(String str)

Tab Complete ALL THE THINGS!

If it’s possible to tab complete a thing, Minibuffer tab completes it. Let’s try it. Hit ’M-x’ then hit 'tab’. This will show the list of available commands. You can scroll up or down the list with your mouse or you can hit 'Ctrl-v’ (or 'C-v’) to scroll down and 'Alt-v’ (or ’M-v’) to scroll up.

Type ’d’ then hit 'tab’. This will show all the commands that begin with d. Type 'es’ then 'tab’ to auto complete 'describe-’ then hit 'b tab’ to complete the describe-bindings command then hit return. This runs the describe-bindings command, which shows all of the key bindings that are currently set. This will report that you can also run the describe-bindings command by hitting 'Ctrl-h b’ or 'C-h b’.

Key Bindings

There are other commands bound to other keys. You can inspect them by running the command ’M-x describe-bindings’ or hitting 'Ctrl-h b’ or 'C-h b’. You may wonder why this tutorial repeats the key bindings in two similar but different ways, e.g., 1) 'Ctrl-h b’ and 2) 'C-h b’. That’s because Minibuffer uses a notation that will be familiar to Emacs users but may not be familiar to everyone. It’s a simple and concise way to refer to key chord sequences.

Key Notation

Minibuffer uses Emacs’ notation for key chords. It’s biggest difference from the standard is calling the 'Alt’ key 'Meta’ and using one character to identify modifiers. In addition, the 'Command’ key on macOS and 'Windows’ key on Windows is called the 'Super’ key.

Standard Emacs Pronounced Meaning
Alt-x M-x Meta X Hold down 'alt’ then hit 'x’.
Ctrl-v C-v Control V Hold down 'control’ then hit 'v’.
Alt-Shift-c M-S-c Meta Shift C Hold down 'alt’ and 'shift’ then hit 'c’.
Command-f s-f Super F Hold down 'command’ then hit 'f’.
Win-f s-f Super F Hold down 'win’ then hit 'f’.
Ctrl-c c C-c c Control C, C Hold down 'control’ then hit 'c’
then release 'control’ then hit 'c’.

Extending Minibuffer

Create a Command

Let’s create a hello-world command for Minibuffer. Create a new MonoBehaviour script in Unity called 'MyCommands.cs’. Add the following code:

  [Command("hello-world")]
  public string HelloWorld() {
    return "Hello, World!";
  }

That’s it. You can run it with ’M-x hello-world’ now. It will print “Hello, World!” to minibuffer’s echo area.

Since this section of the tutorial may have you stopping and starting Unity, you may want to read this file in your browser. Hit ’M-x open-in-browser’ to do that.

Accept an Argument

What if we want our command to accept an argument? We can do that quite simply.

  [Command("hello-world2")]
  public string HelloWorld2(string name) {
    return "Hello, " + name + "!";
  }

This works, but what if we want to enable tab completion?

Tab Completion

If we have an adhoc list of completions that aren’t too long, we can add them using the Prompt attribute.

  [Command("hello-world3")]
  public string HelloWorld3([Prompt("Name: ",
                              completions = new string[]
                                { "John", "Sean", "Shane"})]
                            string name) {
    return "Hello, " + name + "!";
  }

This works but it’s a very limited form of tab completion. We can specify what completer should be used. Perhaps we’d like to say hello to one of our commands, we could do the following:

  [Command("hello-world4")]
  public string HelloWorld4([Prompt("Command name: ",
                              completer = "command")]
                            string name) {
    return "Hello, " + name + "!";
  }

Often times though, one need not supply any meta data using the Prompt attribute at all. Often the type has enough information to infer a completer. For instance, we could say hello to a GameObject, or a Texture, a Material, or many others. To see all the completers, run ’M-x describe-completers’. Remember you can come back to this tutorial by hitting 'C-h t’.

  [Command("hello-world5")]
  public string HelloWorld5(GameObject gameObject) {
    return "Hello, " + gameObject.Name + "!";
  }

Don't miss the next article!