using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine.InputSystem.Utilities; ////TODO: make the FindAction logic available on any IEnumerable<InputAction> and IInputActionCollection via extension methods ////TODO: control schemes, like actions and maps, should have stable IDs so that they can be renamed ////REVIEW: have some way of expressing 'contracts' on action maps? I.e. something like //// "I expect a 'look' and a 'move' action in here" ////REVIEW: rename this from "InputActionAsset" to something else that emphasizes the asset aspect less //// and instead emphasizes the map collection aspect more? namespace UnityEngine.InputSystem { /// <summary> /// An asset that contains action maps and control schemes. /// </summary> /// <remarks> /// InputActionAssets can be created in code but are usually stored in JSON format on /// disk with the ".inputactions" extension. Unity imports them with a custom /// importer. /// /// To create an InputActionAsset in code, use the <c>Singleton</c> API and populate the /// asset with the methods found in <see cref="InputActionSetupExtensions"/>. Alternatively, /// you can use <see cref="FromJson"/> to load an InputActionAsset directly from a string in JSON format. /// /// <example> /// <code> /// // Create and configure an asset in code. /// var asset1 = ScriptableObject.CreateInstance<InputActionAsset>(); /// var actionMap1 = asset1.AddActionMap("map1"); /// action1Map.AddAction("action1", binding: "<Keyboard>/space"); /// </code> /// </example> /// /// If you use the API to modify an InputActionAsset while in Play mode, /// it does not survive the transition back to Edit Mode. Unity tracks and reloads modified assets /// from disk when exiting Play mode. This is done so that you can realistically test the input /// related functionality of your application i.e. control rebinding etc, without inadvertently changing /// the input asset. /// /// Each asset can contain arbitrary many action maps that you can enable and disable individually /// (see <see cref="InputActionMap.Enable"/> and <see cref="InputActionMap.Disable"/>) or in bulk /// (see <see cref="Enable"/> and <see cref="Disable"/>). The name of each action map must be unique. /// The list of action maps can be queried from <see cref="actionMaps"/>. /// /// InputActionAssets can only define <see cref="InputControlScheme"/>s. They can be added to /// an asset with <see cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> /// and can be queried from <see cref="controlSchemes"/>. /// /// Be aware that input action assets do not separate between static (configuration) data and dynamic /// (instance) data. For audio, for example, <c>AudioClip</c> represents the static, /// shared data portion of audio playback whereas <c>AudioSource"</c> represents the /// dynamic, per-instance audio playback portion (referencing the clip through <c>AudioSource.clip</c>). /// /// For input, such a split is less beneficial as the same input is generally not exercised /// multiple times in parallel. Keeping both static and dynamic data together simplifies /// using the system. /// /// However, there are scenarios where you indeed want to take the same input action and /// exercise it multiple times in parallel. A prominent example of such a use case is /// local multiplayer where each player gets the same set of actions but is controlling /// them with a different device (or devices) each. This is easily achieved by simply /// using <c>UnityEngine.Object.Instantiate</c> to instantiate the input action /// asset multiple times. <see cref="PlayerInput"/> will automatically do so in its /// internals. /// /// Note also that all action maps in an asset share binding state. This means that if /// one map in an asset has to resolve its bindings, all maps in the asset have to. /// </remarks> public class InputActionAsset : ScriptableObject, IInputActionCollection2 { /// <summary> /// File extension (without the dot) for InputActionAssets in JSON format. /// </summary> /// <value>File extension for InputActionAsset source files.</value> /// <remarks> /// Files with this extension will automatically be imported by Unity as /// InputActionAssets. /// </remarks> public const string Extension = "inputactions"; /// <summary> /// True if any action in the asset is currently enabled. /// </summary> /// <seealso cref="InputAction.enabled"/> /// <seealso cref="InputActionMap.enabled"/> /// <seealso cref="InputAction.Enable"/> /// <seealso cref="InputActionMap.Enable"/> /// <seealso cref="Enable"/> public bool enabled { get { foreach (var actionMap in actionMaps) if (actionMap.enabled) return true; return false; } } /// <summary> /// List of action maps defined in the asset. /// </summary> /// <value>Action maps contained in the asset.</value> /// <seealso cref="InputActionSetupExtensions.AddActionMap(InputActionAsset,string)"/> /// <seealso cref="InputActionSetupExtensions.RemoveActionMap(InputActionAsset,InputActionMap)"/> /// <seealso cref="FindActionMap(string,bool)"/> public ReadOnlyArray<InputActionMap> actionMaps => new ReadOnlyArray<InputActionMap>(m_ActionMaps); /// <summary> /// List of control schemes defined in the asset. /// </summary> /// <value>Control schemes defined for the asset.</value> /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> /// <seealso cref="InputActionSetupExtensions.RemoveControlScheme"/> public ReadOnlyArray<InputControlScheme> controlSchemes => new ReadOnlyArray<InputControlScheme>(m_ControlSchemes); /// <summary> /// Iterate over all bindings in the asset. /// </summary> /// <remarks> /// This iterates over all action maps in <see cref="actionMaps"/> and, within each /// map, over the set of <see cref="InputActionMap.bindings"/>. /// </remarks> /// <seealso cref="InputActionMap.bindings"/> public IEnumerable<InputBinding> bindings { get { var numActionMaps = m_ActionMaps.LengthSafe(); if (numActionMaps == 0) yield break; for (var i = 0; i < numActionMaps; ++i) { var actionMap = m_ActionMaps[i]; var bindings = actionMap.m_Bindings; var numBindings = bindings.LengthSafe(); for (var n = 0; n < numBindings; ++n) yield return bindings[n]; } } } /// <summary> /// Binding mask to apply to all action maps and actions in the asset. /// </summary> /// <value>Optional mask that determines which bindings in the asset to enable.</value> /// <remarks> /// Binding masks can be applied at three different levels: for an entire asset through /// this property, for a specific map through <see cref="InputActionMap.bindingMask"/>, /// and for single actions through <see cref="InputAction.bindingMask"/>. By default, /// none of the masks will be set (i.e. they will be <c>null</c>). /// /// 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 <see cref="InputBinding.Matches"/>. /// /// Note that if you modify the masks applicable to an action while it is /// enabled, the action's <see cref="InputAction.controls"/> 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 <see cref="InputAction.bindings"/> /// and <see cref="InputActionMap.bindings"/> properties. /// </remarks> /// <seealso cref="InputBinding.MaskByGroup"/> /// <seealso cref="InputAction.bindingMask"/> /// <seealso cref="InputActionMap.bindingMask"/> public InputBinding? bindingMask { get => m_BindingMask; set { if (m_BindingMask == value) return; m_BindingMask = value; ReResolveIfNecessary(fullResolve: true); } } /// <summary> /// Set of devices that bindings in the asset can bind to. /// </summary> /// <value>Optional set of devices to use by bindings in the asset.</value> /// <remarks> /// By default (with this property being <c>null</c>), bindings will bind to any of the /// controls available through <see cref="InputSystem.devices"/>, i.e. controls from all /// devices in the system will be used. /// /// By setting this property, binding resolution can instead be restricted to just specific /// devices. This restriction can either be applied to an entire asset using this property /// or to specific action maps by using <see cref="InputActionMap.devices"/>. Note that if /// both this property and <see cref="InputActionMap.devices"/> is set for a specific action /// map, the list of devices on the action map will take precedence and the list on the /// asset will be ignored for bindings in that action map. /// /// <example> /// <code> /// // Create an asset with a single action map and a single action with a /// // gamepad binding. /// var asset = ScriptableObject.CreateInstance<InputActionAsset>(); /// var actionMap = new InputActionMap(); /// var fireAction = actionMap.AddAction("Fire", binding: "<Gamepad>/buttonSouth"); /// asset.AddActionMap(actionMap); /// /// // Let's assume we have two gamepads connected. If we enable the /// // action map now, the 'Fire' action will bind to both. /// actionMap.Enable(); /// /// // This will print two controls. /// Debug.Log(string.Join("\n", fireAction.controls)); /// /// // To restrict the setup to just the first gamepad, we can assign /// // to the 'devices' property (in this case, we could do so on either /// // the action map or on the asset; we choose the latter here). /// asset.devices = new InputDevice[] { Gamepad.all[0] }; /// /// // Now this will print only one control. /// Debug.Log(string.Join("\n", fireAction.controls)); /// </code> /// </example> /// </remarks> /// <seealso cref="InputActionMap.devices"/> public ReadOnlyArray<InputDevice>? devices { get => m_Devices.Get(); set { if (m_Devices.Set(value)) ReResolveIfNecessary(fullResolve: false); } } /// <summary> /// Look up an action by name or ID. /// </summary> /// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or /// a simple name. In the former case, the name is split at the '/' slash and the first part is used to find /// a map with that name and the second part is used to find an action with that name inside the map. In the /// latter case, all maps are searched in order and the first action that has the given name in any of the maps /// is returned. Note that name comparisons are case-insensitive. /// /// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param> /// <returns>The action with the corresponding name or null if no matching action could be found.</returns> /// <remarks> /// This method is equivalent to <see cref="FindAction(string,bool)"/> except that it throws /// <see cref="KeyNotFoundException"/> if no action with the given name or ID /// could be found. /// </remarks> /// <exception cref="KeyNotFoundException">No action was found matching <paramref name="actionNameOrId"/>.</exception> /// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c> or empty.</exception> /// <seealso cref="FindAction(string,bool)"/> public InputAction this[string actionNameOrId] { get { var action = FindAction(actionNameOrId); if (action == null) throw new KeyNotFoundException($"Cannot find action '{actionNameOrId}' in '{this}'"); return action; } } /// <summary> /// Return a JSON representation of the asset. /// </summary> /// <returns>A string in JSON format that represents the static/configuration data present /// in the asset.</returns> /// <remarks> /// This will not save dynamic execution state such as callbacks installed on /// <see cref="InputAction">actions</see> or enabled/disabled states of individual /// maps and actions. /// /// Use <see cref="LoadFromJson"/> to deserialize the JSON data back into an InputActionAsset. /// /// Be aware that the format used by this method is <em>different</em> than what you /// get if you call <c>JsonUtility.ToJson</c> on an InputActionAsset instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// </remarks> /// <seealso cref="FromJson"/> public string ToJson() { var fileJson = new WriteFileJson { name = name, maps = InputActionMap.WriteFileJson.FromMaps(m_ActionMaps).maps, controlSchemes = InputControlScheme.SchemeJson.ToJson(m_ControlSchemes), }; return JsonUtility.ToJson(fileJson, true); } /// <summary> /// Replace the contents of the asset with the data in the given JSON string. /// </summary> /// <param name="json">JSON contents of an <c>.inputactions</c> asset.</param> /// <remarks> /// <c>.inputactions</c> assets are stored in JSON format. This method allows reading /// the JSON source text of such an asset into an existing <c>InputActionMap</c> instance. /// /// <example> /// <code> /// var asset = ScriptableObject.CreateInstance<InputActionAsset>(); /// asset.LoadFromJson(@" /// { /// ""maps"" : [ /// { /// ""name"" : ""gameplay"", /// ""actions"" : [ /// { ""name"" : ""fire"", ""type"" : ""button"" }, /// { ""name"" : ""look"", ""type"" : ""value"" }, /// { ""name"" : ""move"", ""type"" : ""value"" } /// ], /// ""bindings"" : [ /// { ""path"" : ""<Gamepad>/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""<Gamepad>/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""<Gamepad>/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true }, /// { ""path"" : ""<Keyboard>/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Keyboard>/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Keyboard>/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Keyboard>/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Mouse>/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&Mouse"" }, /// { ""path"" : ""<Mouse>/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&Mouse"" } /// ] /// }, /// { /// ""name"" : ""ui"", /// ""actions"" : [ /// { ""name"" : ""navigate"" } /// ], /// ""bindings"" : [ /// { ""path"" : ""<Gamepad>/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" } /// ] /// } /// ], /// ""controlSchemes"" : [ /// { /// ""name"" : ""Gamepad"", /// ""bindingGroup"" : ""Gamepad"", /// ""devices"" : [ /// { ""devicePath"" : ""<Gamepad>"" } /// ] /// }, /// { /// ""name"" : ""Keyboard&Mouse"", /// ""bindingGroup"" : ""Keyboard&Mouse"", /// ""devices"" : [ /// { ""devicePath"" : ""<Keyboard>"" }, /// { ""devicePath"" : ""<Mouse>"" } /// ] /// } /// ] /// }"); /// </code> /// </example> /// </remarks> /// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c> or empty.</exception> /// <seealso cref="FromJson"/> /// <seealso cref="ToJson"/> public void LoadFromJson(string json) { if (string.IsNullOrEmpty(json)) throw new ArgumentNullException(nameof(json)); var parsedJson = JsonUtility.FromJson<ReadFileJson>(json); parsedJson.ToAsset(this); } /// <summary> /// Replace the contents of the asset with the data in the given JSON string. /// </summary> /// <param name="json">JSON contents of an <c>.inputactions</c> asset.</param> /// <returns>The InputActionAsset instance created from the given JSON string.</returns> /// <remarks> /// <c>.inputactions</c> assets are stored in JSON format. This method allows turning /// the JSON source text of such an asset into a new <c>InputActionMap</c> instance. /// /// Be aware that the format used by this method is <em>different</em> than what you /// get if you call <c>JsonUtility.ToJson</c> on an InputActionAsset instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// /// <example> /// <code> /// var asset = InputActionAsset.FromJson(@" /// { /// ""maps"" : [ /// { /// ""name"" : ""gameplay"", /// ""actions"" : [ /// { ""name"" : ""fire"", ""type"" : ""button"" }, /// { ""name"" : ""look"", ""type"" : ""value"" }, /// { ""name"" : ""move"", ""type"" : ""value"" } /// ], /// ""bindings"" : [ /// { ""path"" : ""<Gamepad>/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""<Gamepad>/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""<Gamepad>/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" }, /// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true }, /// { ""path"" : ""<Keyboard>/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Keyboard>/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Keyboard>/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Keyboard>/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true }, /// { ""path"" : ""<Mouse>/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&Mouse"" }, /// { ""path"" : ""<Mouse>/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&Mouse"" } /// ] /// }, /// { /// ""name"" : ""ui"", /// ""actions"" : [ /// { ""name"" : ""navigate"" } /// ], /// ""bindings"" : [ /// { ""path"" : ""<Gamepad>/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" } /// ] /// } /// ], /// ""controlSchemes"" : [ /// { /// ""name"" : ""Gamepad"", /// ""bindingGroup"" : ""Gamepad"", /// ""devices"" : [ /// { ""devicePath"" : ""<Gamepad>"" } /// ] /// }, /// { /// ""name"" : ""Keyboard&Mouse"", /// ""bindingGroup"" : ""Keyboard&Mouse"", /// ""devices"" : [ /// { ""devicePath"" : ""<Keyboard>"" }, /// { ""devicePath"" : ""<Mouse>"" } /// ] /// } /// ] /// }"); /// </code> /// </example> /// </remarks> /// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c> or empty.</exception> /// <seealso cref="LoadFromJson"/> /// <seealso cref="ToJson"/> public static InputActionAsset FromJson(string json) { if (string.IsNullOrEmpty(json)) throw new ArgumentNullException(nameof(json)); var asset = CreateInstance<InputActionAsset>(); asset.LoadFromJson(json); return asset; } /// <summary> /// Find an <see cref="InputAction"/> by its name in one of the <see cref="InputActionMap"/>s /// in the asset. /// </summary> /// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or /// a simple name. In the former case, the name is split at the '/' slash and the first part is used to find /// a map with that name and the second part is used to find an action with that name inside the map. In the /// latter case, all maps are searched in order and the first action that has the given name in any of the maps /// is returned. Note that name comparisons are case-insensitive. /// /// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param> /// <param name="throwIfNotFound">If <c>true</c>, instead of returning <c>null</c> when the action /// cannot be found, throw <c>ArgumentException</c>.</param> /// <returns>The action with the corresponding name or <c>null</c> if no matching action could be found.</returns> /// <remarks> /// Note that no lookup structures are used internally to speed the operation up. Instead, the search is done /// linearly. For repeated access of an action, it is thus generally best to look up actions once ahead of /// time and cache the result. /// /// If multiple actions have the same name and <paramref name="actionNameOrId"/> is not an ID and not an /// action name qualified by a map name (that is, in the form of <c>"mapName/actionName"</c>), the action that /// is returned will be from the first map in <see cref="actionMaps"/> that has an action with the given name. /// An exception is if, of the multiple actions with the same name, some are enabled and some are disabled. In /// this case, the first action that is enabled is returned. /// /// <example> /// <code> /// var asset = ScriptableObject.CreateInstance<InputActionAsset>(); /// /// var map1 = new InputActionMap("map1"); /// var map2 = new InputActionMap("map2"); /// /// asset.AddActionMap(map1); /// asset.AddActionMap(map2); /// /// var action1 = map1.AddAction("action1"); /// var action2 = map1.AddAction("action2"); /// var action3 = map2.AddAction("action3"); /// /// // Search all maps in the asset for any action that has the given name. /// asset.FindAction("action1") // Returns action1. /// asset.FindAction("action2") // Returns action2 /// asset.FindAction("action3") // Returns action3. /// /// // Search for a specific action in a specific map. /// asset.FindAction("map1/action1") // Returns action1. /// asset.FindAction("map2/action2") // Returns action2. /// asset.FindAction("map3/action3") // Returns action3. /// /// // Search by unique action ID. /// asset.FindAction(action1.id.ToString()) // Returns action1. /// asset.FindAction(action2.id.ToString()) // Returns action2. /// asset.FindAction(action3.id.ToString()) // Returns action3. /// </code> /// </example> /// </remarks> /// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c>.</exception> /// <exception cref="ArgumentException">Thrown if <paramref name="throwIfNotFound"/> is true and the /// action could not be found. -Or- If <paramref name="actionNameOrId"/> contains a slash but is missing /// either the action or the map name.</exception> public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false) { if (actionNameOrId == null) throw new ArgumentNullException(nameof(actionNameOrId)); if (m_ActionMaps != null) { // Check if we have a "map/action" path. var indexOfSlash = actionNameOrId.IndexOf('/'); if (indexOfSlash == -1) { // No slash so it's just a simple action name. Return either first enabled action or, if // none are enabled, first action with the given name. InputAction firstActionFound = null; for (var i = 0; i < m_ActionMaps.Length; ++i) { var action = m_ActionMaps[i].FindAction(actionNameOrId); if (action != null) { if (action.enabled || action.m_Id == actionNameOrId) // Match by ID is always exact. return action; if (firstActionFound == null) firstActionFound = action; } } if (firstActionFound != null) return firstActionFound; } else { // Have a path. First search for the map, then for the action. var mapName = new Substring(actionNameOrId, 0, indexOfSlash); var actionName = new Substring(actionNameOrId, indexOfSlash + 1); if (mapName.isEmpty || actionName.isEmpty) throw new ArgumentException("Malformed action path: " + actionNameOrId, nameof(actionNameOrId)); for (var i = 0; i < m_ActionMaps.Length; ++i) { var map = m_ActionMaps[i]; if (Substring.Compare(map.name, mapName, StringComparison.InvariantCultureIgnoreCase) != 0) continue; var actions = map.m_Actions; for (var n = 0; n < actions.Length; ++n) { var action = actions[n]; if (Substring.Compare(action.name, actionName, StringComparison.InvariantCultureIgnoreCase) == 0) return action; } break; } } } if (throwIfNotFound) throw new ArgumentException($"No action '{actionNameOrId}' in '{this}'"); return null; } /// <inheritdoc/> public int FindBinding(InputBinding mask, out InputAction action) { var numMaps = m_ActionMaps.LengthSafe(); for (var i = 0; i < numMaps; ++i) { var actionMap = m_ActionMaps[i]; var bindingIndex = actionMap.FindBinding(mask, out action); if (bindingIndex >= 0) return bindingIndex; } action = null; return -1; } /// <summary> /// Find an <see cref="InputActionMap"/> in the asset by its name or ID. /// </summary> /// <param name="nameOrId">Name or ID (see <see cref="InputActionMap.id"/>) of the action map /// to look for. Matching is case-insensitive.</param> /// <param name="throwIfNotFound">If true, instead of returning <c>null</c>, throw <c>ArgumentException</c>.</param> /// <returns>The <see cref="InputActionMap"/> with a name or ID matching <paramref name="nameOrId"/> or /// <c>null</c> if no matching map could be found.</returns> /// <exception cref="ArgumentNullException"><paramref name="nameOrId"/> is <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="throwIfNotFound"/> is <c>true</c>, thrown if /// the action map cannot be found.</exception> /// <seealso cref="actionMaps"/> /// <seealso cref="FindActionMap(System.Guid)"/> public InputActionMap FindActionMap(string nameOrId, bool throwIfNotFound = false) { if (nameOrId == null) throw new ArgumentNullException(nameof(nameOrId)); if (m_ActionMaps == null) return null; // If the name contains a hyphen, it may be a GUID. if (nameOrId.Contains('-') && Guid.TryParse(nameOrId, out var id)) { for (var i = 0; i < m_ActionMaps.Length; ++i) { var map = m_ActionMaps[i]; if (map.idDontGenerate == id) return map; } } // Default lookup is by name (case-insensitive). for (var i = 0; i < m_ActionMaps.Length; ++i) { var map = m_ActionMaps[i]; if (string.Compare(nameOrId, map.name, StringComparison.InvariantCultureIgnoreCase) == 0) return map; } if (throwIfNotFound) throw new ArgumentException($"Cannot find action map '{nameOrId}' in '{this}'"); return null; } /// <summary> /// Find an <see cref="InputActionMap"/> in the asset by its ID. /// </summary> /// <param name="id">ID (see <see cref="InputActionMap.id"/>) of the action map /// to look for.</param> /// <returns>The <see cref="InputActionMap"/> with ID matching <paramref name="id"/> or /// <c>null</c> if no map in the asset has the given ID.</returns> /// <seealso cref="actionMaps"/> /// <seealso cref="FindActionMap"/> public InputActionMap FindActionMap(Guid id) { if (m_ActionMaps == null) return null; for (var i = 0; i < m_ActionMaps.Length; ++i) { var map = m_ActionMaps[i]; if (map.idDontGenerate == id) return map; } return null; } /// <summary> /// Find an action by its ID (see <see cref="InputAction.id"/>). /// </summary> /// <param name="guid">ID of the action to look for.</param> /// <returns>The action in the asset with the given ID or null if no action /// in the asset has the given ID.</returns> public InputAction FindAction(Guid guid) { if (m_ActionMaps == null) return null; for (var i = 0; i < m_ActionMaps.Length; ++i) { var map = m_ActionMaps[i]; var action = map.FindAction(guid); if (action != null) return action; } return null; } /// <summary> /// Find the control scheme with the given name and return its index /// in <see cref="controlSchemes"/>. /// </summary> /// <param name="name">Name of the control scheme. Matching is case-insensitive.</param> /// <returns>The index of the given control scheme or -1 if no control scheme /// with the given name could be found.</returns> /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> /// or empty.</exception> public int FindControlSchemeIndex(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); if (m_ControlSchemes == null) return -1; for (var i = 0; i < m_ControlSchemes.Length; ++i) if (string.Compare(name, m_ControlSchemes[i].name, StringComparison.InvariantCultureIgnoreCase) == 0) return i; return -1; } /// <summary> /// Find the control scheme with the given name and return it. /// </summary> /// <param name="name">Name of the control scheme. Matching is case-insensitive.</param> /// <returns>The control scheme with the given name or null if no scheme /// with the given name could be found in the asset.</returns> /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> /// or empty.</exception> public InputControlScheme? FindControlScheme(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); var index = FindControlSchemeIndex(name); if (index == -1) return null; return m_ControlSchemes[index]; } /// <summary> /// Return true if the asset contains bindings (in any of its action maps) that are usable /// with the given <paramref name="device"/>. /// </summary> /// <param name="device">An arbitrary input device.</param> /// <returns></returns> /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception> /// <remarks> /// <example> /// <code> /// // Find out if the actions of the given PlayerInput can be used with /// // a gamepad. /// if (playerInput.actions.IsUsableWithDevice(Gamepad.all[0])) /// /* ... */; /// </code> /// </example> /// </remarks> /// <seealso cref="InputActionMap.IsUsableWithDevice"/> /// <seealso cref="InputControlScheme.SupportsDevice"/> public bool IsUsableWithDevice(InputDevice device) { if (device == null) throw new ArgumentNullException(nameof(device)); // If we have control schemes, we let those dictate our search. var numControlSchemes = m_ControlSchemes.LengthSafe(); if (numControlSchemes > 0) { for (var i = 0; i < numControlSchemes; ++i) { if (m_ControlSchemes[i].SupportsDevice(device)) return true; } } else { // Otherwise, we'll go search bindings. Slow. var actionMapCount = m_ActionMaps.LengthSafe(); for (var i = 0; i < actionMapCount; ++i) if (m_ActionMaps[i].IsUsableWithDevice(device)) return true; } return false; } /// <summary> /// Enable all action maps in the asset. /// </summary> /// <remarks> /// This method is equivalent to calling <see cref="InputActionMap.Enable"/> on /// all maps in <see cref="actionMaps"/>. /// </remarks> public void Enable() { foreach (var map in actionMaps) map.Enable(); } /// <summary> /// Disable all action maps in the asset. /// </summary> /// <remarks> /// This method is equivalent to calling <see cref="InputActionMap.Disable"/> on /// all maps in <see cref="actionMaps"/>. /// </remarks> public void Disable() { foreach (var map in actionMaps) map.Disable(); } /// <summary> /// Return <c>true</c> if the given action is part of the asset. /// </summary> /// <param name="action">An action. Can be null.</param> /// <returns>True if the given action is part of the asset, false otherwise.</returns> public bool Contains(InputAction action) { var map = action?.actionMap; if (map == null) return false; return map.asset == this; } /// <summary> /// Enumerate all actions in the asset. /// </summary> /// <returns>Enumerate over all actions in the asset.</returns> /// <remarks> /// Actions will be enumerated one action map in <see cref="actionMaps"/> /// after the other. The actions from each map will be yielded in turn. /// /// This method will allocate GC heap memory. /// </remarks> public IEnumerator<InputAction> GetEnumerator() { if (m_ActionMaps == null) yield break; for (var i = 0; i < m_ActionMaps.Length; ++i) { var actions = m_ActionMaps[i].actions; var actionCount = actions.Count; for (var n = 0; n < actionCount; ++n) yield return actions[n]; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } internal void MarkAsDirty() { #if UNITY_EDITOR InputSystem.TrackDirtyInputActionAsset(this); #endif } internal void OnWantToChangeSetup() { if (m_ActionMaps.LengthSafe() > 0) m_ActionMaps[0].OnWantToChangeSetup(); } internal void OnSetupChanged() { MarkAsDirty(); if (m_ActionMaps.LengthSafe() > 0) m_ActionMaps[0].OnSetupChanged(); else m_SharedStateForAllMaps = null; } private void ReResolveIfNecessary(bool fullResolve) { if (m_SharedStateForAllMaps == null) return; Debug.Assert(m_ActionMaps != null && m_ActionMaps.Length > 0); // State is share between all action maps in the asset. Resolving bindings for the // first map will resolve them for all maps. m_ActionMaps[0].LazyResolveBindings(fullResolve); } internal void ResolveBindingsIfNecessary() { if (m_ActionMaps.LengthSafe() > 0) foreach (var map in m_ActionMaps) if (map.ResolveBindingsIfNecessary()) break; } private void OnDestroy() { Disable(); if (m_SharedStateForAllMaps != null) { m_SharedStateForAllMaps.Dispose(); // Will clean up InputActionMap state. m_SharedStateForAllMaps = null; } } ////TODO: ApplyBindingOverrides, RemoveBindingOverrides, RemoveAllBindingOverrides [SerializeField] internal InputActionMap[] m_ActionMaps; [SerializeField] internal InputControlScheme[] m_ControlSchemes; ////TODO: make this persistent across domain reloads /// <summary> /// Shared state for all action maps in the asset. /// </summary> [NonSerialized] internal InputActionState m_SharedStateForAllMaps; [NonSerialized] internal InputBinding? m_BindingMask; [NonSerialized] internal int m_ParameterOverridesCount; [NonSerialized] internal InputActionRebindingExtensions.ParameterOverride[] m_ParameterOverrides; [NonSerialized] internal InputActionMap.DeviceArray m_Devices; [Serializable] internal struct WriteFileJson { public string name; public InputActionMap.WriteMapJson[] maps; public InputControlScheme.SchemeJson[] controlSchemes; } [Serializable] internal struct ReadFileJson { public string name; public InputActionMap.ReadMapJson[] maps; public InputControlScheme.SchemeJson[] controlSchemes; public void ToAsset(InputActionAsset asset) { asset.name = name; asset.m_ActionMaps = new InputActionMap.ReadFileJson {maps = maps}.ToMaps(); asset.m_ControlSchemes = InputControlScheme.SchemeJson.ToSchemes(controlSchemes); // Link maps to their asset. if (asset.m_ActionMaps != null) foreach (var map in asset.m_ActionMaps) map.m_Asset = asset; } } } }