Unity is an awesome development engine. I formed that opinion back in 2012 when I first gave it a try (and during the sleepless night that followed), and I still believe it to be true. It’s not flawless though - sooner or later you discover an aspect or functionality that just confounds you. In contrast to what is normal, things suddenly become… unwieldy, raw, malformed. For me such an aspect is handling player input (keyboard, mouse, and gamepad events).
You see, Unity doesn’t have a nice subsystem here that you’d somehow expect. There’s just a low-level API (Input class with static methods), which appears to have been there since the early Bronze Age, and is about as sophisticated. If you read and follow the documentation, it promptly guides you down the easy road to hell called Bad Design. I mean, multiple component scripts each polling Input status every frame, handling the results independently (and unaware) of each other doesn’t bode well for any non-trivial project.
Almost as if to taunt you, there’s the event-system for working with uGUI (the “new” UI system released with Unity 4.6). Once you break through the initial confusion (as the documentation here suddenly stops being helpful and informative), you’ll find this to be a nice, well designed framework, easy to use for both trivial and advanced input operations alike. Alas, this framework really only covers GUI, leaving you empty-handed with everything else your project needs to handle (WASD/arrows keys for camera movement, mouse clicks to designate target, Escape to bring up game menu, etc.).
So, how does one approach this? One problem at a time of course. Just like eating an elephant.
Let’s start with the lack of awareness between scripts. Say you want a key (Spacebar) to do different things in different situation: if the player character is walking around in the game world, he should stop moving; if he is in a dialogue with an NPC, Spacebar should be the hotkey for Continue button and if you’re typing a text (chat command in a multiplayer game, naming your savegame, etc.), space should not do anything else. Or for example the Escape key: depending on what’s visible on screen, it might close Inventory, bring up game menu, or do something else entirely.
A good solution allows scripts to indicate their interest in a specific Input event, mark the event status (has someone “used up” that event already?) and establish priority order when it comes to choosing who handles the Input event. The natural design pattern for this would include a Singleton input manager class with Observers subscribing to input events. Observers of a specific event can then be ordered by their priority (Chain of Responsibility pattern). In addition to actual Input event data, event parameters can include a flag (boolean) for tracking the “used” status.
GameInputManager.cs
using UnityEngine; using System.Collections.Generic; public class GameInputManager : MonoBehaviour { #region Singleton pattern protected static GameInputManager singleton; public static GameInputManager Singleton { get { if (singleton==null) singleton = FindObjectOfType<GameInputManager>(); return singleton; } } #endregion #region Input event parameter public class EventData { public string axis = null; public string button = null; public KeyCode keyCode = KeyCode.None; public bool used = false; public float value = 0f; public EventData(KeyCode keyCode) { this.keyCode = keyCode; } public EventData(string axis, float value) { this.axis = axis; this.value = value; } public EventData(string button) { this.button = button; } } #endregion public const int MAX_PRIORITY = 10000; #region Public static methods (API) /// <summary>Register an axis as one of interest.</summary> public static void ObserveAxis(string axis) { if (!string.IsNullOrEmpty(axis) && Singleton) Singleton.observedAxes.Add(axis); } /// <summary>Register a button as one of interest.</summary> public static void ObserveButton(string button) { if (!string.IsNullOrEmpty(button) && Singleton) Singleton.observedButtons.Add(button); } /// <summary>Register a keycode as one of interest.</summary> public static void ObserveKeyCode(KeyCode keyCode) { if (keyCode!=KeyCode.None && Singleton) Singleton.observedKeycodes.Add(keyCode); } /// <summary>Register a handler method for hotkey event with one above currently highest priority.</summary> /// <param name="Action">Handler method that is called when hotkey event triggers. That method has one EventData parameter.</param> public static void Register(System.Action<EventData> Action) { if (Action!=null && Singleton!=null) Singleton.GetBlock(Singleton.highestPriority + 1).Event += Action; } /// <summary>Register a handler method for hotkey event with the specified priority.</summary> /// <param name="Action">Handler method that is called when hotkey event triggers. That method has one EventData parameter.</param> /// <param name="priority">Callbacks are made in order of priority (from the highest to the lowest).</param> public static void Register(System.Action<EventData> Action, int priority) { if (Action!=null && Singleton!=null) Singleton.GetBlock(priority).Event += Action; } /// <summary>Unregister a callback method from all Input events.</summary> public static void Unregister(System.Action<EventData> Action) { if (Action!=null && Singleton!=null) foreach (EventBlock b in Singleton.eventBlocks) b.Event -= Action; } #endregion #region Unity magic methods protected void Awake() { singleton = this; } protected void Update() { foreach (string a in observedAxes) { SendEvent(new EventData(a, Input.GetAxis(a))); } foreach (string b in observedButtons) { if (Input.GetButtonDown(b)) SendEvent(new EventData(b)); } foreach (KeyCode k in observedKeycodes) { if (Input.GetKeyDown(k)) SendEvent(new EventData(k)); } } #endregion #region Internals (under the hood) protected class EventBlock : System.IComparable<EventBlock> { public int priority; public event System.Action<EventData> Event; public EventBlock(int p) { priority = p; } public void AppendTo(ref System.Action<EventData> deleg) { if (Event!=null) deleg += Event; } // Order highest to lowest public int CompareTo(EventBlock other) { return -priority.CompareTo(other.priority); } public void Invoke(EventData eventData) { if (Event!=null) Event(eventData); } public bool IsEmpty { get { return Event==null; } } } protected List<EventBlock> eventBlocks = new List<EventBlock>(); protected HashSet<string> observedAxes = new HashSet<string>(); protected HashSet<string> observedButtons = new HashSet<string>(); protected HashSet<KeyCode> observedKeycodes = new HashSet<KeyCode>(); protected EventBlock GetBlock(int priority) { foreach (EventBlock b in eventBlocks) if (b.priority==priority) return b; EventBlock newBlock = new EventBlock(priority); eventBlocks.Add(newBlock); eventBlocks.Sort(); return newBlock; } protected int highestPriority { get { // eventBlocks is always sorted in reversed priority order (i.e., highest to lowest), so first non-empty block is the correct result foreach (EventBlock b in eventBlocks) if (b.priority<MAX_PRIORITY && !b.IsEmpty) return b.priority; return 0; } } protected void SendEvent(EventData data) { System.Action<EventData> callStack = null; foreach (EventBlock block in eventBlocks) block.AppendTo(ref callStack); if (callStack!=null) callStack(data); } #endregion }
Observer scripts would then look like this:
DemoInputObserver.cs
using UnityEngine; public class DemoInputObserver : MonoBehaviour { #region Unity magic methods protected void OnEnable() { GameInputManager.ObserveKeyCode(KeyCode.Space); GameInputManager.ObserveKeyCode(KeyCode.Escape); GameInputManager.ObserveAxis("Horizontal"); GameInputManager.Register(OnInputEvent); } protected void OnDisable() { GameInputManager.Unregister(OnInputEvent); } #endregion #region Internals (under the hood) protected void OnInputEvent(GameInputManager.EventData data) { if (data.used) return; if (data.keyCode==KeyCode.Space) { Debug.Log("Spacebar was pressed"); data.used = true; } else if (data.keyCode==KeyCode.Escape) { Debug.Log("Escape was pressed"); data.used = true; } else if (data.axis=="Horizontal") { if (data.value!=0f) { Debug.Log("Horizontal axis = " + data.value.ToString()); } data.used = true; } } #endregion }
Note that if you attach this script to Game Objects multiple times, the Console will only show a single entry for each event. By default, priority (order of calling) is determined by the order that Unity enables components in scene (LIFO: last one to register receives highest priority). If you want to explicitly determine priority, the Register method has an appropriate override.
Alright, that’s enough for one post. Next time, I’ll show you what Reflection and Attributes can bring to this party.