A game about forced loneliness, made by TACStudios
1using System;
2using UnityEngine.InputSystem.Layouts;
3using UnityEngine.InputSystem.Utilities;
4
5////TODO: Rename all the xxxSyntax structs to xxxAccessor
6
7////TODO: Replace all 'WithXXX' in the accessors with just 'SetXXX'; the 'WithXXX' reads too awkwardly
8
9namespace UnityEngine.InputSystem
10{
11 /// <summary>
12 /// Methods to change the setup of <see cref="InputAction"/>, <see cref="InputActionMap"/>,
13 /// and <see cref="InputActionAsset"/> objects.
14 /// </summary>
15 /// <remarks>
16 /// Unlike the methods in <see cref="InputActionRebindingExtensions"/>, the methods here are
17 /// generally destructive, i.e. they will rearrange the data for actions.
18 /// </remarks>
19 public static class InputActionSetupExtensions
20 {
21 /// <summary>
22 /// Create an action map with the given name and add it to the asset.
23 /// </summary>
24 /// <param name="asset">Asset to add the action map to</param>
25 /// <param name="name">Name to assign to the </param>
26 /// <returns>The newly added action map.</returns>
27 /// <exception cref="ArgumentNullException"><paramref name="asset"/> is <c>null</c> or
28 /// <exception cref="InvalidOperationException">An action map with the given <paramref name="name"/>
29 /// already exists in <paramref name="asset"/>.</exception>
30 /// <paramref name="name"/> is <c>null</c> or empty.</exception>
31 public static InputActionMap AddActionMap(this InputActionAsset asset, string name)
32 {
33 if (asset == null)
34 throw new ArgumentNullException(nameof(asset));
35 if (string.IsNullOrEmpty(name))
36 throw new ArgumentNullException(nameof(name));
37 if (asset.FindActionMap(name) != null)
38 throw new InvalidOperationException(
39 $"An action map called '{name}' already exists in the asset");
40
41 var map = new InputActionMap(name);
42 map.GenerateId();
43 asset.AddActionMap(map);
44 return map;
45 }
46
47 /// <summary>
48 /// Add an action map to the asset.
49 /// </summary>
50 /// <param name="asset">Asset to add the map to.</param>
51 /// <param name="map">A named action map.</param>
52 /// <exception cref="ArgumentNullException"><paramref name="map"/> or <paramref name="asset"/> is <c>null</c>.</exception>
53 /// <exception cref="InvalidOperationException"><paramref name="map"/> has no name or asset already contains a
54 /// map with the same name -or- <paramref name="map"/> is currently enabled -or- <paramref name="map"/> is part of
55 /// an <see cref="InputActionAsset"/> that has <see cref="InputActionMap"/>s that are enabled.</exception>
56 /// <seealso cref="InputActionAsset.actionMaps"/>
57 public static void AddActionMap(this InputActionAsset asset, InputActionMap map)
58 {
59 if (asset == null)
60 throw new ArgumentNullException(nameof(asset));
61 if (map == null)
62 throw new ArgumentNullException(nameof(map));
63 if (string.IsNullOrEmpty(map.name))
64 throw new InvalidOperationException("Maps added to an input action asset must be named");
65 if (map.asset != null)
66 throw new InvalidOperationException(
67 $"Cannot add map '{map}' to asset '{asset}' as it has already been added to asset '{map.asset}'");
68 ////REVIEW: some of the rules here seem stupid; just replace?
69 if (asset.FindActionMap(map.name) != null)
70 throw new InvalidOperationException(
71 $"An action map called '{map.name}' already exists in the asset");
72
73 map.OnWantToChangeSetup();
74 asset.OnWantToChangeSetup();
75
76 ArrayHelpers.Append(ref asset.m_ActionMaps, map);
77 map.m_Asset = asset;
78 asset.OnSetupChanged();
79 }
80
81 /// <summary>
82 /// Remove the given action map from the asset.
83 /// </summary>
84 /// <param name="asset">Asset to add the action map to.</param>
85 /// <param name="map">An action map. If the given map is not part of the asset, the method
86 /// does nothing.</param>
87 /// <exception cref="ArgumentNullException"><paramref name="asset"/> or <paramref name="map"/> is <c>null</c>.</exception>
88 /// <exception cref="InvalidOperationException"><paramref name="map"/> is currently enabled (see <see
89 /// cref="InputActionMap.enabled"/>) or is part of an <see cref="InputActionAsset"/> that has <see cref="InputActionMap"/>s
90 /// that are currently enabled.</exception>
91 /// <seealso cref="RemoveActionMap(InputActionAsset,string)"/>
92 /// <seealso cref="InputActionAsset.actionMaps"/>
93 public static void RemoveActionMap(this InputActionAsset asset, InputActionMap map)
94 {
95 if (asset == null)
96 throw new ArgumentNullException(nameof(asset));
97 if (map == null)
98 throw new ArgumentNullException(nameof(map));
99
100 map.OnWantToChangeSetup();
101 asset.OnWantToChangeSetup();
102
103 // Ignore if not part of this asset.
104 if (map.m_Asset != asset)
105 return;
106
107 ArrayHelpers.Erase(ref asset.m_ActionMaps, map);
108 map.m_Asset = null;
109 asset.OnSetupChanged();
110 }
111
112 /// <summary>
113 /// Remove the action map with the given name or ID from the asset.
114 /// </summary>
115 /// <param name="asset">Asset to remove the action map from.</param>
116 /// <param name="nameOrId">The name or ID (see <see cref="InputActionMap.id"/>) of a map in the
117 /// asset. Note that lookup is case-insensitive. If no map with the given name or ID is found,
118 /// the method does nothing.</param>
119 /// <exception cref="ArgumentNullException"><paramref name="asset"/> or <paramref name="nameOrId"/> is <c>null</c>.</exception>
120 /// <exception cref="InvalidOperationException">The map referenced by <paramref name="nameOrId"/> is currently enabled
121 /// (see <see cref="InputActionMap.enabled"/>).</exception>
122 /// <seealso cref="RemoveActionMap(InputActionAsset,string)"/>
123 /// <seealso cref="InputActionAsset.actionMaps"/>
124 public static void RemoveActionMap(this InputActionAsset asset, string nameOrId)
125 {
126 if (asset == null)
127 throw new ArgumentNullException(nameof(asset));
128 if (nameOrId == null)
129 throw new ArgumentNullException(nameof(nameOrId));
130 var map = asset.FindActionMap(nameOrId);
131 if (map != null)
132 asset.RemoveActionMap(map);
133 }
134
135 ////TODO: add method to add an existing InputAction to a map
136
137 /// <summary>
138 /// Add a new <see cref="InputAction"/> to the given <paramref name="map"/>.
139 /// </summary>
140 /// <param name="map">Action map to add the action to. The action will be appended to
141 /// <see cref="InputActionMap.actions"/> of the map. The map must be disabled (see
142 /// <see cref="InputActionMap.enabled"/>).</param>
143 /// <param name="name">Name to give to the action. Must not be <c>null</c> or empty. Also,
144 /// no other action that already exists in <paramref name="map"/> must have this name already.</param>
145 /// <param name="type">Action type. See <see cref="InputAction.type"/>.</param>
146 /// <param name="binding">If not <c>null</c>, a binding is automatically added to the newly created action
147 /// with the value of this parameter being used as the binding's <see cref="InputBinding.path"/>.</param>
148 /// <param name="interactions">If <paramref name="binding"/> is not <c>null</c>, this string is used for
149 /// <see cref="InputBinding.interactions"/> of the binding that is automatically added for the action.</param>
150 /// <param name="processors">If <paramref name="binding"/> is not <c>null</c>, this string is used for
151 /// <see cref="InputBinding.processors"/> of the binding that is automatically added for the action.</param>
152 /// <param name="groups">If <paramref name="binding"/> is not <c>null</c>, this string is used for
153 /// <see cref="InputBinding.groups"/> of the binding that is automatically added for the action.</param>
154 /// <param name="expectedControlLayout">Value for <see cref="InputAction.expectedControlType"/>; <c>null</c>
155 /// by default.</param>
156 /// <returns>The newly added input action.</returns>
157 /// <exception cref="ArgumentNullException"><paramref name="map"/> is <c>null</c>.</exception>
158 /// <exception cref="ArgumentException"><paramref name="name"/> is <c>null</c> or empty.</exception>
159 /// <exception cref="InvalidOperationException"><paramref name="map"/> is enabled (see <see cref="InputActionMap.enabled"/>)
160 /// or is part of an <see cref="InputActionAsset"/> that has <see cref="InputActionMap"/>s that are <see cref="InputActionMap.enabled"/>
161 /// -or- <paramref name="map"/> already contains an action called <paramref name="name"/> (case-insensitive).</exception>
162 public static InputAction AddAction(this InputActionMap map, string name, InputActionType type = default, string binding = null,
163 string interactions = null, string processors = null, string groups = null, string expectedControlLayout = null)
164 {
165 if (map == null)
166 throw new ArgumentNullException(nameof(map));
167 if (string.IsNullOrEmpty(name))
168 throw new ArgumentException("Action must have name", nameof(name));
169 map.OnWantToChangeSetup();
170 if (map.FindAction(name) != null)
171 throw new InvalidOperationException(
172 $"Cannot add action with duplicate name '{name}' to set '{map.name}'");
173
174 // Append action to array.
175 var action = new InputAction(name, type)
176 {
177 expectedControlType = expectedControlLayout
178 };
179 action.GenerateId();
180 ArrayHelpers.Append(ref map.m_Actions, action);
181 action.m_ActionMap = map;
182
183 // Add binding, if supplied.
184 if (!string.IsNullOrEmpty(binding))
185 {
186 // Will trigger OnSetupChanged.
187 action.AddBinding(binding, interactions: interactions, processors: processors, groups: groups);
188 }
189 else
190 {
191 if (!string.IsNullOrEmpty(groups))
192 throw new ArgumentException(
193 $"No binding path was specified for action '{action}' but groups was specified ('{groups}'); cannot apply groups without binding",
194 nameof(groups));
195
196 // If no binding has been supplied but there are interactions and processors, they go on the action itself.
197 action.m_Interactions = interactions;
198 action.m_Processors = processors;
199
200 map.OnSetupChanged();
201 }
202
203 return action;
204 }
205
206 /// <summary>
207 /// Remove the given action from its <see cref="InputActionMap"/>.
208 /// </summary>
209 /// <param name="action">An input action that is part of an <see cref="InputActionMap"/>.</param>
210 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
211 /// <exception cref="ArgumentException"><paramref name="action"/> is a standalone action
212 /// that is not part of an <see cref="InputActionMap"/> and thus cannot be removed from anything.</exception>
213 /// <exception cref="InvalidOperationException"><paramref name="action"/> is part of an <see cref="InputActionMap"/>
214 /// or <see cref="InputActionAsset"/> that has at least one enabled action.</exception>
215 /// <remarks>
216 /// After removal, the action's <see cref="InputAction.actionMap"/> will be set to <c>null</c>
217 /// and the action will effectively become a standalone action that is not associated with
218 /// any action map. Bindings on the action will be preserved. On the action map, the bindings
219 /// for the action will be removed.
220 /// </remarks>
221 /// <seealso cref="AddAction"/>
222 public static void RemoveAction(this InputAction action)
223 {
224 if (action == null)
225 throw new ArgumentNullException(nameof(action));
226
227 var actionMap = action.actionMap;
228 if (actionMap == null)
229 throw new ArgumentException(
230 $"Action '{action}' does not belong to an action map; nowhere to remove from", nameof(action));
231 actionMap.OnWantToChangeSetup();
232
233 var bindingsForAction = action.bindings.ToArray();
234
235 var index = actionMap.m_Actions.IndexOfReference(action);
236 Debug.Assert(index != -1, "Could not find action in map");
237 ArrayHelpers.EraseAt(ref actionMap.m_Actions, index);
238
239 action.m_ActionMap = null;
240 action.m_SingletonActionBindings = bindingsForAction;
241
242 // Remove bindings to action from map.
243 var newActionMapBindingCount = actionMap.m_Bindings.Length - bindingsForAction.Length;
244 if (newActionMapBindingCount == 0)
245 {
246 actionMap.m_Bindings = null;
247 }
248 else
249 {
250 var newActionMapBindings = new InputBinding[newActionMapBindingCount];
251 var oldActionMapBindings = actionMap.m_Bindings;
252 var bindingIndex = 0;
253 for (var i = 0; i < oldActionMapBindings.Length; ++i)
254 {
255 var binding = oldActionMapBindings[i];
256 if (bindingsForAction.IndexOf(b => b == binding) == -1)
257 newActionMapBindings[bindingIndex++] = binding;
258 }
259 actionMap.m_Bindings = newActionMapBindings;
260 }
261
262 actionMap.OnSetupChanged();
263 }
264
265 /// <summary>
266 /// Remove the action with the given name from the asset.
267 /// </summary>
268 /// <param name="asset">Asset to remove the action from.</param>
269 /// <param name="nameOrId">Name or ID of the action. See <see cref="InputActionAsset.FindAction(string,bool)"/> for
270 /// details.</param>
271 /// <exception cref="ArgumentNullException"><paramref name="asset"/> is <c>null</c> -or- <paramref name="nameOrId"/>
272 /// is <c>null</c> or empty.</exception>
273 /// <seealso cref="RemoveAction(InputAction)"/>
274 public static void RemoveAction(this InputActionAsset asset, string nameOrId)
275 {
276 if (asset == null)
277 throw new ArgumentNullException(nameof(asset));
278 if (nameOrId == null)
279 throw new ArgumentNullException(nameof(nameOrId));
280 var action = asset.FindAction(nameOrId);
281 action?.RemoveAction();
282 }
283
284 /// <summary>
285 /// Add a new binding to the given action.
286 /// </summary>
287 /// <param name="action">Action to add the binding to. If the action is part of an <see cref="InputActionMap"/>,
288 /// the newly added binding will be visible on <see cref="InputActionMap.bindings"/>.</param>
289 /// <param name="path">Binding path string. See <see cref="InputBinding.path"/> for details.</param>
290 /// <param name="interactions">Optional list of interactions to apply to the binding. See <see
291 /// cref="InputBinding.interactions"/> for details.</param>
292 /// <param name="processors">Optional list of processors to apply to the binding. See <see
293 /// cref="InputBinding.processors"/> for details.</param>
294 /// <param name="groups">Optional list of binding groups that should be assigned to the binding. See
295 /// <see cref="InputBinding.groups"/> for details.</param>
296 /// <returns>Fluent-style syntax to further configure the binding.</returns>
297 public static BindingSyntax AddBinding(this InputAction action, string path, string interactions = null,
298 string processors = null, string groups = null)
299 {
300 return AddBinding(action, new InputBinding
301 {
302 path = path,
303 interactions = interactions,
304 processors = processors,
305 groups = groups
306 });
307 }
308
309 /// <summary>
310 /// Conditionally compiled helper for logging API usage of code-authored actions.
311 /// </summary>
312 /// <param name="api">The associated API function.</param>
313 /// <remarks>
314 /// Be extremely carefully to review for indirect calls and overloads to not register analytics twice.
315 /// Be extremely careful in enabling/disabling tracking before internal calls since those may otherwise
316 /// be incorrectly registered.
317 /// </remarks>
318 #if UNITY_EDITOR
319 private static void RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api api)
320 {
321 UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Register(api);
322 }
323
324 #endif
325
326 /// <summary>
327 /// Add a binding that references the given <paramref name="control"/> and triggers
328 /// the given <paramref cref="action"/>.
329 /// </summary>
330 /// <param name="action">Action to trigger.</param>
331 /// <param name="control">Control to bind to. The full <see cref="InputControl.path"/> of the control will
332 /// be used in the resulting <see cref="InputBinding">binding</see>.</param>
333 /// <returns>Syntax to configure the binding further.</returns>
334 /// <exception cref="ArgumentNullException"><paramref name="action"/> is null or <paramref name="control"/> is null.</exception>
335 /// <seealso cref="InputAction.bindings"/>
336 public static BindingSyntax AddBinding(this InputAction action, InputControl control)
337 {
338 if (control == null)
339 throw new ArgumentNullException(nameof(control));
340 return AddBinding(action, control.path);
341 }
342
343 /// <summary>
344 /// Add a new binding to the action.
345 /// </summary>
346 /// <param name="action">An action to add the binding to.</param>
347 /// <param name="binding">Binding to add to the action or default. Binding can be further configured via
348 /// the struct returned by the method.</param>
349 /// <returns>
350 /// Returns a fluent-style syntax structure that allows performing additional modifications
351 /// based on the new binding.
352 /// </returns>
353 /// <remarks>
354 /// This works both with actions that are part of an action set as well as with actions that aren't.
355 ///
356 /// Note that actions must be disabled while altering their binding sets. Also, if the action belongs
357 /// to a set, all actions in the set must be disabled.
358 ///
359 /// <example>
360 /// <code>
361 /// fireAction.AddBinding()
362 /// .WithPath("<Gamepad>/buttonSouth")
363 /// .WithGroup("Gamepad");
364 /// </code>
365 /// </example>
366 /// </remarks>
367 public static BindingSyntax AddBinding(this InputAction action, InputBinding binding = default)
368 {
369 #if UNITY_EDITOR
370 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.AddBinding);
371 #endif
372
373 if (action == null)
374 throw new ArgumentNullException(nameof(action));
375
376 ////REVIEW: should this reference actions by ID?
377 Debug.Assert(action.m_Name != null || action.isSingletonAction);
378 binding.action = action.name;
379
380 var actionMap = action.GetOrCreateActionMap();
381 var bindingIndex = AddBindingInternal(actionMap, binding);
382 return new BindingSyntax(actionMap, bindingIndex);
383 }
384
385 /// <summary>
386 /// Add a new binding to the given action map.
387 /// </summary>
388 /// <param name="actionMap">Action map to add the binding to.</param>
389 /// <param name="path">Path of the control(s) to bind to. See <see cref="InputControlPath"/> and
390 /// <see cref="InputBinding.path"/>.</param>
391 /// <param name="interactions">Names and parameters for interactions to apply to the
392 /// binding. See <see cref="InputBinding.interactions"/>.</param>
393 /// <param name="groups">Optional list of groups to apply to the binding. See <see cref="InputBinding.groups"/>.</param>
394 /// <param name="action">Action to trigger from the binding. See <see cref="InputBinding.action"/>.</param>
395 /// <param name="processors">Optional list of processors to apply to the binding. See <see cref="InputBinding.processors"/>.</param>
396 /// <returns>A write-accessor to the newly added binding.</returns>
397 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
398 /// <remarks>
399 /// <example>
400 /// <code>
401 /// // Add a binding for the A button the gamepad and make it trigger
402 /// // the "fire" action.
403 /// var gameplayActions = playerInput.actions.FindActionMap("gameplay");
404 /// gameplayActions.AddBinding("<Gamepad>/buttonSouth", action: "fire");
405 /// </code>
406 /// </example>
407 /// </remarks>
408 /// <seealso cref="InputBinding"/>
409 /// <seealso cref="InputActionMap.bindings"/>
410 public static BindingSyntax AddBinding(this InputActionMap actionMap, string path,
411 string interactions = null, string groups = null, string action = null, string processors = null)
412 {
413 if (path == null)
414 throw new ArgumentNullException(nameof(path), "Binding path cannot be null");
415
416 return AddBinding(actionMap,
417 new InputBinding
418 {
419 path = path,
420 interactions = interactions,
421 groups = groups,
422 action = action,
423 processors = processors,
424 });
425 }
426
427 /// <summary>
428 /// Add a new binding that triggers the given action to the given action map.
429 /// </summary>
430 /// <param name="actionMap">Action map to add the binding to.</param>
431 /// <param name="action">Action to trigger from the binding. See <see cref="InputBinding.action"/>.
432 /// Must be part of <paramref name="actionMap"/>.</param>
433 /// <param name="path">Path of the control(s) to bind to. See <see cref="InputControlPath"/> and
434 /// <see cref="InputBinding.path"/>.</param>
435 /// <param name="interactions">Names and parameters for interactions to apply to the
436 /// binding. See <see cref="InputBinding.interactions"/>.</param>
437 /// <param name="groups">Binding groups to apply to the binding. See <see cref="InputBinding.groups"/>.</param>
438 /// <returns>A write-accessor to the newly added binding.</returns>
439 /// <exception cref="ArgumentException"><paramref name="action"/> is not part of <paramref name="actionMap"/>.</exception>
440 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
441 /// <seealso cref="InputBinding"/>
442 /// <seealso cref="InputActionMap.bindings"/>
443 public static BindingSyntax AddBinding(this InputActionMap actionMap, string path, InputAction action,
444 string interactions = null, string groups = null)
445 {
446 if (action != null && action.actionMap != actionMap)
447 throw new ArgumentException(
448 $"Action '{action}' is not part of action map '{actionMap}'", nameof(action));
449
450 if (action == null)
451 return AddBinding(actionMap, path: path, interactions: interactions, groups: groups);
452
453 return AddBinding(actionMap, path: path, interactions: interactions, groups: groups,
454 action: action.id);
455 }
456
457 /// <summary>
458 /// Add a new binding that triggers the action given by GUID <paramref name="action"/>.
459 /// </summary>
460 /// <param name="actionMap">Action map to add the binding to.</param>
461 /// <param name="path">Path of the control(s) to bind to. See <see cref="InputControlPath"/> and <see cref="InputBinding.path"/>.</param>
462 /// <param name="action">ID of the action as per <see cref="InputAction.id"/>.</param>
463 /// <param name="interactions">Optional list of names and parameters for interactions to apply to the
464 /// binding. See <see cref="InputBinding.interactions"/>.</param>
465 /// <param name="groups">Optional list of groups to apply to the binding. See <see cref="InputBinding.groups"/>.</param>
466 /// <returns>A write-accessor to the newly added binding.</returns>
467 /// <exception cref="ArgumentException"><paramref name="action"/> is not part of <paramref name="actionMap"/>.</exception>
468 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
469 /// <seealso cref="InputBinding"/>
470 /// <seealso cref="InputActionMap.bindings"/>
471 /// <remarks>
472 /// Example of adding a binding to an action map that binds to a Gamepad device "leftStick" control and associates it with an action:
473 /// <example>
474 /// <code>
475 /// var map = new InputActionMap();
476 /// var action = map.AddAction("action");
477 /// map.AddBinding("<Gamepad>/leftStick", action: action.id);
478 /// </code>
479 /// </example>
480 /// </remarks>
481 public static BindingSyntax AddBinding(this InputActionMap actionMap, string path, Guid action,
482 string interactions = null, string groups = null)
483 {
484 if (action == Guid.Empty)
485 return AddBinding(actionMap, path: path, interactions: interactions, groups: groups);
486 return AddBinding(actionMap, path: path, interactions: interactions, groups: groups,
487 action: action.ToString());
488 }
489
490 /// <summary>
491 /// Add a binding to the given action map.
492 /// </summary>
493 /// <param name="actionMap">Action map to add the binding to.</param>
494 /// <param name="binding">Binding to add to the action map.</param>
495 /// <returns>A write-accessor to the newly added binding.</returns>
496 /// <exception cref="ArgumentException"><paramref name="action"/> is not part of <paramref name="actionMap"/>.</exception>
497 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
498 /// <seealso cref="InputBinding"/>
499 /// <seealso cref="InputActionMap.bindings"/>
500 public static BindingSyntax AddBinding(this InputActionMap actionMap, InputBinding binding)
501 {
502 #if UNITY_EDITOR
503 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.AddBinding);
504 #endif
505
506 if (actionMap == null)
507 throw new ArgumentNullException(nameof(actionMap));
508 if (binding.path == null)
509 throw new ArgumentException("Binding path cannot be null", nameof(binding));
510
511 var bindingIndex = AddBindingInternal(actionMap, binding);
512 return new BindingSyntax(actionMap, bindingIndex);
513 }
514
515 /// <summary>
516 /// Add a composite binding to the <see cref="InputAction.bindings"/> of <paramref name="action"/>.
517 /// </summary>
518 /// <param name="action">Action to add the binding to.</param>
519 /// <param name="composite">Type of composite to add. This needs to be the name the composite
520 /// has been registered under using <see cref="InputSystem.RegisterBindingComposite{T}"/>. Case-insensitive.</param>
521 /// <param name="interactions">Interactions to add to the binding. See <see cref="InputBinding.interactions"/>.</param>
522 /// <param name="processors">Processors to add to the binding. See <see cref="InputBinding.processors"/>.</param>
523 /// <returns>A write accessor to the newly added composite binding.</returns>
524 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
525 /// <exception cref="ArgumentException"><paramref name="composite"/> is <c>null</c> or empty.</exception>
526 public static CompositeSyntax AddCompositeBinding(this InputAction action, string composite,
527 string interactions = null, string processors = null)
528 {
529 #if UNITY_EDITOR
530 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.AddCompositeBinding);
531 #endif
532
533 if (action == null)
534 throw new ArgumentNullException(nameof(action));
535 if (string.IsNullOrEmpty(composite))
536 throw new ArgumentException("Composite name cannot be null or empty", nameof(composite));
537
538 var actionMap = action.GetOrCreateActionMap();
539
540 var binding = new InputBinding
541 {
542 name = NameAndParameters.ParseName(composite),
543 path = composite,
544 interactions = interactions,
545 processors = processors,
546 isComposite = true,
547 action = action.name
548 };
549
550 var bindingIndex = AddBindingInternal(actionMap, binding);
551 return new CompositeSyntax(actionMap, action, bindingIndex);
552 }
553
554 ////TODO: AddCompositeBinding<T>
555
556 private static int AddBindingInternal(InputActionMap map, InputBinding binding, int bindingIndex = -1)
557 {
558 Debug.Assert(map != null);
559
560 // Make sure the binding has an ID.
561 if (string.IsNullOrEmpty(binding.m_Id))
562 binding.GenerateId();
563
564 // Append to bindings in set.
565 if (bindingIndex < 0)
566 bindingIndex = ArrayHelpers.Append(ref map.m_Bindings, binding);
567 else
568 ArrayHelpers.InsertAt(ref map.m_Bindings, bindingIndex, binding);
569
570 // Make sure this asset is reloaded from disk when exiting play mode so it isn't inadvertently
571 // changed between play sessions. Only applies when running in the editor.
572 if (map.asset != null)
573 map.asset.MarkAsDirty();
574
575 // If we're looking at a singleton action, make sure m_Bindings is up to date just
576 // in case the action gets serialized.
577 if (map.m_SingletonAction != null)
578 map.m_SingletonAction.m_SingletonActionBindings = map.m_Bindings;
579
580 // NOTE: We treat this as a mere binding modification, even though we have added something.
581 // InputAction.RestoreActionStatesAfterReResolvingBindings() can deal with bindings
582 // having been removed or added.
583 map.OnBindingModified();
584
585 return bindingIndex;
586 }
587
588 /// <summary>
589 /// Get write access to the binding in <see cref="InputAction.bindings"/> of <paramref name="action"/>
590 /// at the given <paramref name="index"/>.
591 /// </summary>
592 /// <param name="action">Action whose bindings to change.</param>
593 /// <param name="index">Index in <paramref name="action"/>'s <see cref="InputAction.bindings"/> of the binding to be changed.</param>
594 /// <returns>A write accessor to the given binding.</returns>
595 /// <remarks>
596 /// <example>
597 /// <code>
598 /// // Grab "fire" action from PlayerInput.
599 /// var fireAction = playerInput.actions["fire"];
600 ///
601 /// // Change its second binding to go to the left mouse button.
602 /// fireAction.ChangeBinding(1)
603 /// .WithPath("<Mouse>/leftButton");
604 /// </code>
605 /// </example>
606 /// </remarks>
607 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
608 /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range (as per <see cref="InputAction.bindings"/>
609 /// of <paramref name="action"/>).</exception>
610 public static BindingSyntax ChangeBinding(this InputAction action, int index)
611 {
612 #if UNITY_EDITOR
613 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ChangeBinding);
614 #endif
615
616 if (action == null)
617 throw new ArgumentNullException(nameof(action));
618
619 var indexOnMap = action.BindingIndexOnActionToBindingIndexOnMap(index);
620 return new BindingSyntax(action.GetOrCreateActionMap(), indexOnMap, action);
621 }
622
623 /// <summary>
624 /// Get write access to the binding in <see cref="InputAction.bindings"/> of <paramref name="action"/>
625 /// with the given <paramref name="name"/>.
626 /// </summary>
627 /// <param name="action">Action whose bindings to change.</param>
628 /// <param name="name">Name of the binding to be changed <see cref="InputAction.bindings"/>.</param>
629 /// <returns>A write accessor to the given binding.</returns>
630 /// <remarks>
631 /// <example>
632 /// <code>
633 /// // Grab "fire" action from PlayerInput.
634 /// var fireAction = playerInput.actions["fire"];
635 ///
636 /// // Change its second binding to go to the left mouse button.
637 /// fireAction.ChangeBinding("fire")
638 /// .WithPath("<Mouse>/leftButton");
639 /// </code>
640 /// </example>
641 /// </remarks>
642 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
643 /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range (as per <see cref="InputAction.bindings"/>
644 /// of <paramref name="action"/>).</exception>
645 public static BindingSyntax ChangeBinding(this InputAction action, string name)
646 {
647 return action.ChangeBinding(new InputBinding { name = name });
648 }
649
650 /// <summary>
651 /// Get write access to the binding in <see cref="InputActionMap.bindings"/> of <paramref name="actionMap"/>
652 /// at the given <paramref name="index"/>.
653 /// </summary>
654 /// <param name="actionMap">Action map whose bindings to change.</param>
655 /// <param name="index">Index in <paramref name="actionMap"/>'s <see cref="InputActionMap.bindings"/> of the binding to be changed.</param>
656 /// <returns>A write accessor to the given binding.</returns>
657 /// <remarks>
658 /// <example>
659 /// <code>
660 /// // Grab "gameplay" actions from PlayerInput.
661 /// var gameplayActions = playerInput.actions.FindActionMap("gameplay");
662 ///
663 /// // Change its second binding to go to the left mouse button.
664 /// gameplayActions.ChangeBinding(1)
665 /// .WithPath("<Mouse>/leftButton");
666 /// </code>
667 /// </example>
668 /// </remarks>
669 /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c>.</exception>
670 /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range (as per <see cref="InputActionMap.bindings"/>
671 /// of <paramref name="actionMap"/>).</exception>
672 public static BindingSyntax ChangeBinding(this InputActionMap actionMap, int index)
673 {
674 #if UNITY_EDITOR
675 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ChangeBinding);
676 #endif
677
678 if (actionMap == null)
679 throw new ArgumentNullException(nameof(actionMap));
680 if (index < 0 || index >= actionMap.m_Bindings.LengthSafe())
681 throw new ArgumentOutOfRangeException(nameof(index));
682
683 return new BindingSyntax(actionMap, index);
684 }
685
686 /// <summary>
687 /// Get write access to the binding in <see cref="InputAction.bindings"/> of <paramref name="action"/>
688 /// that has the given <paramref name="id"/>.
689 /// </summary>
690 /// <param name="action">Action whose bindings to change.</param>
691 /// <param name="id">ID of the binding as per <see cref="InputBinding.id"/>.</param>
692 /// <returns>A write accessor to the binding with the given ID.</returns>
693 /// <remarks>
694 /// <example>
695 /// <code>
696 /// // Grab "fire" action from PlayerInput.
697 /// var fireAction = playerInput.actions["fire"];
698 ///
699 /// // Change the binding with the given ID to go to the left mouse button.
700 /// fireAction.ChangeBindingWithId("c3de9215-31c3-4654-8562-854bf2f7864f")
701 /// .WithPath("<Mouse>/leftButton");
702 /// </code>
703 /// </example>
704 /// </remarks>
705 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
706 /// <exception cref="ArgumentException">No binding with the given <paramref name="id"/> exists
707 /// on <paramref name="action"/>.</exception>
708 public static BindingSyntax ChangeBindingWithId(this InputAction action, string id)
709 {
710 if (action == null)
711 throw new ArgumentNullException(nameof(action));
712
713 return action.ChangeBinding(new InputBinding {m_Id = id});
714 }
715
716 /// <summary>
717 /// Get write access to the binding in <see cref="InputAction.bindings"/> of <paramref name="action"/>
718 /// that has the given <paramref name="id"/>.
719 /// </summary>
720 /// <param name="action">Action whose bindings to change.</param>
721 /// <param name="id">ID of the binding as per <see cref="InputBinding.id"/>.</param>
722 /// <returns>A write accessor to the binding with the given ID.</returns>
723 /// <remarks>
724 /// <example>
725 /// <code>
726 /// // Grab "fire" action from PlayerInput.
727 /// var fireAction = playerInput.actions["fire"];
728 ///
729 /// // Change the binding with the given ID to go to the left mouse button.
730 /// fireAction.ChangeBindingWithId(new Guid("c3de9215-31c3-4654-8562-854bf2f7864f"))
731 /// .WithPath("<Mouse>/leftButton");
732 /// </code>
733 /// </example>
734 /// </remarks>
735 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
736 /// <exception cref="ArgumentException">No binding with the given <paramref name="id"/> exists
737 /// on <paramref name="action"/>.</exception>
738 public static BindingSyntax ChangeBindingWithId(this InputAction action, Guid id)
739 {
740 if (action == null)
741 throw new ArgumentNullException(nameof(action));
742
743 return action.ChangeBinding(new InputBinding {id = id});
744 }
745
746 /// <summary>
747 /// Get write access to the first binding in <see cref="InputAction.bindings"/> of <paramref name="action"/>
748 /// that is assigned to the given binding <paramref name="group"/>.
749 /// </summary>
750 /// <param name="action">Action whose bindings to change.</param>
751 /// <param name="group">Name of the binding group as per <see cref="InputBinding.groups"/>.</param>
752 /// <returns>A write accessor to the first binding on <paramref name="action"/> that is assigned to the
753 /// given binding <paramref name="group"/>.</returns>
754 /// <remarks>
755 /// <example>
756 /// <code>
757 /// // Grab "fire" action from PlayerInput.
758 /// var fireAction = playerInput.actions["fire"];
759 ///
760 /// // Change the binding in the "Keyboard&Mouse" group to go to the left mouse button.
761 /// fireAction.ChangeBindingWithGroup("Keyboard&Mouse")
762 /// .WithPath("<Mouse>/leftButton");
763 /// </code>
764 /// </example>
765 /// </remarks>
766 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
767 /// <exception cref="ArgumentException">No binding on the <paramref name="action"/> is assigned
768 /// to the given binding <paramref name="group"/>.</exception>
769 public static BindingSyntax ChangeBindingWithGroup(this InputAction action, string group)
770 {
771 if (action == null)
772 throw new ArgumentNullException(nameof(action));
773
774 return action.ChangeBinding(new InputBinding {groups = group});
775 }
776
777 /// <summary>
778 /// Get write access to the binding in <see cref="InputAction.bindings"/> of <paramref name="action"/>
779 /// that is bound to the given <paramref name="path"/>.
780 /// </summary>
781 /// <param name="action">Action whose bindings to change.</param>
782 /// <param name="path">Path of the binding as per <see cref="InputBinding.path"/>.</param>
783 /// <returns>A write accessor to the binding on <paramref name="action"/> that is assigned the
784 /// given <paramref name="path"/>.</returns>
785 /// <remarks>
786 /// <example>
787 /// <code>
788 /// // Grab "fire" action from PlayerInput.
789 /// var fireAction = playerInput.actions["fire"];
790 ///
791 /// // Change the binding to the right mouse button to go to the left mouse button instead.
792 /// fireAction.ChangeBindingWithPath("<Mouse>/rightButton")
793 /// .WithPath("<Mouse>/leftButton");
794 /// </code>
795 /// </example>
796 /// </remarks>
797 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
798 /// <exception cref="ArgumentException">No binding on the <paramref name="action"/> is assigned
799 /// the given <paramref name="path"/>.</exception>
800 public static BindingSyntax ChangeBindingWithPath(this InputAction action, string path)
801 {
802 if (action == null)
803 throw new ArgumentNullException(nameof(action));
804
805 return action.ChangeBinding(new InputBinding {path = path});
806 }
807
808 /// <summary>
809 /// Get write access to the binding on <paramref name="action"/> that matches the given
810 /// <paramref name="match"/>.
811 /// </summary>
812 /// <param name="action">Action whose bindings to match against.</param>
813 /// <param name="match">A binding mask. See <see cref="InputBinding.Matches"/> for
814 /// details.</param>
815 /// <returns>A write-accessor to the first binding matching <paramref name="match"/> or
816 /// an invalid accessor (see <see cref="BindingSyntax.valid"/>) if no binding was found to
817 /// match the mask.</returns>
818 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
819 public static BindingSyntax ChangeBinding(this InputAction action, InputBinding match)
820 {
821 if (action == null)
822 throw new ArgumentNullException(nameof(action));
823
824 var actionMap = action.GetOrCreateActionMap();
825
826 int bindingIndexInMap = -1;
827 var id = action.idDontGenerate;
828 if (id != null)
829 {
830 // Prio1: Attempt to match action id (stronger)
831 match.action = action.id.ToString();
832 bindingIndexInMap = actionMap.FindBindingRelativeToMap(match);
833 }
834 if (bindingIndexInMap == -1)
835 {
836 // Prio2: Attempt to match action name (weaker)
837 match.action = action.name;
838 bindingIndexInMap = actionMap.FindBindingRelativeToMap(match);
839 }
840 if (bindingIndexInMap == -1)
841 return default;
842
843 return new BindingSyntax(actionMap, bindingIndexInMap, action);
844 }
845
846 /// <summary>
847 /// Get a write accessor to the binding of <paramref name="action"/> that is both a composite
848 /// (see <see cref="InputBinding.isComposite"/>) and has the given binding name or composite
849 /// type.
850 /// </summary>
851 /// <param name="action">Action to look up the binding on. All bindings in the action's
852 /// <see cref="InputAction.bindings"/> property will be considered.</param>
853 /// <param name="compositeName">Either the name of the composite binding (see <see cref="InputBinding.name"/>)
854 /// to look for or the name of the composite type used in the binding (such as "1DAxis"). Case-insensitive.</param>
855 /// <returns>A write accessor to the given composite binding or an invalid accessor if no composite
856 /// matching <paramref name="compositeName"/> could be found on <paramref name="action"/>.</returns>
857 /// <remarks>
858 /// <example>
859 /// <code>
860 /// // Add arrow keys as alternatives to the WASD Vector2 composite.
861 /// playerInput.actions["move"]
862 /// .ChangeCompositeBinding("WASD")
863 /// .InsertPartBinding("Up", "<Keyboard>/upArrow")
864 /// .InsertPartBinding("Down", "<Keyboard>/downArrow")
865 /// .InsertPartBinding("Left", "<Keyboard>/leftArrow")
866 /// .InsertPartBinding("Right", "<Keyboard>/rightArrow");
867 /// </code>
868 /// </example>
869 /// </remarks>
870 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c> -or- <paramref name="compositeName"/>
871 /// is <c>null</c> or empty.</exception>
872 /// <seealso cref="InputBinding.isComposite"/>
873 /// <seealso cref="InputBindingComposite"/>
874 public static BindingSyntax ChangeCompositeBinding(this InputAction action, string compositeName)
875 {
876 #if UNITY_EDITOR
877 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ChangeCompositeBinding);
878 #endif
879
880 if (action == null)
881 throw new ArgumentNullException(nameof(action));
882 if (string.IsNullOrEmpty(compositeName))
883 throw new ArgumentNullException(nameof(compositeName));
884
885 var actionMap = action.GetOrCreateActionMap();
886 var bindings = actionMap.m_Bindings;
887 var numBindings = bindings.LengthSafe();
888
889 for (var i = 0; i < numBindings; ++i)
890 {
891 ref var binding = ref bindings[i];
892 if (!binding.isComposite || !binding.TriggersAction(action))
893 continue;
894
895 ////REVIEW: should this do a registration lookup to deal with aliases?
896 if (compositeName.Equals(binding.name, StringComparison.InvariantCultureIgnoreCase)
897 || compositeName.Equals(NameAndParameters.ParseName(binding.path),
898 StringComparison.InvariantCultureIgnoreCase))
899 return new BindingSyntax(actionMap, i, action);
900 }
901
902 return default;
903 }
904
905 ////TODO: update binding mask if necessary
906 /// <summary>
907 /// Rename an existing action.
908 /// </summary>
909 /// <param name="action">Action to assign a new name to. Can be singleton action or action that
910 /// is part of a map.</param>
911 /// <param name="newName">New name to assign to action. Cannot be empty.</param>
912 /// <exception cref="ArgumentNullException"><paramref name="action"/> is null or <paramref name="newName"/> is
913 /// null or empty.</exception>
914 /// <exception cref="InvalidOperationException"><see cref="InputAction.actionMap"/> of <paramref name="action"/>
915 /// already contains an action called <paramref name="newName"/>.</exception>
916 /// <remarks>
917 /// Renaming an action will also update the bindings that refer to the action.
918 /// </remarks>
919 public static void Rename(this InputAction action, string newName)
920 {
921 #if UNITY_EDITOR
922 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.Rename);
923 #endif
924
925 if (action == null)
926 throw new ArgumentNullException(nameof(action));
927 if (string.IsNullOrEmpty(newName))
928 throw new ArgumentNullException(nameof(newName));
929
930 if (action.name == newName)
931 return;
932
933 // Make sure name isn't already taken in map.
934 var actionMap = action.actionMap;
935 if (actionMap?.FindAction(newName) != null)
936 throw new InvalidOperationException(
937 $"Cannot rename '{action}' to '{newName}' in map '{actionMap}' as the map already contains an action with that name");
938
939 var oldName = action.m_Name;
940 action.m_Name = newName;
941 actionMap?.ClearActionLookupTable();
942
943 if (actionMap?.asset != null)
944 actionMap?.asset.MarkAsDirty();
945
946 // Update bindings.
947 var bindings = action.GetOrCreateActionMap().m_Bindings;
948 var bindingCount = bindings.LengthSafe();
949 for (var i = 0; i < bindingCount; ++i)
950 if (string.Compare(bindings[i].action, oldName, StringComparison.InvariantCultureIgnoreCase) == 0)
951 bindings[i].action = newName;
952 }
953
954 /// <summary>
955 /// Add a new control scheme to the asset.
956 /// </summary>
957 /// <param name="asset">Asset to add the control scheme to.</param>
958 /// <param name="controlScheme">Control scheme to add.</param>
959 /// <exception cref="ArgumentException"><paramref name="controlScheme"/> has no name.</exception>
960 /// <exception cref="ArgumentNullException"><paramref name="asset"/> is <c>null</c>.</exception>
961 /// <exception cref="InvalidOperationException">A control scheme with the same name as <paramref name="controlScheme"/>
962 /// already exists in the asset.</exception>
963 /// <remarks>
964 /// </remarks>
965 public static void AddControlScheme(this InputActionAsset asset, InputControlScheme controlScheme)
966 {
967 #if UNITY_EDITOR
968 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.AddControlScheme);
969 #endif
970
971 if (asset == null)
972 throw new ArgumentNullException(nameof(asset));
973 if (string.IsNullOrEmpty(controlScheme.name))
974 throw new ArgumentException("Cannot add control scheme without name to asset " + asset.name, nameof(controlScheme));
975 if (asset.FindControlScheme(controlScheme.name) != null)
976 throw new InvalidOperationException(
977 $"Asset '{asset.name}' already contains a control scheme called '{controlScheme.name}'");
978
979 ArrayHelpers.Append(ref asset.m_ControlSchemes, controlScheme);
980
981 asset.MarkAsDirty();
982 }
983
984 /// <summary>
985 /// Add a new control scheme to the given <paramref name="asset"/>.
986 /// </summary>
987 /// <param name="asset">Asset to add the control scheme to.</param>
988 /// <param name="name">Name to give to the control scheme. Must be unique within the control schemes of the
989 /// asset. Also used as default name of <see cref="InputControlScheme.bindingGroup">binding group</see> associated
990 /// with the control scheme.</param>
991 /// <returns>Syntax to allow providing additional configuration for the newly added control scheme.</returns>
992 /// <exception cref="ArgumentNullException"><paramref name="asset"/> is <c>null</c> -or- <paramref name="name"/>
993 /// is <c>null</c> or empty.</exception>
994 /// <remarks>
995 /// <example>
996 /// <code>
997 /// // Create an .inputactions asset.
998 /// var asset = ScriptableObject.CreateInstance<InputActionAsset>();
999 ///
1000 /// // Add an action map to it.
1001 /// var actionMap = asset.AddActionMap("actions");
1002 ///
1003 /// // Add an action to it and bind it to the A button on the gamepad.
1004 /// // Also, associate that binding with the "Gamepad" control scheme.
1005 /// var action = actionMap.AddAction("action");
1006 /// action.AddBinding("<Gamepad>/buttonSouth", groups: "Gamepad");
1007 ///
1008 /// // Add a control scheme called "Gamepad" that requires a Gamepad device.
1009 /// asset.AddControlScheme("Gamepad")
1010 /// .WithRequiredDevice<Gamepad>();
1011 /// </code>
1012 /// </example>
1013 /// </remarks>
1014 public static ControlSchemeSyntax AddControlScheme(this InputActionAsset asset, string name)
1015 {
1016 if (asset == null)
1017 throw new ArgumentNullException(nameof(asset));
1018 if (string.IsNullOrEmpty(name))
1019 throw new ArgumentNullException(nameof(name));
1020
1021 var index = asset.controlSchemes.Count;
1022 asset.AddControlScheme(new InputControlScheme(name));
1023
1024 return new ControlSchemeSyntax(asset, index);
1025 }
1026
1027 /// <summary>
1028 /// Remove the control scheme with the given name from the asset.
1029 /// </summary>
1030 /// <param name="asset">Asset to remove the control scheme from.</param>
1031 /// <param name="name">Name of the control scheme. Matching is case-insensitive.</param>
1032 /// <exception cref="ArgumentNullException"><paramref name="asset"/> is null -or- <paramref name="name"/>
1033 /// is <c>null</c> or empty.</exception>
1034 /// <remarks>
1035 /// If no control scheme with the given name can be found, the method does nothing.
1036 /// </remarks>
1037 public static void RemoveControlScheme(this InputActionAsset asset, string name)
1038 {
1039 #if UNITY_EDITOR
1040 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.RemoveControlScheme);
1041 #endif
1042
1043 if (asset == null)
1044 throw new ArgumentNullException(nameof(asset));
1045 if (string.IsNullOrEmpty(name))
1046 throw new ArgumentNullException(nameof(name));
1047
1048 var index = asset.FindControlSchemeIndex(name);
1049 if (index != -1)
1050 ArrayHelpers.EraseAt(ref asset.m_ControlSchemes, index);
1051
1052 asset.MarkAsDirty();
1053 }
1054
1055 /// <summary>
1056 /// Associates the control scheme given by <paramref name="scheme"/> with the binding group given by <paramref name="bindingGroup"/>.
1057 /// </summary>
1058 /// <param name="scheme">The control scheme to modify.</param>
1059 /// <param name="bindingGroup">The binding group to be associated with the control scheme.</param>
1060 /// <returns><paramref name="scheme"/></returns>
1061 public static InputControlScheme WithBindingGroup(this InputControlScheme scheme, string bindingGroup)
1062 {
1063 #if UNITY_EDITOR
1064 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ControlSchemeWithBindingGroup);
1065 #endif
1066
1067 return new ControlSchemeSyntax(scheme).WithBindingGroup(bindingGroup).Done();
1068 }
1069
1070 public static InputControlScheme WithDevice(this InputControlScheme scheme, string controlPath, bool required)
1071 {
1072 #if UNITY_EDITOR
1073 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ControlSchemeWithDevice);
1074 #endif
1075
1076 if (required)
1077 return new ControlSchemeSyntax(scheme).WithRequiredDevice(controlPath).Done();
1078 return new ControlSchemeSyntax(scheme).WithOptionalDevice(controlPath).Done();
1079 }
1080
1081 public static InputControlScheme WithRequiredDevice(this InputControlScheme scheme, string controlPath)
1082 {
1083 #if UNITY_EDITOR
1084 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ControlSchemeWithRequiredDevice);
1085 #endif
1086
1087 return new ControlSchemeSyntax(scheme).WithRequiredDevice(controlPath).Done();
1088 }
1089
1090 public static InputControlScheme WithOptionalDevice(this InputControlScheme scheme, string controlPath)
1091 {
1092 #if UNITY_EDITOR
1093 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ControlSchemeWithOptionalDevice);
1094 #endif
1095
1096 return new ControlSchemeSyntax(scheme).WithOptionalDevice(controlPath).Done();
1097 }
1098
1099 public static InputControlScheme OrWithRequiredDevice(this InputControlScheme scheme, string controlPath)
1100 {
1101 #if UNITY_EDITOR
1102 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ControlSchemeOrWithRequiredDevice);
1103 #endif
1104
1105 return new ControlSchemeSyntax(scheme).OrWithRequiredDevice(controlPath).Done();
1106 }
1107
1108 public static InputControlScheme OrWithOptionalDevice(this InputControlScheme scheme, string controlPath)
1109 {
1110 #if UNITY_EDITOR
1111 RegisterApiUsage(UnityEngine.InputSystem.Editor.InputExitPlayModeAnalytic.Api.ControlSchemeOrWithOptionalDevice);
1112 #endif
1113
1114 return new ControlSchemeSyntax(scheme).OrWithOptionalDevice(controlPath).Done();
1115 }
1116
1117 /// <summary>
1118 /// Write accessor to a binding on either an <see cref="InputAction"/> or an
1119 /// <see cref="InputActionMap"/>.
1120 /// </summary>
1121 /// <remarks>
1122 /// Both <see cref="InputAction.bindings"/> and <see cref="InputActionMap.bindings"/> are
1123 /// read-only. To modify bindings (other than setting overrides which you can do
1124 /// through <see cref="InputActionRebindingExtensions.ApplyBindingOverride(InputAction,int,InputBinding)"/>),
1125 /// it is necessary to gain indirect write access through this structure.
1126 ///
1127 /// <example>
1128 /// <code>
1129 /// playerInput.actions["fire"]
1130 /// .ChangeBinding(0)
1131 /// .WithPath("<Keyboard>/space");
1132 /// </code>
1133 /// </example>
1134 /// </remarks>
1135 /// <seealso cref="AddBinding(InputAction,InputBinding)"/>
1136 /// <seealso cref="ChangeBinding(InputAction,int)"/>
1137 public struct BindingSyntax
1138 {
1139 private readonly InputActionMap m_ActionMap;
1140 private readonly InputAction m_Action;
1141 internal readonly int m_BindingIndexInMap;
1142
1143 /// <summary>
1144 /// True if the if binding accessor is valid.
1145 /// </summary>
1146 public bool valid => m_ActionMap != null && m_BindingIndexInMap >= 0 && m_BindingIndexInMap < m_ActionMap.m_Bindings.LengthSafe();
1147
1148 /// <summary>
1149 /// Index of the binding that the accessor refers to.
1150 /// </summary>
1151 /// <remarks>
1152 /// When accessing bindings on an <see cref="InputAction"/>, this is the index in
1153 /// <see cref="InputAction.bindings"/> of the action. When accessing bindings on an
1154 /// <see cref="InputActionMap"/>, it is the index <see cref="InputActionMap.bindings"/>
1155 /// of the map.
1156 /// </remarks>
1157 public int bindingIndex
1158 {
1159 get
1160 {
1161 if (!valid)
1162 return -1;
1163 if (m_Action != null)
1164 return m_Action.BindingIndexOnMapToBindingIndexOnAction(m_BindingIndexInMap);
1165 return m_BindingIndexInMap;
1166 }
1167 }
1168
1169 /// <summary>
1170 /// The current binding in entirety.
1171 /// </summary>
1172 /// <exception cref="InvalidOperationException">The accessor is not <see cref="valid"/>.</exception>
1173 public InputBinding binding
1174 {
1175 get
1176 {
1177 if (!valid)
1178 throw new InvalidOperationException("BindingSyntax accessor is not valid");
1179 return m_ActionMap.m_Bindings[m_BindingIndexInMap];
1180 }
1181 }
1182
1183 internal BindingSyntax(InputActionMap map, int bindingIndexInMap, InputAction action = null)
1184 {
1185 m_ActionMap = map;
1186 m_BindingIndexInMap = bindingIndexInMap;
1187 m_Action = action;
1188 }
1189
1190 /// <summary>
1191 /// Set the <see cref="InputBinding.name"/> of the binding.
1192 /// </summary>
1193 /// <param name="name">Name for the binding.</param>
1194 /// <returns>The same binding syntax for further configuration.</returns>
1195 /// <exception cref="InvalidOperationException">The binding accessor is not <see cref="valid"/>.</exception>
1196 /// <seealso cref="InputBinding.name"/>
1197 public BindingSyntax WithName(string name)
1198 {
1199 if (!valid)
1200 throw new InvalidOperationException("Accessor is not valid");
1201 m_ActionMap.m_Bindings[m_BindingIndexInMap].name = name;
1202 m_ActionMap.OnBindingModified();
1203 return this;
1204 }
1205
1206 /// <summary>
1207 /// Set the <see cref="InputBinding.path"/> of the binding.
1208 /// </summary>
1209 /// <param name="path">Path for the binding.</param>
1210 /// <returns>The same binding syntax for further configuration.</returns>
1211 /// <exception cref="InvalidOperationException">The binding accessor is not <see cref="valid"/>.</exception>
1212 /// <seealso cref="InputBinding.path"/>
1213 public BindingSyntax WithPath(string path)
1214 {
1215 if (!valid)
1216 throw new InvalidOperationException("Accessor is not valid");
1217 m_ActionMap.m_Bindings[m_BindingIndexInMap].path = path;
1218 m_ActionMap.OnBindingModified();
1219 return this;
1220 }
1221
1222 /// <summary>
1223 /// Add <paramref name="group"/> to the list of <see cref="InputBinding.groups"/> of the binding.
1224 /// </summary>
1225 /// <param name="group">Name of the binding group (such as "Gamepad").</param>
1226 /// <returns>The same binding syntax for further configuration.</returns>
1227 /// <exception cref="ArgumentException"><paramref name="group"/> is <c>null</c> or empty -or- it contains
1228 /// a <see cref="InputBinding.Separator"/> character.</exception>
1229 public BindingSyntax WithGroup(string group)
1230 {
1231 if (!valid)
1232 throw new InvalidOperationException("Accessor is not valid");
1233 if (string.IsNullOrEmpty(group))
1234 throw new ArgumentException("Group name cannot be null or empty", nameof(group));
1235 if (group.IndexOf(InputBinding.Separator) != -1)
1236 throw new ArgumentException(
1237 $"Group name cannot contain separator character '{InputBinding.Separator}'", nameof(group));
1238
1239 return WithGroups(group);
1240 }
1241
1242 /// <summary>
1243 /// Add all the groups specified in <paramref name="groups"/> to the list of <see cref="InputBinding.groups"/> of the binding.
1244 /// </summary>
1245 /// <param name="groups">A semi-colon separated list of group names.</param>
1246 /// <returns>The same binding syntax for further configuration.</returns>
1247 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1248 public BindingSyntax WithGroups(string groups)
1249 {
1250 if (!valid)
1251 throw new InvalidOperationException("Accessor is not valid");
1252 if (string.IsNullOrEmpty(groups))
1253 return this;
1254
1255 // Join with existing group, if any.
1256 var currentGroups = m_ActionMap.m_Bindings[m_BindingIndexInMap].groups;
1257 if (!string.IsNullOrEmpty(currentGroups))
1258 groups = string.Join(InputBinding.kSeparatorString, currentGroups, groups);
1259
1260 // Set groups on binding.
1261 m_ActionMap.m_Bindings[m_BindingIndexInMap].groups = groups;
1262 m_ActionMap.OnBindingModified();
1263
1264 return this;
1265 }
1266
1267 /// <summary>
1268 /// Add an interaction via specified name in <paramref name="interaction"/> to the list of interactions.
1269 /// </summary>
1270 /// <param name="interaction">Interaction class name</param>
1271 /// <returns>The same binding syntax for further configuration.</returns>
1272 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1273 /// <exception cref="ArgumentException">If interaction name is null or empty.</exception>
1274 public BindingSyntax WithInteraction(string interaction)
1275 {
1276 if (!valid)
1277 throw new InvalidOperationException("Accessor is not valid");
1278 if (string.IsNullOrEmpty(interaction))
1279 throw new ArgumentException("Interaction cannot be null or empty", nameof(interaction));
1280 if (interaction.IndexOf(InputBinding.Separator) != -1)
1281 throw new ArgumentException(
1282 $"Interaction string cannot contain separator character '{InputBinding.Separator}'", nameof(interaction));
1283
1284 return WithInteractions(interaction);
1285 }
1286
1287 /// <summary>
1288 /// Add the set of interactions specified in <paramref name="interactions"/> to the list of interactions.
1289 /// </summary>
1290 /// <param name="interactions">A semi-colon separated list of interaction names.</param>
1291 /// <returns>The same binding syntax for further configuration.</returns>
1292 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1293 public BindingSyntax WithInteractions(string interactions)
1294 {
1295 if (!valid)
1296 throw new InvalidOperationException("Accessor is not valid");
1297 if (string.IsNullOrEmpty(interactions))
1298 return this;
1299
1300 // Join with existing interaction string, if any.
1301 var currentInteractions = m_ActionMap.m_Bindings[m_BindingIndexInMap].interactions;
1302 if (!string.IsNullOrEmpty(currentInteractions))
1303 interactions = string.Join(InputBinding.kSeparatorString, currentInteractions, interactions);
1304
1305 // Set interactions on binding.
1306 m_ActionMap.m_Bindings[m_BindingIndexInMap].interactions = interactions;
1307 m_ActionMap.OnBindingModified();
1308
1309 return this;
1310 }
1311
1312 /// <summary>
1313 /// Add an interaction of type specified in <typeparamref name="TInteraction"/> to the list of interactions.
1314 /// </summary>
1315 /// <typeparam name="TInteraction"></typeparam>
1316 /// <returns>The same binding syntax for further configuration.</returns>
1317 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1318 /// <exception cref="NotSupportedException">Interaction type has not been registered.</exception>
1319 public BindingSyntax WithInteraction<TInteraction>()
1320 where TInteraction : IInputInteraction
1321 {
1322 if (!valid)
1323 throw new InvalidOperationException("Accessor is not valid");
1324
1325 var interactionName = InputInteraction.s_Interactions.FindNameForType(typeof(TInteraction));
1326 if (interactionName.IsEmpty())
1327 throw new NotSupportedException($"Type '{typeof(TInteraction)}' has not been registered as a interaction");
1328
1329 return WithInteraction(interactionName);
1330 }
1331
1332 /// <summary>
1333 /// Add a processor to the list of <see cref="InputBinding.processors"/> of the binding.
1334 /// </summary>
1335 /// <param name="processor">Name of the processor, such as "Scale".</param>
1336 /// <returns>The same binding syntax for further configuration.</returns>
1337 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1338 /// <exception cref="ArgumentException">The processor name is null or empty.</exception>
1339 /// <exception cref="ArgumentException">The processor name contains a semi-colon.</exception>
1340 public BindingSyntax WithProcessor(string processor)
1341 {
1342 if (!valid)
1343 throw new InvalidOperationException("Accessor is not valid");
1344 if (string.IsNullOrEmpty(processor))
1345 throw new ArgumentException("Processor cannot be null or empty", nameof(processor));
1346 if (processor.IndexOf(InputBinding.Separator) != -1)
1347 throw new ArgumentException(
1348 $"Processor string cannot contain separator character '{InputBinding.Separator}'", nameof(processor));
1349
1350 return WithProcessors(processor);
1351 }
1352
1353 /// <summary>
1354 /// Add processors to the list of <see cref="InputBinding.processors"/> of the binding.
1355 /// </summary>
1356 /// <param name="processors">A semi-colon separated list of processor names.</param>
1357 /// <returns>The same binding syntax for further configuration.</returns>
1358 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1359 public BindingSyntax WithProcessors(string processors)
1360 {
1361 if (!valid)
1362 throw new InvalidOperationException("Accessor is not valid");
1363 if (string.IsNullOrEmpty(processors))
1364 return this;
1365
1366 // Join with existing processor string, if any.
1367 var currentProcessors = m_ActionMap.m_Bindings[m_BindingIndexInMap].processors;
1368 if (!string.IsNullOrEmpty(currentProcessors))
1369 processors = string.Join(InputBinding.kSeparatorString, currentProcessors, processors);
1370
1371 // Set processors on binding.
1372 m_ActionMap.m_Bindings[m_BindingIndexInMap].processors = processors;
1373 m_ActionMap.OnBindingModified();
1374
1375 return this;
1376 }
1377
1378 /// <summary>
1379 /// Add a processor to the list of <see cref="InputBinding.processors"/> of the binding.
1380 /// </summary>
1381 /// <typeparam name="TProcessor">Type of processor.</typeparam>
1382 /// <returns>The same binding syntax for further configuration.</returns>
1383 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1384 /// <exception cref="NotSupportedException">Processor type has not been registered.</exception>
1385 public BindingSyntax WithProcessor<TProcessor>()
1386 {
1387 if (!valid)
1388 throw new InvalidOperationException("Accessor is not valid");
1389
1390 var processorName = InputProcessor.s_Processors.FindNameForType(typeof(TProcessor));
1391 if (processorName.IsEmpty())
1392 throw new NotSupportedException($"Type '{typeof(TProcessor)}' has not been registered as a processor");
1393
1394 return WithProcessor(processorName);
1395 }
1396
1397 /// <summary>
1398 /// Specify which action to trigger.
1399 /// </summary>
1400 /// <param name="action">Action to trigger.</param>
1401 /// <returns>The same binding syntax for further configuration.</returns>
1402 /// <exception cref="InvalidOperationException">The binding accessor is invalid.</exception>
1403 /// <exception cref="ArgumentNullException">Provided action is null.</exception>
1404 /// <exception cref="ArgumentException">Provided action is a singleton action (not part of any action maps).</exception>
1405 public BindingSyntax Triggering(InputAction action)
1406 {
1407 if (!valid)
1408 throw new InvalidOperationException("Accessor is not valid");
1409 if (action == null)
1410 throw new ArgumentNullException(nameof(action));
1411 if (action.isSingletonAction)
1412 throw new ArgumentException(
1413 $"Cannot change the action a binding triggers on singleton action '{action}'", nameof(action));
1414 m_ActionMap.m_Bindings[m_BindingIndexInMap].action = action.name;
1415 m_ActionMap.OnBindingModified();
1416 return this;
1417 }
1418
1419 /// <summary>
1420 /// Replace the current binding with the given one.
1421 /// </summary>
1422 /// <param name="binding">An input binding.</param>
1423 /// <returns>The same binding syntax for further configuration.</returns>
1424 /// <remarks>
1425 /// This method replaces the current binding wholesale, i.e. it will overwrite all fields.
1426 /// Be aware that this has the potential of corrupting the binding data in case the given
1427 /// binding is a composite.
1428 /// </remarks>
1429 public BindingSyntax To(InputBinding binding)
1430 {
1431 if (!valid)
1432 throw new InvalidOperationException("Accessor is not valid");
1433
1434 m_ActionMap.m_Bindings[m_BindingIndexInMap] = binding;
1435
1436 // If it's a singleton action, we force the binding to stay with the action.
1437 if (m_ActionMap.m_SingletonAction != null)
1438 m_ActionMap.m_Bindings[m_BindingIndexInMap].action = m_ActionMap.m_SingletonAction.name;
1439
1440 m_ActionMap.OnBindingModified();
1441
1442 return this;
1443 }
1444
1445 /// <summary>
1446 /// Switch to configuring the next binding.
1447 /// </summary>
1448 /// <returns>An instance configured to edit the next binding or an invalid (see <see cref="valid"/>) instance if
1449 /// there is no next binding.</returns>
1450 /// <remarks>If the BindingSyntax is restricted to a single action, the result will be invalid (see <see cref="valid"/>)
1451 /// if there is no next binding on the action. If the BindingSyntax is restricted to an <see cref="InputActionMap"/>, the result will
1452 /// be be invalid if there is no next binding in the map.</remarks>
1453 public BindingSyntax NextBinding()
1454 {
1455 return Iterate(true);
1456 }
1457
1458 /// <summary>
1459 /// Switch to configuring the previous binding.
1460 /// </summary>
1461 /// <returns>An instance configured to edit the previous binding or an invalid (see <see cref="valid"/>) instance if
1462 /// there is no previous binding.</returns>
1463 /// <remarks>If the BindingSyntax is restricted to a single action, the result will be invalid (see <see cref="valid"/>)
1464 /// if there is no previous binding on the action. If the BindingSyntax is restricted to an <see cref="InputActionMap"/>, the result will
1465 /// be be invalid if there is no previous binding in the map.</remarks>
1466 public BindingSyntax PreviousBinding()
1467 {
1468 return Iterate(false);
1469 }
1470
1471 /// <summary>
1472 /// Iterate to the next part binding of the current composite with the given part name.
1473 /// </summary>
1474 /// <param name="partName">Name of the part of the binding, such as <c>"Positive"</c>.</param>
1475 /// <returns>An accessor to the next part binding with the given name or an invalid (see <see cref="valid"/>)
1476 /// accessor if there is no such binding.</returns>
1477 /// <exception cref="ArgumentNullException"><paramref name="partName"/> is <c>null</c> or empty.</exception>
1478 /// <remarks>
1479 /// Each binding that is part of a composite is marked with <see cref="InputBinding.isPartOfComposite"/>
1480 /// set to true. The name of the part is determined by <see cref="InputBinding.name"/> (comparison is
1481 /// case-insensitive). Which parts are relevant to a specific composite is determined by the type of
1482 /// composite. An <see cref="Composites.AxisComposite"/>, for example, has <c>"Negative"</c> and a
1483 /// <c>"Positive"</c> part.
1484 ///
1485 /// <example>
1486 /// <code>
1487 /// // Delete first "Positive" part of "Axis" composite.
1488 /// action.ChangeCompositeBinding("Axis")
1489 /// .NextPartBinding("Positive").Erase();
1490 /// </code>
1491 /// </example>
1492 /// </remarks>
1493 /// <seealso cref="InputBinding.isPartOfComposite"/>
1494 /// <seealso cref="InputBinding.isComposite"/>
1495 /// <seealso cref="InputBindingComposite"/>
1496 public BindingSyntax NextPartBinding(string partName)
1497 {
1498 if (string.IsNullOrEmpty(partName))
1499 throw new ArgumentNullException(nameof(partName));
1500 return IteratePartBinding(true, partName);
1501 }
1502
1503 /// <summary>
1504 /// Iterate to the previous part binding of the current composite with the given part name.
1505 /// </summary>
1506 /// <param name="partName">Name of the part of the binding, such as <c>"Positive"</c>.</param>
1507 /// <returns>An accessor to the previous part binding with the given name or an invalid (see <see cref="valid"/>)
1508 /// accessor if there is no such binding.</returns>
1509 /// <exception cref="ArgumentNullException"><paramref name="partName"/> is <c>null</c> or empty.</exception>
1510 /// <remarks>
1511 /// Each binding that is part of a composite is marked with <see cref="InputBinding.isPartOfComposite"/>
1512 /// set to true. The name of the part is determined by <see cref="InputBinding.name"/> (comparison is
1513 /// case-insensitive). Which parts are relevant to a specific composite is determined by the type of
1514 /// composite. An <see cref="Composites.AxisComposite"/>, for example, has <c>"Negative"</c> and a
1515 /// <c>"Positive"</c> part.
1516 /// </remarks>
1517 /// <seealso cref="InputBinding.isPartOfComposite"/>
1518 /// <seealso cref="InputBinding.isComposite"/>
1519 /// <seealso cref="InputBindingComposite"/>
1520 public BindingSyntax PreviousPartBinding(string partName)
1521 {
1522 if (string.IsNullOrEmpty(partName))
1523 throw new ArgumentNullException(nameof(partName));
1524 return IteratePartBinding(false, partName);
1525 }
1526
1527 /// <summary>
1528 /// Iterate to the next composite binding.
1529 /// </summary>
1530 /// <param name="compositeName">If <c>null</c> (default), an accessor to the next composite binding,
1531 /// regardless of name or type, is returned. If it is not <c>null</c>, can be either the name of
1532 /// the binding (see <see cref="InputBinding.name"/>) or the name of the composite used in the
1533 /// binding (see <see cref="InputSystem.RegisterBindingComposite"/></param>).
1534 /// <returns>A write accessor to the next composite binding or an invalid accessor (see
1535 /// <see cref="valid"/>) if no such binding was found.</returns>
1536 public BindingSyntax NextCompositeBinding(string compositeName = null)
1537 {
1538 return IterateCompositeBinding(true, compositeName);
1539 }
1540
1541 /// <summary>
1542 /// Iterate to the previous composite binding.
1543 /// </summary>
1544 /// <param name="compositeName">If <c>null</c> (default), an accessor to the previous composite binding,
1545 /// regardless of name or type, is returned. If it is not <c>null</c>, can be either the name of
1546 /// the binding (see <see cref="InputBinding.name"/>) or the name of the composite used in the
1547 /// binding (see <see cref="InputSystem.RegisterBindingComposite"/>).</param>
1548 /// <returns>A write accessor to the previous composite binding or an invalid accessor (see
1549 /// <see cref="valid"/>) if no such binding was found.</returns>
1550 public BindingSyntax PreviousCompositeBinding(string compositeName = null)
1551 {
1552 return IterateCompositeBinding(false, compositeName);
1553 }
1554
1555 private BindingSyntax Iterate(bool next)
1556 {
1557 if (m_ActionMap == null)
1558 return default;
1559
1560 var bindings = m_ActionMap.m_Bindings;
1561 if (bindings == null)
1562 return default;
1563
1564 // To find the next binding for a specific action, we may have to jump
1565 // over unrelated bindings in-between.
1566 var index = m_BindingIndexInMap;
1567 while (true)
1568 {
1569 index += next ? 1 : -1;
1570 if (index < 0 || index >= bindings.Length)
1571 return default;
1572
1573 if (m_Action == null || bindings[index].TriggersAction(m_Action))
1574 break;
1575 }
1576
1577 return new BindingSyntax(m_ActionMap, index, m_Action);
1578 }
1579
1580 private BindingSyntax IterateCompositeBinding(bool next, string compositeName)
1581 {
1582 for (var accessor = Iterate(next); accessor.valid; accessor = accessor.Iterate(next))
1583 {
1584 if (!accessor.binding.isComposite)
1585 continue;
1586
1587 if (compositeName == null)
1588 return accessor;
1589
1590 // Try name of binding.
1591 if (compositeName.Equals(accessor.binding.name, StringComparison.InvariantCultureIgnoreCase))
1592 return accessor;
1593
1594 // Try composite type name.
1595 var name = NameAndParameters.ParseName(accessor.binding.path);
1596 if (compositeName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
1597 return accessor;
1598 }
1599
1600 return default;
1601 }
1602
1603 private BindingSyntax IteratePartBinding(bool next, string partName)
1604 {
1605 if (!valid)
1606 return default;
1607
1608 if (binding.isComposite)
1609 {
1610 // If we're at the composite, only proceed if we're iterating down
1611 // instead of up.
1612 if (!next)
1613 return default;
1614 }
1615 else if (!binding.isPartOfComposite)
1616 return default;
1617
1618 for (var accessor = Iterate(next); accessor.valid; accessor = accessor.Iterate(next))
1619 {
1620 if (!accessor.binding.isPartOfComposite)
1621 return default;
1622
1623 if (partName.Equals(accessor.binding.name, StringComparison.InvariantCultureIgnoreCase))
1624 return accessor;
1625 }
1626
1627 return default;
1628 }
1629
1630 ////TODO: allow setting overrides through this accessor
1631
1632 /// <summary>
1633 /// Remove the binding.
1634 /// </summary>
1635 /// <remarks>
1636 /// If the binding is a composite (see <see cref="InputBinding.isComposite"/>), part bindings of the
1637 /// composite will be removed as well.
1638 ///
1639 /// Note that the accessor will not necessarily be invalidated. Instead, it will point to what used
1640 /// to be the next binding in the array (though that means the accessor will be invalid if the binding
1641 /// that got erased was the last one in the array).
1642 /// </remarks>
1643 /// <exception cref="InvalidOperationException">The instance is not <see cref="valid"/>.</exception>
1644 public void Erase()
1645 {
1646 if (!valid)
1647 throw new InvalidOperationException("Instance not valid");
1648
1649 var isComposite = m_ActionMap.m_Bindings[m_BindingIndexInMap].isComposite;
1650 ArrayHelpers.EraseAt(ref m_ActionMap.m_Bindings, m_BindingIndexInMap);
1651
1652 // If it's a composite, also erase part bindings.
1653 if (isComposite)
1654 {
1655 while (m_BindingIndexInMap < m_ActionMap.m_Bindings.LengthSafe() && m_ActionMap.m_Bindings[m_BindingIndexInMap].isPartOfComposite)
1656 {
1657 ArrayHelpers.EraseAt(ref m_ActionMap.m_Bindings, m_BindingIndexInMap);
1658 }
1659 }
1660
1661 m_Action.m_BindingsCount = m_ActionMap.m_Bindings.LengthSafe();
1662 m_ActionMap.OnBindingModified();
1663
1664 // We have switched to a different binding array. For singleton actions, we need to
1665 // sync up the reference that the action itself has.
1666 if (m_ActionMap.m_SingletonAction != null)
1667 m_ActionMap.m_SingletonAction.m_SingletonActionBindings = m_ActionMap.m_Bindings;
1668 }
1669
1670 /// <summary>
1671 /// Insert a composite part into a composite binding.
1672 /// </summary>
1673 /// <param name="partName">Name of the part in composite binding.</param>
1674 /// <param name="path">Control path to bind to.</param>
1675 /// <returns>The same binding syntax for further configuration.</returns>
1676 /// <exception cref="ArgumentNullException">Part name is null or empty.</exception>
1677 /// <exception cref="InvalidOperationException">The binding accessor is invalid or the binding accessor is not pointing to composite or part binding.</exception>
1678 public BindingSyntax InsertPartBinding(string partName, string path)
1679 {
1680 if (string.IsNullOrEmpty(partName))
1681 throw new ArgumentNullException(nameof(partName));
1682 if (!valid)
1683 throw new InvalidOperationException("Binding accessor is not valid");
1684 var binding = this.binding;
1685 if (!binding.isPartOfComposite && !binding.isComposite)
1686 throw new InvalidOperationException("Binding accessor must point to composite or part binding");
1687
1688 AddBindingInternal(m_ActionMap,
1689 new InputBinding { path = path, isPartOfComposite = true, name = partName, action = m_Action?.name },
1690 m_BindingIndexInMap + 1);
1691
1692 return new BindingSyntax(m_ActionMap, m_BindingIndexInMap + 1, m_Action);
1693 }
1694 }
1695
1696 ////TODO: remove this and merge it into BindingSyntax
1697 /// <summary>
1698 /// Write accessor to a composite binding.
1699 /// </summary>
1700 /// <remarks>
1701 /// To add a composite binding to an action, you must first call <see cref="InputActionSetupExtensions.AddCompositeBinding"/>
1702 /// and then use the CompositeSyntax struct to add composite parts.
1703 /// <example>
1704 /// <code>
1705 /// playerInput.actions["fire"]
1706 /// .ChangeBinding(0)
1707 /// .AddCompositeBinding("2DVector")
1708 /// .With("Up", "<Keyboard>/w")
1709 /// .With("Down", "<Keyboard>/s")
1710 /// .With("Left", "<Keyboard>/a")
1711 /// .With("Right", "<Keyboard>/d");
1712 /// </code>
1713 /// </example>
1714 /// </remarks>
1715 /// <seealso cref="AddBinding(InputAction,InputBinding)"/>
1716 /// <seealso cref="ChangeBinding(InputAction,int)"/>
1717 public struct CompositeSyntax
1718 {
1719 private readonly InputAction m_Action;
1720 private readonly InputActionMap m_ActionMap;
1721 private int m_BindingIndexInMap;
1722
1723 /// <summary>
1724 /// Index of the binding that the accessor refers to.
1725 /// </summary>
1726 /// <remarks>
1727 /// When accessing bindings on an <see cref="InputAction"/>, this is the index in
1728 /// <see cref="InputAction.bindings"/> of the action. When accessing bindings on an
1729 /// <see cref="InputActionMap"/>, it is the index <see cref="InputActionMap.bindings"/>
1730 /// of the map.
1731 /// </remarks>
1732 public int bindingIndex
1733 {
1734 get
1735 {
1736 if (m_ActionMap == null)
1737 return -1;
1738 if (m_Action != null)
1739 return m_Action.BindingIndexOnMapToBindingIndexOnAction(m_BindingIndexInMap);
1740 return m_BindingIndexInMap;
1741 }
1742 }
1743
1744 internal CompositeSyntax(InputActionMap map, InputAction action, int compositeIndex)
1745 {
1746 m_Action = action;
1747 m_ActionMap = map;
1748 m_BindingIndexInMap = compositeIndex;
1749 }
1750
1751 /// <summary>
1752 /// Add a part binding to the composite.
1753 /// </summary>
1754 /// <param name="name">Name of the part. This is dependent on the type of composite. For
1755 /// <see cref="Composites.Vector2Composite"/>, for example, the valid parts are <c>"Up"</c>, <c>"Down"</c>,
1756 /// <c>"Left"</c>, and <c>"Right"</c>.</param>
1757 /// <param name="binding">Control path to binding to. See <see cref="InputBinding.path"/>.</param>
1758 /// <param name="groups">Binding groups to assign to the part binding. See <see cref="InputBinding.groups"/>.</param>
1759 /// <param name="processors">Optional list of processors to apply to the binding. See <see cref="InputBinding.processors"/>.</param>
1760 /// <returns>The same composite syntax for further configuration.</returns>
1761 public CompositeSyntax With(string name, string binding, string groups = null, string processors = null)
1762 {
1763 ////TODO: check whether non-composite bindings have been added in-between
1764
1765 using (InputActionRebindingExtensions.DeferBindingResolution())
1766 {
1767 int bindingIndex;
1768 if (m_Action != null)
1769 bindingIndex = m_Action.AddBinding(path: binding, groups: groups, processors: processors)
1770 .m_BindingIndexInMap;
1771 else
1772 bindingIndex = m_ActionMap.AddBinding(path: binding, groups: groups, processors: processors)
1773 .m_BindingIndexInMap;
1774
1775 m_ActionMap.m_Bindings[bindingIndex].name = name;
1776 m_ActionMap.m_Bindings[bindingIndex].isPartOfComposite = true;
1777 }
1778
1779 return this;
1780 }
1781 }
1782
1783 /// <summary>
1784 /// Write accessor to a control scheme.
1785 /// </summary>
1786 public struct ControlSchemeSyntax
1787 {
1788 // REVIEW: Consider removing this for major revision, e.g. v2 or let functionality used by it be internal to reduce API surface.
1789
1790 private readonly InputActionAsset m_Asset;
1791 private readonly int m_ControlSchemeIndex;
1792 private InputControlScheme m_ControlScheme;
1793
1794 internal ControlSchemeSyntax(InputActionAsset asset, int index)
1795 {
1796 m_Asset = asset;
1797 m_ControlSchemeIndex = index;
1798 m_ControlScheme = new InputControlScheme();
1799 }
1800
1801 internal ControlSchemeSyntax(InputControlScheme controlScheme)
1802 {
1803 m_Asset = null;
1804 m_ControlSchemeIndex = -1;
1805 m_ControlScheme = controlScheme;
1806 }
1807
1808 /// <summary>
1809 /// Sets or overwrite the binding group for control scheme.
1810 /// </summary>
1811 /// <param name="bindingGroup">A binding group. See <see cref="InputBinding.groups"/>.</param>
1812 /// <returns>The same control scheme syntax for further configuration.</returns>
1813 /// <exception cref="ArgumentNullException">If provided name is null or empty.</exception>
1814 public ControlSchemeSyntax WithBindingGroup(string bindingGroup)
1815 {
1816 if (string.IsNullOrEmpty(bindingGroup))
1817 throw new ArgumentNullException(nameof(bindingGroup));
1818
1819 if (m_Asset == null)
1820 m_ControlScheme.m_BindingGroup = bindingGroup;
1821 else
1822 m_Asset.m_ControlSchemes[m_ControlSchemeIndex].bindingGroup = bindingGroup;
1823
1824 return this;
1825 }
1826
1827 /// <summary>
1828 /// Adds a required device to control scheme.
1829 /// </summary>
1830 /// <typeparam name="TDevice">Type of device.</typeparam>
1831 /// <returns>The same control scheme syntax for further configuration.</returns>
1832 public ControlSchemeSyntax WithRequiredDevice<TDevice>()
1833 where TDevice : InputDevice
1834 {
1835 return WithRequiredDevice(DeviceTypeToControlPath<TDevice>());
1836 }
1837
1838 /// <summary>
1839 /// Adds an optional device to control scheme.
1840 /// </summary>
1841 /// <typeparam name="TDevice">Type of device.</typeparam>
1842 /// <returns>The same control scheme syntax for further configuration.</returns>
1843 /// <see cref="InputControlScheme.DeviceRequirement.isOptional"/>
1844 public ControlSchemeSyntax WithOptionalDevice<TDevice>()
1845 where TDevice : InputDevice
1846 {
1847 return WithOptionalDevice(DeviceTypeToControlPath<TDevice>());
1848 }
1849
1850 /// <summary>
1851 /// Combines another required device with the previous device using boolean OR
1852 /// logic such that either the previous device or this device are required to be present.
1853 /// </summary>
1854 /// <typeparam name="TDevice">Type of device.</typeparam>
1855 /// <returns>The same control scheme syntax for further configuration.</returns>
1856 /// <remarks>
1857 /// <example>
1858 /// <code>
1859 /// // Create an .inputactions asset.
1860 /// var asset = ScriptableObject.CreateInstance<InputActionAsset>();
1861 ///
1862 /// asset.AddControlScheme("KeyboardAndMouseOrPen")
1863 /// .WithRequiredDevice("<Keyboard>")
1864 /// .WithRequiredDevice("<Mouse>")
1865 /// .OrWithRequiredDevice("<Pen>")
1866 /// </code>
1867 /// </example>
1868 /// </remarks>
1869 /// <see cref="InputControlScheme.DeviceRequirement.isOR"/>
1870 public ControlSchemeSyntax OrWithRequiredDevice<TDevice>()
1871 where TDevice : InputDevice
1872 {
1873 return OrWithRequiredDevice(DeviceTypeToControlPath<TDevice>());
1874 }
1875
1876 /// <summary>
1877 /// Combines another optional device with the previous device using boolean OR
1878 /// logic such that either the previous device or this device are required to be present.
1879 /// If this is the last device in a chain of OR'd devices, the entire chain of
1880 /// devices becomes optional.
1881 /// </summary>
1882 /// <typeparam name="TDevice">Type of device.</typeparam>
1883 /// <returns>The same control scheme syntax for further configuration.</returns>
1884 /// <see cref="InputControlScheme.DeviceRequirement.isOR"/>
1885 public ControlSchemeSyntax OrWithOptionalDevice<TDevice>()
1886 where TDevice : InputDevice
1887 {
1888 return OrWithOptionalDevice(DeviceTypeToControlPath<TDevice>());
1889 }
1890
1891 /// <summary>
1892 /// Adds a required device to control scheme.
1893 /// </summary>
1894 /// <param name="controlPath">Device path, like <Gamepad>.</param>
1895 /// <returns>The same control scheme syntax for further configuration.</returns>
1896 public ControlSchemeSyntax WithRequiredDevice(string controlPath)
1897 {
1898 AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.None);
1899 return this;
1900 }
1901
1902 /// <summary>
1903 /// Add an optional device to the control scheme.
1904 /// </summary>
1905 /// <param name="controlPath">The device path, like <Gamepad>.</param>
1906 /// <returns>The same control scheme syntax for further configuration.</returns>
1907 public ControlSchemeSyntax WithOptionalDevice(string controlPath)
1908 {
1909 AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.Optional);
1910 return this;
1911 }
1912
1913 /// <summary>
1914 /// Adds another possible required device to the control scheme.
1915 /// </summary>
1916 /// <param name="controlPath">Device path, like <Gamepad>.</param>
1917 /// <returns>The same control scheme syntax for further configuration.</returns>
1918 public ControlSchemeSyntax OrWithRequiredDevice(string controlPath)
1919 {
1920 AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.Or);
1921 return this;
1922 }
1923
1924 /// <summary>
1925 /// Adds another possible optional device to control scheme.
1926 /// </summary>
1927 /// <param name="controlPath">Device path, like <Gamepad>.</param>
1928 /// <returns>The same control scheme syntax for further configuration.</returns>
1929 public ControlSchemeSyntax OrWithOptionalDevice(string controlPath)
1930 {
1931 AddDeviceEntry(controlPath,
1932 InputControlScheme.DeviceRequirement.Flags.Optional |
1933 InputControlScheme.DeviceRequirement.Flags.Or);
1934 return this;
1935 }
1936
1937 private string DeviceTypeToControlPath<TDevice>()
1938 where TDevice : InputDevice
1939 {
1940 var layoutName = InputControlLayout.s_Layouts.TryFindLayoutForType(typeof(TDevice)).ToString();
1941 if (string.IsNullOrEmpty(layoutName))
1942 layoutName = typeof(TDevice).Name;
1943 return $"<{layoutName}>";
1944 }
1945
1946 /// <summary>
1947 /// Call this method when done building out the control scheme to return the associated instance.
1948 /// </summary>
1949 /// <returns>The associated control scheme</returns>
1950 public InputControlScheme Done()
1951 {
1952 if (m_Asset != null)
1953 return m_Asset.m_ControlSchemes[m_ControlSchemeIndex];
1954 return m_ControlScheme;
1955 }
1956
1957 private void AddDeviceEntry(string controlPath, InputControlScheme.DeviceRequirement.Flags flags)
1958 {
1959 if (string.IsNullOrEmpty(controlPath))
1960 throw new ArgumentNullException(nameof(controlPath));
1961
1962 var scheme = m_Asset != null ? m_Asset.m_ControlSchemes[m_ControlSchemeIndex] : m_ControlScheme;
1963 ArrayHelpers.Append(ref scheme.m_DeviceRequirements,
1964 new InputControlScheme.DeviceRequirement
1965 {
1966 m_ControlPath = controlPath,
1967 m_Flags = flags,
1968 });
1969
1970 if (m_Asset == null)
1971 m_ControlScheme = scheme;
1972 else
1973 m_Asset.m_ControlSchemes[m_ControlSchemeIndex] = scheme;
1974 }
1975 }
1976 }
1977}