using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Collections; using UnityEngine.InputSystem.Utilities; ////REVIEW: given we have the global ActionPerformed callback, do we really need the per-map callback? ////TODO: remove constraint of not being able to modify bindings while enabled from both actions and maps //// (because of the sharing of state between multiple maps in an asset, we'd have to extend that constraint //// to all maps in an asset in order to uphold it properly) namespace UnityEngine.InputSystem { /// /// A mechanism for collecting a series of input actions (see ) /// and treating them as a group. /// /// /// Each action map is a named collection of bindings and actions. Both are stored /// as a flat list. The bindings are available through the /// property and the actions are available through the property. /// /// The actions in a map are owned by the map. No action can appear in two maps /// at the same time. To find the action map an action belongs to, use the /// property. Note that actions can also stand /// on their own and thus do not necessarily need to belong to a map (in which case /// the property is null). /// /// Within a map, all actions have to have names and each action name must /// be unique. The property of bindings in a map /// are resolved within the in the map. Looking up actions /// by name can be done through . /// /// The of the map itself can be empty, except if the map is part of /// an in which case it is required to have a name /// which also must be unique within the asset. /// /// Action maps are most useful for grouping actions that contextually /// belong together. For example, one common usage is to separate the actions /// that can be performed in the UI or in the main menu from those that can /// be performed during gameplay. However, even within gameplay, multiple action /// maps can be employed. For example, one could have different action maps for /// driving and for walking plus one more map for the actions shared between /// the two modes. /// /// Action maps are usually created in the action /// editor as part of s. However, they can also be /// created standing on their own directly in code or from JSON (see ). /// /// /// /// // Create a free-standing action map. /// var map = new InputActionMap(); /// /// // Add some actions and bindings to it. /// map.AddAction("action1", binding: "<Keyboard>/space"); /// map.AddAction("action2", binding: "<Gamepad>/buttonSouth"); /// /// /// /// Actions in action maps, like actions existing by themselves outside of action /// maps, do not actively process input except if enabled. Actions can either /// be enabled individually (see and ) or in bulk by enabling and disabling the /// entire map (see and ). /// /// /// [Serializable] public sealed class InputActionMap : ICloneable, ISerializationCallbackReceiver, IInputActionCollection2, IDisposable { /// /// Name of the action map. /// /// Name of the action map. /// /// For action maps that are part of s, this will always be /// a non-null, non-empty string that is unique within the maps in the asset. For action maps /// that are standing on their own, this can be null or empty. /// public string name => m_Name; /// /// If the action map is part of an asset, this refers to the asset. Otherwise it is null. /// /// Asset to which the action map belongs. public InputActionAsset asset => m_Asset; /// /// A stable, unique identifier for the map. /// /// Unique ID for the action map. /// /// This can be used instead of the name to refer to the action map. Doing so allows referring to the /// map such that renaming it does not break references. /// /// public Guid id { get { if (string.IsNullOrEmpty(m_Id)) GenerateId(); return new Guid(m_Id); } } internal Guid idDontGenerate { get { if (string.IsNullOrEmpty(m_Id)) return default; return new Guid(m_Id); } } /// /// Whether any action in the map is currently enabled. /// /// True if any action in is currently enabled. /// /// /// public bool enabled => m_EnabledActionsCount > 0; /// /// List of actions contained in the map. /// /// Collection of actions belonging to the map. /// /// Actions are owned by their map. The same action cannot appear in multiple maps. /// /// Accessing this property. Note that values returned by the property become invalid if /// the setup of actions in a map is changed. /// /// public ReadOnlyArray actions => new ReadOnlyArray(m_Actions); /// /// List of bindings contained in the map. /// /// Collection of bindings in the map. /// /// s are owned by action maps and not by individual actions. /// /// Bindings that trigger actions refer to the action by /// or . /// /// Accessing this property does not allocate. Note that values returned by the property /// become invalid if the setup of bindings in a map is changed. /// /// public ReadOnlyArray bindings => new ReadOnlyArray(m_Bindings); IEnumerable IInputActionCollection2.bindings => bindings; /// /// Control schemes defined for the action map. /// /// List of available control schemes. /// /// Control schemes can only be defined at the level of s. /// For action maps that are part of assets, this property will return the control schemes /// from the asset. For free-standing action maps, this will return an empty list. /// /// public ReadOnlyArray controlSchemes { get { if (m_Asset == null) return new ReadOnlyArray(); return m_Asset.controlSchemes; } } /// /// Binding mask to apply to all actions in the asset. /// /// Optional mask that determines which bindings in the action map to enable. /// /// Binding masks can be applied at three different levels: for an entire asset through /// , for a specific map through this property, /// and for single actions through . By default, /// none of the masks will be set (that is, they will be null). /// /// When an action is enabled, all the binding masks that apply to it are taken into /// account. Specifically, this means that any given binding on the action will be /// enabled only if it matches the mask applied to the asset, the mask applied /// to the map that contains the action, and the mask applied to the action itself. /// All the masks are individually optional. /// /// Masks are matched against bindings using . /// /// Note that if you modify the masks applicable to an action while it is /// enabled, the action's will get updated immediately to /// respect the mask. To avoid repeated binding resolution, it is most efficient /// to apply binding masks before enabling actions. /// /// Binding masks are non-destructive. All the bindings on the action are left /// in place. Setting a mask will not affect the value of the /// and properties. /// /// /// /// public InputBinding? bindingMask { get => m_BindingMask; set { if (m_BindingMask == value) return; m_BindingMask = value; LazyResolveBindings(fullResolve: true); } } /// /// Set of devices that bindings in the action map can bind to. /// /// Optional set of devices to use by bindings in the map. /// /// By default (with this property being null), bindings will bind to any of the /// controls available through , that is, controls from all /// devices in the system will be used. /// /// By setting this property, binding resolution can instead be restricted to just specific /// devices. This restriction can either be applied to an entire asset using or to specific action maps by using this property. Note that /// if both this property and is set for a specific action /// map, the list of devices on the action map will take precedence and the list on the /// asset will be ignored for bindings in that action map. /// /// /// /// // Create an action map containing a single action with a gamepad binding. /// var actionMap = new InputActionMap(); /// var fireAction = actionMap.AddAction("Fire", binding: "<Gamepad>/buttonSouth"); /// asset.AddActionMap(actionMap); /// /// // Let's assume we have two gamepads connected. If we enable the /// // action map now, the 'Fire' action will bind to both. /// actionMap.Enable(); /// /// // This will print two controls. /// Debug.Log(string.Join("\n", fireAction.controls)); /// /// // To restrict the setup to just the first gamepad, we can assign /// // to the 'devices' property. /// actionMap.devices = new InputDevice[] { Gamepad.all[0] }; /// /// // Now this will print only one control. /// Debug.Log(string.Join("\n", fireAction.controls)); /// /// /// /// public ReadOnlyArray? devices { // Return asset's device list if we have none (only if we're part of an asset). get => m_Devices.Get() ?? m_Asset?.devices; set { if (m_Devices.Set(value)) LazyResolveBindings(fullResolve: false); } } /// /// Look up an action by name or ID. /// /// Name (as in ) or ID (as in ) /// of the action. Note that matching of names is case-insensitive. /// is null. /// No action with the name or ID of /// was found in the action map. /// /// This method is equivalent to except it throws KeyNotFoundException /// if no action with the given name or ID can be found. /// /// /// /// public InputAction this[string actionNameOrId] { get { if (actionNameOrId == null) throw new ArgumentNullException(nameof(actionNameOrId)); var action = FindAction(actionNameOrId); if (action == null) throw new KeyNotFoundException($"Cannot find action '{actionNameOrId}'"); return action; } } ////REVIEW: inconsistent naming; elsewhere we use "onActionTriggered" (which in turn is inconsistent with InputAction.started etc) /// /// Add or remove a callback that is triggered when an action in the map changes its /// phase. /// /// /// /// public event Action actionTriggered { add => m_ActionCallbacks.AddCallback(value); remove => m_ActionCallbacks.RemoveCallback(value); } /// /// Construct an action map with default values. /// public InputActionMap() { s_NeedToResolveBindings = true; } /// /// Construct an action map with the given name. /// /// Name to give to the action map. By default null, i.e. does /// not assign a name to the map. public InputActionMap(string name) : this() { m_Name = name; } /// /// Release internal state held on to by the action map. /// /// /// Once actions in a map are enabled, the map will allocate a block of state internally that /// it will hold on to until disposed of. All actions in the map will share the same internal /// state. Also, if the map is part of an all maps and actions /// in the same asset will share the same internal state. /// /// Note that the internal state holds on to GC heap memory as well as memory from the /// unmanaged, C++ heap. /// public void Dispose() { m_State?.Dispose(); } internal int FindActionIndex(string nameOrId) { ////REVIEW: have transient lookup table? worth optimizing this? //// Ideally, this should at least be an InternedString comparison but due to serialization, //// that's quite tricky. if (string.IsNullOrEmpty(nameOrId)) return -1; if (m_Actions == null) return -1; // First time we hit this method, we populate the lookup table. SetUpActionLookupTable(); var actionCount = m_Actions.Length; var isOldBracedFormat = nameOrId.StartsWith("{") && nameOrId.EndsWith("}"); if (isOldBracedFormat) { var length = nameOrId.Length - 2; for (var i = 0; i < actionCount; ++i) { if (string.Compare(m_Actions[i].m_Id, 0, nameOrId, 1, length) == 0) return i; } } if (m_ActionIndexByNameOrId.TryGetValue(nameOrId, out var actionIndex)) return actionIndex; for (var i = 0; i < actionCount; ++i) { var action = m_Actions[i]; if (action.m_Id == nameOrId || string.Compare(m_Actions[i].m_Name, nameOrId, StringComparison.InvariantCultureIgnoreCase) == 0) return i; } return InputActionState.kInvalidIndex; } private void SetUpActionLookupTable() { if (m_ActionIndexByNameOrId != null || m_Actions == null) return; m_ActionIndexByNameOrId = new Dictionary(); var actionCount = m_Actions.Length; for (var i = 0; i < actionCount; ++i) { var action = m_Actions[i]; // We want to make sure an action ID cannot change *after* we have created the table. // NOTE: The *name* of an action, however, *may* change. action.MakeSureIdIsInPlace(); // We create two lookup paths for each action: // (1) By case-sensitive name. // (2) By GUID string. m_ActionIndexByNameOrId[action.name] = i; m_ActionIndexByNameOrId[action.m_Id] = i; } } internal void ClearActionLookupTable() { m_ActionIndexByNameOrId?.Clear(); } private int FindActionIndex(Guid id) { if (m_Actions == null) return InputActionState.kInvalidIndex; var actionCount = m_Actions.Length; for (var i = 0; i < actionCount; ++i) if (m_Actions[i].idDontGenerate == id) return i; return InputActionState.kInvalidIndex; } /// /// Find an action in the map by name or ID. /// /// Name (as in ) or ID (as in ) /// of the action. Note that matching of names is case-insensitive. /// If set to will cause an exception to be thrown when the action was not found. /// The action with the given name or ID or null if no matching action /// was found. /// is null. /// public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false) { if (actionNameOrId == null) throw new ArgumentNullException(nameof(actionNameOrId)); var index = FindActionIndex(actionNameOrId); if (index == -1) { if (throwIfNotFound) throw new ArgumentException($"No action '{actionNameOrId}' in '{this}'", nameof(actionNameOrId)); return null; } return m_Actions[index]; } /// /// Find an action by ID. /// /// ID (as in ) of the action. /// The action with the given ID or null if no action in the map has /// the given ID. /// public InputAction FindAction(Guid id) { var index = FindActionIndex(id); if (index == -1) return null; return m_Actions[index]; } /// /// Check whether there are any bindings in the action map that can bind to /// controls on the given device. /// /// An input device. /// True if any of the bindings in the map can resolve to controls on the device, false otherwise. /// is null. /// /// The logic is entirely based on the contents of and, more specifically, /// of each binding. Each path is checked using . If any path matches, the method returns true. /// /// Properties such as and are ignored. /// /// /// /// // Create action map with two actions and bindings. /// var actionMap = new InputActionMap(); /// actionMap.AddAction("action1", binding: "<Gamepad>/buttonSouth"); /// actionMap.AddAction("action2", binding: "<XRController{LeftHand}>/{PrimaryAction}"); /// /// // /// var gamepad = InputSystem.AddDevice<Gamepad>(); /// var xrController = InputSystem.AddDevice<XRController>(); /// /// // Returns true: /// actionMap.IsUsableWith(gamepad); /// /// // Returns false: (the XRController does not have the LeftHand usage assigned to it) /// actionMap.IsUsableWith(xrController); /// /// /// public bool IsUsableWithDevice(InputDevice device) { if (device == null) throw new ArgumentNullException(nameof(device)); if (m_Bindings == null) return false; foreach (var binding in m_Bindings) { var path = binding.effectivePath; if (string.IsNullOrEmpty(path)) continue; if (InputControlPath.Matches(path, device)) return true; } return false; } /// /// Enable all the actions in the map. /// /// /// This is equivalent to calling on each /// action in , but is more efficient as the actions /// will get enabled in bulk. /// /// /// public void Enable() { if (m_Actions == null || m_EnabledActionsCount == m_Actions.Length) return; ResolveBindingsIfNecessary(); m_State.EnableAllActions(this); } /// /// Disable all the actions in the map. /// /// /// This is equivalent to calling on each /// action in , but is more efficient as the actions /// will get disabled in bulk. /// /// /// public void Disable() { if (!enabled) return; m_State.DisableAllActions(this); } /// /// Produce an identical copy of the action map with its actions and bindings. /// /// A copy of the action map. /// /// If the action map is part of an , the clone will not /// be. It will be a free-standing action map and will be null. /// /// Note that the IDs for the map itself as well as for its and /// are not copied. Instead, new IDs will be assigned. Also, callbacks /// installed on actions or on the map itself will not be copied over. /// public InputActionMap Clone() { Debug.Assert(m_SingletonAction == null, "Internal (hidden) action maps of singleton actions should not be cloned"); var clone = new InputActionMap { m_Name = m_Name }; // Clone actions. if (m_Actions != null) { var actionCount = m_Actions.Length; var actions = new InputAction[actionCount]; for (var i = 0; i < actionCount; ++i) { var original = m_Actions[i]; actions[i] = new InputAction { m_Name = original.m_Name, m_ActionMap = clone, m_Type = original.m_Type, m_Interactions = original.m_Interactions, m_Processors = original.m_Processors, m_ExpectedControlType = original.m_ExpectedControlType, m_Flags = original.m_Flags, }; } clone.m_Actions = actions; } // Clone bindings. if (m_Bindings != null) { var bindingCount = m_Bindings.Length; var bindings = new InputBinding[bindingCount]; Array.Copy(m_Bindings, 0, bindings, 0, bindingCount); for (var i = 0; i < bindingCount; ++i) bindings[i].m_Id = default; clone.m_Bindings = bindings; } return clone; } /// /// Return an boxed instance of the action map. /// /// An boxed clone of the action map /// object ICloneable.Clone() { return Clone(); } /// /// Return true if the action map contains the given action. /// /// An input action. Can be null. /// True if the action map contains , false otherwise. public bool Contains(InputAction action) { if (action == null) return false; return action.actionMap == this; } /// /// Return a string representation of the action map useful for debugging. /// /// A string representation of the action map. /// /// For unnamed action maps, this will always be "<Unnamed Action Map>". /// public override string ToString() { if (m_Asset != null) return $"{m_Asset}:{m_Name}"; if (!string.IsNullOrEmpty(m_Name)) return m_Name; return ""; } /// /// Enumerate the actions in the map. /// /// An enumerator going over the actions in the map. /// /// This method supports to generically iterate over the actions in a map. However, it will usually /// lead to GC allocation. Iterating directly over avoids allocating GC memory. /// public IEnumerator GetEnumerator() { return actions.GetEnumerator(); } /// /// Enumerate the actions in the map. /// /// An enumerator going over the actions in the map. /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } // The state we persist is pretty much just a name, a flat list of actions, and a flat // list of bindings. The rest is state we keep at runtime when a map is in use. [SerializeField] internal string m_Name; [SerializeField] internal string m_Id; // Can't serialize System.Guid and Unity's GUID is editor only. [SerializeField] internal InputActionAsset m_Asset; /// /// List of actions in this map. /// [SerializeField] internal InputAction[] m_Actions; /// /// List of bindings in this map. /// /// /// For singleton actions, we ensure this is always the same as . /// [SerializeField] internal InputBinding[] m_Bindings; // These fields are caches. If m_Bindings is modified, these are thrown away // and re-computed only if needed. // NOTE: Because InputBindings are structs, m_BindingsForEachAction actually duplicates each binding // (only in the case where m_Bindings has scattered references to actions). ////REVIEW: this will lead to problems when overrides are thrown into the mix /// /// For each entry in , a slice of this array corresponds to the /// action's bindings. /// /// /// Ideally, this array is the same as (the same as in literally reusing the /// same array). However, we have no guarantee that is sorted by actions. In case it /// isn't, we create a separate array with the bindings sorted by action and have each action reference /// a slice through and . /// /// [NonSerialized] private InputBinding[] m_BindingsForEachAction; [NonSerialized] private InputControl[] m_ControlsForEachAction; /// /// Number of actions currently enabled in the map. /// /// /// This should only be written to by . /// [NonSerialized] internal int m_EnabledActionsCount; // Action maps that are created internally by singleton actions to hold their data // are never exposed and never serialized so there is no point allocating an m_Actions // array. [NonSerialized] internal InputAction m_SingletonAction; [NonSerialized] internal int m_MapIndexInState = InputActionState.kInvalidIndex; /// /// Current execution state. /// /// /// Initialized when map (or any action in it) is first enabled. /// [NonSerialized] internal InputActionState m_State; [NonSerialized] internal InputBinding? m_BindingMask; [NonSerialized] private Flags m_Flags; [NonSerialized] internal int m_ParameterOverridesCount; [NonSerialized] internal InputActionRebindingExtensions.ParameterOverride[] m_ParameterOverrides; [NonSerialized] internal DeviceArray m_Devices; [NonSerialized] internal CallbackArray> m_ActionCallbacks; [NonSerialized] internal Dictionary m_ActionIndexByNameOrId; private bool needToResolveBindings { get => (m_Flags & Flags.NeedToResolveBindings) != 0; set { if (value) m_Flags |= Flags.NeedToResolveBindings; else m_Flags &= ~Flags.NeedToResolveBindings; } } private bool bindingResolutionNeedsFullReResolve { get => (m_Flags & Flags.BindingResolutionNeedsFullReResolve) != 0; set { if (value) m_Flags |= Flags.BindingResolutionNeedsFullReResolve; else m_Flags &= ~Flags.BindingResolutionNeedsFullReResolve; } } private bool controlsForEachActionInitialized { get => (m_Flags & Flags.ControlsForEachActionInitialized) != 0; set { if (value) m_Flags |= Flags.ControlsForEachActionInitialized; else m_Flags &= ~Flags.ControlsForEachActionInitialized; } } private bool bindingsForEachActionInitialized { get => (m_Flags & Flags.BindingsForEachActionInitialized) != 0; set { if (value) m_Flags |= Flags.BindingsForEachActionInitialized; else m_Flags &= ~Flags.BindingsForEachActionInitialized; } } [Flags] private enum Flags { NeedToResolveBindings = 1 << 0, BindingResolutionNeedsFullReResolve = 1 << 1, ControlsForEachActionInitialized = 1 << 2, BindingsForEachActionInitialized = 1 << 3, } internal static int s_DeferBindingResolution; internal static bool s_NeedToResolveBindings; internal struct DeviceArray { private bool m_HaveValue; private int m_DeviceCount; private InputDevice[] m_DeviceArray; // May have extra capacity; we won't let go once allocated. public int IndexOf(InputDevice device) { return m_DeviceArray.IndexOfReference(device, m_DeviceCount); } public bool Remove(InputDevice device) { var index = IndexOf(device); if (index < 0) return false; m_DeviceArray.EraseAtWithCapacity(ref m_DeviceCount, index); return true; } public ReadOnlyArray? Get() { if (!m_HaveValue) return null; return new ReadOnlyArray(m_DeviceArray, 0, m_DeviceCount); } public bool Set(ReadOnlyArray? devices) { if (!devices.HasValue) { if (!m_HaveValue) return false; // No change. if (m_DeviceCount > 0) Array.Clear(m_DeviceArray, 0, m_DeviceCount); m_DeviceCount = 0; m_HaveValue = false; } else { // See if the array actually changes content. Avoids re-resolving when there // is no need to. var array = devices.Value; if (m_HaveValue && array.Count == m_DeviceCount && array.HaveEqualReferences(m_DeviceArray, m_DeviceCount)) return false; if (m_DeviceCount > 0) m_DeviceArray.Clear(ref m_DeviceCount); m_HaveValue = true; m_DeviceCount = 0; ArrayHelpers.AppendListWithCapacity(ref m_DeviceArray, ref m_DeviceCount, array); } return true; } } /// /// Return the list of bindings for just the given actions. /// /// /// /// /// The bindings for a single action may be contiguous in or may be scattered /// around. We don't keep persistent storage for these and instead set up a transient /// array if and when bindings are queried directly from an action. In the simple case, /// we don't even need a separate array but rather just need to find out which slice in the /// bindings array corresponds to which action. /// /// NOTE: Bindings for individual actions aren't queried by the system itself during normal /// runtime operation so we only do this for cases where the user asks for the /// information. If the user never asks for bindings or controls on a per-action basis, /// none of this data gets initialized. /// internal ReadOnlyArray GetBindingsForSingleAction(InputAction action) { Debug.Assert(action != null, "Action cannot be null"); Debug.Assert(action.m_ActionMap == this, "Action must be in action map"); Debug.Assert(!action.isSingletonAction || m_SingletonAction == action, "Action is not a singleton action"); // See if we need to refresh. if (!bindingsForEachActionInitialized) SetUpPerActionControlAndBindingArrays(); return new ReadOnlyArray(m_BindingsForEachAction, action.m_BindingsStartIndex, action.m_BindingsCount); } internal ReadOnlyArray GetControlsForSingleAction(InputAction action) { Debug.Assert(m_State != null); Debug.Assert(m_MapIndexInState != InputActionState.kInvalidIndex); Debug.Assert(m_Actions != null); Debug.Assert(action != null); Debug.Assert(action.m_ActionMap == this); Debug.Assert(!action.isSingletonAction || m_SingletonAction == action); if (!controlsForEachActionInitialized) SetUpPerActionControlAndBindingArrays(); return new ReadOnlyArray(m_ControlsForEachAction, action.m_ControlStartIndex, action.m_ControlCount); } /// /// Collect data from and such that we can /// we can cleanly expose it from and . /// /// /// We set up per-action caches the first time their information is requested. Internally, we do not /// use those arrays and thus they will not get set up by default. /// /// Note that it is important to allow to call this method at a point where we have not resolved /// controls yet (i.e. is null). Otherwise, using /// may trigger a control resolution which would be surprising. /// private unsafe void SetUpPerActionControlAndBindingArrays() { // Handle case where we don't have any bindings. if (m_Bindings == null) { m_ControlsForEachAction = null; m_BindingsForEachAction = null; controlsForEachActionInitialized = true; bindingsForEachActionInitialized = true; return; } if (m_SingletonAction != null) { // Dead simple case: map is internally owned by action. The entire // list of bindings is specific to the action. Debug.Assert(m_Bindings == m_SingletonAction.m_SingletonActionBindings, "For singleton action, bindings array must match that of the action"); m_BindingsForEachAction = m_Bindings; m_ControlsForEachAction = m_State?.controls; m_SingletonAction.m_BindingsStartIndex = 0; m_SingletonAction.m_BindingsCount = m_Bindings.Length; m_SingletonAction.m_ControlStartIndex = 0; m_SingletonAction.m_ControlCount = m_State?.totalControlCount ?? 0; // Only complication, InputActionState allows a control to appear multiple times // on the same action and InputAction.controls[] doesn't. if (m_ControlsForEachAction.HaveDuplicateReferences(0, m_SingletonAction.m_ControlCount)) { var numControls = 0; var controls = new InputControl[m_SingletonAction.m_ControlCount]; for (var i = 0; i < m_SingletonAction.m_ControlCount; ++i) { if (!controls.ContainsReference(m_ControlsForEachAction[i])) { controls[numControls] = m_ControlsForEachAction[i]; ++numControls; } } m_ControlsForEachAction = controls; m_SingletonAction.m_ControlCount = numControls; } } else { ////REVIEW: now that we have per-action binding information in UnmanagedMemory, this here can likely be done more easily // Go through all bindings and slice them out to individual actions. Debug.Assert(m_Actions != null, "Action map is associated with action but action map has no array of actions"); // Action isn't a singleton so this has to be true. var mapIndices = m_State?.FetchMapIndices(this) ?? new InputActionState.ActionMapIndices(); // Reset state on each action. Important if we have actions that are no longer // referred to by bindings. for (var i = 0; i < m_Actions.Length; ++i) { var action = m_Actions[i]; action.m_BindingsCount = 0; action.m_BindingsStartIndex = -1; action.m_ControlCount = 0; action.m_ControlStartIndex = -1; } // Count bindings on each action. // After this loop, we can have one of two situations: // 1) The bindings for any action X start at some index N and occupy the next m_BindingsCount slots. // 2) The bindings for some or all actions are scattered across non-contiguous chunks of the array. var bindingCount = m_Bindings.Length; for (var i = 0; i < bindingCount; ++i) { var action = FindAction(m_Bindings[i].action); if (action != null) ++action.m_BindingsCount; } // Collect the bindings and controls and bundle them into chunks. var newBindingsArrayIndex = 0; if (m_State != null && (m_ControlsForEachAction == null || m_ControlsForEachAction.Length != mapIndices.controlCount)) { if (mapIndices.controlCount == 0) m_ControlsForEachAction = null; else m_ControlsForEachAction = new InputControl[mapIndices.controlCount]; } InputBinding[] newBindingsArray = null; var currentControlIndex = 0; for (var currentBindingIndex = 0; currentBindingIndex < m_Bindings.Length;) { var currentAction = FindAction(m_Bindings[currentBindingIndex].action); if (currentAction == null || currentAction.m_BindingsStartIndex != -1) { // Skip bindings not targeting an action or bindings we have already processed // (when gathering bindings for a single actions scattered across the array we may have // skipping ahead). ++currentBindingIndex; continue; } // Bindings for current action start at current index. currentAction.m_BindingsStartIndex = newBindingsArray != null ? newBindingsArrayIndex : currentBindingIndex; currentAction.m_ControlStartIndex = currentControlIndex; // Collect all bindings for the action. As part of that, also copy the controls // for each binding over to m_ControlsForEachAction. var bindingCountForCurrentAction = currentAction.m_BindingsCount; Debug.Assert(bindingCountForCurrentAction > 0); var sourceBindingToCopy = currentBindingIndex; for (var i = 0; i < bindingCountForCurrentAction; ++i) { // See if we've come across a binding that doesn't belong to our currently looked at action. if (FindAction(m_Bindings[sourceBindingToCopy].action) != currentAction) { // Yes, we have. Means the bindings for our actions are scattered in m_Bindings and // we need to collect them. // If this is the first action that has its bindings scattered around, switch to // having a separate bindings array and copy whatever bindings we already processed // over to it. if (newBindingsArray == null) { newBindingsArray = new InputBinding[m_Bindings.Length]; newBindingsArrayIndex = sourceBindingToCopy; Array.Copy(m_Bindings, 0, newBindingsArray, 0, sourceBindingToCopy); } // Find the next binding belonging to the action. We've counted bindings for // the action in the previous pass so we know exactly how many bindings we // can expect. do { ++sourceBindingToCopy; Debug.Assert(sourceBindingToCopy < m_Bindings.Length); } while (FindAction(m_Bindings[sourceBindingToCopy].action) != currentAction); } else if (currentBindingIndex == sourceBindingToCopy) ++currentBindingIndex; // Copy binding over to new bindings array, if need be. if (newBindingsArray != null) newBindingsArray[newBindingsArrayIndex++] = m_Bindings[sourceBindingToCopy]; // Copy controls for binding, if we have resolved controls already and if the // binding isn't a composite (they refer to the controls from all of their part bindings // but do not really resolve to controls themselves). if (m_State != null && !m_Bindings[sourceBindingToCopy].isComposite) { ref var bindingState = ref m_State.bindingStates[mapIndices.bindingStartIndex + sourceBindingToCopy]; var controlCountForBinding = bindingState.controlCount; if (controlCountForBinding > 0) { // Internally, we allow several bindings on a given action to resolve to the same control. // Externally, however, InputAction.controls[] is a set and thus should not contain duplicates. // So, instead of just doing a straight copy here, we copy controls one by one. var controlStartIndexForBinding = bindingState.controlStartIndex; for (var n = 0; n < controlCountForBinding; ++n) { var control = m_State.controls[controlStartIndexForBinding + n]; if (!m_ControlsForEachAction.ContainsReference(currentAction.m_ControlStartIndex, currentAction.m_ControlCount, control)) { m_ControlsForEachAction[currentControlIndex] = control; ++currentControlIndex; ++currentAction.m_ControlCount; } } } } ++sourceBindingToCopy; } } if (newBindingsArray == null) { // Bindings are already clustered by action in m_Bindings // so we can just stick to having one array only. m_BindingsForEachAction = m_Bindings; } else { // Bindings are not clustered by action in m_Bindings so // we had to allocate a separate array where the bindings are sorted. m_BindingsForEachAction = newBindingsArray; } } controlsForEachActionInitialized = true; bindingsForEachActionInitialized = true; } internal void OnWantToChangeSetup() { if (asset != null) { foreach (var assetMap in asset.actionMaps) if (assetMap.enabled) throw new InvalidOperationException( $"Cannot add, remove, or change elements of InputActionAsset {asset} while one or more of its actions are enabled"); } else if (enabled) { throw new InvalidOperationException( $"Cannot add, remove, or change elements of InputActionMap {this} while one or more of its actions are enabled"); } } internal void OnSetupChanged() { if (m_Asset != null) { m_Asset.MarkAsDirty(); foreach (var map in m_Asset.actionMaps) map.m_State = default; } else { m_State = default; } ClearCachedActionData(); LazyResolveBindings(fullResolve: true); } internal void OnBindingModified() { ClearCachedActionData(); LazyResolveBindings(fullResolve: true); } ////TODO: re-use allocations such that only grow the arrays and hit zero GC allocs when we already have enough memory internal void ClearCachedActionData(bool onlyControls = false) { if (!onlyControls) { bindingsForEachActionInitialized = false; m_BindingsForEachAction = default; m_ActionIndexByNameOrId = default; } controlsForEachActionInitialized = false; m_ControlsForEachAction = default; } internal void GenerateId() { m_Id = Guid.NewGuid().ToString(); } /// /// Resolve bindings right away if we have to. Otherwise defer it to when we next need /// the bindings. /// internal bool LazyResolveBindings(bool fullResolve) { // Clear cached controls for actions. Don't need to necessarily clear m_BindingsForEachAction. m_ControlsForEachAction = null; controlsForEachActionInitialized = false; // Indicate that there is at least one action map that has a change s_NeedToResolveBindings = true; // If we haven't had to resolve bindings yet, we can wait until when we // actually have to. if (m_State == null) return false; // We used to defer binding resolution here in case the map had no enabled actions. That behavior, // however, leads to rather unpredictable BoundControlsChanged notifications (especially for // rebinding UIs), so now we just always re-resolve anything that ever had an InputActionState // created. Unfortunately, this can lead to some unnecessary re-resolving. needToResolveBindings = true; bindingResolutionNeedsFullReResolve |= fullResolve; if (s_DeferBindingResolution > 0) return false; // Have to do it straight away. ResolveBindings(); return true; } internal bool ResolveBindingsIfNecessary() { // NOTE: We only check locally for the current map here. When there are multiple maps // in an asset, we may have maps that require re-resolution while others don't. // We only resolve if a map is used that needs resolution to happen. Note that // this will still resolve bindings for *all* maps in the asset. if (m_State == null || needToResolveBindings) { if (m_State != null && m_State.isProcessingControlStateChange) { Debug.Assert(s_DeferBindingResolution > 0, "While processing control state changes, binding resolution should be suppressed"); return false; } ResolveBindings(); return true; } return false; } // We have three different starting scenarios for binding resolution: // // (1) From scratch. // There is no InputActionState and we resolve everything from a completely fresh start. This happens when // we either have not resolved bindings at all yet or when something touches the action setup (e.g. adds // or removes an action or binding) and we thus throw away the existing InputActionState. // NOTE: // * Actions can be in enabled state. // * No action can be in an in-progress state (since binding resolution is needed for actions to // be processed, no action processing can have happened yet) // // (2) From an existing InputActionState when a device has been added or removed. // There is an InputActionState and the action setup (maps, actions, bindings, binding masks) has not changed. However, // the set of devices usable with the action has changed (either the per-asset/map device list or the global // list, if we're using it). // NOTE: // * Actions can be in enabled state. // * Actions *can* be in an in-progress state. // IF the control currently driving the action is on a device that is no longer usable with the action, the // action is CANCELLED. OTHERWISE, the action will be left as is and keep being in progress from its active control. // * A device CONFIGURATION change will NOT go down this path (e.g. changing the Keyboard layout). This is because // any binding path involving display names may now resolve to different controls -- which may impact currently // active controls of in-progress actions. // * A change in the USAGES of a device will NOT go down this path either. This is for the same reason -- i.e. an // active control may no longer match the binding path it matched before. If, for example, we switch the left-hand // and right-hand roles of two controllers, will will go down path (3) and not (2). // // (3) From an existing InputActionState on any other change not covered before. // There is an InputActionState and the action setup (maps, actions, bindings, binding masks) may have changed. Also, // any change may have happened in the set of usable devices and targeted controls. This includes binding overrides // having been applied. // NOTE: // * Action can be in enabled state. // * Actions *can* be in an in-progress state. // Any such action will be CANCELLED as part of the re-resolution process. // // Both (1) and (3) are considered a "full resolve". (2) is not. /// /// Resolve all bindings to their controls and also add any action interactions /// from the bindings. /// /// /// This is the core method of action binding resolution. All binding resolution goes through here. /// /// The best way is for binding resolution to happen once for each action map at the beginning of the game /// and to then enable and disable the maps as needed. However, the system will also re-resolve /// bindings if the control setup in the system changes (i.e. if devices are added or removed /// or if layouts in the system are changed). /// /// Bindings can be re-resolved while actions are enabled. This happens changing device or binding /// masks on action maps or assets (, , , /// , ). Doing so will /// not affect the enable state of actions and, as much as possible, will try to take current /// action states across. /// internal void ResolveBindings() { // Make sure that if we trigger callbacks as part of disabling and re-enabling actions, // we don't trigger a re-resolve while we're already resolving bindings. using (InputActionRebindingExtensions.DeferBindingResolution()) { // In case we have actions that are currently enabled, we temporarily retain the // UnmanagedMemory of our InputActionState so that we can sync action states after // we have re-resolved bindings. var oldMemory = new InputActionState.UnmanagedMemory(); try { OneOrMore> actionMaps; // Start resolving. var resolver = new InputBindingResolver(); // If we're part of an asset, we share state and thus binding resolution with // all maps in the asset. var needFullResolve = m_State == null; if (m_Asset != null) { actionMaps = m_Asset.actionMaps; Debug.Assert(actionMaps.Count > 0, "Asset referred to by action map does not have action maps"); // If there's a binding mask set on the asset, apply it. resolver.bindingMask = m_Asset.m_BindingMask; foreach (var map in actionMaps) { needFullResolve |= map.bindingResolutionNeedsFullReResolve; map.needToResolveBindings = false; map.bindingResolutionNeedsFullReResolve = false; map.controlsForEachActionInitialized = false; } } else { // Standalone action map (possibly a hidden one created for a singleton action). // Gets its own private state. actionMaps = this; needFullResolve |= bindingResolutionNeedsFullReResolve; needToResolveBindings = false; bindingResolutionNeedsFullReResolve = false; controlsForEachActionInitialized = false; } // If we already have a state, re-use the arrays we have already allocated. // NOTE: We will install the arrays on the very same InputActionState instance below. In the // case where we didn't have to grow the arrays, we should end up with zero GC allocations // here. var hasEnabledActions = false; InputControlList activeControls = default; if (m_State != null) { // Grab a clone of the current memory. We clone because disabling all the actions // in the map will alter the memory state and we want the state before we start // touching it. oldMemory = m_State.memory.Clone(); m_State.PrepareForBindingReResolution(needFullResolve, ref activeControls, ref hasEnabledActions); // Reuse the arrays we have so that we can avoid managed memory allocations, if possible. resolver.StartWithPreviousResolve(m_State, isFullResolve: needFullResolve); // Throw away old memory. m_State.memory.Dispose(); } // Resolve all maps in the asset. foreach (var map in actionMaps) resolver.AddActionMap(map); // Install state. if (m_State == null) { m_State = new InputActionState(); m_State.Initialize(resolver); } else { m_State.ClaimDataFrom(resolver); } if (m_Asset != null) { foreach (var map in actionMaps) map.m_State = m_State; m_Asset.m_SharedStateForAllMaps = m_State; } m_State.FinishBindingResolution(hasEnabledActions, oldMemory, activeControls, isFullResolve: needFullResolve); } finally { oldMemory.Dispose(); } } } /// public int FindBinding(InputBinding mask, out InputAction action) { var index = FindBindingRelativeToMap(mask); if (index == -1) { action = null; return -1; } action = m_SingletonAction ?? FindAction(bindings[index].action); return action.BindingIndexOnMapToBindingIndexOnAction(index); } /// /// Find the index of the first binding that matches the given mask. /// /// A binding. See for details. /// Index into of of the binding /// that matches . If no binding matches, will return -1. /// /// For details about matching bindings by a mask, see . /// /// /// /// var index = playerInput.actions.FindBindingRelativeToMap( /// new InputBinding { path = "<Gamepad>/buttonSouth" }); /// /// if (index != -1) /// Debug.Log($"Found binding with index {index}"); /// /// /// /// /// internal int FindBindingRelativeToMap(InputBinding mask) { var bindings = m_Bindings; var bindingsCount = bindings.LengthSafe(); for (var i = 0; i < bindingsCount; ++i) { ref var binding = ref bindings[i]; if (mask.Matches(ref binding)) return i; } return -1; } #region Serialization ////REVIEW: when GetParameter/SetParameter is coming, should these also be considered part of binding override data? [Serializable] internal struct BindingOverrideListJson { public List bindings; } [Serializable] internal struct BindingOverrideJson { // We save both the "map/action" path of the action as well as the binding ID. // This gives us two avenues into finding our target binding to apply the override // to. public string action; public string id; public string path; public string interactions; public string processors; public static BindingOverrideJson FromBinding(InputBinding binding, string actionName) { return new BindingOverrideJson { action = actionName, id = binding.id.ToString() , path = binding.overridePath ?? "null", interactions = binding.overrideInteractions ?? "null", processors = binding.overrideProcessors ?? "null" }; } public static BindingOverrideJson FromBinding(InputBinding binding) { return FromBinding(binding, binding.action); } public static InputBinding ToBinding(BindingOverrideJson bindingOverride) { return new InputBinding { overridePath = bindingOverride.path != "null" ? bindingOverride.path : null, overrideInteractions = bindingOverride.interactions != "null" ? bindingOverride.interactions : null, overrideProcessors = bindingOverride.processors != "null" ? bindingOverride.processors : null, }; } } // Action maps are serialized in two different ways. For storage as imported assets in Unity's Library/ folder // and in player data and asset bundles as well as for surviving domain reloads, InputActionMaps are serialized // directly by Unity. For storage as source data in user projects, InputActionMaps are serialized indirectly // as JSON by setting up a separate set of structs that are then read and written using Unity's JSON serializer. [Serializable] internal struct BindingJson { public string name; public string id; public string path; public string interactions; public string processors; public string groups; public string action; public bool isComposite; public bool isPartOfComposite; public InputBinding ToBinding() { return new InputBinding { name = string.IsNullOrEmpty(name) ? null : name, m_Id = string.IsNullOrEmpty(id) ? null : id, path = path, action = string.IsNullOrEmpty(action) ? null : action, interactions = string.IsNullOrEmpty(interactions) ? null : interactions, processors = string.IsNullOrEmpty(processors) ? null : processors, groups = string.IsNullOrEmpty(groups) ? null : groups, isComposite = isComposite, isPartOfComposite = isPartOfComposite, }; } public static BindingJson FromBinding(ref InputBinding binding) { return new BindingJson { name = binding.name, id = binding.m_Id, path = binding.path, action = binding.action, interactions = binding.interactions, processors = binding.processors, groups = binding.groups, isComposite = binding.isComposite, isPartOfComposite = binding.isPartOfComposite, }; } } // Backwards-compatible read format. [Serializable] internal struct ReadActionJson { public string name; public string type; public string id; public string expectedControlType; public string expectedControlLayout; public string processors; public string interactions; public bool passThrough; public bool initialStateCheck; // Bindings can either be on the action itself (in which case the action name // for each binding is implied) or listed separately in the action file. public BindingJson[] bindings; public InputAction ToAction(string actionName = null) { // FormerlySerializedAs doesn't seem to work as expected so manually // handling the rename here. if (!string.IsNullOrEmpty(expectedControlLayout)) expectedControlType = expectedControlLayout; // Determine type. InputActionType actionType = default; if (!string.IsNullOrEmpty(type)) actionType = (InputActionType)Enum.Parse(typeof(InputActionType), type, true); else { // Old format that doesn't have type. Try to infer from settings. if (passThrough) actionType = InputActionType.PassThrough; else if (initialStateCheck) actionType = InputActionType.Value; else if (!string.IsNullOrEmpty(expectedControlType) && (expectedControlType == "Button" || expectedControlType == "Key")) actionType = InputActionType.Button; } return new InputAction(actionName ?? name, actionType) { m_Id = string.IsNullOrEmpty(id) ? null : id, m_ExpectedControlType = !string.IsNullOrEmpty(expectedControlType) ? expectedControlType : null, m_Processors = processors, m_Interactions = interactions, wantsInitialStateCheck = initialStateCheck, }; } } [Serializable] internal struct WriteActionJson { public string name; public string type; public string id; public string expectedControlType; public string processors; public string interactions; public bool initialStateCheck; public static WriteActionJson FromAction(InputAction action) { return new WriteActionJson { name = action.m_Name, type = action.m_Type.ToString(), id = action.m_Id, expectedControlType = action.m_ExpectedControlType, processors = action.processors, interactions = action.interactions, initialStateCheck = action.wantsInitialStateCheck, }; } } [Serializable] internal struct ReadMapJson { public string name; public string id; public ReadActionJson[] actions; public BindingJson[] bindings; } [Serializable] internal struct WriteMapJson { public string name; public string id; public WriteActionJson[] actions; public BindingJson[] bindings; public static WriteMapJson FromMap(InputActionMap map) { WriteActionJson[] jsonActions = null; BindingJson[] jsonBindings = null; var actions = map.m_Actions; if (actions != null) { var actionCount = actions.Length; jsonActions = new WriteActionJson[actionCount]; for (var i = 0; i < actionCount; ++i) jsonActions[i] = WriteActionJson.FromAction(actions[i]); } var bindings = map.m_Bindings; if (bindings != null) { var bindingCount = bindings.Length; jsonBindings = new BindingJson[bindingCount]; for (var i = 0; i < bindingCount; ++i) jsonBindings[i] = BindingJson.FromBinding(ref bindings[i]); } return new WriteMapJson { name = map.name, id = map.id.ToString(), actions = jsonActions, bindings = jsonBindings, }; } } // We write JSON in a less flexible format than we allow to be read. JSON files // we read can just be flat lists of actions with the map name being contained in // the action name and containing their own bindings directly. JSON files we write // go map by map and separate bindings and actions. [Serializable] internal struct WriteFileJson { public WriteMapJson[] maps; public static WriteFileJson FromMap(InputActionMap map) { return new WriteFileJson { maps = new[] {WriteMapJson.FromMap(map)} }; } public static WriteFileJson FromMaps(IEnumerable maps) { var mapCount = maps.Count(); if (mapCount == 0) return new WriteFileJson(); var mapsJson = new WriteMapJson[mapCount]; var index = 0; foreach (var map in maps) mapsJson[index++] = WriteMapJson.FromMap(map); return new WriteFileJson {maps = mapsJson}; } } // A JSON representation of one or more sets of actions. // Contains a list of actions. Each action may specify the set it belongs to // as part of its name ("set/action"). [Serializable] internal struct ReadFileJson { public ReadActionJson[] actions; public ReadMapJson[] maps; public InputActionMap[] ToMaps() { var mapList = new List(); var actionLists = new List>(); var bindingLists = new List>(); // Process actions listed at toplevel. var actionCount = actions?.Length ?? 0; for (var i = 0; i < actionCount; ++i) { var jsonAction = actions[i]; if (string.IsNullOrEmpty(jsonAction.name)) throw new InvalidOperationException($"Action number {i + 1} has no name"); ////REVIEW: make sure all action names are unique? // Determine name of action map. string mapName = null; var actionName = jsonAction.name; var indexOfFirstSlash = actionName.IndexOf('/'); if (indexOfFirstSlash != -1) { mapName = actionName.Substring(0, indexOfFirstSlash); actionName = actionName.Substring(indexOfFirstSlash + 1); if (string.IsNullOrEmpty(actionName)) throw new InvalidOperationException( $"Invalid action name '{jsonAction.name}' (missing action name after '/')"); } // Try to find existing map. InputActionMap map = null; var mapIndex = 0; for (; mapIndex < mapList.Count; ++mapIndex) { if (string.Compare(mapList[mapIndex].name, mapName, StringComparison.InvariantCultureIgnoreCase) == 0) { map = mapList[mapIndex]; break; } } // Create new map if it's the first action in the map. if (map == null) { // NOTE: No map IDs supported on this path. map = new InputActionMap(mapName); mapIndex = mapList.Count; mapList.Add(map); actionLists.Add(new List()); bindingLists.Add(new List()); } // Create action. var action = jsonAction.ToAction(actionName); actionLists[mapIndex].Add(action); // Add bindings. if (jsonAction.bindings != null) { var bindingsForMap = bindingLists[mapIndex]; for (var n = 0; n < jsonAction.bindings.Length; ++n) { var jsonBinding = jsonAction.bindings[n]; var binding = jsonBinding.ToBinding(); binding.action = action.m_Name; bindingsForMap.Add(binding); } } } // Process maps. var mapCount = maps?.Length ?? 0; for (var i = 0; i < mapCount; ++i) { var jsonMap = maps[i]; var mapName = jsonMap.name; if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException($"Map number {i + 1} has no name"); // Try to find existing map. InputActionMap map = null; var mapIndex = 0; for (; mapIndex < mapList.Count; ++mapIndex) { if (string.Compare(mapList[mapIndex].name, mapName, StringComparison.InvariantCultureIgnoreCase) == 0) { map = mapList[mapIndex]; break; } } // Create new map if we haven't seen it before. if (map == null) { map = new InputActionMap(mapName) { m_Id = string.IsNullOrEmpty(jsonMap.id) ? null : jsonMap.id }; mapIndex = mapList.Count; mapList.Add(map); actionLists.Add(new List()); bindingLists.Add(new List()); } // Process actions in map. var actionCountInMap = jsonMap.actions?.Length ?? 0; for (var n = 0; n < actionCountInMap; ++n) { var jsonAction = jsonMap.actions[n]; if (string.IsNullOrEmpty(jsonAction.name)) throw new InvalidOperationException($"Action number {i + 1} in map '{mapName}' has no name"); // Create action. var action = jsonAction.ToAction(); actionLists[mapIndex].Add(action); // Add bindings. if (jsonAction.bindings != null) { var bindingList = bindingLists[mapIndex]; for (var k = 0; k < jsonAction.bindings.Length; ++k) { var jsonBinding = jsonAction.bindings[k]; var binding = jsonBinding.ToBinding(); binding.action = action.m_Name; bindingList.Add(binding); } } } // Process bindings in map. var bindingCountInMap = jsonMap.bindings?.Length ?? 0; var bindingsForMap = bindingLists[mapIndex]; for (var n = 0; n < bindingCountInMap; ++n) { var jsonBinding = jsonMap.bindings[n]; var binding = jsonBinding.ToBinding(); bindingsForMap.Add(binding); } } // Finalize arrays. for (var i = 0; i < mapList.Count; ++i) { var map = mapList[i]; var actionArray = actionLists[i].ToArray(); var bindingArray = bindingLists[i].ToArray(); map.m_Actions = actionArray; map.m_Bindings = bindingArray; for (var n = 0; n < actionArray.Length; ++n) { var action = actionArray[n]; action.m_ActionMap = map; } } return mapList.ToArray(); } } /// /// Load one or more action maps from JSON. /// /// JSON representation of the action maps. Can be empty. /// is null. /// The array of action maps (may be empty) read from the given JSON string. Will not be /// null. /// /// Note that the format used by this method is different than what you /// get if you call JsonUtility.ToJson on an InputActionMap instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// /// /// /// var maps = InputActionMap.FromJson(@" /// { /// ""maps"" : [ /// { /// ""name"" : ""Gameplay"", /// ""actions"" : [ /// { ""name"" : ""fire"", ""type"" : ""button"" } /// ], /// ""bindings"" : [ /// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"" } /// ], /// } /// ] /// } /// "); /// /// /// /// /// public static InputActionMap[] FromJson(string json) { if (json == null) throw new ArgumentNullException(nameof(json)); var fileJson = JsonUtility.FromJson(json); return fileJson.ToMaps(); } /// /// Convert a set of action maps to JSON format. /// /// List of action maps to serialize. /// is null. /// JSON representation of the given action maps. /// /// The result of this method can be loaded with . /// /// Note that the format used by this method is different than what you /// get if you call JsonUtility.ToJson on an InputActionMap instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// /// public static string ToJson(IEnumerable maps) { if (maps == null) throw new ArgumentNullException(nameof(maps)); var fileJson = WriteFileJson.FromMaps(maps); return JsonUtility.ToJson(fileJson, true); } /// /// Convert the action map to JSON format. /// /// A JSON representation of the action map. /// /// The result of this method can be loaded with . /// /// Note that the format used by this method is different than what you /// get if you call JsonUtility.ToJson on an InputActionMap instance. In other /// words, the JSON format is not identical to the Unity serialized object representation /// of the asset. /// public string ToJson() { var fileJson = WriteFileJson.FromMap(this); return JsonUtility.ToJson(fileJson, true); } /// /// Called by Unity before the action map is serialized using Unity's /// serialization system. /// public void OnBeforeSerialize() { } /// /// Called by Unity after the action map has been deserialized using Unity's /// serialization system. /// public void OnAfterDeserialize() { // Indicate that there is at least one action map that has a change s_NeedToResolveBindings = true; m_State = null; m_MapIndexInState = InputActionState.kInvalidIndex; m_EnabledActionsCount = 0; // Restore references of actions linking back to us. if (m_Actions != null) { var actionCount = m_Actions.Length; for (var i = 0; i < actionCount; ++i) m_Actions[i].m_ActionMap = this; } // Make sure we don't retain any cached per-action data when using serialization // to doctor around in action map configurations in the editor. ClearCachedActionData(); ClearActionLookupTable(); } #endregion } }