using System; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; ////TODO: Rename all the xxxSyntax structs to xxxAccessor ////TODO: Replace all 'WithXXX' in the accessors with just 'SetXXX'; the 'WithXXX' reads too awkwardly namespace UnityEngine.InputSystem { /// /// Methods to change the setup of , , /// and objects. /// /// /// Unlike the methods in , the methods here are /// generally destructive, i.e. they will rearrange the data for actions. /// public static class InputActionSetupExtensions { /// /// Create an action map with the given name and add it to the asset. /// /// Asset to add the action map to /// Name to assign to the /// The newly added action map. /// is null or /// An action map with the given /// already exists in . /// is null or empty. public static InputActionMap AddActionMap(this InputActionAsset asset, string name) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); if (asset.FindActionMap(name) != null) throw new InvalidOperationException( $"An action map called '{name}' already exists in the asset"); var map = new InputActionMap(name); map.GenerateId(); asset.AddActionMap(map); return map; } /// /// Add an action map to the asset. /// /// Asset to add the map to. /// A named action map. /// or is null. /// has no name or asset already contains a /// map with the same name -or- is currently enabled -or- is part of /// an that has s that are enabled. /// public static void AddActionMap(this InputActionAsset asset, InputActionMap map) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (map == null) throw new ArgumentNullException(nameof(map)); if (string.IsNullOrEmpty(map.name)) throw new InvalidOperationException("Maps added to an input action asset must be named"); if (map.asset != null) throw new InvalidOperationException( $"Cannot add map '{map}' to asset '{asset}' as it has already been added to asset '{map.asset}'"); ////REVIEW: some of the rules here seem stupid; just replace? if (asset.FindActionMap(map.name) != null) throw new InvalidOperationException( $"An action map called '{map.name}' already exists in the asset"); map.OnWantToChangeSetup(); asset.OnWantToChangeSetup(); ArrayHelpers.Append(ref asset.m_ActionMaps, map); map.m_Asset = asset; asset.OnSetupChanged(); } /// /// Remove the given action map from the asset. /// /// Asset to add the action map to. /// An action map. If the given map is not part of the asset, the method /// does nothing. /// or is null. /// is currently enabled (see ) or is part of an that has s /// that are currently enabled. /// /// public static void RemoveActionMap(this InputActionAsset asset, InputActionMap map) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (map == null) throw new ArgumentNullException(nameof(map)); map.OnWantToChangeSetup(); asset.OnWantToChangeSetup(); // Ignore if not part of this asset. if (map.m_Asset != asset) return; ArrayHelpers.Erase(ref asset.m_ActionMaps, map); map.m_Asset = null; asset.OnSetupChanged(); } /// /// Remove the action map with the given name or ID from the asset. /// /// Asset to remove the action map from. /// The name or ID (see ) of a map in the /// asset. Note that lookup is case-insensitive. If no map with the given name or ID is found, /// the method does nothing. /// or is null. /// The map referenced by is currently enabled /// (see ). /// /// public static void RemoveActionMap(this InputActionAsset asset, string nameOrId) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (nameOrId == null) throw new ArgumentNullException(nameof(nameOrId)); var map = asset.FindActionMap(nameOrId); if (map != null) asset.RemoveActionMap(map); } ////TODO: add method to add an existing InputAction to a map /// /// Add a new to the given . /// /// Action map to add the action to. The action will be appended to /// of the map. The map must be disabled (see /// ). /// Name to give to the action. Must not be null or empty. Also, /// no other action that already exists in must have this name already. /// Action type. See . /// If not null, a binding is automatically added to the newly created action /// with the value of this parameter being used as the binding's . /// If is not null, this string is used for /// of the binding that is automatically added for the action. /// If is not null, this string is used for /// of the binding that is automatically added for the action. /// If is not null, this string is used for /// of the binding that is automatically added for the action. /// Value for ; null /// by default. /// The newly added input action. /// is null. /// is null or empty. /// is enabled (see ) /// or is part of an that has s that are /// -or- already contains an action called (case-insensitive). public static InputAction AddAction(this InputActionMap map, string name, InputActionType type = default, string binding = null, string interactions = null, string processors = null, string groups = null, string expectedControlLayout = null) { if (map == null) throw new ArgumentNullException(nameof(map)); if (string.IsNullOrEmpty(name)) throw new ArgumentException("Action must have name", nameof(name)); map.OnWantToChangeSetup(); if (map.FindAction(name) != null) throw new InvalidOperationException( $"Cannot add action with duplicate name '{name}' to set '{map.name}'"); // Append action to array. var action = new InputAction(name, type) { expectedControlType = expectedControlLayout }; action.GenerateId(); ArrayHelpers.Append(ref map.m_Actions, action); action.m_ActionMap = map; // Add binding, if supplied. if (!string.IsNullOrEmpty(binding)) { // Will trigger OnSetupChanged. action.AddBinding(binding, interactions: interactions, processors: processors, groups: groups); } else { if (!string.IsNullOrEmpty(groups)) throw new ArgumentException( $"No binding path was specified for action '{action}' but groups was specified ('{groups}'); cannot apply groups without binding", nameof(groups)); // If no binding has been supplied but there are interactions and processors, they go on the action itself. action.m_Interactions = interactions; action.m_Processors = processors; map.OnSetupChanged(); } return action; } /// /// Remove the given action from its . /// /// An input action that is part of an . /// is null. /// is a standalone action /// that is not part of an and thus cannot be removed from anything. /// is part of an /// or that has at least one enabled action. /// /// After removal, the action's will be set to null /// and the action will effectively become a standalone action that is not associated with /// any action map. Bindings on the action will be preserved. On the action map, the bindings /// for the action will be removed. /// /// public static void RemoveAction(this InputAction action) { if (action == null) throw new ArgumentNullException(nameof(action)); var actionMap = action.actionMap; if (actionMap == null) throw new ArgumentException( $"Action '{action}' does not belong to an action map; nowhere to remove from", nameof(action)); actionMap.OnWantToChangeSetup(); var bindingsForAction = action.bindings.ToArray(); var index = actionMap.m_Actions.IndexOfReference(action); Debug.Assert(index != -1, "Could not find action in map"); ArrayHelpers.EraseAt(ref actionMap.m_Actions, index); action.m_ActionMap = null; action.m_SingletonActionBindings = bindingsForAction; // Remove bindings to action from map. var newActionMapBindingCount = actionMap.m_Bindings.Length - bindingsForAction.Length; if (newActionMapBindingCount == 0) { actionMap.m_Bindings = null; } else { var newActionMapBindings = new InputBinding[newActionMapBindingCount]; var oldActionMapBindings = actionMap.m_Bindings; var bindingIndex = 0; for (var i = 0; i < oldActionMapBindings.Length; ++i) { var binding = oldActionMapBindings[i]; if (bindingsForAction.IndexOf(b => b == binding) == -1) newActionMapBindings[bindingIndex++] = binding; } actionMap.m_Bindings = newActionMapBindings; } actionMap.OnSetupChanged(); } /// /// Remove the action with the given name from the asset. /// /// Asset to remove the action from. /// Name or ID of the action. See for /// details. /// is null -or- /// is null or empty. /// public static void RemoveAction(this InputActionAsset asset, string nameOrId) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (nameOrId == null) throw new ArgumentNullException(nameof(nameOrId)); var action = asset.FindAction(nameOrId); action?.RemoveAction(); } /// /// Add a new binding to the given action. /// /// Action to add the binding to. If the action is part of an , /// the newly added binding will be visible on . /// Binding path string. See for details. /// Optional list of interactions to apply to the binding. See for details. /// Optional list of processors to apply to the binding. See for details. /// Optional list of binding groups that should be assigned to the binding. See /// for details. /// Fluent-style syntax to further configure the binding. public static BindingSyntax AddBinding(this InputAction action, string path, string interactions = null, string processors = null, string groups = null) { return AddBinding(action, new InputBinding { path = path, interactions = interactions, processors = processors, groups = groups }); } /// /// Add a binding that references the given and triggers /// the given . /// /// Action to trigger. /// Control to bind to. The full of the control will /// be used in the resulting binding. /// Syntax to configure the binding further. /// is null or is null. /// public static BindingSyntax AddBinding(this InputAction action, InputControl control) { if (control == null) throw new ArgumentNullException(nameof(control)); return AddBinding(action, control.path); } /// /// Add a new binding to the action. /// /// An action to add the binding to. /// Binding to add to the action or default. Binding can be further configured via /// the struct returned by the method. /// /// Returns a fluent-style syntax structure that allows performing additional modifications /// based on the new binding. /// /// /// This works both with actions that are part of an action set as well as with actions that aren't. /// /// Note that actions must be disabled while altering their binding sets. Also, if the action belongs /// to a set, all actions in the set must be disabled. /// /// /// /// fireAction.AddBinding() /// .WithPath("<Gamepad>/buttonSouth") /// .WithGroup("Gamepad"); /// /// /// public static BindingSyntax AddBinding(this InputAction action, InputBinding binding = default) { if (action == null) throw new ArgumentNullException(nameof(action)); ////REVIEW: should this reference actions by ID? Debug.Assert(action.m_Name != null || action.isSingletonAction); binding.action = action.name; var actionMap = action.GetOrCreateActionMap(); var bindingIndex = AddBindingInternal(actionMap, binding); return new BindingSyntax(actionMap, bindingIndex); } /// /// Add a new binding to the given action map. /// /// Action map to add the binding to. /// Path of the control(s) to bind to. See and /// . /// Names and parameters for interactions to apply to the /// binding. See . /// Optional list of groups to apply to the binding. See . /// Action to trigger from the binding. See . /// Optional list of processors to apply to the binding. See . /// A write-accessor to the newly added binding. /// is null. /// /// /// /// // Add a binding for the A button the gamepad and make it trigger /// // the "fire" action. /// var gameplayActions = playerInput.actions.FindActionMap("gameplay"); /// gameplayActions.AddBinding("<Gamepad>/buttonSouth", action: "fire"); /// /// /// /// /// public static BindingSyntax AddBinding(this InputActionMap actionMap, string path, string interactions = null, string groups = null, string action = null, string processors = null) { if (path == null) throw new ArgumentNullException(nameof(path), "Binding path cannot be null"); return AddBinding(actionMap, new InputBinding { path = path, interactions = interactions, groups = groups, action = action, processors = processors, }); } /// /// Add a new binding that triggers the given action to the given action map. /// /// Action map to add the binding to. /// Action to trigger from the binding. See . /// Must be part of . /// Path of the control(s) to bind to. See and /// . /// Names and parameters for interactions to apply to the /// binding. See . /// Binding groups to apply to the binding. See . /// A write-accessor to the newly added binding. /// is not part of . /// is null. /// /// public static BindingSyntax AddBinding(this InputActionMap actionMap, string path, InputAction action, string interactions = null, string groups = null) { if (action != null && action.actionMap != actionMap) throw new ArgumentException( $"Action '{action}' is not part of action map '{actionMap}'", nameof(action)); if (action == null) return AddBinding(actionMap, path: path, interactions: interactions, groups: groups); return AddBinding(actionMap, path: path, interactions: interactions, groups: groups, action: action.id); } public static BindingSyntax AddBinding(this InputActionMap actionMap, string path, Guid action, string interactions = null, string groups = null) { if (action == Guid.Empty) return AddBinding(actionMap, path: path, interactions: interactions, groups: groups); return AddBinding(actionMap, path: path, interactions: interactions, groups: groups, action: action.ToString()); } public static BindingSyntax AddBinding(this InputActionMap actionMap, InputBinding binding) { if (actionMap == null) throw new ArgumentNullException(nameof(actionMap)); if (binding.path == null) throw new ArgumentException("Binding path cannot be null", nameof(binding)); var bindingIndex = AddBindingInternal(actionMap, binding); return new BindingSyntax(actionMap, bindingIndex); } /// /// Add a composite binding to the of . /// /// Action to add the binding to. /// Type of composite to add. This needs to be the name the composite /// has been registered under using . Case-insensitive. /// Interactions to add to the binding. See . /// Processors to add to the binding. See . /// A write accessor to the newly added composite binding. /// is null. /// is null or empty. public static CompositeSyntax AddCompositeBinding(this InputAction action, string composite, string interactions = null, string processors = null) { if (action == null) throw new ArgumentNullException(nameof(action)); if (string.IsNullOrEmpty(composite)) throw new ArgumentException("Composite name cannot be null or empty", nameof(composite)); var actionMap = action.GetOrCreateActionMap(); var binding = new InputBinding { name = NameAndParameters.ParseName(composite), path = composite, interactions = interactions, processors = processors, isComposite = true, action = action.name }; var bindingIndex = AddBindingInternal(actionMap, binding); return new CompositeSyntax(actionMap, action, bindingIndex); } ////TODO: AddCompositeBinding private static int AddBindingInternal(InputActionMap map, InputBinding binding, int bindingIndex = -1) { Debug.Assert(map != null); // Make sure the binding has an ID. if (string.IsNullOrEmpty(binding.m_Id)) binding.GenerateId(); // Append to bindings in set. if (bindingIndex < 0) bindingIndex = ArrayHelpers.Append(ref map.m_Bindings, binding); else ArrayHelpers.InsertAt(ref map.m_Bindings, bindingIndex, binding); // Make sure this asset is reloaded from disk when exiting play mode so it isn't inadvertently // changed between play sessions. Only applies when running in the editor. if (map.asset != null) map.asset.MarkAsDirty(); // If we're looking at a singleton action, make sure m_Bindings is up to date just // in case the action gets serialized. if (map.m_SingletonAction != null) map.m_SingletonAction.m_SingletonActionBindings = map.m_Bindings; // NOTE: We treat this as a mere binding modification, even though we have added something. // InputAction.RestoreActionStatesAfterReResolvingBindings() can deal with bindings // having been removed or added. map.OnBindingModified(); return bindingIndex; } /// /// Get write access to the binding in of /// at the given . /// /// Action whose bindings to change. /// Index in 's of the binding to be changed. /// A write accessor to the given binding. /// /// /// /// // Grab "fire" action from PlayerInput. /// var fireAction = playerInput.actions["fire"]; /// /// // Change its second binding to go to the left mouse button. /// fireAction.ChangeBinding(1) /// .WithPath("<Mouse>/leftButton"); /// /// /// /// is null. /// is out of range (as per /// of ). public static BindingSyntax ChangeBinding(this InputAction action, int index) { if (action == null) throw new ArgumentNullException(nameof(action)); var indexOnMap = action.BindingIndexOnActionToBindingIndexOnMap(index); return new BindingSyntax(action.GetOrCreateActionMap(), indexOnMap, action); } public static BindingSyntax ChangeBinding(this InputAction action, string name) { return action.ChangeBinding(new InputBinding { name = name }); } /// /// Get write access to the binding in of /// at the given . /// /// Action map whose bindings to change. /// Index in 's of the binding to be changed. /// A write accessor to the given binding. /// /// /// /// // Grab "gameplay" actions from PlayerInput. /// var gameplayActions = playerInput.actions.FindActionMap("gameplay"); /// /// // Change its second binding to go to the left mouse button. /// gameplayActions.ChangeBinding(1) /// .WithPath("<Mouse>/leftButton"); /// /// /// /// is null. /// is out of range (as per /// of ). public static BindingSyntax ChangeBinding(this InputActionMap actionMap, int index) { if (actionMap == null) throw new ArgumentNullException(nameof(actionMap)); if (index < 0 || index >= actionMap.m_Bindings.LengthSafe()) throw new ArgumentOutOfRangeException(nameof(index)); return new BindingSyntax(actionMap, index); } /// /// Get write access to the binding in of /// that has the given . /// /// Action whose bindings to change. /// ID of the binding as per . /// A write accessor to the binding with the given ID. /// /// /// /// // Grab "fire" action from PlayerInput. /// var fireAction = playerInput.actions["fire"]; /// /// // Change the binding with the given ID to go to the left mouse button. /// fireAction.ChangeBindingWithId("c3de9215-31c3-4654-8562-854bf2f7864f") /// .WithPath("<Mouse>/leftButton"); /// /// /// /// is null. /// No binding with the given exists /// on . public static BindingSyntax ChangeBindingWithId(this InputAction action, string id) { if (action == null) throw new ArgumentNullException(nameof(action)); return action.ChangeBinding(new InputBinding {m_Id = id}); } /// /// Get write access to the binding in of /// that has the given . /// /// Action whose bindings to change. /// ID of the binding as per . /// A write accessor to the binding with the given ID. /// /// /// /// // Grab "fire" action from PlayerInput. /// var fireAction = playerInput.actions["fire"]; /// /// // Change the binding with the given ID to go to the left mouse button. /// fireAction.ChangeBindingWithId(new Guid("c3de9215-31c3-4654-8562-854bf2f7864f")) /// .WithPath("<Mouse>/leftButton"); /// /// /// /// is null. /// No binding with the given exists /// on . public static BindingSyntax ChangeBindingWithId(this InputAction action, Guid id) { if (action == null) throw new ArgumentNullException(nameof(action)); return action.ChangeBinding(new InputBinding {id = id}); } /// /// Get write access to the first binding in of /// that is assigned to the given binding . /// /// Action whose bindings to change. /// Name of the binding group as per . /// A write accessor to the first binding on that is assigned to the /// given binding . /// /// /// /// // Grab "fire" action from PlayerInput. /// var fireAction = playerInput.actions["fire"]; /// /// // Change the binding in the "Keyboard&Mouse" group to go to the left mouse button. /// fireAction.ChangeBindingWithGroup("Keyboard&Mouse") /// .WithPath("<Mouse>/leftButton"); /// /// /// /// is null. /// No binding on the is assigned /// to the given binding . public static BindingSyntax ChangeBindingWithGroup(this InputAction action, string group) { if (action == null) throw new ArgumentNullException(nameof(action)); return action.ChangeBinding(new InputBinding {groups = group}); } /// /// Get write access to the binding in of /// that is bound to the given . /// /// Action whose bindings to change. /// Path of the binding as per . /// A write accessor to the binding on that is assigned the /// given . /// /// /// /// // Grab "fire" action from PlayerInput. /// var fireAction = playerInput.actions["fire"]; /// /// // Change the binding to the right mouse button to go to the left mouse button instead. /// fireAction.ChangeBindingWithPath("<Mouse>/rightButton") /// .WithPath("<Mouse>/leftButton"); /// /// /// /// is null. /// No binding on the is assigned /// the given . public static BindingSyntax ChangeBindingWithPath(this InputAction action, string path) { if (action == null) throw new ArgumentNullException(nameof(action)); return action.ChangeBinding(new InputBinding {path = path}); } /// /// Get write access to the binding on that matches the given /// . /// /// Action whose bindings to match against. /// A binding mask. See for /// details. /// A write-accessor to the first binding matching or /// an invalid accessor (see ) if no binding was found to /// match the mask. /// is null. public static BindingSyntax ChangeBinding(this InputAction action, InputBinding match) { if (action == null) throw new ArgumentNullException(nameof(action)); var actionMap = action.GetOrCreateActionMap(); int bindingIndexInMap = -1; var id = action.idDontGenerate; if (id != null) { // Prio1: Attempt to match action id (stronger) match.action = action.id.ToString(); bindingIndexInMap = actionMap.FindBindingRelativeToMap(match); } if (bindingIndexInMap == -1) { // Prio2: Attempt to match action name (weaker) match.action = action.name; bindingIndexInMap = actionMap.FindBindingRelativeToMap(match); } if (bindingIndexInMap == -1) return default; return new BindingSyntax(actionMap, bindingIndexInMap); } /// /// Get a write accessor to the binding of that is both a composite /// (see ) and has the given binding name or composite /// type. /// /// Action to look up the binding on. All bindings in the action's /// property will be considered. /// Either the name of the composite binding (see ) /// to look for or the name of the composite type used in the binding (such as "1DAxis"). Case-insensitive. /// A write accessor to the given composite binding or an invalid accessor if no composite /// matching could be found on . /// /// /// /// // Add arrow keys as alternatives to the WASD Vector2 composite. /// playerInput.actions["move"] /// .ChangeCompositeBinding("WASD") /// .InsertPartBinding("Up", "<Keyboard>/upArrow") /// .InsertPartBinding("Down", "<Keyboard>/downArrow") /// .InsertPartBinding("Left", "<Keyboard>/leftArrow") /// .InsertPartBinding("Right", "<Keyboard>/rightArrow"); /// /// /// /// is null -or- /// is null or empty. /// /// public static BindingSyntax ChangeCompositeBinding(this InputAction action, string compositeName) { if (action == null) throw new ArgumentNullException(nameof(action)); if (string.IsNullOrEmpty(compositeName)) throw new ArgumentNullException(nameof(compositeName)); var actionMap = action.GetOrCreateActionMap(); var bindings = actionMap.m_Bindings; var numBindings = bindings.LengthSafe(); for (var i = 0; i < numBindings; ++i) { ref var binding = ref bindings[i]; if (!binding.isComposite || !binding.TriggersAction(action)) continue; ////REVIEW: should this do a registration lookup to deal with aliases? if (compositeName.Equals(binding.name, StringComparison.InvariantCultureIgnoreCase) || compositeName.Equals(NameAndParameters.ParseName(binding.path), StringComparison.InvariantCultureIgnoreCase)) return new BindingSyntax(actionMap, i, action); } return default; } ////TODO: update binding mask if necessary /// /// Rename an existing action. /// /// Action to assign a new name to. Can be singleton action or action that /// is part of a map. /// New name to assign to action. Cannot be empty. /// is null or is /// null or empty. /// of /// already contains an action called . /// /// Renaming an action will also update the bindings that refer to the action. /// public static void Rename(this InputAction action, string newName) { if (action == null) throw new ArgumentNullException(nameof(action)); if (string.IsNullOrEmpty(newName)) throw new ArgumentNullException(nameof(newName)); if (action.name == newName) return; // Make sure name isn't already taken in map. var actionMap = action.actionMap; if (actionMap?.FindAction(newName) != null) throw new InvalidOperationException( $"Cannot rename '{action}' to '{newName}' in map '{actionMap}' as the map already contains an action with that name"); var oldName = action.m_Name; action.m_Name = newName; actionMap?.ClearActionLookupTable(); if (actionMap?.asset != null) actionMap?.asset.MarkAsDirty(); // Update bindings. var bindings = action.GetOrCreateActionMap().m_Bindings; var bindingCount = bindings.LengthSafe(); for (var i = 0; i < bindingCount; ++i) if (string.Compare(bindings[i].action, oldName, StringComparison.InvariantCultureIgnoreCase) == 0) bindings[i].action = newName; } /// /// Add a new control scheme to the asset. /// /// Asset to add the control scheme to. /// Control scheme to add. /// has no name. /// is null. /// A control scheme with the same name as /// already exists in the asset. /// /// public static void AddControlScheme(this InputActionAsset asset, InputControlScheme controlScheme) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (string.IsNullOrEmpty(controlScheme.name)) throw new ArgumentException("Cannot add control scheme without name to asset " + asset.name, nameof(controlScheme)); if (asset.FindControlScheme(controlScheme.name) != null) throw new InvalidOperationException( $"Asset '{asset.name}' already contains a control scheme called '{controlScheme.name}'"); ArrayHelpers.Append(ref asset.m_ControlSchemes, controlScheme); asset.MarkAsDirty(); } /// /// Add a new control scheme to the given . /// /// Asset to add the control scheme to. /// Name to give to the control scheme. Must be unique within the control schemes of the /// asset. Also used as default name of binding group associated /// with the control scheme. /// Syntax to allow providing additional configuration for the newly added control scheme. /// is null -or- /// is null or empty. /// /// /// /// // Create an .inputactions asset. /// var asset = ScriptableObject.CreateInstance<InputActionAsset>(); /// /// // Add an action map to it. /// var actionMap = asset.AddActionMap("actions"); /// /// // Add an action to it and bind it to the A button on the gamepad. /// // Also, associate that binding with the "Gamepad" control scheme. /// var action = actionMap.AddAction("action"); /// action.AddBinding("<Gamepad>/buttonSouth", groups: "Gamepad"); /// /// // Add a control scheme called "Gamepad" that requires a Gamepad device. /// asset.AddControlScheme("Gamepad") /// .WithRequiredDevice<Gamepad>(); /// /// /// public static ControlSchemeSyntax AddControlScheme(this InputActionAsset asset, string name) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); var index = asset.controlSchemes.Count; asset.AddControlScheme(new InputControlScheme(name)); return new ControlSchemeSyntax(asset, index); } /// /// Remove the control scheme with the given name from the asset. /// /// Asset to remove the control scheme from. /// Name of the control scheme. Matching is case-insensitive. /// is null -or- /// is null or empty. /// /// If no control scheme with the given name can be found, the method does nothing. /// public static void RemoveControlScheme(this InputActionAsset asset, string name) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); var index = asset.FindControlSchemeIndex(name); if (index != -1) ArrayHelpers.EraseAt(ref asset.m_ControlSchemes, index); asset.MarkAsDirty(); } public static InputControlScheme WithBindingGroup(this InputControlScheme scheme, string bindingGroup) { return new ControlSchemeSyntax(scheme).WithBindingGroup(bindingGroup).Done(); } public static InputControlScheme WithDevice(this InputControlScheme scheme, string controlPath, bool required) { if (required) return new ControlSchemeSyntax(scheme).WithRequiredDevice(controlPath).Done(); return new ControlSchemeSyntax(scheme).WithOptionalDevice(controlPath).Done(); } public static InputControlScheme WithRequiredDevice(this InputControlScheme scheme, string controlPath) { return new ControlSchemeSyntax(scheme).WithRequiredDevice(controlPath).Done(); } public static InputControlScheme WithOptionalDevice(this InputControlScheme scheme, string controlPath) { return new ControlSchemeSyntax(scheme).WithOptionalDevice(controlPath).Done(); } public static InputControlScheme OrWithRequiredDevice(this InputControlScheme scheme, string controlPath) { return new ControlSchemeSyntax(scheme).OrWithRequiredDevice(controlPath).Done(); } public static InputControlScheme OrWithOptionalDevice(this InputControlScheme scheme, string controlPath) { return new ControlSchemeSyntax(scheme).OrWithOptionalDevice(controlPath).Done(); } /// /// Write accessor to a binding on either an or an /// . /// /// /// Both and are /// read-only. To modify bindings (other than setting overrides which you can do /// through ), /// it is necessary to gain indirect write access through this structure. /// /// /// /// playerInput.actions["fire"] /// .ChangeBinding(0) /// .WithPath("<Keyboard>/space"); /// /// /// /// /// public struct BindingSyntax { private readonly InputActionMap m_ActionMap; private readonly InputAction m_Action; internal readonly int m_BindingIndexInMap; /// /// True if the if binding accessor is valid. /// public bool valid => m_ActionMap != null && m_BindingIndexInMap >= 0 && m_BindingIndexInMap < m_ActionMap.m_Bindings.LengthSafe(); /// /// Index of the binding that the accessor refers to. /// /// /// When accessing bindings on an , this is the index in /// of the action. When accessing bindings on an /// , it is the index /// of the map. /// public int bindingIndex { get { if (!valid) return -1; if (m_Action != null) return m_Action.BindingIndexOnMapToBindingIndexOnAction(m_BindingIndexInMap); return m_BindingIndexInMap; } } /// /// The current binding in entirety. /// /// The accessor is not . public InputBinding binding { get { if (!valid) throw new InvalidOperationException("BindingSyntax accessor is not valid"); return m_ActionMap.m_Bindings[m_BindingIndexInMap]; } } internal BindingSyntax(InputActionMap map, int bindingIndexInMap, InputAction action = null) { m_ActionMap = map; m_BindingIndexInMap = bindingIndexInMap; m_Action = action; } /// /// Set the of the binding. /// /// Name for the binding. /// The same binding syntax for further configuration. /// The binding accessor is not . /// public BindingSyntax WithName(string name) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); m_ActionMap.m_Bindings[m_BindingIndexInMap].name = name; m_ActionMap.OnBindingModified(); return this; } /// /// Set the of the binding. /// /// Path for the binding. /// The same binding syntax for further configuration. /// The binding accessor is not . /// public BindingSyntax WithPath(string path) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); m_ActionMap.m_Bindings[m_BindingIndexInMap].path = path; m_ActionMap.OnBindingModified(); return this; } /// /// Add to the list of of the binding. /// /// Name of the binding group (such as "Gamepad"). /// The same binding syntax for further configuration. /// is null or empty -or- it contains /// a character. public BindingSyntax WithGroup(string group) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (string.IsNullOrEmpty(group)) throw new ArgumentException("Group name cannot be null or empty", nameof(group)); if (group.IndexOf(InputBinding.Separator) != -1) throw new ArgumentException( $"Group name cannot contain separator character '{InputBinding.Separator}'", nameof(group)); return WithGroups(group); } public BindingSyntax WithGroups(string groups) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (string.IsNullOrEmpty(groups)) return this; // Join with existing group, if any. var currentGroups = m_ActionMap.m_Bindings[m_BindingIndexInMap].groups; if (!string.IsNullOrEmpty(currentGroups)) groups = string.Join(InputBinding.kSeparatorString, currentGroups, groups); // Set groups on binding. m_ActionMap.m_Bindings[m_BindingIndexInMap].groups = groups; m_ActionMap.OnBindingModified(); return this; } public BindingSyntax WithInteraction(string interaction) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (string.IsNullOrEmpty(interaction)) throw new ArgumentException("Interaction cannot be null or empty", nameof(interaction)); if (interaction.IndexOf(InputBinding.Separator) != -1) throw new ArgumentException( $"Interaction string cannot contain separator character '{InputBinding.Separator}'", nameof(interaction)); return WithInteractions(interaction); } public BindingSyntax WithInteractions(string interactions) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (string.IsNullOrEmpty(interactions)) return this; // Join with existing interaction string, if any. var currentInteractions = m_ActionMap.m_Bindings[m_BindingIndexInMap].interactions; if (!string.IsNullOrEmpty(currentInteractions)) interactions = string.Join(InputBinding.kSeparatorString, currentInteractions, interactions); // Set interactions on binding. m_ActionMap.m_Bindings[m_BindingIndexInMap].interactions = interactions; m_ActionMap.OnBindingModified(); return this; } public BindingSyntax WithInteraction() where TInteraction : IInputInteraction { if (!valid) throw new InvalidOperationException("Accessor is not valid"); var interactionName = InputProcessor.s_Processors.FindNameForType(typeof(TInteraction)); if (interactionName.IsEmpty()) throw new NotSupportedException($"Type '{typeof(TInteraction)}' has not been registered as a processor"); return WithInteraction(interactionName); } public BindingSyntax WithProcessor(string processor) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (string.IsNullOrEmpty(processor)) throw new ArgumentException("Processor cannot be null or empty", nameof(processor)); if (processor.IndexOf(InputBinding.Separator) != -1) throw new ArgumentException( $"Interaction string cannot contain separator character '{InputBinding.Separator}'", nameof(processor)); return WithProcessors(processor); } public BindingSyntax WithProcessors(string processors) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (string.IsNullOrEmpty(processors)) return this; // Join with existing processor string, if any. var currentProcessors = m_ActionMap.m_Bindings[m_BindingIndexInMap].processors; if (!string.IsNullOrEmpty(currentProcessors)) processors = string.Join(InputBinding.kSeparatorString, currentProcessors, processors); // Set processors on binding. m_ActionMap.m_Bindings[m_BindingIndexInMap].processors = processors; m_ActionMap.OnBindingModified(); return this; } public BindingSyntax WithProcessor() { if (!valid) throw new InvalidOperationException("Accessor is not valid"); var processorName = InputProcessor.s_Processors.FindNameForType(typeof(TProcessor)); if (processorName.IsEmpty()) throw new NotSupportedException($"Type '{typeof(TProcessor)}' has not been registered as a processor"); return WithProcessor(processorName); } public BindingSyntax Triggering(InputAction action) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); if (action == null) throw new ArgumentNullException(nameof(action)); if (action.isSingletonAction) throw new ArgumentException( $"Cannot change the action a binding triggers on singleton action '{action}'", nameof(action)); m_ActionMap.m_Bindings[m_BindingIndexInMap].action = action.name; m_ActionMap.OnBindingModified(); return this; } /// /// Replace the current binding with the given one. /// /// An input binding. /// The same binding syntax for further configuration. /// /// This method replaces the current binding wholesale, i.e. it will overwrite all fields. /// Be aware that this has the potential of corrupting the binding data in case the given /// binding is a composite. /// public BindingSyntax To(InputBinding binding) { if (!valid) throw new InvalidOperationException("Accessor is not valid"); m_ActionMap.m_Bindings[m_BindingIndexInMap] = binding; // If it's a singleton action, we force the binding to stay with the action. if (m_ActionMap.m_SingletonAction != null) m_ActionMap.m_Bindings[m_BindingIndexInMap].action = m_ActionMap.m_SingletonAction.name; m_ActionMap.OnBindingModified(); return this; } /// /// Switch to configuring the next binding. /// /// An instance configured to edit the next binding or an invalid (see ) instance if /// there is no next binding. /// If the BindingSyntax is restricted to a single action, the result will be invalid (see ) /// if there is no next binding on the action. If the BindingSyntax is restricted to an , the result will /// be be invalid if there is no next binding in the map. public BindingSyntax NextBinding() { return Iterate(true); } /// /// Switch to configuring the previous binding. /// /// An instance configured to edit the previous binding or an invalid (see ) instance if /// there is no previous binding. /// If the BindingSyntax is restricted to a single action, the result will be invalid (see ) /// if there is no previous binding on the action. If the BindingSyntax is restricted to an , the result will /// be be invalid if there is no previous binding in the map. public BindingSyntax PreviousBinding() { return Iterate(false); } /// /// Iterate to the next part binding of the current composite with the given part name. /// /// Name of the part of the binding, such as "Positive". /// An accessor to the next part binding with the given name or an invalid (see ) /// accessor if there is no such binding. /// is null or empty. /// /// Each binding that is part of a composite is marked with /// set to true. The name of the part is determined by (comparison is /// case-insensitive). Which parts are relevant to a specific composite is determined by the type of /// composite. An , for example, has "Negative" and a /// "Positive" part. /// /// /// /// // Delete first "Positive" part of "Axis" composite. /// action.ChangeCompositeBinding("Axis") /// .NextPartBinding("Positive").Erase(); /// /// /// /// /// /// public BindingSyntax NextPartBinding(string partName) { if (string.IsNullOrEmpty(partName)) throw new ArgumentNullException(nameof(partName)); return IteratePartBinding(true, partName); } /// /// Iterate to the previous part binding of the current composite with the given part name. /// /// Name of the part of the binding, such as "Positive". /// An accessor to the previous part binding with the given name or an invalid (see ) /// accessor if there is no such binding. /// is null or empty. /// /// Each binding that is part of a composite is marked with /// set to true. The name of the part is determined by (comparison is /// case-insensitive). Which parts are relevant to a specific composite is determined by the type of /// composite. An , for example, has "Negative" and a /// "Positive" part. /// /// /// /// public BindingSyntax PreviousPartBinding(string partName) { if (string.IsNullOrEmpty(partName)) throw new ArgumentNullException(nameof(partName)); return IteratePartBinding(false, partName); } /// /// Iterate to the next composite binding. /// /// If null (default), an accessor to the next composite binding, /// regardless of name or type, is returned. If it is not null, can be either the name of /// the binding (see ) or the name of the composite used in the /// binding (see ). /// A write accessor to the next composite binding or an invalid accessor (see /// ) if no such binding was found. /// /// /// /// var accessor = playerInput.actions["fire"].ChangeCompositeBinding("WASD") /// /// /// public BindingSyntax NextCompositeBinding(string compositeName = null) { return IterateCompositeBinding(true, compositeName); } public BindingSyntax PreviousCompositeBinding(string compositeName = null) { return IterateCompositeBinding(false, compositeName); } private BindingSyntax Iterate(bool next) { if (m_ActionMap == null) return default; var bindings = m_ActionMap.m_Bindings; if (bindings == null) return default; // To find the next binding for a specific action, we may have to jump // over unrelated bindings in-between. var index = m_BindingIndexInMap; while (true) { index += next ? 1 : -1; if (index < 0 || index >= bindings.Length) return default; if (m_Action == null || bindings[index].TriggersAction(m_Action)) break; } return new BindingSyntax(m_ActionMap, index, m_Action); } private BindingSyntax IterateCompositeBinding(bool next, string compositeName) { for (var accessor = Iterate(next); accessor.valid; accessor = accessor.Iterate(next)) { if (!accessor.binding.isComposite) continue; if (compositeName == null) return accessor; // Try name of binding. if (compositeName.Equals(accessor.binding.name, StringComparison.InvariantCultureIgnoreCase)) return accessor; // Try composite type name. var name = NameAndParameters.ParseName(accessor.binding.path); if (compositeName.Equals(name, StringComparison.InvariantCultureIgnoreCase)) return accessor; } return default; } private BindingSyntax IteratePartBinding(bool next, string partName) { if (!valid) return default; if (binding.isComposite) { // If we're at the composite, only proceed if we're iterating down // instead of up. if (!next) return default; } else if (!binding.isPartOfComposite) return default; for (var accessor = Iterate(next); accessor.valid; accessor = accessor.Iterate(next)) { if (!accessor.binding.isPartOfComposite) return default; if (partName.Equals(accessor.binding.name, StringComparison.InvariantCultureIgnoreCase)) return accessor; } return default; } ////TODO: allow setting overrides through this accessor /// /// Remove the binding. /// /// /// If the binding is a composite (see ), part bindings of the /// composite will be removed as well. /// /// Note that the accessor will not necessarily be invalidated. Instead, it will point to what used /// to be the next binding in the array (though that means the accessor will be invalid if the binding /// that got erased was the last one in the array). /// /// The instance is not . public void Erase() { if (!valid) throw new InvalidOperationException("Instance not valid"); var isComposite = m_ActionMap.m_Bindings[m_BindingIndexInMap].isComposite; ArrayHelpers.EraseAt(ref m_ActionMap.m_Bindings, m_BindingIndexInMap); // If it's a composite, also erase part bindings. if (isComposite) { while (m_BindingIndexInMap < m_ActionMap.m_Bindings.LengthSafe() && m_ActionMap.m_Bindings[m_BindingIndexInMap].isPartOfComposite) ArrayHelpers.EraseAt(ref m_ActionMap.m_Bindings, m_BindingIndexInMap); } m_ActionMap.OnBindingModified(); // We have switched to a different binding array. For singleton actions, we need to // sync up the reference that the action itself has. if (m_ActionMap.m_SingletonAction != null) m_ActionMap.m_SingletonAction.m_SingletonActionBindings = m_ActionMap.m_Bindings; } public BindingSyntax InsertPartBinding(string partName, string path) { if (string.IsNullOrEmpty(partName)) throw new ArgumentNullException(nameof(partName)); if (!valid) throw new InvalidOperationException("Binding accessor is not valid"); var binding = this.binding; if (!binding.isPartOfComposite && !binding.isComposite) throw new InvalidOperationException("Binding accessor must point to composite or part binding"); AddBindingInternal(m_ActionMap, new InputBinding { path = path, isPartOfComposite = true, name = partName }, m_BindingIndexInMap + 1); return new BindingSyntax(m_ActionMap, m_BindingIndexInMap + 1, m_Action); } } ////TODO: remove this and merge it into BindingSyntax public struct CompositeSyntax { private readonly InputAction m_Action; private readonly InputActionMap m_ActionMap; private int m_BindingIndexInMap; /// /// Index of the binding that the accessor refers to. /// /// /// When accessing bindings on an , this is the index in /// of the action. When accessing bindings on an /// , it is the index /// of the map. /// public int bindingIndex { get { if (m_ActionMap == null) return -1; if (m_Action != null) return m_Action.BindingIndexOnMapToBindingIndexOnAction(m_BindingIndexInMap); return m_BindingIndexInMap; } } internal CompositeSyntax(InputActionMap map, InputAction action, int compositeIndex) { m_Action = action; m_ActionMap = map; m_BindingIndexInMap = compositeIndex; } /// /// Add a part binding to the composite. /// /// Name of the part. This is dependent on the type of composite. For /// , for example, the valid parts are "Up", "Down", /// "Left", and "Right". /// Control path to binding to. See . /// Binding groups to assign to the part binding. See . /// Optional list of processors to apply to the binding. See . /// The same composite syntax for further configuration. public CompositeSyntax With(string name, string binding, string groups = null, string processors = null) { ////TODO: check whether non-composite bindings have been added in-between using (InputActionRebindingExtensions.DeferBindingResolution()) { int bindingIndex; if (m_Action != null) bindingIndex = m_Action.AddBinding(path: binding, groups: groups, processors: processors) .m_BindingIndexInMap; else bindingIndex = m_ActionMap.AddBinding(path: binding, groups: groups, processors: processors) .m_BindingIndexInMap; m_ActionMap.m_Bindings[bindingIndex].name = name; m_ActionMap.m_Bindings[bindingIndex].isPartOfComposite = true; } return this; } } public struct ControlSchemeSyntax { private readonly InputActionAsset m_Asset; private readonly int m_ControlSchemeIndex; private InputControlScheme m_ControlScheme; internal ControlSchemeSyntax(InputActionAsset asset, int index) { m_Asset = asset; m_ControlSchemeIndex = index; m_ControlScheme = new InputControlScheme(); } internal ControlSchemeSyntax(InputControlScheme controlScheme) { m_Asset = null; m_ControlSchemeIndex = -1; m_ControlScheme = controlScheme; } public ControlSchemeSyntax WithBindingGroup(string bindingGroup) { if (string.IsNullOrEmpty(bindingGroup)) throw new ArgumentNullException(nameof(bindingGroup)); if (m_Asset == null) m_ControlScheme.m_BindingGroup = bindingGroup; else m_Asset.m_ControlSchemes[m_ControlSchemeIndex].bindingGroup = bindingGroup; return this; } public ControlSchemeSyntax WithRequiredDevice() where TDevice : InputDevice { return WithRequiredDevice(DeviceTypeToControlPath()); } public ControlSchemeSyntax WithOptionalDevice() where TDevice : InputDevice { return WithOptionalDevice(DeviceTypeToControlPath()); } public ControlSchemeSyntax OrWithRequiredDevice() where TDevice : InputDevice { return WithRequiredDevice(DeviceTypeToControlPath()); } public ControlSchemeSyntax OrWithOptionalDevice() where TDevice : InputDevice { return WithOptionalDevice(DeviceTypeToControlPath()); } public ControlSchemeSyntax WithRequiredDevice(string controlPath) { AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.None); return this; } public ControlSchemeSyntax WithOptionalDevice(string controlPath) { AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.Optional); return this; } public ControlSchemeSyntax OrWithRequiredDevice(string controlPath) { AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.Or); return this; } public ControlSchemeSyntax OrWithOptionalDevice(string controlPath) { AddDeviceEntry(controlPath, InputControlScheme.DeviceRequirement.Flags.Optional | InputControlScheme.DeviceRequirement.Flags.Or); return this; } private string DeviceTypeToControlPath() where TDevice : InputDevice { var layoutName = InputControlLayout.s_Layouts.TryFindLayoutForType(typeof(TDevice)).ToString(); if (string.IsNullOrEmpty(layoutName)) layoutName = typeof(TDevice).Name; return $"<{layoutName}>"; } public InputControlScheme Done() { if (m_Asset != null) return m_Asset.m_ControlSchemes[m_ControlSchemeIndex]; return m_ControlScheme; } private void AddDeviceEntry(string controlPath, InputControlScheme.DeviceRequirement.Flags flags) { if (string.IsNullOrEmpty(controlPath)) throw new ArgumentNullException(nameof(controlPath)); var scheme = m_Asset != null ? m_Asset.m_ControlSchemes[m_ControlSchemeIndex] : m_ControlScheme; ArrayHelpers.Append(ref scheme.m_DeviceRequirements, new InputControlScheme.DeviceRequirement { m_ControlPath = controlPath, m_Flags = flags, }); if (m_Asset == null) m_ControlScheme = scheme; else m_Asset.m_ControlSchemes[m_ControlSchemeIndex] = scheme; } } } }