Making an In-Game Console in Unity Part 2

Eliot Lash

December 28th, 2015

In part 1, I started walking you through making a console using uGUI, Unity’s built-in GUI framework. I'm showing you how to implement a simple input parser. Let's continue where we left off here in Part 2.

We’re going to program the behavior of the console. I split this into a ConsoleController, which handles parsing and executing commands, and a view component that handles the communication between the ConsoleController and uGUI. This makes the parser code cleaner, and easier to switch to a different GUI system in the future if needed.

First, make a new script called ConsoleController. Completely delete its contents and replace them with the following class:

/// <summary>
/// Handles parsing and execution of console commands, as well as collecting log output.
/// Copyright (c) 2014-2015 Eliot Lash
/// </summary>
using UnityEngine;

using System;
using System.Collections.Generic;
using System.Text;

public delegate void CommandHandler(string[] args);

public class ConsoleController {
	
	#region Event declarations
	// Used to communicate with ConsoleView
	public delegate void LogChangedHandler(string[] log);
	public event LogChangedHandler logChanged;
	
	public delegate void VisibilityChangedHandler(bool visible);
	public event VisibilityChangedHandler visibilityChanged;
	#endregion

	/// <summary>
	/// Object to hold information about each command
	/// </summary>
	class CommandRegistration {
		public string command { get; private set; }
		public CommandHandler handler { get; private set; }
		public string help { get; private set; }
		
