using System; using UnityEngine.InputSystem.LowLevel; namespace UnityEngine.InputSystem { /// <summary> /// Information passed to <see cref="IInputInteraction">interactions</see> /// when their associated controls trigger. /// </summary> /// <seealso cref="IInputInteraction.Process"/> public struct InputInteractionContext { /// <summary> /// The action associated with the binding. /// </summary> /// <remarks> /// If the binding is not associated with an action, this is <c>null</c>. /// </remarks> /// <seealso cref="InputBinding.action"/> public InputAction action => m_State.GetActionOrNull(ref m_TriggerState); /// <summary> /// The bound control that changed its state to trigger the binding associated /// with the interaction. /// </summary> /// <remarks> /// In case the binding associated with the interaction is a composite, this is /// one of the controls that are part of the composite. /// </remarks> /// <seealso cref="InputBinding.path"/> public InputControl control => m_State.GetControl(ref m_TriggerState); /// <summary> /// The phase the interaction is currently in. /// </summary> /// <remarks> /// 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. /// </remarks> /// <seealso cref="InputAction.phase"/> /// <seealso cref="Started"/> /// <seealso cref="Waiting"/> /// <seealso cref="Performed"/> /// <seealso cref="Canceled"/> public InputActionPhase phase => m_TriggerState.phase; /// <summary> /// Time stamp of the input event that caused <see cref="control"/> to trigger a change in the /// state of <see cref="action"/>. /// </summary> /// <seealso cref="InputEvent.time"/> public double time => m_TriggerState.time; /// <summary> /// Timestamp of the <see cref="InputEvent"/> that caused the interaction to transition /// to <see cref="InputActionPhase.Started"/>. /// </summary> /// <seealso cref="InputEvent.time"/> public double startTime => m_TriggerState.startTime; /// <summary> /// Whether the interaction's <see cref="IInputInteraction.Process"/> method has been called because /// a timer set by <see cref="SetTimeout"/> has expired. /// </summary> /// <seealso cref="SetTimeout"/> public bool timerHasExpired { get => (m_Flags & Flags.TimerHasExpired) != 0; internal set { if (value) m_Flags |= Flags.TimerHasExpired; else m_Flags &= ~Flags.TimerHasExpired; } } /// <summary> /// True if the interaction is waiting for input /// </summary> /// <remarks> /// By default, an interaction will return this this phase after every time it has been performed /// (<see cref="InputActionPhase.Performed"/>). This can be changed by using <see cref="PerformedAndStayStarted"/> /// or <see cref="PerformedAndStayPerformed"/>. /// </remarks> /// <seealso cref="InputActionPhase.Waiting"/> public bool isWaiting => phase == InputActionPhase.Waiting; /// <summary> /// True if the interaction has been started. /// </summary> /// <seealso cref="InputActionPhase.Started"/> /// <seealso cref="Started"/> public bool isStarted => phase == InputActionPhase.Started; /// <summary> /// Compute the current level of control actuation. /// </summary> /// <returns>The current level of control actuation (usually [0..1]) or -1 if the control is actuated /// but does not support computing magnitudes.</returns> /// <seealso cref="ControlIsActuated"/> /// <seealso cref="InputControl.EvaluateMagnitude()"/> public float ComputeMagnitude() { return m_TriggerState.magnitude; } /// <summary> /// Return true if the control that triggered the interaction has been actuated beyond the given threshold. /// </summary> /// <param name="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.</param> /// <returns>True if the trigger control is actuated.</returns> /// <seealso cref="InputControlExtensions.IsActuated"/> /// <seealso cref="ComputeMagnitude"/> public bool ControlIsActuated(float threshold = 0) { return InputActionState.IsActuated(ref m_TriggerState, threshold); } /// <summary> /// Mark the interaction has having begun. /// </summary> /// <remarks> /// Note that this affects the current interaction only. There may be multiple interactions on a binding /// and arbitrary many interactions may 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. /// /// <example> /// <code> /// 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. /// } /// } /// </code> /// </example> /// </remarks> public void Started() { m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Started, ref m_TriggerState); } public void Performed() { if (m_TriggerState.phase == InputActionPhase.Waiting) m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState); } public void PerformedAndStayStarted() { if (m_TriggerState.phase == InputActionPhase.Waiting) m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState, phaseAfterPerformed: InputActionPhase.Started); } public void PerformedAndStayPerformed() { if (m_TriggerState.phase == InputActionPhase.Waiting) m_TriggerState.startTime = time; m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState, phaseAfterPerformed: InputActionPhase.Performed); } public void Canceled() { if (m_TriggerState.phase != InputActionPhase.Canceled) m_State.ChangePhaseOfInteraction(InputActionPhase.Canceled, ref m_TriggerState); } /// <summary> /// Put the interaction back into <see cref="InputActionPhase.Waiting"/> state. /// </summary> /// <seealso cref="InputAction.phase"/> /// <seealso cref="InputActionPhase"/> /// <seealso cref="Started"/> /// <seealso cref="Performed"/> /// <seealso cref="Canceled"/> public void Waiting() { if (m_TriggerState.phase != InputActionPhase.Waiting) m_State.ChangePhaseOfInteraction(InputActionPhase.Waiting, ref m_TriggerState); } /// <summary> /// Start a timeout that triggers within <paramref name="seconds"/>. /// </summary> /// <param name="seconds">Number of seconds before the timeout is triggered.</param> /// <remarks> /// 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 <see cref="IInputInteraction.Process"/> method is called with <see cref="timerHasExpired"/> /// being true. /// /// Changing the phase of the interaction while a timeout is running will implicitly cancel /// the timeout. /// /// <example> /// <code> /// // 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(); /// } /// } /// </code> /// </example> /// </remarks> /// <seealso cref="timerHasExpired"/> public void SetTimeout(float seconds) { m_State.StartTimeout(seconds, ref m_TriggerState); } /// <summary> /// Override the default timeout value used by <see cref="InputAction.GetTimeoutCompletionPercentage"/>. /// </summary> /// <param name="seconds">Amount of total successive timeouts TODO</param> /// <exception cref="ArgumentException"></exception> /// <remarks> /// By default, timeout completion will be entirely determine by the timeout that is currently /// running, if any. However, some interactions (such as <see cref="Interactions.MultiTapInteraction"/>) /// 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. /// /// <example> /// <code> /// // 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); /// </code> /// </example> /// </remarks> /// <seealso cref="InputAction.GetTimeoutCompletionPercentage"/> 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); } /// <summary> /// Read the value of the binding that triggered processing of the interaction. /// </summary> /// <typeparam name="TValue">Type of value to read from the binding. Must match the value type of the control /// or composite in effect for the binding.</typeparam> /// <returns>Value read from the binding.</returns> public TValue ReadValue<TValue>() where TValue : struct { return m_State.ReadValue<TValue>(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 } } }