using System; 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: 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: have single delegate instead of separate performed/started/canceled callbacks? ////REVIEW: Do we need to have separate display names for actions? ////TODO: allow changing bindings without having to disable ////REVIEW: what about having the concept of "consumed" on the callback context? ////REVIEW: have "Always Enabled" toggle on actions? 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 /// (i.e. 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(); } } /// /// 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. /// 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). /// Also, interactions can choose their /// /// While an action is disabled, its phase is . /// public InputActionPhase phase => currentState.phase; /// /// 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.Append(value); remove => m_OnStarted.Remove(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.Append(value); remove => m_OnCanceled.Remove(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.Append(value); remove => m_OnPerformed.Remove(value); } /// /// Whether the action was triggered (i.e. had called) this frame. /// /// /// Unlike , which will reset when the action goes back to waiting /// state, this property will stay true for the duration of the current frame (i.e. until the next /// runs) as long as the action was triggered at least once. /// /// /// /// if (myControls.gameplay.fire.triggered) /// Fire(); /// /// /// /// /// public unsafe bool triggered { get { var map = GetOrCreateActionMap(); if (map.m_State == null) return false; var lastTriggeredInUpdate = map.m_State.actionStates[m_ActionIndexInState].lastTriggeredInUpdate; return lastTriggeredInUpdate != 0 && lastTriggeredInUpdate == InputUpdate.s_UpdateStepCount; } } /// /// The currently active control that is driving the action. Null 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; } } /// /// Whether the action wants a state check on its bound controls as soon as it is enabled. /// internal bool wantsInitialStateCheck => type == InputActionType.Value; /// /// 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() { } /// /// 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 } }; m_BindingsStartIndex = 0; m_BindingsCount = 1; } else { m_Interactions = interactions; m_Processors = processors; } m_ExpectedControlType = expectedControlType; } /// /// 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, }; return clone; } object ICloneable.Clone() { return Clone(); } ////TODO: ReadValue(void*, int) /// /// Read the current value of the action. This is the last value received on , /// or . If the action is in canceled or waiting phase, returns default(TValue). /// /// Value type to read. Must match the value type of the binding/control that triggered. /// The current value of the action or default(TValue) if the action is not currently in-progress. /// /// 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 result = default(TValue); var state = GetOrCreateActionMap().m_State; if (state != null) { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var controlIndex = actionStatePtr->controlIndex; if (controlIndex != InputActionState.kInvalidIndex) result = state.ReadValue(actionStatePtr->bindingIndex, controlIndex); } return result; } /// /// 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]; var controlIndex = actionStatePtr->controlIndex; if (controlIndex != InputActionState.kInvalidIndex) return state.ReadValueAsObject(actionStatePtr->bindingIndex, controlIndex); return null; } ////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; [SerializeField] internal InputActionType m_Type; [FormerlySerializedAs("m_ExpectedControlLayout")] [Tooltip("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; [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 InlinedArray> m_OnStarted; [NonSerialized] internal InlinedArray> m_OnCanceled; [NonSerialized] internal InlinedArray> 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); private InputActionState.TriggerState currentState { get { if (m_ActionIndexInState == InputActionState.kInvalidIndex) return new InputActionState.TriggerState(); Debug.Assert(m_ActionMap != null); Debug.Assert(m_ActionMap.m_State != null); 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 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]; // Match both name and ID on binding. if (string.Compare(binding.action, actionName, StringComparison.InvariantCultureIgnoreCase) != 0 && binding.action != m_Id) 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 /// /// 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); } } ////TODO: need ability to read as button /// /// 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)); m_State?.ReadValue(bindingIndex, controlIndex, buffer, bufferSize); } /// /// 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 && phase != InputActionPhase.Canceled) value = m_State.ReadValue(bindingIndex, controlIndex); 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 != InputActionPhase.Canceled) 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. /// /// This method allocates GC heap memory. Using it during normal gameplay will lead /// to frame-rate instabilities. /// /// public object ReadValueAsObject() { return m_State?.ReadValueAsObject(bindingIndex, controlIndex); } /// /// 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} }}"; } } } }