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