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<InputActionAsset>();
34 /// var actionMap1 = asset1.AddActionMap("map1");
35 /// action1Map.AddAction("action1", binding: "<Keyboard>/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<InputActionAsset>();
217 /// var actionMap = new InputActionMap();
218 /// var fireAction = actionMap.AddAction("Fire", binding: "<Gamepad>/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<InputActionAsset>();
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"" : ""<Gamepad>/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
330 /// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
331 /// { ""path"" : ""<Gamepad>/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" },
332 /// { ""path"" : ""<Gamepad>/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" },
333 /// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true },
334 /// { ""path"" : ""<Keyboard>/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
335 /// { ""path"" : ""<Keyboard>/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
336 /// { ""path"" : ""<Keyboard>/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
337 /// { ""path"" : ""<Keyboard>/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
338 /// { ""path"" : ""<Mouse>/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&Mouse"" },
339 /// { ""path"" : ""<Mouse>/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&Mouse"" }
340 /// ]
341 /// },
342 /// {
343 /// ""name"" : ""ui"",
344 /// ""actions"" : [
345 /// { ""name"" : ""navigate"" }
346 /// ],
347 /// ""bindings"" : [
348 /// { ""path"" : ""<Gamepad>/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" }
349 /// ]
350 /// }
351 /// ],
352 /// ""controlSchemes"" : [
353 /// {
354 /// ""name"" : ""Gamepad"",
355 /// ""bindingGroup"" : ""Gamepad"",
356 /// ""devices"" : [
357 /// { ""devicePath"" : ""<Gamepad>"" }
358 /// ]
359 /// },
360 /// {
361 /// ""name"" : ""Keyboard&Mouse"",
362 /// ""bindingGroup"" : ""Keyboard&Mouse"",
363 /// ""devices"" : [
364 /// { ""devicePath"" : ""<Keyboard>"" },
365 /// { ""devicePath"" : ""<Mouse>"" }
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"" : ""<Gamepad>/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
413 /// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
414 /// { ""path"" : ""<Gamepad>/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" },
415 /// { ""path"" : ""<Gamepad>/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" },
416 /// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true },
417 /// { ""path"" : ""<Keyboard>/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
418 /// { ""path"" : ""<Keyboard>/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
419 /// { ""path"" : ""<Keyboard>/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
420 /// { ""path"" : ""<Keyboard>/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
421 /// { ""path"" : ""<Mouse>/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&Mouse"" },
422 /// { ""path"" : ""<Mouse>/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&Mouse"" }
423 /// ]
424 /// },
425 /// {
426 /// ""name"" : ""ui"",
427 /// ""actions"" : [
428 /// { ""name"" : ""navigate"" }
429 /// ],
430 /// ""bindings"" : [
431 /// { ""path"" : ""<Gamepad>/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" }
432 /// ]
433 /// }
434 /// ],
435 /// ""controlSchemes"" : [
436 /// {
437 /// ""name"" : ""Gamepad"",
438 /// ""bindingGroup"" : ""Gamepad"",
439 /// ""devices"" : [
440 /// { ""devicePath"" : ""<Gamepad>"" }
441 /// ]
442 /// },
443 /// {
444 /// ""name"" : ""Keyboard&Mouse"",
445 /// ""bindingGroup"" : ""Keyboard&Mouse"",
446 /// ""devices"" : [
447 /// { ""devicePath"" : ""<Keyboard>"" },
448 /// { ""devicePath"" : ""<Mouse>"" }
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<InputActionAsset>();
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}