using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using Framework.Input.States;
using Framework.Services;
namespace Framework.Input {
/// Automatically maps input actions to input states
[ServiceScope(Scope.Global)]
public class InputManager : Service, IInputManager {
#region enum InputType
/// Data type an action is using
private enum InputType {
/// Input field is a simple flag
Boolean,
/// Input field is an analogue axis
Float,
/// Input field is a trigger which remembers press events
Trigger
}
#endregion // enum InputType
#region class InputField
/// Stores informations about how an input field is filled
private class InputField {
/// Initializes an input field using a field
/// Field the input state will be written to
/// Type of the field
public InputField(FieldInfo stateField, InputType type) {
this.Type = type;
this.StateField = stateField;
}
/// Initializes an input field using a property
/// Property the input state will be written to
/// Type of the field
public InputField(PropertyInfo stateProperty, InputType type) {
this.Type = type;
this.StateProperty = stateProperty;
}
/// Retrieves the value of the input field
/// State in which the input field resides
/// The value of the input
public object RetrieveValue(object state, object value) {
if(this.StateField != null) {
return this.StateField.GetValue(state);
}
if(this.StateProperty != null) {
return this.StateProperty.GetValue(state, null);
}
return null;
}
/// Assigns the value of the input field
/// State in which the input field resides
/// Value that will be assigned to the input field
public void AssignValue(object state, object value) {
if(this.StateField != null) {
this.StateField.SetValue(state, value);
}
if(this.StateProperty != null) {
this.StateProperty.SetValue(state, value, null);
}
}
/// Assigns the value of a trigger input
/// State on which the trigger will be assigned
/// Value the IsActive field of the trigger will be set to
/// Value the WasTriggered field will be set to
public void AssignTrigger(object state, bool isActive, bool wasTriggered) {
TriggerActiveField.SetValue(boxedTrigger, isActive);
TriggerTriggeredField.SetValue(boxedTrigger, wasTriggered);
AssignValue(state, boxedTrigger);
}
/// Retrieves the default action name to associate with this input
/// The default action name for this input
public string GetDefaultActionName() {
if(this.StateField != null) {
return this.StateField.Name;
}
if(this.StateProperty != null) {
return this.StateProperty.Name;
}
return null;
}
/// Looks up an attribute attached to the input's field or property
/// Type of attribute to query for
/// The attribute, if attached, or null
public TAttribute GetAttribute() where TAttribute : class {
object[] attributes = null;
if(this.StateField != null) {
attributes = this.StateField.GetCustomAttributes(typeof(TAttribute), false);
}
if(this.StateProperty != null) {
attributes = this.StateProperty.GetCustomAttributes(typeof(TAttribute), false);
}
if((attributes != null) && (attributes.Length > 0)) {
return attributes[0] as TAttribute;
} else {
return null;
}
}
/// Looks up attributes attached to the input's field or property
/// Type of attributes to query for
/// The attributes, if attached, or null
public TAttribute[] GetAttributes() where TAttribute : class {
object[] attributes = null;
if(this.StateField != null) {
attributes = this.StateField.GetCustomAttributes(typeof(TAttribute), false);
}
if(this.StateProperty != null) {
attributes = this.StateProperty.GetCustomAttributes(typeof(TAttribute), false);
}
if((attributes != null) && (attributes.Length > 0)) {
TAttribute[] typedAttributes = new TAttribute[attributes.Length];
for(int index = 0; index < attributes.Length; ++index) {
typedAttributes[index] = (TAttribute)attributes[index];
}
return typedAttributes;
} else {
return null;
}
}
/// Reflection information for a trigger's IsActive field
public static FieldInfo TriggerActiveField = typeof(Trigger).GetField(
"IsActive", BindingFlags.Public | BindingFlags.Instance
);
/// Reflection information for a trigger's WasTriggered field
public static FieldInfo TriggerTriggeredField = typeof(Trigger).GetField(
"WasTriggered", BindingFlags.Public | BindingFlags.Instance
);
/// Boxed trigger structure that's use to fill trigger fields
private static object boxedTrigger = new Trigger();
/// Type of the input field
public InputType Type;
/// Action from which the input field receives negative input
public IAction NegativeAction;
/// Action from which the input field receives positive input
public IAction PositiveAction;
/// Field to assign to, if a field is used
public FieldInfo StateField;
/// Property to assign to, if a property is used
public PropertyInfo StateProperty;
}
#endregion // class InputField
#region class InputState
/// Stores an input state that can be queried by the user
private class InputState {
/// Initializes a new input state
public InputState() {
this.Fields = new List();
}
/// Input state the actions will be filled into
public object State;
/// Whether the input state has been updated this cycle
public bool WasUpdated;
/// Fields that need to be filled in the state structure
public IList Fields;
}
#endregion // class InputState
/// Initializes a new input manager
public InputManager() {
this.inputStateLookup = new Dictionary();
this.inputStates = new List();
}
/// Injects the instance's dependencies
/// Input mapper the instance will use
///
/// This method is called automatically by the services framework. Any parameters
/// it requires will be filled out by looking up the respective services and creating
/// them as needed.
///
protected void Inject(IInputMapper inputMapper) {
this.inputMapper = inputMapper;
}
/// Retrieves the current input state
/// Type of input state that will be retrieved
/// The current input state
public TState GetState() where TState : new() {
// Time will only advance if Time.timeScale is > 0.0, so to work with time stopped,
// we also have to force an update each update cycle!
float time = Time.time;
if((time > this.lastUpdateTime) || (!this.currentFrameUpdateDone)) {
this.inputMapper.UpdateIfNeeded();
resetUpdateFlags();
this.lastUpdateTime = time;
this.currentFrameUpdateDone = true;
}
// Locate the input state if it already exists, otherwise create a new one
InputState inputState;
if(this.inputStateLookup.TryGetValue(typeof(TState), out inputState)) {
if(!inputState.WasUpdated) {
updateInputState(inputState);
}
} else {
inputState = new InputState() {
State = new TState()
};
prepareInputState(inputState);
integrateInputState(inputState);
updateInputState(inputState);
}
return (TState)inputState.State;
}
/// Called after all normal update functions have finished
protected void LateUpdate() {
this.currentFrameUpdateDone = false;
}
///
/// Prepares the fields and properties in an input state for mapping
///
///
/// Input state in which fields and properties will be prepared
///
private void prepareInputState(InputState inputState) {
prepareFields(inputState);
prepareProperties(inputState);
}
///
/// Sets up the input state so it can be queried under any of its interfaces
///
/// Input state that will be set up for querying
private void integrateInputState(InputState inputState) {
this.inputStates.Add(inputState);
Type type = inputState.State.GetType();
while(type != typeof(object)) {
if(this.inputStateLookup.ContainsKey(type)) {
// This type has a base class that was already queried separately. We remove
// it so that future queries can be optimized. If the game is querying two
// different input states sharing the same base class, this will not have any
// effect, but if we didn't do this, there would be no optimization in
// the first case described above.
this.inputStates.Remove(this.inputStateLookup[type]);
this.inputStateLookup[type] = inputState;
} else {
this.inputStateLookup.Add(type, inputState);
}
type = type.BaseType;
}
}
/// Updates the inputs in an input state from the input mapper
/// Input state that will be updated
private void updateInputState(InputState inputState) {
for(int index = 0; index < inputState.Fields.Count; ++index) {
InputField inputField = inputState.Fields[index];
if(inputField.PositiveAction != null) {
if(inputField.PositiveAction.IsAbandoned) {
associateAction(inputField);
}
}
switch(inputField.Type) {
// Booleans are automatically mapped to an action's 'IsActive' flag
case InputType.Boolean: {
inputField.AssignValue(inputState.State, inputField.PositiveAction.IsActive);
break;
}
// Floats are mapped to the analogous value of an action ranging form 0.0 to 1.0.
// If they specify positive and negative actions, the analogous values of two actions
// are combined to form a -1.0 to +1.0 range.
case InputType.Float: {
if(inputField.NegativeAction != null) {
if(inputField.NegativeAction.IsAbandoned) {
associateAction(inputField);
}
inputField.AssignValue(
inputState.State,
inputField.PositiveAction.State - inputField.NegativeAction.State
);
} else {
inputField.AssignValue(inputState.State, inputField.PositiveAction.State);
}
break;
}
// Triggers record both whether the action is currently active and whether it
// has been triggered since the last frame.
case InputType.Trigger: {
inputField.AssignTrigger(
inputState.State,
inputField.PositiveAction.IsActive, inputField.PositiveAction.WasTriggered
);
break;
}
}
}
inputState.WasUpdated = true;
}
/// Prepares an input state's public fields for mapping to actions
/// Input state whose public fields will be prepared
private void prepareFields(InputState inputState) {
var flags = BindingFlags.Instance | BindingFlags.Public;
FieldInfo[] fields = inputState.State.GetType().GetFields(flags);
for(int index = 0; index < fields.Length; ++index) {
InputField inputField = null;
if(fields[index].FieldType == typeof(bool)) {
inputField = new InputField(fields[index], InputType.Boolean);
} else if(fields[index].FieldType == typeof(float)) {
inputField = new InputField(fields[index], InputType.Float);
} else if(fields[index].FieldType == typeof(Trigger)) {
inputField = new InputField(fields[index], InputType.Trigger);
}
if(inputField != null) {
inputState.Fields.Add(associateAction(inputField));
}
}
}
/// Prepares an input state's public properties for mapping to actions
/// Input state whose public properties will be prepared
private void prepareProperties(InputState inputState) {
var flags = BindingFlags.Instance | BindingFlags.Public;
PropertyInfo[] properties = inputState.State.GetType().GetProperties(flags);
for(int index = 0; index < properties.Length; ++index) {
InputField inputField = null;
if(properties[index].PropertyType == typeof(bool)) {
inputField = new InputField(properties[index], InputType.Boolean);
} else if(properties[index].PropertyType == typeof(float)) {
inputField = new InputField(properties[index], InputType.Float);
} else if(properties[index].PropertyType == typeof(Trigger)) {
inputField = new InputField(properties[index], InputType.Trigger);
}
if(inputField != null) {
inputState.Fields.Add(associateAction(inputField));
}
}
}
/// Looks up the action from which a field receives its input
/// Field an action will be looked up for
private InputField associateAction(InputField inputField) {
ActionAttribute actionAttribute = inputField.GetAttribute();
if(actionAttribute == null) {
string actionName = inputField.GetDefaultActionName();
if(!this.inputMapper.Actions.TryGet(actionName, out inputField.PositiveAction)) {
inputField.PositiveAction = this.inputMapper.CreateAction(
inputField.GetDefaultActionName()
);
addDefaultKeyMappings(inputField, false);
addDefaultAxisMappings(inputField, false);
}
} else {
string actionName = actionAttribute.NegativeActionName;
if(!this.inputMapper.Actions.TryGet(actionName, out inputField.NegativeAction)) {
inputField.NegativeAction = this.inputMapper.CreateAction(actionName);
addDefaultKeyMappings(inputField, true);
addDefaultAxisMappings(inputField, true);
}
actionName = actionAttribute.PositiveActionName;
if(!this.inputMapper.Actions.TryGet(actionName, out inputField.PositiveAction)) {
inputField.PositiveAction = this.inputMapper.CreateAction(actionName);
addDefaultKeyMappings(inputField, false);
addDefaultAxisMappings(inputField, false);
}
}
return inputField;
}
/// Adds default key mappings to the specified input field
/// Input field to which default mappings will be added
/// Whether to map the negative axis direction
private void addDefaultKeyMappings(InputField inputField, bool negative) {
DefaultKeyAttribute[] keyAttributes = inputField.GetAttributes();
if(keyAttributes == null) {
return;
}
if(negative) {
for(int index = 0; index < keyAttributes.Length; ++index) {
inputField.NegativeAction.BoundKeys.Add(keyAttributes[index].NegativeDefaultKey);
}
} else {
for(int index = 0; index < keyAttributes.Length; ++index) {
inputField.PositiveAction.BoundKeys.Add(keyAttributes[index].PositiveDefaultKey);
}
}
}
/// Adds default joystick axis mappings to the specified input field
/// Input field to which default mappings will be added
/// Whether to map the negative axis direction
private void addDefaultAxisMappings(InputField inputField, bool negative) {
DefaultJoystickAxisAttribute[] axisAttributes;
axisAttributes = inputField.GetAttributes();
if(axisAttributes == null) {
return;
}
if(negative) {
for(int index = 0; index < axisAttributes.Length; ++index) {
inputField.NegativeAction.BoundJoystickAxes.Add(
axisAttributes[index].DefaultNegativeAxis
);
}
} else {
for(int index = 0; index < axisAttributes.Length; ++index) {
inputField.PositiveAction.BoundJoystickAxes.Add(
axisAttributes[index].DefaultPositiveAxis
);
}
}
}
/// Resets the update flags in all input states
private void resetUpdateFlags() {
for(int index = 0; index < this.inputStates.Count; ++index) {
this.inputStates[index].WasUpdated = false;
}
}
/// Maps types queryable by the user to prepared input states
private IDictionary inputStateLookup;
/// Input states that need to be updated
private IList inputStates;
/// Maps input devices to actions
private IInputMapper inputMapper;
/// 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