2023-03-28 13:24:16 -04:00
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
{
/// <summary>
/// A mechanism for collecting a series of input actions (see <see cref="InputAction"/>)
/// and treating them as a group.
/// </summary>
/// <remarks>
/// Each action map is a named collection of bindings and actions. Both are stored
/// as a flat list. The bindings are available through the <see cref="bindings"/>
/// property and the actions are available through the <see cref="actions"/> 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
/// <see cref="InputAction.actionMap"/> 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 <see cref="InputAction.actionMap"/> property is <c>null</c>).
///
/// Within a map, all actions have to have names and each action name must
/// be unique. The <see cref="InputBinding.action"/> property of bindings in a map
/// are resolved within the <see cref="actions"/> in the map. Looking up actions
/// by name can be done through <see cref="FindAction(string,bool)"/>.
///
/// The <see cref="name"/> of the map itself can be empty, except if the map is part of
/// an <see cref="InputActionAsset"/> 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 <a href="../manual/ActionAssets.html">action
/// editor</a> as part of <see cref="InputActionAsset"/>s. However, they can also be
/// created standing on their own directly in code or from JSON (see <see cref="FromJson"/>).
///
/// <example>
/// <code>
/// // 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");
/// </code>
/// </example>
///
/// 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 <see cref="InputAction.Enable"/> and <see
/// cref="InputAction.Disable"/>) or in bulk by enabling and disabling the
/// entire map (see <see cref="Enable"/> and <see cref="Disable"/>).
/// </remarks>
/// <seealso cref="InputActionAsset"/>
/// <seealso cref="InputAction"/>
[Serializable]
public sealed class InputActionMap : ICloneable , ISerializationCallbackReceiver , IInputActionCollection2 , IDisposable
{
/// <summary>
/// Name of the action map.
/// </summary>
/// <value>Name of the action map.</value>
/// <remarks>
/// For action maps that are part of <see cref="InputActionAsset"/>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.
/// </remarks>
public string name = > m_Name ;
/// <summary>
/// If the action map is part of an asset, this refers to the asset. Otherwise it is <c>null</c>.
/// </summary>
/// <value>Asset to which the action map belongs.</value>
public InputActionAsset asset = > m_Asset ;
/// <summary>
/// A stable, unique identifier for the map.
/// </summary>
/// <value>Unique ID for the action map.</value>
/// <remarks>
/// 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.
/// </remarks>
/// <seealso cref="InputAction.id"/>
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 ) ;
}
}
/// <summary>
/// Whether any action in the map is currently enabled.
/// </summary>
/// <value>True if any action in <see cref="actions"/> is currently enabled.</value>
/// <seealso cref="InputAction.enabled"/>
/// <seealso cref="Enable"/>
/// <seealso cref="InputAction.Enable"/>
public bool enabled = > m_EnabledActionsCount > 0 ;
/// <summary>
/// List of actions contained in the map.
/// </summary>
/// <value>Collection of actions belonging to the map.</value>
/// <remarks>
/// 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.
/// </remarks>
/// <seealso cref="InputAction.actionMap"/>
public ReadOnlyArray < InputAction > actions = > new ReadOnlyArray < InputAction > ( m_Actions ) ;
/// <summary>
/// List of bindings contained in the map.
/// </summary>
/// <value>Collection of bindings in the map.</value>
/// <remarks>
/// <see cref="InputBinding"/>s are owned by action maps and not by individual actions.
///
/// Bindings that trigger actions refer to the action by <see cref="InputAction.name"/>
/// or <see cref="InputAction.id"/>.
///
/// 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.
/// </remarks>
/// <seealso cref="InputAction.bindings"/>
public ReadOnlyArray < InputBinding > bindings = > new ReadOnlyArray < InputBinding > ( m_Bindings ) ;
IEnumerable < InputBinding > IInputActionCollection2 . bindings = > bindings ;
/// <summary>
/// Control schemes defined for the action map.
/// </summary>
/// <value>List of available control schemes.</value>
/// <remarks>
/// Control schemes can only be defined at the level of <see cref="InputActionAsset"/>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.
/// </remarks>
/// <seealso cref="InputActionAsset.controlSchemes"/>
public ReadOnlyArray < InputControlScheme > controlSchemes
{
get
{
if ( m_Asset = = null )
return new ReadOnlyArray < InputControlScheme > ( ) ;
return m_Asset . controlSchemes ;
}
}
/// <summary>
/// Binding mask to apply to all actions in the asset.
/// </summary>
/// <value>Optional mask that determines which bindings in the action map to enable.</value>
/// <remarks>
/// Binding masks can be applied at three different levels: for an entire asset through
/// <see cref="InputActionAsset.bindingMask"/>, for a specific map through this property,
/// and for single actions through <see cref="InputAction.bindingMask"/>. By default,
/// none of the masks will be set (that is, they will be <c>null</c>).
///
/// 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 <see cref="InputBinding.Matches"/>.
///
/// Note that if you modify the masks applicable to an action while it is
/// enabled, the action's <see cref="InputAction.controls"/> 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 <see cref="InputAction.bindings"/>
/// and <see cref="bindings"/> properties.
/// </remarks>
/// <seealso cref="InputBinding.MaskByGroup"/>
/// <seealso cref="InputAction.bindingMask"/>
/// <seealso cref="InputActionAsset.bindingMask"/>
public InputBinding ? bindingMask
{
get = > m_BindingMask ;
set
{
if ( m_BindingMask = = value )
return ;
m_BindingMask = value ;
LazyResolveBindings ( fullResolve : true ) ;
}
}
/// <summary>
/// Set of devices that bindings in the action map can bind to.
/// </summary>
/// <value>Optional set of devices to use by bindings in the map.</value>
/// <remarks>
/// By default (with this property being <c>null</c>), bindings will bind to any of the
/// controls available through <see cref="InputSystem.devices"/>, 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 <see
/// cref="InputActionMap.devices"/> or to specific action maps by using this property. Note that
/// if both this property and <see cref="InputActionAsset.devices"/> 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.
///
/// <example>
/// <code>
/// // 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));
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputActionAsset.devices"/>
public ReadOnlyArray < InputDevice > ? 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 ) ;
}
}
/// <summary>
/// Look up an action by name or ID.
/// </summary>
/// <param name="actionNameOrId">Name (as in <see cref="InputAction.name"/>) or ID (as in <see cref="InputAction.id"/>)
/// of the action. Note that matching of names is case-insensitive.</param>
/// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c>.</exception>
/// <exception cref="KeyNotFoundException">No action with the name or ID of <paramref name="actionNameOrId"/>
/// was found in the action map.</exception>
/// <remarks>
/// This method is equivalent to <see cref="FindAction(string,bool)"/> except it throws <c>KeyNotFoundException</c>
/// if no action with the given name or ID can be found.
/// </remarks>
/// <seealso cref="FindAction(string,bool)"/>
/// <seealso cref="FindAction(Guid)"/>
/// <see cref="actions"/>
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)
/// <summary>
/// Add or remove a callback that is triggered when an action in the map changes its <see cref="InputActionPhase">
/// phase</see>.
/// </summary>
/// <seealso cref="InputAction.started"/>
/// <seealso cref="InputAction.performed"/>
/// <seealso cref="InputAction.canceled"/>
public event Action < InputAction . CallbackContext > actionTriggered
{
add = > m_ActionCallbacks . AddCallback ( value ) ;
remove = > m_ActionCallbacks . RemoveCallback ( value ) ;
}
2023-05-07 18:43:11 -04:00
/// <summary>
/// Construct an action map with default values.
/// </summary>
2023-03-28 13:24:16 -04:00
public InputActionMap ( )
{
}
/// <summary>
/// Construct an action map with the given name.
/// </summary>
/// <param name="name">Name to give to the action map. By default <c>null</c>, i.e. does
/// not assign a name to the map.</param>
public InputActionMap ( string name )
: this ( )
{
m_Name = name ;
}
/// <summary>
/// Release internal state held on to by the action map.
/// </summary>
/// <remarks>
/// 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 <see cref="InputActionAsset"/> 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.
/// </remarks>
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 < string , int > ( ) ;
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 ;
}
/// <summary>
/// Find an action in the map by name or ID.
/// </summary>
/// <param name="actionNameOrId">Name (as in <see cref="InputAction.name"/>) or ID (as in <see cref="InputAction.id"/>)
/// of the action. Note that matching of names is case-insensitive.</param>
2023-05-07 18:43:11 -04:00
/// <param name="throwIfNotFound">If set to <see langword="true"/> will cause an exception to be thrown when the action was not found.</param>
2023-03-28 13:24:16 -04:00
/// <returns>The action with the given name or ID or <c>null</c> if no matching action
/// was found.</returns>
/// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c>.</exception>
/// <seealso cref="FindAction(Guid)"/>
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 ] ;
}
/// <summary>
/// Find an action by ID.
/// </summary>
/// <param name="id">ID (as in <see cref="InputAction.id"/>) of the action.</param>
/// <returns>The action with the given ID or null if no action in the map has
/// the given ID.</returns>
/// <seealso cref="FindAction(string,bool)"/>
public InputAction FindAction ( Guid id )
{
var index = FindActionIndex ( id ) ;
if ( index = = - 1 )
return null ;
return m_Actions [ index ] ;
}
/// <summary>
/// Check whether there are any bindings in the action map that can bind to
/// controls on the given device.
/// </summary>
/// <param name="device">An input device.</param>
/// <returns>True if any of the bindings in the map can resolve to controls on the device, false otherwise.</returns>
/// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
/// <remarks>
/// The logic is entirely based on the contents of <see cref="bindings"/> and, more specifically,
/// <see cref="InputBinding.effectivePath"/> of each binding. Each path is checked using <see
/// cref="InputControlPath.Matches"/>. If any path matches, the method returns <c>true</c>.
///
/// Properties such as <see cref="devices"/> and <see cref="bindingMask"/> are ignored.
///
/// <example>
/// <code>
/// // 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);
/// </code>
/// </example>
/// </remarks>
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 ;
}
/// <summary>
/// Enable all the actions in the map.
/// </summary>
/// <remarks>
/// This is equivalent to calling <see cref="InputAction.Enable"/> on each
/// action in <see cref="actions"/>, but is more efficient as the actions
/// will get enabled in bulk.
/// </remarks>
/// <seealso cref="Disable"/>
/// <seealso cref="enabled"/>
public void Enable ( )
{
if ( m_Actions = = null | | m_EnabledActionsCount = = m_Actions . Length )
return ;
ResolveBindingsIfNecessary ( ) ;
m_State . EnableAllActions ( this ) ;
}
/// <summary>
/// Disable all the actions in the map.
/// </summary>
/// <remarks>
/// This is equivalent to calling <see cref="InputAction.Disable"/> on each
/// action in <see cref="actions"/>, but is more efficient as the actions
/// will get disabled in bulk.
/// </remarks>
/// <seealso cref="Enable"/>
/// <seealso cref="enabled"/>
public void Disable ( )
{
if ( ! enabled )
return ;
m_State . DisableAllActions ( this ) ;
}
/// <summary>
/// Produce an identical copy of the action map with its actions and bindings.
/// </summary>
/// <returns>A copy of the action map.</returns>
/// <remarks>
/// If the action map is part of an <see cref="InputActionAsset"/>, the clone will <em>not</em>
/// be. It will be a free-standing action map and <see cref="asset"/> will be <c>null</c>.
///
/// Note that the IDs for the map itself as well as for its <see cref="actions"/> and
/// <see cref="bindings"/> are not copied. Instead, new IDs will be assigned. Also, callbacks
/// installed on actions or on the map itself will not be copied over.
/// </remarks>
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 ,
} ;
}
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 ;
}
2023-05-07 18:43:11 -04:00
/// <summary>
/// Return an boxed instance of the action map.
/// </summary>
/// <returns>An boxed clone of the action map</returns>
/// <seealso cref="Clone"/>
2023-03-28 13:24:16 -04:00
object ICloneable . Clone ( )
{
return Clone ( ) ;
}
/// <summary>
/// Return <c>true</c> if the action map contains the given action.
/// </summary>
/// <param name="action">An input action. Can be <c>null</c>.</param>
/// <returns>True if the action map contains <paramref name="action"/>, false otherwise.</returns>
public bool Contains ( InputAction action )
{
if ( action = = null )
return false ;
return action . actionMap = = this ;
}
/// <summary>
/// Return a string representation of the action map useful for debugging.
/// </summary>
/// <returns>A string representation of the action map.</returns>
/// <remarks>
/// For unnamed action maps, this will always be <c>"<Unnamed Action Map>"</c>.
/// </remarks>
public override string ToString ( )
{
if ( m_Asset ! = null )
return $"{m_Asset}:{m_Name}" ;
if ( ! string . IsNullOrEmpty ( m_Name ) )
return m_Name ;
return "<Unnamed Action Map>" ;
}
/// <summary>
/// Enumerate the actions in the map.
/// </summary>
/// <returns>An enumerator going over the actions in the map.</returns>
/// <remarks>
/// This method supports to generically iterate over the actions in a map. However, it will usually
/// lead to GC allocation. Iterating directly over <see cref="actions"/> avoids allocating GC memory.
/// </remarks>
public IEnumerator < InputAction > GetEnumerator ( )
{
return actions . GetEnumerator ( ) ;
}
2023-05-07 18:43:11 -04:00
/// <summary>
/// Enumerate the actions in the map.
/// </summary>
/// <returns>An enumerator going over the actions in the map.</returns>
/// <seealso cref="GetEnumerator"/>
2023-03-28 13:24:16 -04:00
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 ;
/// <summary>
/// List of actions in this map.
/// </summary>
[SerializeField] internal InputAction [ ] m_Actions ;
/// <summary>
/// List of bindings in this map.
/// </summary>
/// <remarks>
/// For singleton actions, we ensure this is always the same as <see cref="InputAction.m_SingletonActionBindings"/>.
/// </remarks>
[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
/// <summary>
/// For each entry in <see cref="m_Actions"/>, a slice of this array corresponds to the
/// action's bindings.
/// </summary>
/// <remarks>
/// Ideally, this array is the same as <see cref="m_Bindings"/> (the same as in literally reusing the
/// same array). However, we have no guarantee that <see cref="m_Bindings"/> 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 <see cref="InputAction.m_BindingsStartIndex"/> and <see cref="InputAction.m_BindingsCount"/>.
/// </remarks>
/// <seealso cref="SetUpPerActionControlAndBindingArrays"/>
[NonSerialized] private InputBinding [ ] m_BindingsForEachAction ;
[NonSerialized] private InputControl [ ] m_ControlsForEachAction ;
/// <summary>
/// Number of actions currently enabled in the map.
/// </summary>
/// <remarks>
/// This should only be written to by <see cref="InputActionState"/>.
/// </remarks>
[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 ;
/// <summary>
/// Current execution state.
/// </summary>
/// <remarks>
/// Initialized when map (or any action in it) is first enabled.
/// </remarks>
[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 < Action < InputAction . CallbackContext > > m_ActionCallbacks ;
[NonSerialized] internal Dictionary < string , int > 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 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 < InputDevice > ? Get ( )
{
if ( ! m_HaveValue )
return null ;
return new ReadOnlyArray < InputDevice > ( m_DeviceArray , 0 , m_DeviceCount ) ;
}
public bool Set ( ReadOnlyArray < InputDevice > ? 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 ;
}
}
/// <summary>
/// Return the list of bindings for just the given actions.
/// </summary>
/// <param name="action"></param>
/// <returns></returns>
/// <remarks>
/// The bindings for a single action may be contiguous in <see cref="m_Bindings"/> 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.
/// </remarks>
internal ReadOnlyArray < InputBinding > 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 < InputBinding > ( m_BindingsForEachAction , action . m_BindingsStartIndex ,
action . m_BindingsCount ) ;
}
internal ReadOnlyArray < InputControl > 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 < InputControl > ( m_ControlsForEachAction , action . m_ControlStartIndex ,
action . m_ControlCount ) ;
}
/// <summary>
/// Collect data from <see cref="m_Bindings"/> and <see cref="m_Actions"/> such that we can
/// we can cleanly expose it from <see cref="InputAction.bindings"/> and <see cref="InputAction.controls"/>.
/// </summary>
/// <remarks>
/// 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. <see cref="m_State"/> is <c>null</c>). Otherwise, using <see cref="InputAction.bindings"/>
/// may trigger a control resolution which would be surprising.
/// </remarks>
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 ( ) ;
}
/// <summary>
/// Resolve bindings right away if we have to. Otherwise defer it to when we next need
/// the bindings.
/// </summary>
internal bool LazyResolveBindings ( bool fullResolve )
{
// Clear cached controls for actions. Don't need to necessarily clear m_BindingsForEachAction.
m_ControlsForEachAction = null ;
controlsForEachActionInitialized = false ;
// 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.
/// <summary>
/// Resolve all bindings to their controls and also add any action interactions
/// from the bindings.
/// </summary>
/// <remarks>
/// 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 (<see cref="devices"/>, <see cref="bindingMask"/>, <see cref="InputAction.bindingMask"/>,
/// <see cref="InputActionAsset.devices"/>, <see cref="InputActionAsset.bindingMask"/>). Doing so will
/// not affect the enable state of actions and, as much as possible, will try to take current
/// action states across.
/// </remarks>
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 < InputActionMap , ReadOnlyArray < InputActionMap > > 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 < InputControl > 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 ( ) ;
}
}
}
/// <inheritdoc/>
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 ) ;
}
/// <summary>
/// Find the index of the first binding that matches the given mask.
/// </summary>
/// <param name="mask">A binding. See <see cref="InputBinding.Matches"/> for details.</param>
/// <returns>Index into <see cref="InputAction.bindings"/> of <paramref name="action"/> of the binding
/// that matches <paramref name="mask"/>. If no binding matches, will return -1.</returns>
/// <remarks>
/// For details about matching bindings by a mask, see <see cref="InputBinding.Matches"/>.
///
/// <example>
/// <code>
/// var index = playerInput.actions.FindBindingRelativeToMap(
/// new InputBinding { path = "<Gamepad>/buttonSouth" });
///
/// if (index != -1)
/// Debug.Log($"Found binding with index {index}");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputBinding.Matches"/>
/// <seealso cref="bindings"/>
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 < BindingOverrideJson > 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 ;
2023-05-07 18:43:11 -04:00
public static BindingOverrideJson FromBinding ( InputBinding binding , string actionName )
2023-03-28 13:24:16 -04:00
{
return new BindingOverrideJson
{
2023-05-07 18:43:11 -04:00
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 ,
2023-03-28 13:24:16 -04:00
} ;
}
}
// 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 < InputActionMap > 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 < InputActionMap > ( ) ;
var actionLists = new List < List < InputAction > > ( ) ;
var bindingLists = new List < List < InputBinding > > ( ) ;
// 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 < InputAction > ( ) ) ;
bindingLists . Add ( new List < InputBinding > ( ) ) ;
}
// 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 < InputAction > ( ) ) ;
bindingLists . Add ( new List < InputBinding > ( ) ) ;
}
// 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 ( ) ;
}
}
/// <summary>
/// Load one or more action maps from JSON.
/// </summary>
/// <param name="json">JSON representation of the action maps. Can be empty.</param>
/// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c>.</exception>
/// <returns>The array of action maps (may be empty) read from the given JSON string. Will not be
/// <c>null</c>.</returns>
/// <remarks>
/// Note that the format used by this method is different than what you
/// get if you call <c>JsonUtility.ToJson</c> on an InputActionMap instance. In other
/// words, the JSON format is not identical to the Unity serialized object representation
/// of the asset.
///
/// <example>
/// <code>
/// var maps = InputActionMap.FromJson(@"
/// {
/// ""maps"" : [
/// {
/// ""name"" : ""Gameplay"",
/// ""actions"" : [
/// { ""name"" : ""fire"", ""type"" : ""button"" }
/// ],
/// ""bindings"" : [
/// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"" }
/// ],
/// }
/// ]
/// }
/// ");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputActionAsset.FromJson"/>
/// <seealso cref="ToJson(IEnumerable{InputActionMap})"/>
public static InputActionMap [ ] FromJson ( string json )
{
if ( json = = null )
throw new ArgumentNullException ( nameof ( json ) ) ;
var fileJson = JsonUtility . FromJson < ReadFileJson > ( json ) ;
return fileJson . ToMaps ( ) ;
}
/// <summary>
/// Convert a set of action maps to JSON format.
/// </summary>
/// <param name="maps">List of action maps to serialize.</param>
/// <exception cref="ArgumentNullException"><paramref name="maps"/> is <c>null</c>.</exception>
/// <returns>JSON representation of the given action maps.</returns>
/// <remarks>
/// The result of this method can be loaded with <see cref="FromJson"/>.
///
/// Note that the format used by this method is different than what you
/// get if you call <c>JsonUtility.ToJson</c> on an InputActionMap instance. In other
/// words, the JSON format is not identical to the Unity serialized object representation
/// of the asset.
/// </remarks>
/// <seealso cref="FromJson"/>
public static string ToJson ( IEnumerable < InputActionMap > maps )
{
if ( maps = = null )
throw new ArgumentNullException ( nameof ( maps ) ) ;
var fileJson = WriteFileJson . FromMaps ( maps ) ;
return JsonUtility . ToJson ( fileJson , true ) ;
}
/// <summary>
/// Convert the action map to JSON format.
/// </summary>
/// <returns>A JSON representation of the action map.</returns>
/// <remarks>
/// The result of this method can be loaded with <see cref="FromJson"/>.
///
/// Note that the format used by this method is different than what you
/// get if you call <c>JsonUtility.ToJson</c> on an InputActionMap instance. In other
/// words, the JSON format is not identical to the Unity serialized object representation
/// of the asset.
/// </remarks>
public string ToJson ( )
{
var fileJson = WriteFileJson . FromMap ( this ) ;
return JsonUtility . ToJson ( fileJson , true ) ;
}
/// <summary>
/// Called by Unity before the action map is serialized using Unity's
/// serialization system.
/// </summary>
public void OnBeforeSerialize ( )
{
}
/// <summary>
/// Called by Unity after the action map has been deserialized using Unity's
/// serialization system.
/// </summary>
public void OnAfterDeserialize ( )
{
m_State = null ;
m_MapIndexInState = InputActionState . kInvalidIndex ;
// 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
}
}