using System; using System.Collections.Generic; using UnityEngine; using Framework.Services; using UnityInput = UnityEngine.Input; namespace Framework.Input { /// Maps input from any number of devices to actions /// /// /// Actions need to be created through the method /// before they can be used. It is a good idea to assign default key bindings /// at the start of your game (even if loading the player's settings overwrites /// these bindings again) to ensure the game never ends up in a state where /// the player has no usable controls. /// /// /// Consider adding another layer between your code and the input mapper instead /// of querying inputs directory in order to allow for unit testing with simulated /// inputs. The can collect input and package it in /// arbitrary state structures that you could mock in your tests. /// /// /// Basic usage example: /// /// /// /// /// IAction fireAction = this.myInputManager.CreateAction("Fire"); /// fireAction.BoundKeys.Add(KeyCode.Mouse0); /// fireAction.BoundKeys.Add(KeyCode.Joystick1Button); /// /// IAction forwardAction = this.myInputManager.CreateAction("Forward"); /// forwardAction.BoundKeys.Add(KeyCode.W); /// forwardAction.BoundJoystickAxes.Add("Joystick1_Axis1"); /// /// if(fireAction.IsActive) { /// fireWeapon(); /// } /// /// transform.Translate(0.0f, 0.0f, forwardAction.State, Space.Self); /// /// /// /// [ServiceScope(Scope.Global)] public class InputMapper : Service, IInputMapper, Action.IBindingObserver { #region class KeyMapping /// Stores the actions a key or button has been mapped to private class KeyMapping { /// Initializes a new key or button mapping public KeyMapping() { this.Actions = new List(); } /// Key or button that has been mapped public KeyCode Key; /// Actions the key or button has been mapped to public IList Actions; } #endregion // class KeyMapping #region class AxisMapping /// Stores the actions a joystick axis has been mapped to private class AxisMapping { /// Initializes a new joystick axis mapping public AxisMapping() { this.Actions = new List(); } /// Joystick axis that has been mapped public JoystickAxis Axis; /// Actions the joystick axis has been mapped to public IList Actions; } #endregion // class AxisMapping #region class ReferenceCountedAction /// Stores an action and the number of references to it private class ReferenceCountedAction { /// /// Initializes a new reference counted action starting with a reference count of 1 /// /// Action that will be reference counted public ReferenceCountedAction(Action action) { this.ReferenceCount = 1; this.Action = action; } /// Number of references that exist to this action public int ReferenceCount; /// Action that needs to be update public Action Action; } #endregion // class ReferenceCountedAction /// Initializes a new input mapper public InputMapper() { this.keyMappings = new List(); this.axisMappings = new List(); this.actionsToUpdate = new List(); this.actions = new ActionCollection(); this.actions.ActionsCleared += actionsCleared; this.actions.ActionRemoved += actionRemoved; } /// Actions that have been set up for binding inputs public IActionCollection Actions { get { return this.actions; } } /// Creates a new input action ready for binding /// Name of the action that will be created /// The newly created action /// /// If an action with the same name already exists, the existing action will /// be returned. /// public IAction CreateAction(string name) { return CreateAction(name, null); } /// Creates a new input action ready for binding /// Name of the action that will be created /// The newly created action /// /// If an action with the same name already exists, the existing action will /// be returned. /// public IAction CreateAction(string name, string description) { IAction action; if(this.actions.TryGet(name, out action)) { return action; } else { var actionImplementation = new Action(name, description); actionImplementation.AddObserver(this); this.actions.InternalAdd(actionImplementation); return actionImplementation; } } /// Updates the states of all inputs in Unity if needed /// /// /// Writing an input manager that relies on Update() or FixedUpdate() always runs /// the risk that it will update somewhere between other game objects (as the update /// order is undefined). If the game object carrying the InputMapper ends up near /// the beginning of the update queue, input will be fresh. If it's near the end /// of the udpate queue, input will already be one frame old when it's checked. /// /// /// To avoid any lag, you can call this method before processing any input to /// ensure that the input update for the current frame is performed before querying /// any inputs. /// /// public void UpdateIfNeeded() { float time = Time.time; if((time > this.lastUpdateTime) || (!this.currentFrameUpdateDone)) { // Always reset the triggered flags, so that even if the user is handling inputs // from within FixedUpdate(), the WasTriggered flags will be cleared again. resetTriggeredFlags(); // Only re-query the actual inputs if we're being called from Update(). There's no // point in doing this from FixedUpdate() because Unity only updated its inputs once // per visual frame. if(!this.currentFrameUpdateDone) { resetActionStates(); collectKeyInputs(); collectAxisInputs(); applyCollectedInputs(); this.currentFrameUpdateDone = true; } this.lastUpdateTime = time; } } /// Called once per visual frame to update the scene protected void Update() { UpdateIfNeeded(); } /// Called once per physics frame protected void FixedUpdate() { UpdateIfNeeded(); } /// Called after all normal update functions have finished protected void LateUpdate() { this.currentFrameUpdateDone = false; } /// Called when all bindings have been removed from an action /// Action from which all bindings are being removed void Action.IBindingObserver.RemovingAllBindings(Action action) { removeKeyMappingsForAction(action); removeAxisMappingsForAction(action); // We can skip reference counting here because we know the reference count // will be 0 after all mappings have been removed! int actionIndex = getActionToUpdateIndex(action); if(actionIndex != -1) { clearAction(this.actionsToUpdate[actionIndex].Action); this.actionsToUpdate.RemoveAt(actionIndex); } } /// Called when a key or button binding was added to an action /// Action the key or button binding was added to /// Key or button that was bound to the action void Action.IBindingObserver.KeyBindingAdded(Action action, KeyCode key) { int keyMappingIndex = getKeyMappingIndex(key); KeyMapping keyMapping; if(keyMappingIndex == -1) { keyMapping = new KeyMapping() { Key = key, Actions = new List() }; this.keyMappings.Add(keyMapping); } else { keyMapping = this.keyMappings[keyMappingIndex]; } keyMapping.Actions.Add(action); addToUpdateList(action); } /// Called when a key or button binding is removed from an action /// Action the key or button binding was removed from /// Key or button that was removed from the action void Action.IBindingObserver.KeyBindingRemoved(Action action, KeyCode key) { int keyMappingIndex = getKeyMappingIndex(key); if(keyMappingIndex == -1) { Debug.LogWarning( "Internal inconsistency: tried to remove key binding '" + key.ToString() + "' " + "from the mappings when it wasn't in it." ); } else { this.keyMappings[keyMappingIndex].Actions.Remove(action); if(this.keyMappings[keyMappingIndex].Actions.Count == 0) { this.keyMappings.RemoveAt(keyMappingIndex); } removeFromUpdateList(action); } } /// Called when a joystick axis binding was added to an action /// Action the joystick axis binding was added to /// Joystick axis that was bound to the action void Action.IBindingObserver.axisAdded(Action action, JoystickAxis axis) { int axisMappingIndex = getAxisMappingIndex(axis); AxisMapping axisMapping; if(axisMappingIndex == -1) { axisMapping = new AxisMapping() { Axis = axis, Actions = new List() }; this.axisMappings.Add(axisMapping); } else { axisMapping = this.axisMappings[axisMappingIndex]; } axisMapping.Actions.Add(action); addToUpdateList(action); } /// Called when a joystick axis binding was removed from an action /// Action the joystick axis binding was removed from /// Joystick axis that was removed from the action void Action.IBindingObserver.axisRemoved(Action action, JoystickAxis axis) { int axisMappingIndex = getAxisMappingIndex(axis); if(axisMappingIndex == -1) { Debug.LogWarning( "Internal inconsistency: tried to remove axis binding '" + axis.ToString() + "' " + "from the mappings when it wasn't in it." ); } else { this.axisMappings[axisMappingIndex].Actions.Remove(action); if(this.axisMappings[axisMappingIndex].Actions.Count == 0) { this.axisMappings.RemoveAt(axisMappingIndex); } removeFromUpdateList(action); } } /// Resets the states of all actions to prepare for an update private void resetActionStates() { for(int index = 0; index < this.actionsToUpdate.Count; ++index) { this.actionsToUpdate[index].Action.State = 0.0f; } } /// /// Collects inputs for keys on the keyboard and buttons on the joysticks /// private void collectKeyInputs() { for(int index = 0; index < this.keyMappings.Count; ++index) { if(UnityInput.GetKey(this.keyMappings[index].Key)) { IList actionList = this.keyMappings[index].Actions; for(int actionIndex = 0; actionIndex < actionList.Count; ++actionIndex) { actionList[actionIndex].State = 1.0f; } } } } /// Collects inputs for the joystick axes private void collectAxisInputs() { for(int index = 0; index < this.axisMappings.Count; ++index) { float state = UnityInput.GetAxisRaw(this.axisMappings[index].Axis.AxisName); if(this.axisMappings[index].Axis.IsNegative) { state = -state; } IList actionList = this.axisMappings[index].Actions; for(int actionIndex = 0; actionIndex < actionList.Count; ++actionIndex) { if(actionList[actionIndex].State < state) { actionList[actionIndex].State = state; } } } } /// Applies the collected input states to the actions private void applyCollectedInputs() { for(int index = 0; index < this.actionsToUpdate.Count; ++index) { Action action = this.actionsToUpdate[index].Action; if(action.State > 0.667f) { if(!action.IsActive) { action.IsActive = true; action.WasTriggered = true; } } else if(action.State < 0.333f) { action.IsActive = false; } } } /// /// Resets the triggered flags for all actions the user has checked /// private void resetTriggeredFlags() { for(int index = 0; index < this.actionsToUpdate.Count; ++index) { if(this.actionsToUpdate[index].Action.WasChecked) { this.actionsToUpdate[index].Action.WasTriggered = false; this.actionsToUpdate[index].Action.WasChecked = false; } } } /// Called when an action has been removed from the action collection /// Action collection from which the action was removed /// Action that has been removed from the collection private void actionRemoved(ActionCollection sender, IAction action) { removeKeyMappingsForAction(action); removeAxisMappingsForAction(action); int actionIndex = getActionToUpdateIndex(action); if(actionIndex != -1) { this.actionsToUpdate.RemoveAt(actionIndex); } Action actionImplementation = action as Action; if(actionImplementation != null) { actionImplementation.RemoveObserver(this); clearAction(actionImplementation); } } /// Removes the specified action from all key mappings /// Action that will be removed from all key mappings /// From how many mappings the action was removed private void removeKeyMappingsForAction(IAction action) { for(int index = 0; index < this.keyMappings.Count; ) { IList actionList = this.keyMappings[index].Actions; for(int actionIndex = 0; actionIndex < actionList.Count; ++actionIndex) { if(ReferenceEquals(actionList[actionIndex], action)) { actionList.RemoveAt(actionIndex); break; } } if(actionList.Count == 0) { this.keyMappings.RemoveAt(index); } else { ++index; } } } /// Removes the specified action from all axis mappings /// Action that will be removed from all axis mappings private void removeAxisMappingsForAction(IAction action) { for(int index = 0; index < this.axisMappings.Count; ) { IList actionList = this.axisMappings[index].Actions; for(int actionIndex = 0; actionIndex < actionList.Count; ++actionIndex) { if(ReferenceEquals(actionList[actionIndex], action)) { actionList.RemoveAt(actionIndex); break; } } if(actionList.Count == 0) { this.axisMappings.RemoveAt(index); } else { ++index; } } } /// Called when the action collection has been cleared of its contents /// Action collection that has been cleared private void actionsCleared(ActionCollection sender) { for(int index = 0; index < this.actionsToUpdate.Count; ++index) { clearAction(this.actionsToUpdate[index].Action); } this.keyMappings.Clear(); this.axisMappings.Clear(); this.actionsToUpdate.Clear(); } /// Looks up the index of the mapping for the specified key /// Key for which the mapping will be looked up /// The index of the mapping for the specified key or -1 if not found private int getKeyMappingIndex(KeyCode key) { for(int index = 0; index < this.keyMappings.Count; ++index) { if(this.keyMappings[index].Key == key) { return index; } } return -1; } /// Looks up the index of the mapping for the specified axis /// Axis for which the mapping will be looked up /// The index of the mapping for the specified axis or -1 if not found private int getAxisMappingIndex(JoystickAxis axis) { for(int index = 0; index < this.axisMappings.Count; ++index) { if(this.axisMappings[index].Axis == axis) { return index; } } return -1; } /// Adds an action to the update list /// Action that will be added to the update list /// /// Should be called when an input binding was made to the action. An action /// counts the number of references that exist to it, so adding it to the update /// list twice means it has to be removed twice, too. /// private void addToUpdateList(Action action) { int actionIndex = getActionToUpdateIndex(action); if(actionIndex == -1) { this.actionsToUpdate.Add(new ReferenceCountedAction(action)); } else { ++this.actionsToUpdate[actionIndex].ReferenceCount; } } /// Removes the specified action from the update list /// Action that will be removed from the update list private void removeFromUpdateList(Action action) { int actionIndex = getActionToUpdateIndex(action); if(actionIndex == -1) { Debug.LogWarning( "Internal inconsistency: tried to remove action '" + action.Name + "' from " + "the update list when it wasn't in it." ); } else { --this.actionsToUpdate[actionIndex].ReferenceCount; if(this.actionsToUpdate[actionIndex].ReferenceCount == 0) { clearAction(this.actionsToUpdate[actionIndex].Action); } } } /// Looks up the index of an action in the actions-to-update list /// Action whose index will be looked up /// The index of the action in the list if found, -1 otherwise private int getActionToUpdateIndex(IAction action) { for(int index = 0; index < this.actionsToUpdate.Count; ++index) { if(ReferenceEquals(this.actionsToUpdate[index].Action, action)) { return index; } } return -1; } /// Disconnects an action from the input mapper /// Action that will be disconnected private void clearAction(Action action) { action.IsActive = false; action.WasTriggered = false; action.WasChecked = false; action.State = 0.0f; } /// Actions that have been set up private ActionCollection actions; /// Keys or buttons and the actions they have been mapped to private IList keyMappings; /// Joystick axes and the actions they have been mapped to private IList axisMappings; /// Actions that have inputs mapped to them private IList actionsToUpdate; /// Time at which the input mapper last updated its inputs private float lastUpdateTime; /// Whether the input mapper update for the current frame is already done private bool currentFrameUpdateDone; } } // namespace Framework.Input