using System; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; using UnityEngine.Serialization; ////TODO: add way to retrieve the binding correspond to a control ////TODO: add way to retrieve the currently ongoing interaction and also add way to know how long it's been going on ////FIXME: control goes back to invalid when the action ends, so there's no guarantee you can get to the control through the polling API ////FIXME: Whether a control from a binding that's part of a composite appears on an action is currently not consistently enforced. //// If it mentions the action, it appears on the action. Otherwise it doesn't. The controls should consistently appear on the //// action based on what action the *composite* references. ////REVIEW: Should we bring the checkboxes for actions back? We tried to "simplify" things by collapsing everything into a InputActionTypes //// and making the various behavior toggles implicit in that. However, my impression is that this has largely backfired by making //// it opaque what the choices actually entail and by giving no way out if the choices for one reason or another don't work out //// perfectly. //// //// My impression is that at least two the following two checkboxes would make sense: //// 1) Initial State Check? Whether the action should immediately sync to the current state of controls when enabled. //// 2) Resolve Conflicting Inputs? Whether the action should try to resolve conflicts between multiple concurrent inputs. //// //// I'm fine hiding this under an "Advanced" foldout or something. But IMO, control over this should be available to the user. //// //// In the same vein, we probably also should expose control over how an action behaves on focus loss (https://forum.unity.com/threads/actions-canceled-when-game-loses-focus.855217/). ////REVIEW: I think the action system as it is today offers too many ways to shoot yourself in the foot. It has //// flexibility but at the same time has abundant opportunity for ending up with dysfunction. Common setups //// have to come preconfigured and work robustly for the user without requiring much understanding of how //// the system fits together. ////REVIEW: add "lastControl" property? (and maybe a lastDevice at the InputActionMap/Asset level?) ////REVIEW: have single delegate instead of separate performed/started/canceled callbacks? ////REVIEW: Do we need to have separate display names for actions? ////REVIEW: what about having the concept of "consumed" on the callback context? ////REVIEW: have "Always Enabled" toggle on actions? ////TODO: allow temporarily disabling individual bindings (flag on binding) such that no re-resolve is needed //// (SilenceBinding? DisableBinding) namespace UnityEngine.InputSystem { /// /// A named input signal that can flexibly decide which input data to tap. /// /// /// An input action is an abstraction over the source of input(s) it receives. They are /// most useful for representing input as "logical" concepts (e.g. "jump") rather than /// as "physical" inputs (e.g. "space bar on keyboard pressed"). /// /// In its most basic form, an action is simply an object along with a collection of /// bindings that trigger the action. /// /// /// /// // A simple action can be created directly using `new`. If desired, a binding /// // can be specified directly as part of construction. /// var action = new InputAction(binding: "<Gamepad>/buttonSouth"); /// /// // Additional bindings can be added using `AddBinding`. /// action.AddBinding("<Mouse>/leftButton"); /// /// /// /// Bindings use control path expressions to reference controls. See /// for more details. There may be arbitrary many bindings targeting a single action. The /// list of bindings targeting an action can be obtained through . /// /// By itself an action does not do anything until it is enabled: /// /// /// /// action.Enable(); /// /// /// /// Once enabled, the action will actively monitor all controls on devices present /// in the system (see ) that match any of the binding paths /// associated with the action. If you want to restrict the set of bindings used at runtime /// or restrict the set of devices which controls are chosen from, you can do so using /// or, if the action is part of an , /// by setting the property of the action map. The /// controls that an action uses can be queried using the property. /// /// When input is received on controls bound to an action, the action will trigger callbacks /// in response. These callbacks are , , and /// . The callbacks are triggered as part of input system updates /// (see ), i.e. they happen before the respective /// MonoBehaviour.Update or MonoBehaviour.FixedUpdate methods /// get executed (depending on which the system is /// set to). /// /// In what order and how those callbacks get triggered depends on both the /// of the action as well as on the interactions (see ) present /// on the bindings of the action. The default behavior is that when a control is actuated /// (that is, moving away from its resting position), is called and then /// . Subsequently, whenever the a control further changes value to /// anything other than its default value, will be called again. /// Finally, when the control moves back to its default value (i.e. resting position), /// is called. /// /// To hook into the callbacks, there are several options available to you. The most obvious /// one is to hook directly into , , and/or /// . In these callbacks, you will receive a /// with information about how the action got triggered. For example, you can use to read the value from the binding that triggered /// or use to find the interaction that is in progress. /// /// /// /// action.started += context => Debug.Log($"{context.action} started"); /// action.performed += context => Debug.Log($"{context.action} performed"); /// action.canceled += context => Debug.Log($"{context.action} canceled"); /// /// /// /// Alternatively, you can use the callback for /// actions that are part of an action map or the global /// callback to globally listen for action activity. To simply record action activity instead /// of responding to it directly, you can use . /// /// If you prefer to poll an action directly as part of your MonoBehaviour.Update /// or MonoBehaviour.FixedUpdate logic, you can do so using the /// and methods. /// /// /// /// protected void Update() /// { /// // For a button type action. /// if (action.triggered) /// /* ... */; /// /// // For a value type action. /// // (Vector2 is just an example; pick the value type that is the right /// // one according to the bindings you have) /// var v = action.ReadValue<Vector2>(); /// } /// /// /// /// Note that actions are not generally frame-based. What this means is that an action /// will observe any value change on its connected controls, even if the control changes /// value multiple times in the same frame. In practice, this means that, for example, /// no button press will get missed. /// /// Actions can be grouped into maps (see ) which can in turn /// be grouped into assets (see ). /// /// Please note that actions are a player-only feature. They are not supported in /// edit mode. /// /// For more in-depth reading on actions, see the manual. /// /// /// /// [Serializable] public sealed class InputAction : ICloneable, IDisposable { /// /// Name of the action. /// /// Plain-text name of the action. /// /// Can be null for anonymous actions created in code. /// /// If the action is part of an , it will have a name and the name /// will be unique in the map. The name is just the name of the action alone, not a "mapName/actionName" /// combination. /// /// The name should not contain slashes or dots but can contain spaces and other punctuation. /// /// An action can be renamed after creation using .. /// /// public string name => m_Name; /// /// Behavior type of the action. /// /// General behavior type of the action. /// /// Determines how the action gets triggered in response to control value changes. /// /// For details about how the action type affects an action, see . /// public InputActionType type => m_Type; /// /// A stable, unique identifier for the action. /// /// Unique ID of the action. /// /// This can be used instead of the name to refer to the action. Doing so allows referring to the /// action such that renaming the action does not break references. /// public Guid id { get { MakeSureIdIsInPlace(); return new Guid(m_Id); } } internal Guid idDontGenerate { get { if (string.IsNullOrEmpty(m_Id)) return default; return new Guid(m_Id); } } /// /// Name of control layout expected for controls bound to this action. /// /// /// This is optional and is null by default. /// /// Constraining an action to a particular control layout allows determine the value /// type and expected input behavior of an action without being reliant on any particular /// binding. /// public string expectedControlType { get => m_ExpectedControlType; set => m_ExpectedControlType = value; } /// /// Processors applied to every binding on the action. /// /// Processors added to all bindings on the action. /// /// This property is equivalent to appending the same string to the /// field of every binding that targets /// the action. It is thus simply a means of avoiding the need configure the /// same processor the same way on every binding in case it uniformly applies /// to all of them. /// /// /// /// var action = new InputAction(processors: "scaleVector2(x=2, y=2)"); /// /// // Both of the following bindings will implicitly have a /// // ScaleVector2Processor applied to them. /// action.AddBinding("<Gamepad>/leftStick"); /// action.AddBinding("<Joystick>/stick"); /// /// /// /// /// /// public string processors => m_Processors; /// /// Interactions applied to every binding on the action. /// /// Interactions added to all bindings on the action. /// /// This property is equivalent to appending the same string to the /// field of every binding that targets /// the action. It is thus simply a means of avoiding the need configure the /// same interaction the same way on every binding in case it uniformly applies /// to all of them. /// /// /// /// var action = new InputAction(interactions: "press"); /// /// // Both of the following bindings will implicitly have a /// // Press interaction applied to them. /// action.AddBinding("<Gamepad>/buttonSouth"); /// action.AddBinding("<Joystick>/trigger"); /// /// /// /// /// /// public string interactions => m_Interactions; /// /// The map the action belongs to. /// /// that the action belongs to or null. /// /// If the action is a loose action created in code, this will be null. /// /// /// /// var action1 = new InputAction(); // action1.actionMap will be null /// /// var actionMap = new InputActionMap(); /// var action2 = actionMap.AddAction("action"); // action2.actionMap will point to actionMap /// /// /// /// public InputActionMap actionMap => isSingletonAction ? null : m_ActionMap; /// /// An optional mask that determines which bindings of the action to enable and /// which to ignore. /// /// Optional mask that determines which bindings on the action to enable. /// /// Binding masks can be applied at three different levels: for an entire asset through /// , for a specific map through , and for single actions through this property. /// By default, none of the masks will be set (i.e. they will be null). /// /// When an action is enabled, all the binding masks that apply to it are taken into /// account. Specifically, this means that any given binding on the action will be /// enabled only if it matches the mask applied to the asset, the mask applied /// to the map that contains the action, and the mask applied to the action itself. /// All the masks are individually optional. /// /// Masks are matched against bindings using . /// /// Note that if you modify the masks applicable to an action while it is /// enabled, the action's will get updated immediately to /// respect the mask. To avoid repeated binding resolution, it is most efficient /// to apply binding masks before enabling actions. /// /// Binding masks are non-destructive. All the bindings on the action are left /// in place. Setting a mask will not affect the value of the /// property. /// /// /// /// // Create a free-standing action with two bindings, one in the /// // "Keyboard" group and one in the "Gamepad" group. /// var action = new InputAction(); /// action.AddBinding("<Gamepad>/buttonSouth", groups: "Gamepad"); /// action.AddBinding("<Keyboard>/space", groups: "Keyboard"); /// /// // By default, all bindings will be enabled. This means if both /// // a keyboard and gamepad (or several of them) is present, the action /// // will respond to input from all of them. /// action.Enable(); /// /// // With a binding mask we can restrict the action to just specific /// // bindings. For example, to only enable the gamepad binding: /// action.bindingMask = InputBinding.MaskByGroup("Gamepad"); /// /// // Note that we can mask by more than just by group. Masking by path /// // or by action as well as a combination of these is also possible. /// // We could, for example, mask for just a specific binding path: /// action.bindingMask = new InputBinding() /// { /// // Select the keyboard binding based on its specific path. /// path = "<Keyboard>/space" /// }; /// /// /// /// /// /// public InputBinding? bindingMask { get => m_BindingMask; set { if (value == m_BindingMask) return; if (value != null) { var v = value.Value; v.action = name; value = v; } m_BindingMask = value; var map = GetOrCreateActionMap(); if (map.m_State != null) map.LazyResolveBindings(fullResolve: true); } } /// /// The list of bindings associated with the action. /// /// List of bindings for the action. /// /// This list contains all bindings from of the action's /// that reference the action through their /// property. /// /// Note that on the first call, the list may have to be extracted from the action map first which /// may require allocating GC memory. However, once initialized, no further GC allocation hits should occur. /// If the binding setup on the map is changed, re-initialization may be required. /// /// public ReadOnlyArray bindings => GetOrCreateActionMap().GetBindingsForSingleAction(this); /// /// The set of controls to which the action's resolve. /// /// Controls resolved from the action's . /// /// This property can be queried whether the action is enabled or not and will return the /// set of controls that match the action's bindings according to the current setup of /// binding masks () and device restrictions (). /// /// Note that internally, controls are not stored on a per-action basis. This means /// that on the first read of this property, the list of controls for just the action /// may have to be extracted which in turn may allocate GC memory. After the first read, /// no further GC allocations should occur except if the set of controls is changed (e.g. /// by changing the binding mask or by adding/removing devices to/from the system). /// /// If the property is queried when the action has not been enabled yet, the system /// will first resolve controls on the action (and for all actions in the map and/or /// the asset). See Binding Resolution /// in the manual for details. /// /// To map a control in this array to an index into , use /// . /// /// /// /// // Map control list to binding indices. /// var bindingIndices = myAction.controls.Select(c => myAction.GetBindingIndexForControl(c)); /// /// /// /// Note that this array will not contain the same control multiple times even if more than /// one binding on an action references the same control. /// /// /// /// var action1 = new InputAction(); /// action1.AddBinding("<Gamepad>/buttonSouth"); /// action1.AddBinding("<Gamepad>/buttonSouth"); // This binding will be ignored. /// /// // Contains only one instance of buttonSouth which is associated /// // with the first binding (at index #0). /// var action1Controls = action1.controls; /// /// var action2 = new InputAction(); /// action2.AddBinding("<Gamepad>/buttonSouth"); /// // Add a binding that implicitly matches the first binding, too. When binding resolution /// // happens, this binding will only receive buttonNorth, buttonWest, and buttonEast, but not /// // buttonSouth as the first binding already received that control. /// action2.AddBinding("<Gamepad>/button*"); /// /// // Contains only all four face buttons (buttonSouth, buttonNorth, buttonEast, buttonWest) /// // but buttonSouth is associated with the first button and only buttonNorth, buttonEast, /// // and buttonWest are associated with the second binding. /// var action2Controls = action2.controls; /// /// /// /// /// public ReadOnlyArray controls { get { var map = GetOrCreateActionMap(); map.ResolveBindingsIfNecessary(); return map.GetControlsForSingleAction(this); } } /// /// The current phase of the action. /// /// /// When listening for control input and when responding to control value changes, /// actions will go through several possible phases. /// /// In general, when an action starts receiving input, it will go to /// and when it stops receiving input, it will go to . /// When is used depends primarily on the type /// of action. will trigger /// whenever the value of the control changes (including the first time; i.e. it will first /// trigger and then /// right after) whereas will trigger /// as soon as the button press threshold () /// has been crossed. /// /// Note that both interactions and the action can affect the phases /// that an action goes through. actions will /// only ever use and not go to or (as /// pass-through actions do not follow the start-performed-canceled model in general). /// /// While an action is disabled, its phase is . /// public InputActionPhase phase => currentState.phase; /// /// True if the action is currently in or /// phase. False in all other cases. /// /// public bool inProgress => phase.IsInProgress(); /// /// Whether the action is currently enabled, i.e. responds to input, or not. /// /// True if the action is currently enabled. /// /// An action is enabled by either calling on it directly or by calling /// on the containing the action. /// When enabled, an action will listen for changes on the controls it is bound to and trigger /// callbacks such as , , and /// in response. /// /// /// /// /// /// public bool enabled => phase != InputActionPhase.Disabled; /// /// Event that is triggered when the action has been started. /// /// /// See for details of how an action progresses through phases /// and triggers this callback. /// /// public event Action started { add => m_OnStarted.AddCallback(value); remove => m_OnStarted.RemoveCallback(value); } /// /// Event that is triggered when the action has been /// but then canceled before being fully . /// /// /// See for details of how an action progresses through phases /// and triggers this callback. /// /// public event Action canceled { add => m_OnCanceled.AddCallback(value); remove => m_OnCanceled.RemoveCallback(value); } /// /// Event that is triggered when the action has been fully performed. /// /// /// See for details of how an action progresses through phases /// and triggers this callback. /// /// public event Action performed { add => m_OnPerformed.AddCallback(value); remove => m_OnPerformed.RemoveCallback(value); } ////TODO: Obsolete and drop this when we can break API /// /// Equivalent to . /// /// public bool triggered => WasPerformedThisFrame(); /// /// The currently active control that is driving the action. while the action /// is in waiting () or canceled () /// state. Otherwise the control that last had activity on it which wasn't ignored. /// /// /// Note that the control's value does not necessarily correspond to the value of the /// action () as the control may be part of a composite. /// /// public unsafe InputControl activeControl { get { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var controlIndex = actionStatePtr->controlIndex; if (controlIndex != InputActionState.kInvalidIndex) return state.controls[controlIndex]; } return null; } } /// /// Type of value returned by and currently expected /// by . while the action /// is in waiting () or canceled () /// state as this is based on the currently active control that is driving the action. /// /// Type of object returned when reading a value. /// /// The type of value returned by an action is usually determined by the /// that triggered the action, i.e. by the /// control referenced from . /// /// However, if the binding that triggered is a composite, then the composite /// will determine values and not the individual control that triggered (that /// one just feeds values into the composite). /// /// The active value type may change depending on which controls are actuated if there are multiple /// bindings with different control types. This property can be used to ensure you are calling the /// method with the expected type parameter if your action is /// configured to allow multiple control types as otherwise that method will throw an /// if the type of the control that triggered the action does not match the type parameter. /// /// /// /// public unsafe Type activeValueType { get { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var controlIndex = actionStatePtr->controlIndex; if (controlIndex != InputActionState.kInvalidIndex) return state.GetValueType(actionStatePtr->bindingIndex, controlIndex); } return null; } } /// /// Whether the action wants a state check on its bound controls as soon as it is enabled. This is always /// true for actions but can optionally be enabled for /// or actions. /// /// /// Usually, when an action is (e.g. via ), it will start listening for input /// and then trigger once the first input arrives. However, bound to an action may already be /// actuated when an action is enabled. For example, if a "jump" action is bound to , /// the space bar may already be pressed when the jump action is enabled. /// /// actions handle this differently by immediately performing an "initial state check" /// in the next input update (see ) after being enabled. If any of the bound controls /// is already actuated, the action will trigger right away -- even with no change in state on the controls. /// /// This same behavior can be enabled explicitly for and /// actions using this property. /// /// /// public bool wantsInitialStateCheck { get => type == InputActionType.Value || (m_Flags & ActionFlags.WantsInitialStateCheck) != 0; set { if (value) m_Flags |= ActionFlags.WantsInitialStateCheck; else m_Flags &= ~ActionFlags.WantsInitialStateCheck; } } /// /// Construct an unnamed, free-standing action that is not part of any map or asset /// and has no bindings. Bindings can be added with . /// The action type defaults to . /// /// /// The action will not have an associated and /// will thus be null. Use instead if /// you want to add a new action to an action map. /// /// The action will remain disabled after construction and thus not listen/react to input yet. /// Use to enable the action. /// /// /// /// // Create an action with two bindings. /// var action = new InputAction(); /// action.AddBinding("<Gamepad>/leftStick"); /// action.AddBinding("<Mouse>/delta"); /// /// action.performed += ctx => Debug.Log("Value: " + ctx.ReadValue<Vector2>()); /// /// action.Enable(); /// /// /// public InputAction() { m_Id = Guid.NewGuid().ToString(); } /// /// Construct a free-standing action that is not part of an . /// /// Name of the action. If null or empty, the action will be unnamed. /// Type of action to create. Defaults to , i.e. /// an action that provides continuous values. /// If not null or empty, a binding with the given path will be added to the action /// right away. The format of the string is the as for . /// If is not null or empty, this parameter represents /// the interaction to apply to the newly created binding (i.e. ). If /// is not supplied, this parameter represents the interactions to apply to the action /// (i.e. the value of ). /// If is not null or empty, this parameter represents /// the processors to apply to the newly created binding (i.e. ). If /// is not supplied, this parameter represents the processors to apply to the /// action (i.e. the value of ). /// The optional expected control type for the action (i.e. ). /// /// The action will not have an associated and /// will thus be null. Use instead if /// you want to add a new action to an action map. /// /// The action will remain disabled after construction and thus not listen/react to input yet. /// Use to enable the action. /// /// Additional bindings can be added with . /// /// /// /// // Create a button action responding to the gamepad A button. /// var action = new InputAction(type: InputActionType.Button, binding: "<Gamepad>/buttonSouth"); /// action.performed += ctx => Debug.Log("Pressed"); /// action.Enable(); /// /// /// public InputAction(string name = null, InputActionType type = default, string binding = null, string interactions = null, string processors = null, string expectedControlType = null) { m_Name = name; m_Type = type; if (!string.IsNullOrEmpty(binding)) { m_SingletonActionBindings = new[] { new InputBinding { path = binding, interactions = interactions, processors = processors, action = m_Name, id = Guid.NewGuid(), }, }; m_BindingsStartIndex = 0; m_BindingsCount = 1; } else { m_Interactions = interactions; m_Processors = processors; } m_ExpectedControlType = expectedControlType; m_Id = Guid.NewGuid().ToString(); } /// /// Release internal state held on to by the action. /// /// /// Once enabled, actions will allocate a block of state internally that they will hold on to /// until disposed of. For free-standing actions, that state is private to just the action. /// For actions that are part of s, the state is shared by all /// actions in the map and, if the map itself is part of an , /// also by all the maps that are part of the asset. /// /// Note that the internal state holds on to GC heap memory as well as memory from the /// unmanaged, C++ heap. /// public void Dispose() { m_ActionMap?.m_State?.Dispose(); } /// /// Return a string version of the action. Mainly useful for debugging. /// /// A string version of the action. public override string ToString() { string str; if (m_Name == null) str = ""; else if (m_ActionMap != null && !isSingletonAction && !string.IsNullOrEmpty(m_ActionMap.name)) str = $"{m_ActionMap.name}/{m_Name}"; else str = m_Name; var controls = this.controls; if (controls.Count > 0) { str += "["; var isFirst = true; foreach (var control in controls) { if (!isFirst) str += ","; str += control.path; isFirst = false; } str += "]"; } return str; } /// /// Enable the action such that it actively listens for input and runs callbacks /// in response. /// /// /// If the action is already enabled, this method does nothing. /// /// By default, actions start out disabled, i.e. with being false. /// When enabled, two things happen. /// /// First, if it hasn't already happened, an action will resolve all of its bindings /// to s. This also happens if, since the action was last enabled, /// the setup of devices in the system has changed such that it may impact the action. /// /// Second, for all the bound to an action, change monitors (see /// ) will be added to the system. If any of the /// controls changes state in the future, the action will get notified and respond. /// /// type actions will also perform an initial state /// check in the input system update following the call to Enable. This means that if /// any of the bound controls are already actuated and produce a non-default value, /// the action will immediately trigger in response. /// /// Note that this method only enables a single action. This is also allowed for action /// that are part of an . To enable all actions in a map, /// call . /// /// The associated with an action (if any), will immediately /// toggle to being enabled (see ) as soon as the first /// action in the map is enabled and for as long as any action in the map is still enabled. /// /// The first time an action is enabled, it will allocate a block of state internally that it /// will hold on to until disposed of. For free-standing actions, that state is private to /// just the action. For actions that are part of s, the state /// is shared by all actions in the map and, if the map itself is part of an , also by all the maps that are part of the asset. /// /// To dispose of the state, call . /// /// /// /// var gamepad = InputSystem.AddDevice<Gamepad>(); /// /// var action = new InputAction(type: InputActionType.Value, binding: "<Gamepad>/leftTrigger"); /// action.performed = ctx => Debug.Log("Action triggered!"); /// /// // Perform some fake input on the gamepad. Note that the action /// // will *NOT* get triggered as it is not enabled. /// // NOTE: We use Update() here only for demonstration purposes. In most cases, /// // it's not a good method to call directly as it basically injects artificial /// // input frames into the player loop. Usually a recipe for breakage. /// InputSystem.QueueStateEvent(gamepad, new GamepadState { leftTrigger = 0.5f }); /// InputSystem.Update(); /// /// action.Enable(); /// /// // Now, with the left trigger already being down and the action enabled, it will /// // trigger in the next frame. /// InputSystem.Update(); /// /// /// /// /// public void Enable() { if (enabled) return; // For singleton actions, we create an internal-only InputActionMap // private to the action. var map = GetOrCreateActionMap(); // First time we're enabled, find all controls. map.ResolveBindingsIfNecessary(); // Go live. map.m_State.EnableSingleAction(this); } /// /// Disable the action such that is stop listening/responding to input. /// /// /// If the action is already disabled, this method does nothing. /// /// If the action is currently in progress, i.e. if is /// , the action will be canceled as /// part of being disabled. This means that you will see a call on /// from within the call to Disable(). /// /// /// public void Disable() { if (!enabled) return; m_ActionMap.m_State.DisableSingleAction(this); } ////REVIEW: is *not* cloning IDs here really the right thing to do? /// /// Return an identical instance of the action. /// /// An identical clone of the action /// /// Note that if you clone an action that is part of an , /// you will not get a new action that is part of the same map. Instead, you will /// get a free-standing action not associated with any action map. /// /// Also, note that the of the action is not cloned. Instead, the /// clone will receive a new unique ID. Also, callbacks install on events such /// as will not be copied over to the clone. /// public InputAction Clone() { var clone = new InputAction(name: m_Name, type: m_Type) { m_SingletonActionBindings = bindings.ToArray(), m_BindingsCount = m_BindingsCount, m_ExpectedControlType = m_ExpectedControlType, m_Interactions = m_Interactions, m_Processors = m_Processors, m_Flags = m_Flags, }; return clone; } /// /// Return an boxed instance of the action. /// /// An boxed clone of the action /// object ICloneable.Clone() { return Clone(); } ////TODO: ReadValue(void*, int) /// /// Read the current value of the control that is driving this action. If no bound control is actuated, returns /// default(TValue), but note that binding processors are always applied. /// /// Value type to read. Must match the value type of the binding/control that triggered. /// The current value of the control/binding that is driving this action with all binding processors applied. /// /// This method can be used as an alternative to hooking into , , /// and/or and reading out the value using /// there. Instead, this API acts more like a polling API that can be called, for example, as part of /// MonoBehaviour.Update. /// /// /// /// // Let's say you have a MyControls.inputactions file with "Generate C# Class" enabled /// // and it has an action map called "gameplay" with a "move" action of type Vector2. /// public class MyBehavior : MonoBehaviour /// { /// public MyControls controls; /// public float moveSpeed = 4; /// /// protected void Awake() /// { /// controls = new MyControls(); /// } /// /// protected void OnEnable() /// { /// controls.gameplay.Enable(); /// } /// /// protected void OnDisable() /// { /// controls.gameplay.Disable(); /// } /// /// protected void Update() /// { /// var moveVector = controls.gameplay.move.ReadValue<Vector2>() * (moveSpeed * Time.deltaTime); /// //... /// } /// } /// /// /// /// If the action has button-like behavior, then is usually a better alternative to /// reading out a float and checking if it is above the button press point. /// /// The given type does not match /// the value type of the control or composite currently driving the action. /// /// /// public unsafe TValue ReadValue() where TValue : struct { var state = GetOrCreateActionMap().m_State; if (state == null) return default(TValue); var actionStatePtr = &state.actionStates[m_ActionIndexInState]; return actionStatePtr->phase.IsInProgress() ? state.ReadValue(actionStatePtr->bindingIndex, actionStatePtr->controlIndex) : state.ApplyProcessors(actionStatePtr->bindingIndex, default(TValue)); } /// /// Same as but read the value without having to know the value type /// of the action. /// /// The current value of the action or null if the action is not currently in /// or phase. /// /// This method allocates GC memory and is thus not a good choice for getting called as part of gameplay /// logic. /// /// /// public unsafe object ReadValueAsObject() { var state = GetOrCreateActionMap().m_State; if (state == null) return null; var actionStatePtr = &state.actionStates[m_ActionIndexInState]; if (actionStatePtr->phase.IsInProgress()) { var controlIndex = actionStatePtr->controlIndex; if (controlIndex != InputActionState.kInvalidIndex) return state.ReadValueAsObject(actionStatePtr->bindingIndex, controlIndex); } return null; } /// /// Read the current amount of actuation of the control that is driving this action. /// /// Returns the current level of control actuation (usually [0..1]) or -1 if /// the control is actuated but does not support computing magnitudes. /// /// Magnitudes do not make sense for all types of controls. Controls that have no meaningful magnitude /// will return -1 when calling this method. Any negative magnitude value should be considered an invalid value. ///
/// The magnitude returned by an action is usually determined by the /// that triggered the action, i.e. by the /// control referenced from . ///
/// However, if the binding that triggered is a composite, then the composite /// will determine the magnitude and not the individual control that triggered. /// Instead, the value of the control that triggered the action will be fed into the composite magnitude calculation. ///
/// /// public unsafe float GetControlMagnitude() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; if (actionStatePtr->haveMagnitude) return actionStatePtr->magnitude; } return 0f; } /// /// Reset the action state to default. /// /// /// This method can be used to forcibly cancel an action even while it is in progress. Note that unlike /// disabling an action, for example, this also effects APIs such as . /// /// Note that invoking this method will not modify enabled state. /// /// /// /// /// public void Reset() { var state = GetOrCreateActionMap().m_State; state?.ResetActionState(m_ActionIndexInState, toPhase: enabled ? InputActionPhase.Waiting : InputActionPhase.Disabled, hardReset: true); } /// /// Check whether the current actuation of the action has crossed the button press threshold (see /// ) and has not yet fallen back below the /// release threshold (see ). /// /// True if the action is considered to be in "pressed" state, false otherwise. /// /// This method is different from simply reading the action's current float value and comparing /// it to the press threshold and is also different from comparing the current actuation of /// to it. This is because the current level of actuation might have already /// fallen below the press threshold but might not yet have reached the release threshold. /// /// This method works with any of action, not just buttons. /// /// Also note that because this operates on the results of , /// it works with many kind of controls, not just buttons. For example, if an action is bound /// to a , the control will be considered "pressed" once the magnitude /// of the Vector2 of the control has crossed the press threshold. /// /// Finally, note that custom button press points of controls (see ) /// are respected and will take precedence over . /// /// /// /// var up = playerInput.actions["up"]; /// if (up.IsPressed()) /// transform.Translate(0, 10 * Time.deltaTime, 0); /// /// /// /// Disabled actions will always return false from this method, even if a control bound to the action /// is currently pressed. Also, re-enabling an action will not restore the state to when the action /// was disabled even if the control is still actuated. /// /// /// /// /// /// public unsafe bool IsPressed() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; return actionStatePtr->isPressed; } return false; } /// /// Whether the action has been or . /// /// True if the action is currently triggering. /// public unsafe bool IsInProgress() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; return actionStatePtr->phase.IsInProgress(); } return false; } /// /// Returns true if the action's value crossed the press threshold (see ) /// at any point in the frame. /// /// True if the action was pressed this frame. /// /// This method is different from in that it is not bound /// to . Instead, if the action's level of actuation (that is, the level of /// magnitude -- see -- of the control(s) bound /// to the action) crossed the press threshold (see ) /// at any point in the frame, this method will return true. It will do so even if there is an /// interaction on the action that has not yet performed the action in response to the press. /// /// This method works with any of action, not just buttons. /// /// Also note that because this operates on the results of , /// it works with many kind of controls, not just buttons. For example, if an action is bound /// to a , the control will be considered "pressed" once the magnitude /// of the Vector2 of the control has crossed the press threshold. /// /// Finally, note that custom button press points of controls (see ) /// are respected and will take precedence over . /// /// /// /// var fire = playerInput.actions["fire"]; /// if (fire.WasPressedThisFrame() && fire.IsPressed()) /// StartFiring(); /// else if (fire.WasReleasedThisFrame()) /// StopFiring(); /// /// /// /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. /// /// /// /// /// public unsafe bool WasPressedThisFrame() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; return actionStatePtr->pressedInUpdate == currentUpdateStep && currentUpdateStep != default; } return false; } /// /// Returns true if the action's value crossed the release threshold (see ) /// at any point in the frame after being in pressed state. /// /// True if the action was released this frame. /// /// This method works with any of action, not just buttons. /// /// Also note that because this operates on the results of , /// it works with many kind of controls, not just buttons. For example, if an action is bound /// to a , the control will be considered "pressed" once the magnitude /// of the Vector2 of the control has crossed the press threshold. /// /// Finally, note that custom button press points of controls (see ) /// are respected and will take precedence over . /// /// /// /// var fire = playerInput.actions["fire"]; /// if (fire.WasPressedThisFrame() && fire.IsPressed()) /// StartFiring(); /// else if (fire.WasReleasedThisFrame()) /// StopFiring(); /// /// /// /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. /// /// /// /// /// public unsafe bool WasReleasedThisFrame() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; return actionStatePtr->releasedInUpdate == currentUpdateStep && currentUpdateStep != default; } return false; } ////REVIEW: Should we also have WasStartedThisFrame()? (and WasCanceledThisFrame()?) /// /// Check whether was at any point /// in the current frame. /// /// True if the action performed this frame. /// /// This method is different from in that it depends directly on the /// interaction(s) driving the action (including the default interaction if no specific interaction /// has been added to the action or binding). /// /// For example, let's say the action is bound to the space bar and that the binding has a /// assigned to it. In the frame where the space bar /// is pressed, will be true (because the button/key is now pressed) /// but WasPerformedThisFrame will still be false (because the hold has not been performed yet). /// Only after the hold time has expired will WasPerformedThisFrame be true and only in the frame /// where the hold performed. /// /// This is different from checking directly as the action might have already progressed /// to a different phase after performing. In other words, even if an action performed in a frame, /// might no longer be , whereas WasPerformedThisFrame will remain /// true for the entirety of the frame regardless of what else the action does. /// /// Unlike , which will reset when the action goes back to waiting /// state, this property will stay true for the duration of the current frame (that is, until the next /// runs) as long as the action was triggered at least once. /// /// /// /// var warp = playerInput.actions["Warp"]; /// if (warp.WasPerformedThisFrame()) /// InitiateWarp(); /// /// /// /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. /// /// /// /// public unsafe bool WasPerformedThisFrame() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; return actionStatePtr->lastPerformedInUpdate == currentUpdateStep && currentUpdateStep != default; } return false; } /// /// Check whether transitioned from to any other phase /// value at least once in the current frame. /// /// True if the action completed this frame. /// /// Although is technically a phase, this method does not consider disabling /// the action while the action is in to be "completed". /// /// This method is different from in that it depends directly on the /// interaction(s) driving the action (including the default interaction if no specific interaction /// has been added to the action or binding). /// /// For example, let's say the action is bound to the space bar and that the binding has a /// assigned to it. In the frame where the space bar /// is pressed, will be true (because the button/key is now pressed) /// but will still be false (because the hold has not been performed yet). /// If at that time the space bar is released, will be true (because the /// button/key is now released) but WasCompletedThisFrame will still be false (because the hold /// had not been performed yet). If instead the space bar is held down for long enough for the hold interaction, /// the phase will change to and stay and /// will be true for one frame as it meets the duration threshold. Once released, WasCompletedThisFrame will be true /// (because the action is no longer performed) and only in the frame where the hold transitioned away from Performed. /// /// For another example where the action could be considered pressed but also completed, let's say the action /// is bound to the thumbstick and that the binding has a Sector interaction from the XR Interaction Toolkit assigned /// to it such that it only performs in the forward sector area past a button press threshold. In the frame where the /// thumbstick is pushed forward, both will be true (because the thumbstick actuation is /// now considered pressed) and will be true (because the thumbstick is in /// the forward sector). If the thumbstick is then moved to the left in a sweeping motion, /// will still be true. However, WasCompletedThisFrame will also be true (because the thumbstick is /// no longer in the forward sector while still crossed the button press threshold) and only in the frame where /// the thumbstick was no longer within the forward sector. For more details about the Sector interaction, see /// SectorInteraction /// in the XR Interaction Toolkit Scripting API documentation. ///
/// Unlike , which will reset when the action goes back to waiting /// state, this property will stay true for the duration of the current frame (that is, until the next /// runs) as long as the action was completed at least once. /// /// /// /// var teleport = playerInput.actions["Teleport"]; /// if (teleport.WasPerformedThisFrame()) /// InitiateTeleport(); /// else if (teleport.WasCompletedThisFrame()) /// StopTeleport(); /// /// /// /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. ///
/// /// /// public unsafe bool WasCompletedThisFrame() { var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; return actionStatePtr->lastCompletedInUpdate == currentUpdateStep && currentUpdateStep != default; } return false; } /// /// Return the completion percentage of the timeout (if any) running on the current interaction. /// /// A value >= 0 (no progress) and <= 1 (finished) indicating the level of completion /// of the currently running timeout. /// /// This method is useful, for example, when providing UI feedback for an ongoing action. If, say, /// you have a on a binding, you might want to show a /// progress indicator in the UI and need to know how far into the hold the action /// current is. Once the hold has been started, this method will return how far into the hold /// the action currently is. /// /// Note that if an interaction performs and stays performed (see ), /// the completion percentage will remain at 1 until the interaction is canceled. /// /// Also note that completion is based on the progression of time and not dependent on input /// updates. This means that if, for example, the timeout for a /// has expired according the current time but the expiration has not yet been processed by /// an input update (thus causing the hold to perform), the returned completion percentage /// will still be 1. In other words, there isn't always a correlation between the current /// completion percentage and . /// /// The meaning of the timeout is dependent on the interaction in play. For a , /// "completion" represents the duration timeout (that is, the time until a "hold" is considered to be performed), whereas /// for a "completion" represents "time to failure" (that is, the remaining time window /// that the interaction can be completed within). /// /// Note that an interaction might run multiple timeouts in succession. One such example is . /// In this case, progression towards a single timeout does not necessarily mean progression towards completion /// of the whole interaction. An interaction can call /// to inform the Input System of the total length of timeouts to run. If this is done, the result of the /// GetTimeoutCompletionPercentage method will return a value reflecting the progression with respect /// to total time. /// /// /// /// // Scale a UI element in response to the completion of a hold on the gamepad's A button. /// /// Transform uiObjectToScale; /// /// InputAction holdAction; /// /// void OnEnable() /// { /// if (holdAction == null) /// { /// // Create hold action with a 2 second timeout. /// // NOTE: Here we create the action in code. You can, of course, grab the action from an .inputactions /// // asset created in the editor instead. /// holdAction = new InputAction(type: InputActionType.Button, interactions: "hold(duration=2)"); /// /// // Show the UI object when the hold starts and hide it when it ends. /// holdAction.started += _ => uiObjectToScale.SetActive(true); /// holdAction.canceled += _ => uiObjectToScale.SetActive(false); /// /// // If you want to play a visual effect when the action performs, you can initiate from /// // the performed callback. /// holdAction.performed += _ => /* InitiateVisualEffectWhenHoldIsComplete() */; /// } /// /// holdAction.Enable(); /// /// // Hide the UI object until the action is started. /// uiObjectToScale.gameObject.SetActive(false); /// } /// /// void OnDisable() /// { /// holdAction.Disable(); /// } /// /// void Update() /// { /// var completion = holdAction.GetTimeoutCompletionPercentage(); /// uiObjectToScale.localScale = new Vector3(1, completion, 1); /// } /// /// /// /// /// /// public unsafe float GetTimeoutCompletionPercentage() { var actionMap = GetOrCreateActionMap(); var state = actionMap.m_State; // If there's no state, there can't be activity on the action so our completion // percentage must be zero. if (state == null) return 0; ref var actionState = ref state.actionStates[m_ActionIndexInState]; var interactionIndex = actionState.interactionIndex; if (interactionIndex == -1) { ////REVIEW: should this use WasPerformedThisFrame()? // There's no interactions on the action or on the currently active binding, so go // entirely by the current phase. Performed is 100%, everything else is 0%. return actionState.phase == InputActionPhase.Performed ? 1 : 0; } ref var interactionState = ref state.interactionStates[interactionIndex]; switch (interactionState.phase) { case InputActionPhase.Started: // If the interaction was started and there is a timer running, the completion level // is determined by far we are between the interaction start time and timer expiration. var timerCompletion = 0f; if (interactionState.isTimerRunning) { var duration = interactionState.timerDuration; var startTime = interactionState.timerStartTime; var endTime = startTime + duration; var remainingTime = endTime - InputState.currentTime; if (remainingTime <= 0) timerCompletion = 1; else timerCompletion = (float)((duration - remainingTime) / duration); } if (interactionState.totalTimeoutCompletionTimeRemaining > 0) { return (interactionState.totalTimeoutCompletionDone + timerCompletion * interactionState.timerDuration) / (interactionState.totalTimeoutCompletionDone + interactionState.totalTimeoutCompletionTimeRemaining); } else { return timerCompletion; } case InputActionPhase.Performed: return 1; } return 0; } ////REVIEW: it would be best if these were InternedStrings; however, for serialization, it has to be strings [Tooltip("Human readable name of the action. Must be unique within its action map (case is ignored). Can be changed " + "without breaking references to the action.")] [SerializeField] internal string m_Name; [Tooltip("Determines how the action triggers.\n" + "\n" + "A Value action will start and perform when a control moves from its default value and then " + "perform on every value change. It will cancel when controls go back to default value. Also, when enabled, a Value " + "action will respond right away to a control's current value.\n" + "\n" + "A Button action will start when a button is pressed and perform when the press threshold (see 'Default Button Press Point' in settings) " + "is reached. It will cancel when the button is going below the release threshold (see 'Button Release Threshold' in settings). Also, " + "if a button is already pressed when the action is enabled, the button has to be released first.\n" + "\n" + "A Pass-Through action will not explicitly start and will never cancel. Instead, for every value change on any bound control, " + "the action will perform.")] [SerializeField] internal InputActionType m_Type; [FormerlySerializedAs("m_ExpectedControlLayout")] [Tooltip("The type of control expected by the action (e.g. \"Button\" or \"Stick\"). This will limit the controls shown " + "when setting up bindings in the UI and will also limit which controls can be bound interactively to the action.")] [SerializeField] internal string m_ExpectedControlType; [Tooltip("Unique ID of the action (GUID). Used to reference the action from bindings such that actions can be renamed " + "without breaking references.")] [SerializeField] internal string m_Id; // Can't serialize System.Guid and Unity's GUID is editor only. [SerializeField] internal string m_Processors; [SerializeField] internal string m_Interactions; // For singleton actions, we serialize the bindings directly as part of the action. // For any other type of action, this is null. [SerializeField] internal InputBinding[] m_SingletonActionBindings; [SerializeField] internal ActionFlags m_Flags; [NonSerialized] internal InputBinding? m_BindingMask; [NonSerialized] internal int m_BindingsStartIndex; [NonSerialized] internal int m_BindingsCount; [NonSerialized] internal int m_ControlStartIndex; [NonSerialized] internal int m_ControlCount; /// /// Index of the action in the associated with the /// action's . /// /// /// This is not necessarily the same as the index of the action in its map. /// /// [NonSerialized] internal int m_ActionIndexInState = InputActionState.kInvalidIndex; /// /// The action map that owns the action. /// /// /// This is not serialized. The action map will restore this back references after deserialization. /// [NonSerialized] internal InputActionMap m_ActionMap; // Listeners. No array allocations if only a single listener. [NonSerialized] internal CallbackArray> m_OnStarted; [NonSerialized] internal CallbackArray> m_OnCanceled; [NonSerialized] internal CallbackArray> m_OnPerformed; /// /// Whether the action is a loose action created in code (e.g. as a property on a component). /// /// /// Singleton actions are not contained in maps visible to the user. Internally, we do create /// a map for them that contains just the singleton action. To the action system, there are no /// actions without action maps. /// internal bool isSingletonAction => m_ActionMap == null || ReferenceEquals(m_ActionMap.m_SingletonAction, this); [Flags] internal enum ActionFlags { WantsInitialStateCheck = 1 << 0, } private InputActionState.TriggerState currentState { get { if (m_ActionIndexInState == InputActionState.kInvalidIndex) return new InputActionState.TriggerState(); Debug.Assert(m_ActionMap != null, "Action must have associated action map"); Debug.Assert(m_ActionMap.m_State != null, "Action map must have state at this point"); return m_ActionMap.m_State.FetchActionState(this); } } internal string MakeSureIdIsInPlace() { if (string.IsNullOrEmpty(m_Id)) GenerateId(); return m_Id; } internal void GenerateId() { m_Id = Guid.NewGuid().ToString(); } internal InputActionMap GetOrCreateActionMap() { if (m_ActionMap == null) CreateInternalActionMapForSingletonAction(); return m_ActionMap; } private void CreateInternalActionMapForSingletonAction() { m_ActionMap = new InputActionMap { m_Actions = new[] { this }, m_SingletonAction = this, m_Bindings = m_SingletonActionBindings }; } internal void RequestInitialStateCheckOnEnabledAction() { Debug.Assert(enabled, "This should only be called on actions that are enabled"); var map = GetOrCreateActionMap(); var state = map.m_State; state.SetInitialStateCheckPending(m_ActionIndexInState); } // NOTE: This does *NOT* check whether the control is valid according to the binding it // resolved from and/or the current binding mask. If, for example, the binding is // "/#(รค)" and the keyboard switches from a DE layout to a US layout, the // key would still be considered valid even if the path in the binding would actually // no longer resolve to it. internal bool ActiveControlIsValid(InputControl control) { if (control == null) return false; // Device must still be added. var device = control.device; if (!device.added) return false; // If we have a device list in the map or asset, device // must be in list. var map = GetOrCreateActionMap(); var deviceList = map.devices; if (deviceList != null && !deviceList.Value.ContainsReference(device)) return false; return true; } internal InputBinding? FindEffectiveBindingMask() { if (m_BindingMask.HasValue) return m_BindingMask; if (m_ActionMap?.m_BindingMask != null) return m_ActionMap.m_BindingMask; return m_ActionMap?.m_Asset?.m_BindingMask; } internal int BindingIndexOnActionToBindingIndexOnMap(int indexOfBindingOnAction) { // We don't want to hit InputAction.bindings here as this requires setting up per-action // binding info which we then nuke as part of the override process. Calling ApplyBindingOverride // repeatedly with an index would thus cause the same data to be computed and thrown away // over and over. // Instead we manually search through the map's bindings to find the right binding index // in the map. var actionMap = GetOrCreateActionMap(); var bindingsInMap = actionMap.m_Bindings; var bindingCountInMap = bindingsInMap.LengthSafe(); var actionName = name; var currentBindingIndexOnAction = -1; for (var i = 0; i < bindingCountInMap; ++i) { ref var binding = ref bindingsInMap[i]; if (!binding.TriggersAction(this)) continue; ++currentBindingIndexOnAction; if (currentBindingIndexOnAction == indexOfBindingOnAction) return i; } throw new ArgumentOutOfRangeException(nameof(indexOfBindingOnAction), $"Binding index {indexOfBindingOnAction} is out of range for action '{this}' with {currentBindingIndexOnAction + 1} bindings"); } internal int BindingIndexOnMapToBindingIndexOnAction(int indexOfBindingOnMap) { var actionMap = GetOrCreateActionMap(); var bindingsInMap = actionMap.m_Bindings; var actionName = name; var bindingIndexOnAction = 0; for (var i = indexOfBindingOnMap - 1; i >= 0; --i) { ref var binding = ref bindingsInMap[i]; if (string.Compare(binding.action, actionName, StringComparison.InvariantCultureIgnoreCase) == 0 || binding.action == m_Id) ++bindingIndexOnAction; } return bindingIndexOnAction; } ////TODO: make current event available in some form ////TODO: make source binding info available (binding index? binding instance?) /// /// Information provided to action callbacks about what triggered an action. /// /// /// This struct should not be held on to past the duration of the callback. /// /// /// /// /// public struct CallbackContext // Ideally would be a ref struct but couldn't use it in lambdas then. { internal InputActionState m_State; internal int m_ActionIndex; ////REVIEW: there should probably be a mechanism for the user to be able to correlate //// the callback to a specific binding on the action private int actionIndex => m_ActionIndex; private unsafe int bindingIndex => m_State.actionStates[actionIndex].bindingIndex; private unsafe int controlIndex => m_State.actionStates[actionIndex].controlIndex; private unsafe int interactionIndex => m_State.actionStates[actionIndex].interactionIndex; /// /// Current phase of the action. Equivalent to accessing /// on . /// /// Current phase of the action. /// /// /// /// public unsafe InputActionPhase phase { get { if (m_State == null) return InputActionPhase.Disabled; return m_State.actionStates[actionIndex].phase; } } /// /// Whether the has just been started. /// /// If true, the action was just started. /// public bool started => phase == InputActionPhase.Started; /// /// Whether the has just been performed. /// /// If true, the action was just performed. /// public bool performed => phase == InputActionPhase.Performed; /// /// Whether the has just been canceled. /// /// If true, the action was just canceled. /// public bool canceled => phase == InputActionPhase.Canceled; /// /// The action that got triggered. /// /// Action that got triggered. public InputAction action => m_State?.GetActionOrNull(bindingIndex); /// /// The control that triggered the action. /// /// Control that triggered the action. /// /// In case of a composite binding, this is the control of the composite that activated the /// composite as a whole. For example, in case of a WASD-style binding, it could be the W key. /// /// Note that an action may also change its in response to a timeout. /// For example, a will cancel itself if the /// button control is not released within a certain time. When this happens, the control /// property will be the control that last fed input into the action. /// /// /// public InputControl control => m_State?.controls[controlIndex]; /// /// The interaction that triggered the action or null if the binding that triggered does not /// have any particular interaction set on it. /// /// Interaction that triggered the callback. /// /// /// /// void FirePerformed(InputAction.CallbackContext context) /// { /// // If SlowTap interaction was performed, perform a charged /// // firing. Otherwise, fire normally. /// if (context.interaction is SlowTapInteraction) /// FireChargedProjectile(); /// else /// FireNormalProjectile(); /// } /// /// /// /// /// public IInputInteraction interaction { get { if (m_State == null) return null; var index = interactionIndex; if (index == InputActionState.kInvalidIndex) return null; return m_State.interactions[index]; } } /// /// The time at which the action got triggered. /// /// Time relative to Time.realtimeSinceStartup at which /// the action got triggered. /// /// This is usually determined by the timestamp of the input event that activated a control /// bound to the action. What this means is that this is normally not the /// value of Time.realtimeSinceStartup when the input system calls the /// callback but rather the time at which the input was generated that triggered /// the action. /// /// public unsafe double time { get { if (m_State == null) return 0; return m_State.actionStates[actionIndex].time; } } /// /// Time at which the action was started. /// /// Value relative to Time.realtimeSinceStartup when the action /// changed to . /// /// This is only relevant for actions that go through distinct a /// cycle as driven by interactions. /// /// The value of this property is that of when was called. See the /// property for how the timestamp works. /// public unsafe double startTime { get { if (m_State == null) return 0; return m_State.actionStates[actionIndex].startTime; } } /// /// Time difference between and . /// /// Difference between and . /// /// This property can be used, for example, to determine how long a button /// was held down. /// /// /// /// // Let's create a button action bound to the A button /// // on the gamepad. /// var action = new InputAction( /// type: InputActionType.Button, /// binding: "<Gamepad>/buttonSouth"); /// /// // When the action is performed (which will happen when the /// // button is pressed and then released) we take the duration /// // of the press to determine how many projectiles to spawn. /// action.performed += /// context => /// { /// const float kSpawnRate = 3; // 3 projectiles per second /// var projectileCount = kSpawnRate * context.duration; /// for (var i = 0; i < projectileCount; ++i) /// { /// var projectile = UnityEngine.Object.Instantiate(projectile); /// // Apply other changes to the projectile... /// } /// }; /// /// /// public double duration => time - startTime; /// /// Type of value returned by and expected /// by . /// /// Type of object returned when reading a value. /// /// The type of value returned by an action is usually determined by the /// that triggered the action, i.e. by the /// control referenced from . /// /// However, if the binding that triggered is a composite, then the composite /// will determine values and not the individual control that triggered (that /// one just feeds values into the composite). /// /// /// /// public Type valueType => m_State?.GetValueType(bindingIndex, controlIndex); /// /// Size of values returned by . /// /// Size of value returned when reading. /// /// All input values passed around by the system are required to be "blittable", /// i.e. they cannot contain references, cannot be heap objects themselves, and /// must be trivially mem-copyable. This means that any value can be read out /// and retained in a raw byte buffer. /// /// The value of this property determines how many bytes will be written /// by . /// /// /// /// public int valueSizeInBytes { get { if (m_State == null) return 0; return m_State.GetValueSizeInBytes(bindingIndex, controlIndex); } } /// /// Read the value of the action as a raw byte buffer. This allows reading /// values without having to know value types but also, unlike , /// without allocating GC heap memory. /// /// Memory buffer to read the value into. /// Size of buffer allocated at . Must be /// at least . /// is null. /// is too small. /// /// /// /// // Read a Vector2 using the raw memory ReadValue API. /// // Here we just read into a local variable which we could /// // just as well (and more easily) do using ReadValue<Vector2>. /// // Still, it serves as a demonstration for how the API /// // operates in general. /// unsafe /// { /// var value = default(Vector2); /// var valuePtr = UnsafeUtility.AddressOf(ref value); /// context.ReadValue(buffer, UnsafeUtility.SizeOf<Vector2>()); /// } /// /// /// /// /// /// public unsafe void ReadValue(void* buffer, int bufferSize) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); if (m_State != null && phase.IsInProgress()) { m_State.ReadValue(bindingIndex, controlIndex, buffer, bufferSize); } else { var valueSize = valueSizeInBytes; if (bufferSize < valueSize) throw new ArgumentException( $"Expected buffer of at least {valueSize} bytes but got buffer of only {bufferSize} bytes", nameof(bufferSize)); UnsafeUtility.MemClear(buffer, valueSizeInBytes); } } /// /// Read the value of the action. /// /// Type of value to read. This must correspond to the /// expected by either or, if it is a composite, by the /// in use. /// The value read from the action. /// The given type /// does not match the value type expected by the control or binding composite. /// /// /// public TValue ReadValue() where TValue : struct { var value = default(TValue); if (m_State != null) { value = phase.IsInProgress() ? m_State.ReadValue(bindingIndex, controlIndex) : m_State.ApplyProcessors(bindingIndex, value); } return value; } /// /// Read the current value of the action as a float and return true if it is equal to /// or greater than the button press threshold. /// /// True if the action is considered in "pressed" state, false otherwise. /// /// If the currently active control is a , the /// of the button will be taken into account (if set). If there is no custom button press point, the /// global will be used. /// /// /// public bool ReadValueAsButton() { var value = false; if (m_State != null && phase.IsInProgress()) value = m_State.ReadValueAsButton(bindingIndex, controlIndex); return value; } /// /// Same as except that it is not necessary to /// know the type of value at compile time. /// /// The current value from the binding that triggered the action or null if the action /// is not currently in progress. /// /// This method allocates GC heap memory. Using it during normal gameplay will lead /// to frame-rate instabilities. /// /// /// public object ReadValueAsObject() { if (m_State != null && phase.IsInProgress()) return m_State.ReadValueAsObject(bindingIndex, controlIndex); return null; } /// /// Return a string representation of the context useful for debugging. /// /// String representation of the context. public override string ToString() { return $"{{ action={action} phase={phase} time={time} control={control} value={ReadValueAsObject()} interaction={interaction} }}"; } } } }