using System; using UnityEngine.InputSystem.LowLevel; namespace UnityEngine.InputSystem { /// /// Information passed to interactions /// when their associated controls trigger. /// /// public struct InputInteractionContext { /// /// The action associated with the binding. /// /// /// If the binding is not associated with an action, this is null. /// /// public InputAction action => m_State.GetActionOrNull(ref m_TriggerState); /// /// The bound control that changed its state to trigger the binding associated /// with the interaction. /// /// /// In case the binding associated with the interaction is a composite, this is /// one of the controls that are part of the composite. /// /// public InputControl control => m_State.GetControl(ref m_TriggerState); /// /// The phase the interaction is currently in. /// /// /// Each interaction on a binding has its own phase independent of the action the binding is applied to. /// If an interaction gets to "drive" an action at a particular point in time, its phase will determine /// the phase of the action. /// /// /// /// /// /// public InputActionPhase phase => m_TriggerState.phase; /// /// Time stamp of the input event that caused to trigger a change in the /// state of . /// /// public double time => m_TriggerState.time; /// /// Timestamp of the that caused the interaction to transition /// to . /// /// public double startTime => m_TriggerState.startTime; /// /// Whether the interaction's method has been called because /// a timer set by has expired. /// /// public bool timerHasExpired { get => (m_Flags & Flags.TimerHasExpired) != 0; internal set { if (value) m_Flags |= Flags.TimerHasExpired; else m_Flags &= ~Flags.TimerHasExpired; } } /// /// True if the interaction is waiting for input /// /// /// By default, an interaction will return this this phase after every time it has been performed /// (). This can be changed by using /// or . /// /// public bool isWaiting => phase == InputActionPhase.Waiting; /// /// True if the interaction has been started. /// /// /// public bool isStarted => phase == InputActionPhase.Started; /// /// Compute the current level of control actuation. /// /// The current level of control actuation (usually [0..1]) or -1 if the control is actuated /// but does not support computing magnitudes. /// /// public float ComputeMagnitude() { return m_TriggerState.magnitude; } /// /// Return true if the control that triggered the interaction has been actuated beyond the given threshold. /// /// Threshold that must be reached for the control to be considered actuated. If this is zero, /// the threshold must be exceeded. If it is any positive value, the value must be at least matched. /// True if the trigger control is actuated. /// /// public bool ControlIsActuated(float threshold = 0) { return InputActionState.IsActuated(ref m_TriggerState, threshold); } /// /// Mark the interaction has having begun. /// /// /// This affects the current interaction only. There might be multiple interactions on a binding /// and arbitrary many interactions might concurrently be in started state. However, only one interaction /// (usually the one that starts first) is allowed to drive the action's state as a whole. If an interaction /// that is currently driving an action is canceled, however, the next interaction in the list that has /// been started will take over and continue driving the action. /// /// /// /// public class MyInteraction : IInputInteraction<float> /// { /// public void Process(ref IInputInteractionContext context) /// { /// if (context.isWaiting && context.ControlIsActuated()) /// { /// // We've waited for input and got it. Start the interaction. /// context.Started(); /// } /// else if (context.isStarted && !context.ControlIsActuated()) /// { /// // Interaction has been completed. /// context.Performed(); /// } /// } /// /// public void Reset() /// { /// // No reset code needed. We're not keeping any state locally in the interaction. /// } /// } /// /// /// public void Started() { m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Started, ref m_TriggerState); } /// /// Marks the interaction as being performed and then transitions back to /// to wait for input. This behavior is desirable for interaction events that are instant and reflect /// a transitional interaction pattern such as or . /// /// /// Note that this affects the current interaction only. There might be multiple interactions on a binding /// and arbitrary many interactions might concurrently be in started state. However, only one interaction /// (usually the one that starts first) is allowed to drive the action's state as a whole. If an interaction /// that is currently driving an action is canceled, however, the next interaction in the list that has /// been started will take over and continue driving the action. /// public void Performed() { if (m_TriggerState.phase == InputActionPhase.Waiting) m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState); } /// /// Marks the interaction as being performed and then transitions into I /// to wait for an initial trigger condition to be true before being performed again. This behavior /// may be desirable for interaction events that reflect transitional interaction patterns but should /// be considered as started until a cancellation condition is true, such as releasing a button. /// public void PerformedAndStayStarted() { if (m_TriggerState.phase == InputActionPhase.Waiting) m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState, phaseAfterPerformed: InputActionPhase.Started); } /// /// Marks the interaction as being performed and then stays in that state waiting for an input to /// cancel the interactions active state. This behavior is desirable for interaction events that /// are active for a duration until a cancellation condition is true, such as or where releasing /// the associated button cancels the interaction.. /// public void PerformedAndStayPerformed() { if (m_TriggerState.phase == InputActionPhase.Waiting) m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState, phaseAfterPerformed: InputActionPhase.Performed); } /// /// Marks the interaction as being interrupted or aborted. This is relevant to signal that the interaction /// pattern was not completed, for example, the user pressed and then released a button before the minimum /// time required for a to complete. /// /// /// This is used by most existing interactions to cancel the transitions in the interaction state machine /// when a condition required to proceed turned false or other indirect requirements were not met, such as /// time-based conditions. /// public void Canceled() { if (m_TriggerState.phase != InputActionPhase.Canceled) m_State.ChangePhaseOfInteraction(InputActionPhase.Canceled, ref m_TriggerState); } /// /// Put the interaction back into state. /// /// /// /// /// /// public void Waiting() { if (m_TriggerState.phase != InputActionPhase.Waiting) m_State.ChangePhaseOfInteraction(InputActionPhase.Waiting, ref m_TriggerState); } /// /// Start a timeout that triggers within . /// /// Number of seconds before the timeout is triggered. /// /// An interaction might wait a set amount of time for something to happen and then /// do something depending on whether it did or did not happen. By calling this method, /// a timeout is installed such that in the input update that the timer expires in, the /// interaction's method is called with /// being true. /// /// Changing the phase of the interaction while a timeout is running will implicitly cancel /// the timeout. /// /// /// /// // Let's say we're writing a Process() method for an interaction that, /// // after a control has been actuated, waits for 1 second for it to be /// // released again. If that happens, the interaction performs. If not, /// // it cancels. /// public void Process(ref InputInteractionContext context) /// { /// // timerHasExpired will be true if we get called when our timeout /// // has expired. /// if (context.timerHasExpired) /// { /// // The user did not release the control quickly enough. /// // Our interaction is not successful, so cancel. /// context.Canceled(); /// return; /// } /// /// if (context.ControlIsActuated()) /// { /// if (!context.isStarted) /// { /// // The control has been actuated. We want to give the user a max /// // of 1 second to release it. So we start the interaction now and then /// // set the timeout. /// context.Started(); /// context.SetTimeout(1); /// } /// } /// else /// { /// // Control has been released. If we're currently waiting for a release, /// // it has come in time before out timeout expired. In other words, the /// // interaction has been successfully performed. We call Performed() /// // which implicitly removes our ongoing timeout. /// if (context.isStarted) /// context.Performed(); /// } /// } /// /// /// /// public void SetTimeout(float seconds) { m_State.StartTimeout(seconds, ref m_TriggerState); } /// /// Override the default timeout value used by . /// /// Amount of total successive timeouts TODO /// /// /// By default, timeout completion will be entirely determine by the timeout that is currently /// running, if any. However, some interactions (such as ) /// will have to run multiple timeouts in succession. Thus, completion of a single timeout is not /// the same as completion of the interaction. /// /// You can use this method to account for this. /// /// Whenever a timeout completes, the timeout duration will automatically be accumulated towards /// the total timeout completion time. /// /// /// /// // Let's say we're starting our first timeout and we know that we will run three timeouts /// // in succession of 2 seconds each. By calling SetTotalTimeoutCompletionTime(), we can account for this. /// SetTotalTimeoutCompletionTime(3 * 2); /// /// // Start the first timeout. When this timeout expires, it will automatically /// // count one second towards the total timeout completion time. /// SetTimeout(2); /// /// /// /// public void SetTotalTimeoutCompletionTime(float seconds) { if (seconds <= 0) throw new ArgumentException("Seconds must be a positive value", nameof(seconds)); m_State.SetTotalTimeoutCompletionTime(seconds, ref m_TriggerState); } /// /// Read the value of the binding that triggered processing of the interaction. /// /// Type of value to read from the binding. Must match the value type of the control /// or composite in effect for the binding. /// Value read from the binding. public TValue ReadValue() where TValue : struct { return m_State.ReadValue(m_TriggerState.bindingIndex, m_TriggerState.controlIndex); } internal InputActionState m_State; internal Flags m_Flags; internal InputActionState.TriggerState m_TriggerState; internal int mapIndex => m_TriggerState.mapIndex; internal int controlIndex => m_TriggerState.controlIndex; internal int bindingIndex => m_TriggerState.bindingIndex; internal int interactionIndex => m_TriggerState.interactionIndex; [Flags] internal enum Flags { TimerHasExpired = 1 << 1 } } }