“Mirror, mirror on the wall, what’s the ugliest code of ’em all?”
The answer might very well be the annoying amount of technical code we need to implement in our components. Luckily, it annoyed me enough to start pondering about a better way. Specifically, about using attributes and reflection to hide all that ugliness inside a base class.
In the magical world of unicorns and rainbows, I want my Observer component to look pretty:
DemoInputObserver2.cs
using UnityEngine; public class DemoInputObserver2 : BaseComponent { [InputKey(KeyCode.Space)] protected void OnSpacebar() { Debug.Log("Spacebar was pressed"); } [InputKey(KeyCode.Escape, priority = 10001)] protected void OnEscape() { Debug.Log("Escape was pressed"); } [InputAxis("Horizontal")] protected void OnHorizontal(float value) { Debug.Log("Horizontal axis = " + value.ToString()); } }
The idea is to use custom attributes to establish usage context: what type of input do we want to handle, and values for relevant parameters (KeyCode, name of the axis, handling priority, etc.). We rely on a common base class to discover those attributes and register thus marked methods with input manager.
Implementation-wise, starting with simple things, the custom attributes themselves are quite straight-forward:
InputAttribute.cs
using UnityEngine; public abstract class InputAttribute : System.Attribute { protected int? _priority; public bool hasPriority { get { return _priority!=null; } } public int priority { get { return _priority!=null ? (int)_priority : 0; } set { _priority = value; } } public InputAttribute() { _priority = null; } } /// <summary>Attaching this attribute to a method marks it as HotKeyEventData handler. That method should have a single HotKeyEventData type argument.</summary> [System.AttributeUsage(System.AttributeTargets.Method)] public class InputEventDataAttribute : InputAttribute { public InputEventDataAttribute() : base() {} } /// <summary>Attaching this attribute to a method marks it as Input.GetAxis value handler. That method should have a single float type argument.</summary> [System.AttributeUsage(System.AttributeTargets.Method)] public class InputAxisAttribute : InputAttribute { public string axis { get; set; } public bool IsAxis(string a) { return !string.IsNullOrEmpty(a) && string.Equals(axis, a); } public InputAxisAttribute(string axis) : base() { this.axis = axis; } } /// <summary>Attaching this attribute to a method marks it as Input.GetButtonDown event handler. That method should have no arguments.</summary> [System.AttributeUsage(System.AttributeTargets.Method)] public class InputButtonAttribute : InputAttribute { public string button { get; set; } public bool IsButton(string b) { return !string.IsNullOrEmpty(b) && string.Equals(button, b); } public InputButtonAttribute(string button) : base() { this.button = button; } } /// <summary>Attaching this attribute to a method marks it as Input.GetKeyDown event handler. That method should have no arguments.</summary> [System.AttributeUsage(System.AttributeTargets.Method)] public class InputKeyAttribute : InputAttribute { public virtual KeyCode keyCode { get; set; } public InputKeyAttribute(KeyCode keyCode) : base() { this.keyCode = keyCode; } }
Reflection also turns out to be easy to use. The base class that you inherit from can be as simple as:
using UnityEngine; using System.Reflection; public class BaseComponent : MonoBehaviour { protected void InitManagerAgents() { MemberInfo[] members = this.GetType().GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); foreach (MemberInfo m in members) { System.Attribute[] attribs = System.Attribute.GetCustomAttributes(m, false); foreach (System.Attribute a in attribs) { //if (a is UpdateAttribute) UpdateManager.Register(this, m, a as UpdateAttribute); if ((a is InputAttribute) && (m is MethodInfo)) InputManager.Register(this, m as MethodInfo, a as InputAttribute); } } //if (this is InputManager.IKeyHandler) InputManager.Register(this as InputManager.IKeyHandler); } protected virtual void Awake() { InitManagerAgents(); } }
Virtually all of the complexity is left for InputManager to handle. All we’re providing is reference to self, MethodInfo (which has Invoke method for callback), and InputAttribute providing context.
Lo and behold, the magic module.
InputManager.cs
[Persistent, SelfSpawning(name="InputManager")] public class InputManager : SingletonComponent<InputManager> { public interface IKeyHandler { KeyCode inputKey { get; } void OnInputKey(); } public interface IPriority { int inputPriority { get; } } 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; } } /// <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 HotKeyEventData 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 HotKeyEventData 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; } public static void Register(IKeyHandler handler) { if (handler!=null && Singleton!=null) Singleton.AddAgent(new KeyHandlerAgent(handler)); } public static void Register(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputAttribute attribute) { if (methodInfo==null || !parent || !Singleton) return; if (attribute is InputEventDataAttribute) { Singleton.AddAgent(new EventDataAgent(parent, methodInfo, attribute as InputEventDataAttribute)); } else if (attribute is InputAxisAttribute) { Singleton.AddAgent(new AxisAgent(parent, methodInfo, attribute as InputAxisAttribute)); } else if (attribute is InputButtonAttribute) { Singleton.AddAgent(new ButtonAgent(parent, methodInfo, attribute as InputButtonAttribute)); } else if (attribute is InputKeyAttribute) { Singleton.AddAgent(new KeyCodeAgent(parent, methodInfo, attribute as InputKeyAttribute)); } } /// <summary>Unregister a callback method from all timer events.</summary> public static void Unregister(System.Action<EventData> Action) { if (Action!=null && Singleton!=null) foreach (EventBlock b in Singleton.eventBlocks) b.Event -= Action; } protected abstract class EventHandlerAgent : Agent { protected System.Action<EventData> Action; public EventHandlerAgent(Behaviour parent) : base(parent) {} public override void Dispose() { if (Action!=null) Unregister(Action); Action = null; base.Dispose(); } } protected class EventDataAgent : EventHandlerAgent { public EventDataAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputEventDataAttribute attribute) : base(parent) { if (IsFinished) return; Action = (x => { if (!IsFinished && parent.isActiveAndEnabled) methodInfo.Invoke(parent, new object[] { x }); }); if (attribute!=null && attribute.hasPriority) Register(Action, attribute.priority); else Register(Action); } } protected class AxisAgent : EventHandlerAgent { public AxisAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputAxisAttribute attribute) : base(parent) { if (IsFinished) return; Action = (x => { if (!IsFinished && !x.used && attribute.IsAxis(x.axis) && parent.isActiveAndEnabled) { object res = methodInfo.Invoke(parent, new object[] { x.value }); if (res is bool) x.used = (bool)res; else x.used = true; } }); ObserveAxis(attribute.axis); if (attribute.hasPriority) Register(Action, attribute.priority); else Register(Action); } } protected class ButtonAgent : EventHandlerAgent { public ButtonAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputButtonAttribute attribute) : base(parent) { if (IsFinished) return; Action = (x => { if (!IsFinished && !x.used && attribute.IsButton(x.button) && parent.isActiveAndEnabled) { object res = methodInfo.Invoke(parent, null); if (res is bool) x.used = (bool)res; else x.used = true; } }); ObserveButton(attribute.button); if (attribute.hasPriority) Register(Action, attribute.priority); else Register(Action); } } protected class KeyCodeAgent : EventHandlerAgent { public KeyCodeAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputKeyAttribute attribute) : base(parent) { if (IsFinished) return; Action = (x => { if (!IsFinished && !x.used && x.keyCode!=KeyCode.None && x.keyCode==attribute.keyCode && parent.isActiveAndEnabled) { object res = methodInfo.Invoke(parent, null); if (res is bool) x.used = (bool)res; else x.used = true; } }); ObserveKeyCode(attribute.keyCode); if (attribute.hasPriority) Register(Action, attribute.priority); else Register(Action); } } protected class KeyHandlerAgent : EventHandlerAgent { public KeyHandlerAgent(IKeyHandler handler) : base(handler as Behaviour) { if (IsFinished) return; Action = (x => { if (!IsFinished && !x.used && x.keyCode!=KeyCode.None && x.keyCode==handler.inputKey && parent.isActiveAndEnabled) { handler.OnInputKey(); x.used = true; } }); ObserveKeyCode(handler.inputKey); if (parent is IPriority) Register(Action, (parent as IPriority).inputPriority); else Register(Action); } } 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 AgentCollection agents = 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 bool AddAgent(Agent item) { if (agents==null) agents = new AgentCollection(); return agents.Add(item); } 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<10000 && !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); } protected void Update() { foreach (KeyCode k in observedKeycodes) { if (Input.GetKeyDown(k)) SendEvent(new EventData(k)); } 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)); } agents.Update(Time.deltaTime); } protected override void OnDestroy() { if (agents!=null) agents.Clear(); base.OnDestroy(); } }
As you can see, there’s a bit more going on here than I have revealed the code for. All that has to wait for Part 3, as time is up for today. Sorry!