A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections; 3using System.Collections.Generic; 4using UnityEngine.InputSystem.Utilities; 5 6////TODO: make the FindAction logic available on any IEnumerable<InputAction> and IInputActionCollection via extension methods 7 8////TODO: control schemes, like actions and maps, should have stable IDs so that they can be renamed 9 10////REVIEW: have some way of expressing 'contracts' on action maps? I.e. something like 11//// "I expect a 'look' and a 'move' action in here" 12 13////REVIEW: rename this from "InputActionAsset" to something else that emphasizes the asset aspect less 14//// and instead emphasizes the map collection aspect more? 15 16namespace UnityEngine.InputSystem 17{ 18 /// <summary> 19 /// An asset that contains action maps and control schemes. 20 /// </summary> 21 /// <remarks> 22 /// InputActionAssets can be created in code but are usually stored in JSON format on 23 /// disk with the ".inputactions" extension. Unity imports them with a custom 24 /// importer. 25 /// 26 /// To create an InputActionAsset in code, use the <c>Singleton</c> API and populate the 27 /// asset with the methods found in <see cref="InputActionSetupExtensions"/>. Alternatively, 28 /// you can use <see cref="FromJson"/> to load an InputActionAsset directly from a string in JSON format. 29 /// 30 /// <example> 31 /// <code> 32 /// // Create and configure an asset in code. 33 /// var asset1 = ScriptableObject.CreateInstance&lt;InputActionAsset&gt;(); 34 /// var actionMap1 = asset1.AddActionMap("map1"); 35 /// action1Map.AddAction("action1", binding: "&lt;Keyboard&gt;/space"); 36 /// </code> 37 /// </example> 38 /// 39 /// If you use the API to modify an InputActionAsset while in Play mode, 40 /// it does not survive the transition back to Edit Mode. Unity tracks and reloads modified assets 41 /// from disk when exiting Play mode. This is done so that you can realistically test the input 42 /// related functionality of your application i.e. control rebinding etc, without inadvertently changing 43 /// the input asset. 44 /// 45 /// Each asset can contain arbitrary many action maps that you can enable and disable individually 46 /// (see <see cref="InputActionMap.Enable"/> and <see cref="InputActionMap.Disable"/>) or in bulk 47 /// (see <see cref="Enable"/> and <see cref="Disable"/>). The name of each action map must be unique. 48 /// The list of action maps can be queried from <see cref="actionMaps"/>. 49 /// 50 /// InputActionAssets can only define <see cref="InputControlScheme"/>s. They can be added to 51 /// an asset with <see cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> 52 /// and can be queried from <see cref="controlSchemes"/>. 53 /// 54 /// Be aware that input action assets do not separate between static (configuration) data and dynamic 55 /// (instance) data. For audio, for example, <c>AudioClip</c> represents the static, 56 /// shared data portion of audio playback whereas <c>AudioSource"</c> represents the 57 /// dynamic, per-instance audio playback portion (referencing the clip through <c>AudioSource.clip</c>). 58 /// 59 /// For input, such a split is less beneficial as the same input is generally not exercised 60 /// multiple times in parallel. Keeping both static and dynamic data together simplifies 61 /// using the system. 62 /// 63 /// However, there are scenarios where you indeed want to take the same input action and 64 /// exercise it multiple times in parallel. A prominent example of such a use case is 65 /// local multiplayer where each player gets the same set of actions but is controlling 66 /// them with a different device (or devices) each. This is easily achieved by simply 67 /// using <c>UnityEngine.Object.Instantiate</c> to instantiate the input action 68 /// asset multiple times. <see cref="PlayerInput"/> will automatically do so in its 69 /// internals. 70 /// 71 /// Note also that all action maps in an asset share binding state. This means that if 72 /// one map in an asset has to resolve its bindings, all maps in the asset have to. 73 /// </remarks> 74 public class InputActionAsset : ScriptableObject, IInputActionCollection2 75 { 76 /// <summary> 77 /// File extension (without the dot) for InputActionAssets in JSON format. 78 /// </summary> 79 /// <value>File extension for InputActionAsset source files.</value> 80 /// <remarks> 81 /// Files with this extension will automatically be imported by Unity as 82 /// InputActionAssets. 83 /// </remarks> 84 public const string Extension = "inputactions"; 85 ////REVIEW: actually pre-populate with some stuff? 86 internal const string kDefaultAssetLayoutJson = "{}"; 87 88 /// <summary> 89 /// True if any action in the asset is currently enabled. 90 /// </summary> 91 /// <seealso cref="InputAction.enabled"/> 92 /// <seealso cref="InputActionMap.enabled"/> 93 /// <seealso cref="InputAction.Enable"/> 94 /// <seealso cref="InputActionMap.Enable"/> 95 /// <seealso cref="Enable"/> 96 public bool enabled 97 { 98 get 99 { 100 foreach (var actionMap in actionMaps) 101 if (actionMap.enabled) 102 return true; 103 return false; 104 } 105 } 106 107 /// <summary> 108 /// List of action maps defined in the asset. 109 /// </summary> 110 /// <value>Action maps contained in the asset.</value> 111 /// <seealso cref="InputActionSetupExtensions.AddActionMap(InputActionAsset,string)"/> 112 /// <seealso cref="InputActionSetupExtensions.RemoveActionMap(InputActionAsset,InputActionMap)"/> 113 /// <seealso cref="FindActionMap(string,bool)"/> 114 public ReadOnlyArray<InputActionMap> actionMaps => new ReadOnlyArray<InputActionMap>(m_ActionMaps); 115 116 /// <summary> 117 /// List of control schemes defined in the asset. 118 /// </summary> 119 /// <value>Control schemes defined for the asset.</value> 120 /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> 121 /// <seealso cref="InputActionSetupExtensions.RemoveControlScheme"/> 122 public ReadOnlyArray<InputControlScheme> controlSchemes => new ReadOnlyArray<InputControlScheme>(m_ControlSchemes); 123 124 /// <summary> 125 /// Iterate over all bindings in the asset. 126 /// </summary> 127 /// <remarks> 128 /// This iterates over all action maps in <see cref="actionMaps"/> and, within each 129 /// map, over the set of <see cref="InputActionMap.bindings"/>. 130 /// </remarks> 131 /// <seealso cref="InputActionMap.bindings"/> 132 public IEnumerable<InputBinding> bindings 133 { 134 get 135 { 136 var numActionMaps = m_ActionMaps.LengthSafe(); 137 if (numActionMaps == 0) 138 yield break; 139 140 for (var i = 0; i < numActionMaps; ++i) 141 { 142 var actionMap = m_ActionMaps[i]; 143 var bindings = actionMap.m_Bindings; 144 var numBindings = bindings.LengthSafe(); 145 146 for (var n = 0; n < numBindings; ++n) 147 yield return bindings[n]; 148 } 149 } 150 } 151 152 /// <summary> 153 /// Binding mask to apply to all action maps and actions in the asset. 154 /// </summary> 155 /// <value>Optional mask that determines which bindings in the asset to enable.</value> 156 /// <remarks> 157 /// Binding masks can be applied at three different levels: for an entire asset through 158 /// this property, for a specific map through <see cref="InputActionMap.bindingMask"/>, 159 /// and for single actions through <see cref="InputAction.bindingMask"/>. By default, 160 /// none of the masks will be set (i.e. they will be <c>null</c>). 161 /// 162 /// When an action is enabled, all the binding masks that apply to it are taken into 163 /// account. Specifically, this means that any given binding on the action will be 164 /// enabled only if it matches the mask applied to the asset, the mask applied 165 /// to the map that contains the action, and the mask applied to the action itself. 166 /// All the masks are individually optional. 167 /// 168 /// Masks are matched against bindings using <see cref="InputBinding.Matches"/>. 169 /// 170 /// Note that if you modify the masks applicable to an action while it is 171 /// enabled, the action's <see cref="InputAction.controls"/> will get updated immediately to 172 /// respect the mask. To avoid repeated binding resolution, it is most efficient 173 /// to apply binding masks before enabling actions. 174 /// 175 /// Binding masks are non-destructive. All the bindings on the action are left 176 /// in place. Setting a mask will not affect the value of the <see cref="InputAction.bindings"/> 177 /// and <see cref="InputActionMap.bindings"/> properties. 178 /// </remarks> 179 /// <seealso cref="InputBinding.MaskByGroup"/> 180 /// <seealso cref="InputAction.bindingMask"/> 181 /// <seealso cref="InputActionMap.bindingMask"/> 182 public InputBinding? bindingMask 183 { 184 get => m_BindingMask; 185 set 186 { 187 if (m_BindingMask == value) 188 return; 189 190 m_BindingMask = value; 191 192 ReResolveIfNecessary(fullResolve: true); 193 } 194 } 195 196 /// <summary> 197 /// Set of devices that bindings in the asset can bind to. 198 /// </summary> 199 /// <value>Optional set of devices to use by bindings in the asset.</value> 200 /// <remarks> 201 /// By default (with this property being <c>null</c>), bindings will bind to any of the 202 /// controls available through <see cref="InputSystem.devices"/>, i.e. controls from all 203 /// devices in the system will be used. 204 /// 205 /// By setting this property, binding resolution can instead be restricted to just specific 206 /// devices. This restriction can either be applied to an entire asset using this property 207 /// or to specific action maps by using <see cref="InputActionMap.devices"/>. Note that if 208 /// both this property and <see cref="InputActionMap.devices"/> is set for a specific action 209 /// map, the list of devices on the action map will take precedence and the list on the 210 /// asset will be ignored for bindings in that action map. 211 /// 212 /// <example> 213 /// <code> 214 /// // Create an asset with a single action map and a single action with a 215 /// // gamepad binding. 216 /// var asset = ScriptableObject.CreateInstance&lt;InputActionAsset&gt;(); 217 /// var actionMap = new InputActionMap(); 218 /// var fireAction = actionMap.AddAction("Fire", binding: "&lt;Gamepad&gt;/buttonSouth"); 219 /// asset.AddActionMap(actionMap); 220 /// 221 /// // Let's assume we have two gamepads connected. If we enable the 222 /// // action map now, the 'Fire' action will bind to both. 223 /// actionMap.Enable(); 224 /// 225 /// // This will print two controls. 226 /// Debug.Log(string.Join("\n", fireAction.controls)); 227 /// 228 /// // To restrict the setup to just the first gamepad, we can assign 229 /// // to the 'devices' property (in this case, we could do so on either 230 /// // the action map or on the asset; we choose the latter here). 231 /// asset.devices = new InputDevice[] { Gamepad.all[0] }; 232 /// 233 /// // Now this will print only one control. 234 /// Debug.Log(string.Join("\n", fireAction.controls)); 235 /// </code> 236 /// </example> 237 /// </remarks> 238 /// <seealso cref="InputActionMap.devices"/> 239 public ReadOnlyArray<InputDevice>? devices 240 { 241 get => m_Devices.Get(); 242 set 243 { 244 if (m_Devices.Set(value)) 245 ReResolveIfNecessary(fullResolve: false); 246 } 247 } 248 249 /// <summary> 250 /// Look up an action by name or ID. 251 /// </summary> 252 /// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or 253 /// a simple name. In the former case, the name is split at the '/' slash and the first part is used to find 254 /// a map with that name and the second part is used to find an action with that name inside the map. In the 255 /// latter case, all maps are searched in order and the first action that has the given name in any of the maps 256 /// is returned. Note that name comparisons are case-insensitive. 257 /// 258 /// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param> 259 /// <returns>The action with the corresponding name or null if no matching action could be found.</returns> 260 /// <remarks> 261 /// This method is equivalent to <see cref="FindAction(string,bool)"/> except that it throws 262 /// <see cref="KeyNotFoundException"/> if no action with the given name or ID 263 /// could be found. 264 /// </remarks> 265 /// <exception cref="KeyNotFoundException">No action was found matching <paramref name="actionNameOrId"/>.</exception> 266 /// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c> or empty.</exception> 267 /// <seealso cref="FindAction(string,bool)"/> 268 public InputAction this[string actionNameOrId] 269 { 270 get 271 { 272 var action = FindAction(actionNameOrId); 273 if (action == null) 274 throw new KeyNotFoundException($"Cannot find action '{actionNameOrId}' in '{this}'"); 275 return action; 276 } 277 } 278 279 /// <summary> 280 /// Return a JSON representation of the asset. 281 /// </summary> 282 /// <returns>A string in JSON format that represents the static/configuration data present 283 /// in the asset.</returns> 284 /// <remarks> 285 /// This will not save dynamic execution state such as callbacks installed on 286 /// <see cref="InputAction">actions</see> or enabled/disabled states of individual 287 /// maps and actions. 288 /// 289 /// Use <see cref="LoadFromJson"/> to deserialize the JSON data back into an InputActionAsset. 290 /// 291 /// Be aware that the format used by this method is <em>different</em> than what you 292 /// get if you call <c>JsonUtility.ToJson</c> on an InputActionAsset instance. In other 293 /// words, the JSON format is not identical to the Unity serialized object representation 294 /// of the asset. 295 /// </remarks> 296 /// <seealso cref="FromJson"/> 297 public string ToJson() 298 { 299 return JsonUtility.ToJson(new WriteFileJson 300 { 301 name = name, 302 maps = InputActionMap.WriteFileJson.FromMaps(m_ActionMaps).maps, 303 controlSchemes = InputControlScheme.SchemeJson.ToJson(m_ControlSchemes), 304 }, true); 305 } 306 307 /// <summary> 308 /// Replace the contents of the asset with the data in the given JSON string. 309 /// </summary> 310 /// <param name="json">JSON contents of an <c>.inputactions</c> asset.</param> 311 /// <remarks> 312 /// <c>.inputactions</c> assets are stored in JSON format. This method allows reading 313 /// the JSON source text of such an asset into an existing <c>InputActionMap</c> instance. 314 /// 315 /// <example> 316 /// <code> 317 /// var asset = ScriptableObject.CreateInstance&lt;InputActionAsset&gt;(); 318 /// asset.LoadFromJson(@" 319 /// { 320 /// ""maps"" : [ 321 /// { 322 /// ""name"" : ""gameplay"", 323 /// ""actions"" : [ 324 /// { ""name"" : ""fire"", ""type"" : ""button"" }, 325 /// { ""name"" : ""look"", ""type"" : ""value"" }, 326 /// { ""name"" : ""move"", ""type"" : ""value"" } 327 /// ], 328 /// ""bindings"" : [ 329 /// { ""path"" : ""&lt;Gamepad&gt;/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, 330 /// { ""path"" : ""&lt;Gamepad&gt;/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, 331 /// { ""path"" : ""&lt;Gamepad&gt;/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" }, 332 /// { ""path"" : ""&lt;Gamepad&gt;/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" }, 333 /// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true }, 334 /// { ""path"" : ""&lt;Keyboard&gt;/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 335 /// { ""path"" : ""&lt;Keyboard&gt;/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 336 /// { ""path"" : ""&lt;Keyboard&gt;/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 337 /// { ""path"" : ""&lt;Keyboard&gt;/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 338 /// { ""path"" : ""&lt;Mouse&gt;/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&amp;Mouse"" }, 339 /// { ""path"" : ""&lt;Mouse&gt;/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&amp;Mouse"" } 340 /// ] 341 /// }, 342 /// { 343 /// ""name"" : ""ui"", 344 /// ""actions"" : [ 345 /// { ""name"" : ""navigate"" } 346 /// ], 347 /// ""bindings"" : [ 348 /// { ""path"" : ""&lt;Gamepad&gt;/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" } 349 /// ] 350 /// } 351 /// ], 352 /// ""controlSchemes"" : [ 353 /// { 354 /// ""name"" : ""Gamepad"", 355 /// ""bindingGroup"" : ""Gamepad"", 356 /// ""devices"" : [ 357 /// { ""devicePath"" : ""&lt;Gamepad&gt;"" } 358 /// ] 359 /// }, 360 /// { 361 /// ""name"" : ""Keyboard&amp;Mouse"", 362 /// ""bindingGroup"" : ""Keyboard&amp;Mouse"", 363 /// ""devices"" : [ 364 /// { ""devicePath"" : ""&lt;Keyboard&gt;"" }, 365 /// { ""devicePath"" : ""&lt;Mouse&gt;"" } 366 /// ] 367 /// } 368 /// ] 369 /// }"); 370 /// </code> 371 /// </example> 372 /// </remarks> 373 /// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c> or empty.</exception> 374 /// <seealso cref="FromJson"/> 375 /// <seealso cref="ToJson"/> 376 public void LoadFromJson(string json) 377 { 378 if (string.IsNullOrEmpty(json)) 379 throw new ArgumentNullException(nameof(json)); 380 381 var parsedJson = JsonUtility.FromJson<ReadFileJson>(json); 382 parsedJson.ToAsset(this); 383 } 384 385 /// <summary> 386 /// Replace the contents of the asset with the data in the given JSON string. 387 /// </summary> 388 /// <param name="json">JSON contents of an <c>.inputactions</c> asset.</param> 389 /// <returns>The InputActionAsset instance created from the given JSON string.</returns> 390 /// <remarks> 391 /// <c>.inputactions</c> assets are stored in JSON format. This method allows turning 392 /// the JSON source text of such an asset into a new <c>InputActionMap</c> instance. 393 /// 394 /// Be aware that the format used by this method is <em>different</em> than what you 395 /// get if you call <c>JsonUtility.ToJson</c> on an InputActionAsset instance. In other 396 /// words, the JSON format is not identical to the Unity serialized object representation 397 /// of the asset. 398 /// 399 /// <example> 400 /// <code> 401 /// var asset = InputActionAsset.FromJson(@" 402 /// { 403 /// ""maps"" : [ 404 /// { 405 /// ""name"" : ""gameplay"", 406 /// ""actions"" : [ 407 /// { ""name"" : ""fire"", ""type"" : ""button"" }, 408 /// { ""name"" : ""look"", ""type"" : ""value"" }, 409 /// { ""name"" : ""move"", ""type"" : ""value"" } 410 /// ], 411 /// ""bindings"" : [ 412 /// { ""path"" : ""&lt;Gamepad&gt;/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, 413 /// { ""path"" : ""&lt;Gamepad&gt;/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" }, 414 /// { ""path"" : ""&lt;Gamepad&gt;/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" }, 415 /// { ""path"" : ""&lt;Gamepad&gt;/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" }, 416 /// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true }, 417 /// { ""path"" : ""&lt;Keyboard&gt;/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 418 /// { ""path"" : ""&lt;Keyboard&gt;/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 419 /// { ""path"" : ""&lt;Keyboard&gt;/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 420 /// { ""path"" : ""&lt;Keyboard&gt;/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&amp;Mouse"", ""isPartOfComposite"" : true }, 421 /// { ""path"" : ""&lt;Mouse&gt;/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&amp;Mouse"" }, 422 /// { ""path"" : ""&lt;Mouse&gt;/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&amp;Mouse"" } 423 /// ] 424 /// }, 425 /// { 426 /// ""name"" : ""ui"", 427 /// ""actions"" : [ 428 /// { ""name"" : ""navigate"" } 429 /// ], 430 /// ""bindings"" : [ 431 /// { ""path"" : ""&lt;Gamepad&gt;/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" } 432 /// ] 433 /// } 434 /// ], 435 /// ""controlSchemes"" : [ 436 /// { 437 /// ""name"" : ""Gamepad"", 438 /// ""bindingGroup"" : ""Gamepad"", 439 /// ""devices"" : [ 440 /// { ""devicePath"" : ""&lt;Gamepad&gt;"" } 441 /// ] 442 /// }, 443 /// { 444 /// ""name"" : ""Keyboard&amp;Mouse"", 445 /// ""bindingGroup"" : ""Keyboard&amp;Mouse"", 446 /// ""devices"" : [ 447 /// { ""devicePath"" : ""&lt;Keyboard&gt;"" }, 448 /// { ""devicePath"" : ""&lt;Mouse&gt;"" } 449 /// ] 450 /// } 451 /// ] 452 /// }"); 453 /// </code> 454 /// </example> 455 /// </remarks> 456 /// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c> or empty.</exception> 457 /// <seealso cref="LoadFromJson"/> 458 /// <seealso cref="ToJson"/> 459 public static InputActionAsset FromJson(string json) 460 { 461 if (string.IsNullOrEmpty(json)) 462 throw new ArgumentNullException(nameof(json)); 463 464 var asset = CreateInstance<InputActionAsset>(); 465 asset.LoadFromJson(json); 466 return asset; 467 } 468 469 /// <summary> 470 /// Find an <see cref="InputAction"/> by its name in one of the <see cref="InputActionMap"/>s 471 /// in the asset. 472 /// </summary> 473 /// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or 474 /// a simple name. In the former case, the name is split at the '/' slash and the first part is used to find 475 /// a map with that name and the second part is used to find an action with that name inside the map. In the 476 /// latter case, all maps are searched in order and the first action that has the given name in any of the maps 477 /// is returned. Note that name comparisons are case-insensitive. 478 /// 479 /// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param> 480 /// <param name="throwIfNotFound">If <c>true</c>, instead of returning <c>null</c> when the action 481 /// cannot be found, throw <c>ArgumentException</c>.</param> 482 /// <returns>The action with the corresponding name or <c>null</c> if no matching action could be found.</returns> 483 /// <remarks> 484 /// Note that no lookup structures are used internally to speed the operation up. Instead, the search is done 485 /// linearly. For repeated access of an action, it is thus generally best to look up actions once ahead of 486 /// time and cache the result. 487 /// 488 /// If multiple actions have the same name and <paramref name="actionNameOrId"/> is not an ID and not an 489 /// action name qualified by a map name (that is, in the form of <c>"mapName/actionName"</c>), the action that 490 /// is returned will be from the first map in <see cref="actionMaps"/> that has an action with the given name. 491 /// An exception is if, of the multiple actions with the same name, some are enabled and some are disabled. In 492 /// this case, the first action that is enabled is returned. 493 /// 494 /// <example> 495 /// <code> 496 /// var asset = ScriptableObject.CreateInstance&lt;InputActionAsset&gt;(); 497 /// 498 /// var map1 = new InputActionMap("map1"); 499 /// var map2 = new InputActionMap("map2"); 500 /// 501 /// asset.AddActionMap(map1); 502 /// asset.AddActionMap(map2); 503 /// 504 /// var action1 = map1.AddAction("action1"); 505 /// var action2 = map1.AddAction("action2"); 506 /// var action3 = map2.AddAction("action3"); 507 /// 508 /// // Search all maps in the asset for any action that has the given name. 509 /// asset.FindAction("action1") // Returns action1. 510 /// asset.FindAction("action2") // Returns action2 511 /// asset.FindAction("action3") // Returns action3. 512 /// 513 /// // Search for a specific action in a specific map. 514 /// asset.FindAction("map1/action1") // Returns action1. 515 /// asset.FindAction("map2/action2") // Returns action2. 516 /// asset.FindAction("map3/action3") // Returns action3. 517 /// 518 /// // Search by unique action ID. 519 /// asset.FindAction(action1.id.ToString()) // Returns action1. 520 /// asset.FindAction(action2.id.ToString()) // Returns action2. 521 /// asset.FindAction(action3.id.ToString()) // Returns action3. 522 /// </code> 523 /// </example> 524 /// </remarks> 525 /// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c>.</exception> 526 /// <exception cref="ArgumentException">Thrown if <paramref name="throwIfNotFound"/> is true and the 527 /// action could not be found. -Or- If <paramref name="actionNameOrId"/> contains a slash but is missing 528 /// either the action or the map name.</exception> 529 public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false) 530 { 531 if (actionNameOrId == null) 532 throw new ArgumentNullException(nameof(actionNameOrId)); 533 534 if (m_ActionMaps != null) 535 { 536 // Check if we have a "map/action" path. 537 var indexOfSlash = actionNameOrId.IndexOf('/'); 538 if (indexOfSlash == -1) 539 { 540 // No slash so it's just a simple action name. Return either first enabled action or, if 541 // none are enabled, first action with the given name. 542 InputAction firstActionFound = null; 543 for (var i = 0; i < m_ActionMaps.Length; ++i) 544 { 545 var action = m_ActionMaps[i].FindAction(actionNameOrId); 546 if (action != null) 547 { 548 if (action.enabled || action.m_Id == actionNameOrId) // Match by ID is always exact. 549 return action; 550 if (firstActionFound == null) 551 firstActionFound = action; 552 } 553 } 554 if (firstActionFound != null) 555 return firstActionFound; 556 } 557 else 558 { 559 // Have a path. First search for the map, then for the action. 560 var mapName = new Substring(actionNameOrId, 0, indexOfSlash); 561 var actionName = new Substring(actionNameOrId, indexOfSlash + 1); 562 563 if (mapName.isEmpty || actionName.isEmpty) 564 throw new ArgumentException("Malformed action path: " + actionNameOrId, nameof(actionNameOrId)); 565 566 for (var i = 0; i < m_ActionMaps.Length; ++i) 567 { 568 var map = m_ActionMaps[i]; 569 if (Substring.Compare(map.name, mapName, StringComparison.InvariantCultureIgnoreCase) != 0) 570 continue; 571 572 var actions = map.m_Actions; 573 if (actions != null) 574 { 575 for (var n = 0; n < actions.Length; ++n) 576 { 577 var action = actions[n]; 578 if (Substring.Compare(action.name, actionName, 579 StringComparison.InvariantCultureIgnoreCase) == 0) 580 return action; 581 } 582 } 583 break; 584 } 585 } 586 } 587 588 if (throwIfNotFound) 589 throw new ArgumentException($"No action '{actionNameOrId}' in '{this}'"); 590 591 return null; 592 } 593 594 /// <inheritdoc/> 595 public int FindBinding(InputBinding mask, out InputAction action) 596 { 597 var numMaps = m_ActionMaps.LengthSafe(); 598 599 for (var i = 0; i < numMaps; ++i) 600 { 601 var actionMap = m_ActionMaps[i]; 602 603 var bindingIndex = actionMap.FindBinding(mask, out action); 604 if (bindingIndex >= 0) 605 return bindingIndex; 606 } 607 608 action = null; 609 return -1; 610 } 611 612 /// <summary> 613 /// Find an <see cref="InputActionMap"/> in the asset by its name or ID. 614 /// </summary> 615 /// <param name="nameOrId">Name or ID (see <see cref="InputActionMap.id"/>) of the action map 616 /// to look for. Matching is case-insensitive.</param> 617 /// <param name="throwIfNotFound">If true, instead of returning <c>null</c>, throw <c>ArgumentException</c>.</param> 618 /// <returns>The <see cref="InputActionMap"/> with a name or ID matching <paramref name="nameOrId"/> or 619 /// <c>null</c> if no matching map could be found.</returns> 620 /// <exception cref="ArgumentNullException"><paramref name="nameOrId"/> is <c>null</c>.</exception> 621 /// <exception cref="ArgumentException">If <paramref name="throwIfNotFound"/> is <c>true</c>, thrown if 622 /// the action map cannot be found.</exception> 623 /// <seealso cref="actionMaps"/> 624 /// <seealso cref="FindActionMap(System.Guid)"/> 625 public InputActionMap FindActionMap(string nameOrId, bool throwIfNotFound = false) 626 { 627 if (nameOrId == null) 628 throw new ArgumentNullException(nameof(nameOrId)); 629 630 if (m_ActionMaps == null) 631 return null; 632 633 // If the name contains a hyphen, it may be a GUID. 634 if (nameOrId.Contains('-') && Guid.TryParse(nameOrId, out var id)) 635 { 636 for (var i = 0; i < m_ActionMaps.Length; ++i) 637 { 638 var map = m_ActionMaps[i]; 639 if (map.idDontGenerate == id) 640 return map; 641 } 642 } 643 644 // Default lookup is by name (case-insensitive). 645 for (var i = 0; i < m_ActionMaps.Length; ++i) 646 { 647 var map = m_ActionMaps[i]; 648 if (string.Compare(nameOrId, map.name, StringComparison.InvariantCultureIgnoreCase) == 0) 649 return map; 650 } 651 652 if (throwIfNotFound) 653 throw new ArgumentException($"Cannot find action map '{nameOrId}' in '{this}'"); 654 655 return null; 656 } 657 658 /// <summary> 659 /// Find an <see cref="InputActionMap"/> in the asset by its ID. 660 /// </summary> 661 /// <param name="id">ID (see <see cref="InputActionMap.id"/>) of the action map 662 /// to look for.</param> 663 /// <returns>The <see cref="InputActionMap"/> with ID matching <paramref name="id"/> or 664 /// <c>null</c> if no map in the asset has the given ID.</returns> 665 /// <seealso cref="actionMaps"/> 666 /// <seealso cref="FindActionMap"/> 667 public InputActionMap FindActionMap(Guid id) 668 { 669 if (m_ActionMaps == null) 670 return null; 671 672 for (var i = 0; i < m_ActionMaps.Length; ++i) 673 { 674 var map = m_ActionMaps[i]; 675 if (map.idDontGenerate == id) 676 return map; 677 } 678 679 return null; 680 } 681 682 /// <summary> 683 /// Find an action by its ID (see <see cref="InputAction.id"/>). 684 /// </summary> 685 /// <param name="guid">ID of the action to look for.</param> 686 /// <returns>The action in the asset with the given ID or null if no action 687 /// in the asset has the given ID.</returns> 688 public InputAction FindAction(Guid guid) 689 { 690 if (m_ActionMaps == null) 691 return null; 692 693 for (var i = 0; i < m_ActionMaps.Length; ++i) 694 { 695 var map = m_ActionMaps[i]; 696 var action = map.FindAction(guid); 697 if (action != null) 698 return action; 699 } 700 701 return null; 702 } 703 704 /// <summary> 705 /// Find the control scheme with the given name and return its index 706 /// in <see cref="controlSchemes"/>. 707 /// </summary> 708 /// <param name="name">Name of the control scheme. Matching is case-insensitive.</param> 709 /// <returns>The index of the given control scheme or -1 if no control scheme 710 /// with the given name could be found.</returns> 711 /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> 712 /// or empty.</exception> 713 public int FindControlSchemeIndex(string name) 714 { 715 if (string.IsNullOrEmpty(name)) 716 throw new ArgumentNullException(nameof(name)); 717 718 if (m_ControlSchemes == null) 719 return -1; 720 721 for (var i = 0; i < m_ControlSchemes.Length; ++i) 722 if (string.Compare(name, m_ControlSchemes[i].name, StringComparison.InvariantCultureIgnoreCase) == 0) 723 return i; 724 725 return -1; 726 } 727 728 /// <summary> 729 /// Find the control scheme with the given name and return it. 730 /// </summary> 731 /// <param name="name">Name of the control scheme. Matching is case-insensitive.</param> 732 /// <returns>The control scheme with the given name or null if no scheme 733 /// with the given name could be found in the asset.</returns> 734 /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> 735 /// or empty.</exception> 736 public InputControlScheme? FindControlScheme(string name) 737 { 738 if (string.IsNullOrEmpty(name)) 739 throw new ArgumentNullException(nameof(name)); 740 741 var index = FindControlSchemeIndex(name); 742 if (index == -1) 743 return null; 744 745 return m_ControlSchemes[index]; 746 } 747 748 /// <summary> 749 /// Return true if the asset contains bindings (in any of its action maps) that are usable 750 /// with the given <paramref name="device"/>. 751 /// </summary> 752 /// <param name="device">An arbitrary input device.</param> 753 /// <returns></returns> 754 /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception> 755 /// <remarks> 756 /// <example> 757 /// <code> 758 /// // Find out if the actions of the given PlayerInput can be used with 759 /// // a gamepad. 760 /// if (playerInput.actions.IsUsableWithDevice(Gamepad.all[0])) 761 /// /* ... */; 762 /// </code> 763 /// </example> 764 /// </remarks> 765 /// <seealso cref="InputActionMap.IsUsableWithDevice"/> 766 /// <seealso cref="InputControlScheme.SupportsDevice"/> 767 public bool IsUsableWithDevice(InputDevice device) 768 { 769 if (device == null) 770 throw new ArgumentNullException(nameof(device)); 771 772 // If we have control schemes, we let those dictate our search. 773 var numControlSchemes = m_ControlSchemes.LengthSafe(); 774 if (numControlSchemes > 0) 775 { 776 for (var i = 0; i < numControlSchemes; ++i) 777 { 778 if (m_ControlSchemes[i].SupportsDevice(device)) 779 return true; 780 } 781 } 782 else 783 { 784 // Otherwise, we'll go search bindings. Slow. 785 var actionMapCount = m_ActionMaps.LengthSafe(); 786 for (var i = 0; i < actionMapCount; ++i) 787 if (m_ActionMaps[i].IsUsableWithDevice(device)) 788 return true; 789 } 790 791 return false; 792 } 793 794 /// <summary> 795 /// Enable all action maps in the asset. 796 /// </summary> 797 /// <remarks> 798 /// This method is equivalent to calling <see cref="InputActionMap.Enable"/> on 799 /// all maps in <see cref="actionMaps"/>. 800 /// </remarks> 801 public void Enable() 802 { 803 foreach (var map in actionMaps) 804 map.Enable(); 805 } 806 807 /// <summary> 808 /// Disable all action maps in the asset. 809 /// </summary> 810 /// <remarks> 811 /// This method is equivalent to calling <see cref="InputActionMap.Disable"/> on 812 /// all maps in <see cref="actionMaps"/>. 813 /// </remarks> 814 public void Disable() 815 { 816 foreach (var map in actionMaps) 817 map.Disable(); 818 } 819 820 /// <summary> 821 /// Return <c>true</c> if the given action is part of the asset. 822 /// </summary> 823 /// <param name="action">An action. Can be null.</param> 824 /// <returns>True if the given action is part of the asset, false otherwise.</returns> 825 public bool Contains(InputAction action) 826 { 827 var map = action?.actionMap; 828 if (map == null) 829 return false; 830 831 return map.asset == this; 832 } 833 834 /// <summary> 835 /// Enumerate all actions in the asset. 836 /// </summary> 837 /// <returns>An enumerator going over the actions in the asset.</returns> 838 /// <remarks> 839 /// Actions will be enumerated one action map in <see cref="actionMaps"/> 840 /// after the other. The actions from each map will be yielded in turn. 841 /// 842 /// This method will allocate GC heap memory. 843 /// </remarks> 844 public IEnumerator<InputAction> GetEnumerator() 845 { 846 if (m_ActionMaps == null) 847 yield break; 848 849 for (var i = 0; i < m_ActionMaps.Length; ++i) 850 { 851 var actions = m_ActionMaps[i].actions; 852 var actionCount = actions.Count; 853 854 for (var n = 0; n < actionCount; ++n) 855 yield return actions[n]; 856 } 857 } 858 859 /// <summary> 860 /// Enumerate all actions in the asset. 861 /// </summary> 862 /// <returns>An enumerator going over the actions in the asset.</returns> 863 /// <seealso cref="GetEnumerator"/> 864 IEnumerator IEnumerable.GetEnumerator() 865 { 866 return GetEnumerator(); 867 } 868 869 internal void MarkAsDirty() 870 { 871#if UNITY_EDITOR 872 InputSystem.TrackDirtyInputActionAsset(this); 873#endif 874 } 875 876 internal bool IsEmpty() 877 { 878 return actionMaps.Count == 0 && controlSchemes.Count == 0; 879 } 880 881 internal void OnWantToChangeSetup() 882 { 883 if (m_ActionMaps.LengthSafe() > 0) 884 m_ActionMaps[0].OnWantToChangeSetup(); 885 } 886 887 internal void OnSetupChanged() 888 { 889 MarkAsDirty(); 890 891 if (m_ActionMaps.LengthSafe() > 0) 892 m_ActionMaps[0].OnSetupChanged(); 893 else 894 m_SharedStateForAllMaps = null; 895 } 896 897 private void ReResolveIfNecessary(bool fullResolve) 898 { 899 if (m_SharedStateForAllMaps == null) 900 return; 901 902 Debug.Assert(m_ActionMaps != null && m_ActionMaps.Length > 0); 903 // State is share between all action maps in the asset. Resolving bindings for the 904 // first map will resolve them for all maps. 905 m_ActionMaps[0].LazyResolveBindings(fullResolve); 906 } 907 908 internal void ResolveBindingsIfNecessary() 909 { 910 if (m_ActionMaps.LengthSafe() > 0) 911 foreach (var map in m_ActionMaps) 912 if (map.ResolveBindingsIfNecessary()) 913 break; 914 } 915 916 private void OnDestroy() 917 { 918 Disable(); 919 if (m_SharedStateForAllMaps != null) 920 { 921 m_SharedStateForAllMaps.Dispose(); // Will clean up InputActionMap state. 922 m_SharedStateForAllMaps = null; 923 } 924 } 925 926 ////TODO: ApplyBindingOverrides, RemoveBindingOverrides, RemoveAllBindingOverrides 927 928 [SerializeField] internal InputActionMap[] m_ActionMaps; 929 [SerializeField] internal InputControlScheme[] m_ControlSchemes; 930 #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 931 [SerializeField] internal bool m_IsProjectWide; 932 #endif 933 934 ////TODO: make this persistent across domain reloads 935 /// <summary> 936 /// Shared state for all action maps in the asset. 937 /// </summary> 938 [NonSerialized] internal InputActionState m_SharedStateForAllMaps; 939 [NonSerialized] internal InputBinding? m_BindingMask; 940 [NonSerialized] internal int m_ParameterOverridesCount; 941 [NonSerialized] internal InputActionRebindingExtensions.ParameterOverride[] m_ParameterOverrides; 942 943 [NonSerialized] internal InputActionMap.DeviceArray m_Devices; 944 945 [Serializable] 946 internal struct WriteFileJson 947 { 948 public string name; 949 public InputActionMap.WriteMapJson[] maps; 950 public InputControlScheme.SchemeJson[] controlSchemes; 951 } 952 953 [Serializable] 954 internal struct WriteFileJsonNoName 955 { 956 public InputActionMap.WriteMapJson[] maps; 957 public InputControlScheme.SchemeJson[] controlSchemes; 958 } 959 960 [Serializable] 961 internal struct ReadFileJson 962 { 963 public string name; 964 public InputActionMap.ReadMapJson[] maps; 965 public InputControlScheme.SchemeJson[] controlSchemes; 966 967 public void ToAsset(InputActionAsset asset) 968 { 969 asset.name = name; 970 asset.m_ActionMaps = new InputActionMap.ReadFileJson {maps = maps}.ToMaps(); 971 asset.m_ControlSchemes = InputControlScheme.SchemeJson.ToSchemes(controlSchemes); 972 973 // Link maps to their asset. 974 if (asset.m_ActionMaps != null) 975 foreach (var map in asset.m_ActionMaps) 976 map.m_Asset = asset; 977 } 978 } 979 } 980}