using System; using System.Collections; using System.Collections.Generic; using UnityEngine.InputSystem.Utilities; ////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 { /// /// An asset containing action maps and control schemes. /// /// /// InputActionAssets can be created in code but are usually stored in JSON format on /// disk with the ".inputactions" extension and are imported by Unity using a custom /// importer. /// /// To create an InputActionAsset in code, use the Singleton API and populate the /// asset with the methods found in . Alternatively, /// you can load an InputActionAsset directly from a string in JSON format using . /// /// /// /// // Create and configure an asset in code. /// var asset1 = ScriptableObject.CreateInstance<InputActionAsset>(); /// var actionMap1 = asset1.CreateActionMap("map1"); /// action1Map.AddAction("action1", binding: "<Keyboard>/space"); /// /// /// /// Each asset can contain arbitrary many action maps that can be enabled and disabled individually /// (see and ) or in bulk /// (see and ). The name of each action map must be unique. /// The list of action maps can be queried from . /// /// InputActionAssets can only define s. They can be added to /// an asset with /// and can be queried from . /// /// Be aware that input action assets do not separate between static (configuration) data and dynamic /// (instance) data. For audio, for example, AudioClip represents the static, /// shared data portion of audio playback whereas AudioSource" represents the /// dynamic, per-instance audio playback portion (referencing the clip through AudioSource.clip). /// /// 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 UnityEngine.Object.Instantiate to instantiate the input action /// asset multiple times. 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. /// public class InputActionAsset : ScriptableObject, IInputActionCollection { /// /// File extension (without the dot) for InputActionAssets in JSON format. /// /// File extension for InputActionAsset source files. /// /// Files with this extension will automatically be imported by Unity as /// InputActionAssets. /// public const string Extension = "inputactions"; /// /// True if any action in the asset is currently enabled. /// /// /// /// /// /// public bool enabled { get { foreach (var actionMap in actionMaps) if (actionMap.enabled) return true; return false; } } /// /// List of action maps defined in the asset. /// /// Action maps contained in the asset. /// /// /// public ReadOnlyArray actionMaps => new ReadOnlyArray(m_ActionMaps); /// /// List of control schemes defined in the asset. /// /// Control schemes defined for the asset. /// /// public ReadOnlyArray controlSchemes => new ReadOnlyArray(m_ControlSchemes); /// /// Binding mask to apply to all action maps and actions in the asset. /// /// Optional mask that determines which bindings in the asset to enable. /// /// Binding masks can be applied at three different levels: for an entire asset through /// this property, for a specific map through , /// and for single actions through . 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 /// and properties. /// /// /// /// public InputBinding? bindingMask { get => m_BindingMask; set { if (m_BindingMask == value) return; m_BindingMask = value; ReResolveIfNecessary(); } } /// /// Set of devices that bindings in the asset can bind to. /// /// Optional set of devices to use by bindings in the asset. /// /// By default (with this property being null), bindings will bind to any of the /// controls available through , 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 . Note that if /// both this property and 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. /// /// /// /// // 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)); /// /// /// /// public ReadOnlyArray? devices { get { if (m_DevicesCount < 0) return null; return new ReadOnlyArray(m_DevicesArray, 0, m_DevicesCount); } set { if (value == null) { if (m_DevicesCount < 0) return; // No change. if (m_DevicesArray != null & m_DevicesCount > 0) Array.Clear(m_DevicesArray, 0, m_DevicesCount); m_DevicesCount = -1; } else { // See if the array actually changes content. Avoids re-resolving when there // is no need to. if (m_DevicesCount == value.Value.Count) { var noChange = true; for (var i = 0; i < m_DevicesCount; ++i) { if (!ReferenceEquals(m_DevicesArray[i], value.Value[i])) { noChange = false; break; } } if (noChange) return; } if (m_DevicesCount > 0) m_DevicesArray.Clear(ref m_DevicesCount); m_DevicesCount = 0; ArrayHelpers.AppendListWithCapacity(ref m_DevicesArray, ref m_DevicesCount, value.Value); } ReResolveIfNecessary(); } } /// /// Look up an action by name or ID. /// /// 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 . /// The action with the corresponding name or null if no matching action could be found. /// /// This method is equivalent to except that it throws /// if no action with the given name or ID /// could be found. /// /// No action was found matching . /// is null or empty. /// public InputAction this[string actionNameOrId] { get { var action = FindAction(actionNameOrId); if (action == null) throw new KeyNotFoundException($"Cannot find action '{actionNameOrId}' in '{this}'"); return action; } } /// /// Return a JSON representation of the asset. /// /// A string in JSON format that represents the static/configuration data present /// in the asset. /// /// This will not save dynamic execution state such as callbacks installed on /// actions or enabled/disabled states of individual /// maps and actions. /// /// Use to deserialize the JSON data back into an InputActionAsset. /// /// Be aware that the format used by this method is different than what you /// get if you call JsonUtility.ToJson on an InputActionAsset instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// /// 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); } /// /// Replace the contents of the asset with the data in the given JSON string. /// /// JSON contents of an .inputactions asset. /// /// .inputactions assets are stored in JSON format. This method allows reading /// the JSON source text of such an asset into an existing InputActionMap instance. /// /// /// /// 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>"" } /// ] /// } /// ] /// }"); /// /// /// /// is null or empty. /// /// public void LoadFromJson(string json) { if (string.IsNullOrEmpty(json)) throw new ArgumentNullException(nameof(json)); var parsedJson = JsonUtility.FromJson(json); parsedJson.ToAsset(this); } /// /// Replace the contents of the asset with the data in the given JSON string. /// /// JSON contents of an .inputactions asset. /// The InputActionAsset instance created from the given JSON string. /// /// .inputactions assets are stored in JSON format. This method allows turning /// the JSON source text of such an asset into a new InputActionMap instance. /// /// Be aware that the format used by this method is different than what you /// get if you call JsonUtility.ToJson on an InputActionAsset instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// /// /// /// 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>"" } /// ] /// } /// ] /// }"); /// /// /// /// is null or empty. /// /// public static InputActionAsset FromJson(string json) { if (string.IsNullOrEmpty(json)) throw new ArgumentNullException(nameof(json)); var asset = CreateInstance(); asset.LoadFromJson(json); return asset; } /// /// Find an by its name in one of the s /// in the asset. /// /// 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 . /// If true, instead of returning null when the action /// cannot be found, throw ArgumentException. /// The action with the corresponding name or null if no matching action could be found. /// /// /// /// 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. /// /// /// /// is null. /// Thrown if is true and the /// action could not be found. -Or- If contains a slash but is missing /// either the action or the map name. 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. for (var i = 0; i < m_ActionMaps.Length; ++i) { var action = m_ActionMaps[i].FindAction(actionNameOrId); if (action != null) return action; } } 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; } /// /// Find an in the asset by its name or ID. /// /// Name or ID (see ) of the action map /// to look for. Matching is case-insensitive. /// If true, instead of returning null, throw ArgumentException. /// The with a name or ID matching or /// null if no matching map could be found. /// is null. /// If is true, thrown if /// the action map cannot be found. /// /// 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; } /// /// Find an in the asset by its ID. /// /// ID (see ) of the action map /// to look for. /// The with ID matching or /// null if no map in the asset has the given ID. /// /// 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; } /// /// Find an action by its ID (see ). /// /// ID of the action to look for. /// The action in the asset with the given ID or null if no action /// in the asset has the given ID. 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; } /// /// Find the control scheme with the given name and return its index /// in . /// /// Name of the control scheme. Matching is case-insensitive. /// The index of the given control scheme or -1 if no control scheme /// with the given name could be found. /// is null /// or empty. 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; } /// /// Find the control scheme with the given name and return it. /// /// Name of the control scheme. Matching is case-insensitive. /// The control scheme with the given name or null if no scheme /// with the given name could be found in the asset. /// is null /// or empty. 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]; } /// /// Enable all action maps in the asset. /// /// /// This method is equivalent to calling on /// all maps in . /// public void Enable() { foreach (var map in actionMaps) map.Enable(); } /// /// Disable all action maps in the asset. /// /// /// This method is equivalent to calling on /// all maps in . /// public void Disable() { foreach (var map in actionMaps) map.Disable(); } /// /// Return true if the given action is part of the asset. /// /// An action. Can be null. /// True if the given action is part of the asset, false otherwise. public bool Contains(InputAction action) { var map = action?.actionMap; if (map == null) return false; return map.asset == this; } /// /// Enumerate all actions in the asset. /// /// Enumerate over all actions in the asset. /// /// Actions will be enumerated one action map in /// after the other. The actions from each map will be yielded in turn. /// /// This method will allocate GC heap memory. /// public IEnumerator 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(); } private void ReResolveIfNecessary() { 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(); } 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 /// /// Shared state for all action maps in the asset. /// [NonSerialized] internal InputActionState m_SharedStateForAllMaps; [NonSerialized] internal InputBinding? m_BindingMask; [NonSerialized] private int m_DevicesCount = -1; [NonSerialized] private InputDevice[] m_DevicesArray; [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; } } } }