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