		public CommandRegistration(string command, CommandHandler handler, 
string help) { this.command = command; this.handler = handler; this.help = help; } } /// <summary> /// How many log lines should be retained? /// Note that strings submitted to appendLogLine with embedded newlines will
be counted as a single line. /// </summary> const int scrollbackSize = 20; Queue<string> scrollback = new Queue<string>(scrollbackSize); List<string> commandHistory = new List<string>(); Dictionary<string, CommandRegistration> commands = new Dictionary<string,
CommandRegistration>(); public string[] log { get; private set; } //Copy of scrollback as an array for
easier use by ConsoleView const string repeatCmdName = "!!"; //Name of the repeat command, constant since it needs
to skip these if they are in the command history public ConsoleController() { //When adding commands, you must add a call below to registerCommand() with its
name, implementation method, and help text. registerCommand("babble", babble, "Example command that demonstrates how to parse
arguments. babble [word]
[# of times to repeat]"); registerCommand("echo", echo, "echoes arguments back as array
(for testing argument parser)"); registerCommand("help", help, "Print this help."); registerCommand("hide", hide, "Hide the console."); registerCommand(repeatCmdName, repeatCommand, "Repeat last command."); registerCommand("reload", reload, "Reload game."); registerCommand("resetprefs", resetPrefs, "Reset & saves PlayerPrefs."); } void registerCommand(string command, CommandHandler handler, string help) { commands.Add(command, new CommandRegistration(command, handler, help)); } public void appendLogLine(string line) { Debug.Log(line); if (scrollback.Count >= ConsoleController.scrollbackSize) { scrollback.Dequeue(); } scrollback.Enqueue(line); log = scrollback.ToArray(); if (logChanged != null) { logChanged(log); } } public void runCommandString(string commandString) { appendLogLine("$ " + commandString); string[] commandSplit = parseArguments(commandString); string[] args = new string[0]; if (commandSplit.Length < 1) { appendLogLine(string.Format("Unable to process command '{0}'",
commandString)); return; } else if (commandSplit.Length >= 2) { int numArgs = commandSplit.Length - 1; args = new string[numArgs]; Array.Copy(commandSplit, 1, args, 0, numArgs); } runCommand(commandSplit[0].ToLower(), args); commandHistory.Add(commandString); } public void runCommand(string command, string[] args) { CommandRegistration reg = null; if (!commands.TryGetValue(command, out reg)) { appendLogLine(string.Format("Unknown command '{0}',
type 'help' for list.", command)); } else { if (reg.handler == null) { appendLogLine(string.Format("Unable to process command '{0}',
handler was null.", command)); } else { reg.handler(args); } } } static string[] parseArguments(string commandString) { LinkedList<char> parmChars = new LinkedList<char>(commandString.ToCharArray()); bool inQuote = false; var node = parmChars.First; while (node != null) { var next = node.Next; if (node.Value == '"') { inQuote = !inQuote; parmChars.Remove(node); } if (!inQuote && node.Value == ' ') { node.Value = '\ n'; } node = next; } char[] parmCharsArr = new char[parmChars.Count]; parmChars.CopyTo(parmCharsArr, 0); return (new string(parmCharsArr)).Split(new char[] {'\ n'} ,
StringSplitOptions.RemoveEmptyEntries); } #region Command handlers //Implement new commands in this region of the file. /// <summary> /// A test command to demonstrate argument checking/parsing. /// Will repeat the given word a specified number of times. /// </summary> void babble(string[] args) { if (args.Length < 2) { appendLogLine("Expected 2 arguments."); return; } string text = args[0]; if (string.IsNullOrEmpty(text)) { appendLogLine("Expected arg1 to be text."); } else { int repeat = 0; if (!Int32.TryParse(args[1], out repeat)) { appendLogLine("Expected an integer for arg2."); } else { for(int i = 0; i < repeat; ++i) { appendLogLine(string.Format("{0} {1}", text, i)); } } } } void echo(string[] args) { StringBuilder sb = new StringBuilder(); foreach (string arg in args) { sb.AppendFormat("{0},", arg); } sb.Remove(sb.Length - 1, 1); appendLogLine(sb.ToString()); } void help(string[] args) { foreach(CommandRegistration reg in commands.Values) { appendLogLine(string.Format("{0}: {1}", reg.command, reg.help)); } } void hide(string[] args) { if (visibilityChanged != null) { visibilityChanged(false); } } void repeatCommand(string[] args) { for (int cmdIdx = commandHistory.Count - 1; cmdIdx >= 0; --cmdIdx) { string cmd = commandHistory[cmdIdx]; if (String.Equals(repeatCmdName, cmd)) { continue; } runCommandString(cmd); break; } } void reload(string[] args) { Application.LoadLevel(Application.loadedLevel); } void resetPrefs(string[] args) { PlayerPrefs.DeleteAll(); PlayerPrefs.Save(); } #endregion }

I’ve tried to comment where appropriate, but I’ll give you a basic rundown of this class. It maintains a registry of methods that are mapped to string command names, as well as associated help text. This allows the “help” command to print out all the available commands along with extra info on each one. It keeps track of the output scrollback as well as the history of user-entered commands (this is to aid implementation of bash-style command history paging, which is left as an exercise to the reader. Although I have implemented a simple command, ‘!!’ which repeats the most recent command.)

When the view receives command input, it passes it to runCommandString() which calls parseArguments() to perform some rudimentary string parsing using a space as a delimiter. It then calls runCommand() which tries to look up the corresponding method in the command registration dictionary, and if it finds it, calling it with the remaining arguments. Commands can call appendLogLine() to write to the in-game console log, and of course execute arbitrary code.

Moving on, we will implement the view. Attach a new script to the ConsoleView object (the parent of ConsoleViewContainer) and call it ConsoleView. Replace its contents with the following:

/// <summary>
/// Marshals events and data between ConsoleController and uGUI.
/// Copyright (c) 2014-2015 Eliot Lash
/// </summary>
using UnityEngine;
using UnityEngine.UI;
using System.Text;
using System.Collections;

public class ConsoleView : MonoBehaviour {
	ConsoleController console = new ConsoleController();
	
	bool didShow = false;

	public GameObject viewContainer; //Container for console view, should be a 
child of this GameObject public Text logTextArea; public InputField inputField; void Start() { if (console != null) { console.visibilityChanged += onVisibilityChanged; console.logChanged += onLogChanged; } updateLogStr(console.log); } ~ConsoleView() { console.visibilityChanged -= onVisibilityChanged; console.logChanged -= onLogChanged; } void Update() { //Toggle visibility when tilde key pressed if (Input.GetKeyUp("`")) { toggleVisibility(); } //Toggle visibility when 5 fingers touch. if (Input.touches.Length == 5) { if (!didShow) { toggleVisibility(); didShow = true; } } else { didShow = false; } } void toggleVisibility() { setVisibility(!viewContainer.activeSelf); } void setVisibility(bool visible) { viewContainer.SetActive(visible); } void onVisibilityChanged(bool visible) { setVisibility(visible); } void onLogChanged(string[] newLog) { updateLogStr(newLog); } void updateLogStr(string[] newLog) { if (newLog == null) { logTextArea.text = ""; } else { logTextArea.text = string.Join("\ n", newLog); } } /// <summary> /// Event that should be called by anything wanting to submit the
current input to the console. /// </summary> public void runCommand() { console.runCommandString(inputField.text); inputField.text = ""; } }

The ConsoleView script manages the GUI and forwards events to the ConsoleController. It also watches the ConsoleController and updates the GUI when necessary.

Back in the inspector, select ConsoleView. We’re going to hook up the Console View component properties. Drag ConsoleViewContainer into the “View Container” property. Do the same for LogText into “Log Text Area” and InputField into “Input Field.”

i

Now we’ve just got a bit more hooking up to do. Select InputField, and in the Input Field component, find the “End Edit” event list. Click the plus button and drag ConsoleView in to the new row. In the function list, select ConsoleView > runCommand ().

Finally, select EnterBtn and find the “On Click” event list in the Button component. Click the plus button and drag ConsoleView in to the new row. In the function list, select ConsoleView > onCommand ().

Now we’re ready to test! Save your scene and run the game. The console should be visible. Type “help” into the input field:

Now press the enter/return key. You should see the help text print out like so:

Try out another test command, “echo foo bar baz”. It will show you how it splits the command arguments into a string array, printed out as a comma separated list:

Also make sure the fallback “Ent” button is working to submit the input. Lastly, check if the console toggle key works: press the backtick/tilde key (located right above the left tab key.) It looks like this:

The console should disappear. Press it again and it should reappear. On a mobile device, tapping five fingers at once will toggle the console instead. If you want to use a different means of toggling the console, you can edit this in ConsoleView.Update().

If anything is not working as expected, please go back over the instructions again and try to see if you’ve missed anything.

Lastly, we don’t want the console to show when the game first starts. Stop the game and find ConsoleViewContainer in the hierarchy, then disable it by unchecking the box next to its name in the inspector.

Now, save and run the game again. The console should be hidden until you press the backtick key.

And that’s it! You now have an in-game, interactive console. It’s an extremely versatile debugging tool that’s easy to extend. Use it to implement cheat codes, enable or disable experimental features, obtain diagnostic output, or whatever else you can think of! When you want to create a new console command, just write a new method in ConsoleController “Command handlers” region and add a registerCommand() line for it in the constructor. Use the commands I’ve included as examples.

If you want to have other scripts be able to log to the console, you can make the ConsoleController into a service as I described in my article “One-liner Singletons in Unity”. Make the ConsoleController set itself as a service in its constructor, Then have the other script get the ConsoleController instance and call appendLogLine() with its message.

I hope having an in-game console will be as useful for you as it has been for me. Lastly, don’t forget to disable or delete the ConsoleView before shipping production builds unless you want your players to have access to all of your debug cheats!

About the author

Eliot Lash is an independent game developer and consultant with several mobile titles in the works. In the past, he has worked on Tiny Death Star and the Tap Tap Revenge series. You can find him at eliotlash.com.

Get your free eBook today

Continue on your journey as a game developer by deciding which game engine and language suits you best with the help of our free eBook.