using System; using System.Collections.Generic; using UnityEngine.Events; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Users; using UnityEngine.InputSystem.Utilities; #if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI using UnityEngine.InputSystem.UI; #endif ////TODO: add support for keeping a player's InputUser alive and reconnecting back to it ////TODO: when joining is *off*, allow auto-switching even in multiplayer ////TODO: differentiate not only by already paired devices but rather take control schemes into account; allow two players to be on the same //// device as long as they are using different control schemes ////TODO: allow PlayerInput to be set up in a way where it's in an unpaired/non-functional state and expects additional configuration ////REVIEW: callback behaviors have been very confusing for users; simplify&clarify this ////REVIEW: having everything coupled to component enable/disable is quite restrictive; can we allow PlayerInputs //// to be disabled without them leaving the game? would help when wanting to keep players around in the background //// and only temporarily disable them ////TODO: add support for "continuous" callbacks ////TODO: add event for control scheme switches ////TODO: add ability to name players ////TODO: refresh caches when asset is modified at runtime ////TODO: handle required actions ahead of time so that we catch it if a device matches by type but doesn't otherwise ////TODO: handle case of control scheme not having any devices in its requirements ////TODO: add method to pass an object implementing a generated action interface (IXXXActions) and have it hooked up automatically //// (or maybe look for implementation on components in same object?) ////TODO: warn if control schemes have no device requirements ////FIXME: why can't I join with a mouse left click? namespace UnityEngine.InputSystem { /// /// Represents a separate player in the game complete with a set of actions exclusive /// to the player and a set of paired device. /// /// /// PlayerInput is a high-level wrapper around much of the input system's functionality /// which is meant to help getting set up with the new input system quickly. It takes /// care of bookkeeping and has a custom UI(requires the "Unity UI" package) to help /// setting up input. /// /// The component supports local multiplayer implicitly. Each PlayerInput instance /// represents a distinct user with its own set of devices and actions. To orchestrate /// player management and facilitate mechanics such as joining by device activity, use /// . /// /// The way PlayerInput notifies script code of events is determined by . /// By default, this is set to which will use /// to send messages to the /// that PlayerInput sits on. /// /// /// /// // Component to sit next to PlayerInput. /// [RequireComponent(typeof(PlayerInput))] /// public class MyPlayerLogic : MonoBehaviour /// { /// public GameObject projectilePrefab; /// /// private Vector2 m_Look; /// private Vector2 m_Move; /// private bool m_Fire; /// /// // 'Fire' input action has been triggered. For 'Fire' we want continuous /// // action (that is, firing) while the fire button is held such that the action /// // gets triggered repeatedly while the button is down. We can easily set this /// // up by having a "Press" interaction on the button and setting it to repeat /// // at fixed intervals. /// public void OnFire() /// { /// Instantiate(projectilePrefab); /// } /// /// // 'Move' input action has been triggered. /// public void OnMove(InputValue value) /// { /// m_Move = value.Get<Vector2>(); /// } /// /// // 'Look' input action has been triggered. /// public void OnLook(InputValue value) /// { /// m_Look = value.Get<Vector2>(); /// } /// /// public void OnUpdate() /// { /// // Update transform from m_Move and m_Look /// } /// } /// /// /// /// It is also possible to use the polling API of s (see /// and ) /// in combination with PlayerInput. /// /// /// /// // Component to sit next to PlayerInput. /// [RequireComponent(typeof(PlayerInput))] /// public class MyPlayerLogic : MonoBehaviour /// { /// public GameObject projectilePrefab; /// /// private PlayerInput m_PlayerInput; /// private InputAction m_LookAction; /// private InputAction m_MoveAction; /// private InputAction m_FireAction; /// /// public void OnUpdate() /// { /// // First update we look up all the data we need. /// // NOTE: We don't do this in OnEnable as PlayerInput itself performing some /// // initialization work in OnEnable. /// if (m_PlayerInput == null) /// { /// m_PlayerInput = GetComponent<PlayerInput>(); /// m_FireAction = m_PlayerInput.actions["fire"]; /// m_LookAction = m_PlayerInput.actions["look"]; /// m_MoveAction = m_PlayerInput.actions["move"]; /// } /// /// if (m_FireAction.triggered) /// /* firing logic... */; /// /// var move = m_MoveAction.ReadValue<Vector2>(); /// var look = m_LookAction.ReadValue<Vector2>(); /// /* Update transform from move&look... */ /// } /// } /// /// /// /// When enabled, PlayerInput will create an and pair devices to the /// user which are then specific to the player. The set of devices can be controlled explicitly /// when instantiating a PlayerInput through /// or . This also makes it possible /// to assign the same device to two different players, e.g. for split-keyboard play. /// /// /// /// var p1 = PlayerInput.Instantiate(playerPrefab, /// controlScheme: "KeyboardLeft", device: Keyboard.current); /// var p2 = PlayerInput.Instantiate(playerPrefab, /// controlScheme: "KeyboardRight", device: Keyboard.current); /// /// /// /// If no specific devices are given to a PlayerInput, the component will look for compatible /// devices present in the system and pair them to itself automatically. If the PlayerInput's /// have control schemes defined for them, PlayerInput will look for a /// control scheme for which all required devices are available and not paired to any other player. /// It will try first (if set), but then fall back to trying /// all available schemes in order. Once a scheme is found for which all required devices are /// available, PlayerInput will pair those devices to itself and select the given scheme. /// /// If no control schemes are defined, PlayerInput will try to bind as many as-of-yet unpaired /// devices to itself as it can match to bindings present in the . This means /// that if, for example, there's binding for both keyboard and gamepad and there is one keyboard /// and two gamepads available when PlayerInput is enabled, all three devices will be paired to /// the player. /// /// Note that when using , device pairing to players is controlled /// from the joining logic. In that case, PlayerInput will automatically pair the device from which /// the player joined. If control schemes are present in , the first one compatible /// with that device is chosen. If additional devices are required, these will be paired from the pool /// of currently unpaired devices. /// /// Device pairings can be changed at any time by either manually controlling pairing through /// (and related methods) using a PlayerInput's /// assigned or by switching control schemes (e.g. using /// ), if any are present in the PlayerInput's /// . /// /// When a player loses a device paired to it (e.g. when it is unplugged or loses power), /// will signal which is also surfaced as a message, /// , or (depending on ). /// When a device is reconnected, will signal /// which also is surfaced as a message, as , or /// (depending on ). /// /// When there is only a single active PlayerInput in the game, joining is not enabled (see /// ), and is not /// set to true, device pairings for the player will also update automatically based on device usage. /// /// If control schemes are present in , then if a device is used (not merely plugged in /// but rather receives input on a non-noisy, non-synthetic control) which is compatible with a control scheme /// other than the currently used one, PlayerInput will attempt to switch to that control scheme. Success depends /// on whether all device requirements for that scheme are met from the set of available devices. If a control /// scheme happens, signals on /// . /// /// If no control schemes are present in , PlayerInput will automatically pair any newly /// available device to itself if the given device has any bindings available for it. /// /// Both behaviors described in the previous two paragraphs are automatically disabled if more than one /// PlayerInput is active. /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")] [AddComponentMenu("Input/Player Input")] [DisallowMultipleComponent] [HelpURL(InputSystem.kDocUrl + "/manual/Components.html#playerinput-component")] public class PlayerInput : MonoBehaviour { /// /// Name of the message that is sent with UnityEngine.Object.SendMessage when a /// player loses a device. /// /// public const string DeviceLostMessage = "OnDeviceLost"; /// /// Name of the message that is sent with UnityEngine.Object.SendMessage when a /// player regains a device. /// /// public const string DeviceRegainedMessage = "OnDeviceRegained"; /// /// Name of the message that is sent with UnityEngine.Object.SendMessage when the /// controls used by a player are changed. /// /// public const string ControlsChangedMessage = "OnControlsChanged"; /// /// Whether input is on the player is active. /// /// If true, the player is receiving input. /// /// public bool inputIsActive => m_InputActive; [Obsolete("Use inputIsActive instead.")] public bool active => inputIsActive; /// /// Unique, zero-based index of the player. For example, 2 for the third player. /// /// Unique index of the player. /// /// Once assigned, a player index will not change. /// /// Note that the player index does not necessarily correspond to the player's index in . /// The array will always contain all currently enabled players so when a player is disabled or destroyed, /// it will be removed from the array. However, the player index of the remaining players will not change. /// public int playerIndex => m_PlayerIndex; /// /// If split-screen is enabled (), /// this is the index of the screen area used by the player. /// /// Index of split-screen area assigned to player or -1 if the player is not /// using split-screen. /// /// Split screen areas are enumerated row by row and within rows, column by column. So, if, for example, /// there are four separate split-screen areas, the upper left one is #0, the upper right one is #1, /// the lower left one is #2, and the lower right one is #3. /// /// Split screen areas are usually assigned automatically but players can also be assigned to /// areas explicitly through or /// . /// /// /// public int splitScreenIndex => m_SplitScreenIndex; /// /// Input actions associated with the player. /// /// Asset holding the player's input actions. /// /// Note that every player will maintain a unique copy of the given actions such that /// each player receives an identical copy. When assigning the same actions to multiple players, /// the first player will use the given actions as is but any subsequent player will make a copy /// of the actions using . /// /// The asset may contain an arbitrary number of action maps. By setting , /// one of them can be selected to enabled automatically when PlayerInput is enabled. If no default /// action map is selected, none of the action maps will be enabled by PlayerInput itself. Use /// or just call directly /// to enable a specific map. /// /// Notifications will be sent for all actions in the asset, not just for those in the first action /// map. This means that if additional maps are manually enabled and disabled, notifications will /// be sent for their actions as they receive input. /// /// /// public InputActionAsset actions { get { if (!m_ActionsInitialized && gameObject.activeInHierarchy) InitializeActions(); return m_Actions; } set { if (m_Actions == value) return; // Make sure that if we already have actions, they get disabled. if (m_Actions != null) { m_Actions.Disable(); if (m_ActionsInitialized) UninitializeActions(); } m_Actions = value; if (m_Enabled) { ClearCaches(); AssignUserAndDevices(); InitializeActions(); if (m_InputActive) ActivateInput(); } } } /// /// Name of the currently active control scheme. /// /// Name of the currently active control scheme or null. /// /// Note that this property will be null if there are no control schemes /// defined in . /// /// /// /// public string currentControlScheme { get { if (!m_InputUser.valid) return null; var scheme = m_InputUser.controlScheme; return scheme?.name; } } /// /// The default control scheme to try. /// /// Name of the default control scheme. /// /// When PlayerInput is enabled and this is not null and not empty, the PlayerInput /// will look up the control scheme in of /// . If found, PlayerInput will try to activate the scheme. This will /// succeed only if all devices required by the control scheme are either already paired to /// the player or are available as devices not used by other PlayerInputs. /// /// Note that this property only determines the first control scheme to try. If using the /// control scheme fails, PlayerInput will fall back to trying the other control schemes /// (if any) available from . /// /// /// public string defaultControlScheme { get => m_DefaultControlScheme; set => m_DefaultControlScheme = value; } /// /// If true, do not automatically switch control schemes even when there is only a single player. /// By default, this property is false. /// /// If true, do not switch control schemes when other devices are used. /// /// By default, when there is only a single PlayerInput enabled, we assume that the game is in /// single-player mode and that the player should be able to freely switch between the control schemes /// supported by the game. For example, if the player is currently using mouse and keyboard, but is /// then switching to a gamepad, PlayerInput should automatically switch to the control scheme for /// gamepads, if present. /// /// When there is more than one PlayerInput or when joining is enabled , /// this behavior is automatically turned off as we wouldn't know which player is switching if a /// currently unpaired device is used. /// /// By setting this property to true, auto-switching of control schemes is forcibly turned off and /// will thus not be performed even if there is only a single PlayerInput in the game. /// /// Note that you can still switch control schemes manually using . /// /// /// public bool neverAutoSwitchControlSchemes { get => m_NeverAutoSwitchControlSchemes; set { if (m_NeverAutoSwitchControlSchemes == value) return; m_NeverAutoSwitchControlSchemes = value; if (m_Enabled) { if (!value && !m_OnUnpairedDeviceUsedHooked) StartListeningForUnpairedDeviceActivity(); else if (value && m_OnUnpairedDeviceUsedHooked) StopListeningForUnpairedDeviceActivity(); } } } ////REVIEW: this is inconsistent; currentControlScheme is a string, this is an InputActionMap /// /// The currently enabled action map. /// /// Reference to the currently enabled action or null if no action /// map has been enabled by PlayerInput. /// /// Note that the concept of "current action map" is local to PlayerInput. You can still freely /// enable and disable action maps directly on the asset. This property /// only tracks which action map has been enabled under the control of PlayerInput, i.e. either /// by means of or by using . /// /// public InputActionMap currentActionMap { get => m_CurrentActionMap; set { // If someone switches maps from an action callback, we may get here recursively // from Disable(). To avoid that, we null out the current action map while // we disable it. var oldMap = m_CurrentActionMap; m_CurrentActionMap = null; oldMap?.Disable(); // Switch to new map. m_CurrentActionMap = value; m_CurrentActionMap?.Enable(); } } /// /// Name (see ) or ID (see ) of the action /// map to enable by default. /// /// Action map to enable by default or null. /// /// By default, when enabled, PlayerInput will not enable any of the actions in the /// asset. By setting this property, however, PlayerInput can be made to automatically enable the respective /// action map. /// /// /// public string defaultActionMap { get => m_DefaultActionMap; set => m_DefaultActionMap = value; } /// /// Determines how the component notifies listeners about input actions and other input-related /// events pertaining to the player. /// /// How to trigger notifications on events. /// /// By default, the component will use to send messages /// to the . This can be changed by selecting a different /// behavior. /// /// /// /// public PlayerNotifications notificationBehavior { get => m_NotificationBehavior; set { if (m_NotificationBehavior == value) return; if (m_Enabled) UninitializeActions(); m_NotificationBehavior = value; if (m_Enabled) InitializeActions(); } } /// /// List of events invoked in response to actions being triggered. /// /// /// This array is only used if is set to /// . /// public ReadOnlyArray actionEvents { get => m_ActionEvents; set { if (m_Enabled) UninitializeActions(); m_ActionEvents = value.ToArray(); if (m_Enabled) InitializeActions(); } } /// /// Event that is triggered when the player loses a device (e.g. the batteries run out). /// /// /// This event is only used if is set to /// . /// public DeviceLostEvent deviceLostEvent { get { if (m_DeviceLostEvent == null) m_DeviceLostEvent = new DeviceLostEvent(); return m_DeviceLostEvent; } } /// /// Event that is triggered when the player recovers from device loss and is good to go again. /// /// /// This event is only used if is set to /// . /// public DeviceRegainedEvent deviceRegainedEvent { get { if (m_DeviceRegainedEvent == null) m_DeviceRegainedEvent = new DeviceRegainedEvent(); return m_DeviceRegainedEvent; } } /// /// Event that is triggered when the controls used by the player change. /// /// /// This event is only used if is set to /// . /// /// The event is trigger when the set of used by the player change, /// when the player switches to a different control scheme (see ), /// or when the bindings used by the player are changed (e.g. when rebinding them). Also, /// for devices, the event is triggered when the currently used /// keyboard layout (see ) changes. /// public ControlsChangedEvent controlsChangedEvent { get { if (m_ControlsChangedEvent == null) m_ControlsChangedEvent = new ControlsChangedEvent(); return m_ControlsChangedEvent; } } /// /// If is set to , this /// event is triggered when an action fires. /// /// Callbacks that get called when an action triggers. /// /// If is not set to , the /// value of this property is ignored. /// /// The callbacks are called in sync (and with the same argument) with , /// , and . /// /// /// /// /// /// public event Action onActionTriggered { add { if (value == null) throw new ArgumentNullException(nameof(value)); m_ActionTriggeredCallbacks.AddCallback(value); } remove { if (value == null) throw new ArgumentNullException(nameof(value)); m_ActionTriggeredCallbacks.RemoveCallback(value); } } /// /// If is , this event /// is triggered when a device paired to the player is disconnected. /// /// Callbacks that get called when the player loses a device. /// /// If is not , the value /// of this property is ignored. /// /// The argument is the player that lost its device (i.e. the player on which the callback is installed). /// /// /// public event Action onDeviceLost { add { if (value == null) throw new ArgumentNullException(nameof(value)); m_DeviceLostCallbacks.AddCallback(value); } remove { if (value == null) throw new ArgumentNullException(nameof(value)); m_DeviceLostCallbacks.RemoveCallback(value); } } /// /// If is , this event /// is triggered when the player previously lost a device and has now regained it or an equivalent device. /// /// Callbacks that get called when the player regains a device. /// /// If is not , the value /// of this property is ignored. /// /// The argument is the player that regained a device (i.e. the player on which the callback is installed). /// /// /// public event Action onDeviceRegained { add { if (value == null) throw new ArgumentNullException(nameof(value)); m_DeviceRegainedCallbacks.AddCallback(value); } remove { if (value == null) throw new ArgumentNullException(nameof(value)); m_DeviceRegainedCallbacks.RemoveCallback(value); } } /// /// If is , this event /// is triggered when the controls used by the players are changed. /// /// /// The callback is invoked when the set of used by the player change, /// when the player switches to a different control scheme (see ), /// or when the bindings used by the player are changed (e.g. when rebinding them). Also, /// for devices, the callback is invoked when the currently used /// keyboard layout (see ) changes. /// public event Action onControlsChanged { add { if (value == null) throw new ArgumentNullException(nameof(value)); m_ControlsChangedCallbacks.AddCallback(value); } remove { if (value == null) throw new ArgumentNullException(nameof(value)); m_ControlsChangedCallbacks.RemoveCallback(value); } } ////TODO: clarify the relationship to raycasting in the UI input module /// /// Optional camera associated with the player. /// /// Camera specific to the player or null. /// /// This is null by default. /// /// Associating a camera with a player is necessary only when using split-screen (see ). /// public #if UNITY_EDITOR // camera property is deprecated and only available in Editor. new #endif Camera camera { get => m_Camera; set => m_Camera = value; } #if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI /// /// UI InputModule that should have it's input actions synchronized to this PlayerInput's actions. /// public InputSystemUIInputModule uiInputModule { get => m_UIInputModule; set { if (m_UIInputModule == value) return; if (m_UIInputModule != null && m_UIInputModule.actionsAsset == m_Actions) m_UIInputModule.actionsAsset = null; m_UIInputModule = value; if (m_UIInputModule != null && m_Actions != null) m_UIInputModule.actionsAsset = m_Actions; } } #endif /// /// The internal user tied to the player. /// public InputUser user => m_InputUser; /// /// The devices paired to the player. /// /// List of devices paired to player. /// /// /// public ReadOnlyArray devices { get { if (!m_InputUser.valid) return new ReadOnlyArray(); return m_InputUser.pairedDevices; } } /// /// Whether the player is missed required devices. This means that the player's /// input setup is probably at least partially non-functional. /// /// True if the player is missing devices required by the control scheme. /// /// This can happen, for example, if the a device is unplugged during the game. /// /// /// public bool hasMissingRequiredDevices => user.valid && user.hasMissingRequiredDevices; /// /// List of all players that are currently joined. Sorted by in /// increasing order. /// /// List of active PlayerInputs. /// /// While the list is sorted by , note that this does not mean that the /// of a player corresponds to the index in this list. If, for example, three players join and then the second player leaves, /// the list will contain one player with 0 followed by one player with 2. /// /// /// public static ReadOnlyArray all => new ReadOnlyArray(s_AllActivePlayers, 0, s_AllActivePlayersCount); /// /// Whether PlayerInput operates in single-player mode. /// /// If true, there is at most a single PlayerInput. /// /// Single-player mode is active while there is at most one PlayerInput (there can also be none) and /// while joining is not enabled in (if one exists). See . /// /// Automatic control scheme switching (if enabled) is predicated on single-player mode being active. /// /// public static bool isSinglePlayer => s_AllActivePlayersCount <= 1 && (PlayerInputManager.instance == null || !PlayerInputManager.instance.joiningEnabled); /// /// Return the first device of the given type from paired to the player. /// If no device of this type is paired to the player, return null. /// /// Type of device to look for (such as ). Can be a supertype /// of the actual device type. For example, querying for , may return a . /// The first device paired to the player that is of the given type or null if the player /// does not have a matching device. /// public TDevice GetDevice() where TDevice : InputDevice { foreach (var device in devices) if (device is TDevice deviceOfType) return deviceOfType; return null; } /// /// Enable input on the player. /// /// /// Input will automatically be activated when the PlayerInput component is enabled. However, this method /// can be called to reactivate input after deactivating it with . /// /// Note that activating input will activate the current action map only (see ). /// /// /// public void ActivateInput() { m_InputActive = true; // If we have no current action map but there's a default // action map, make it current. if (m_CurrentActionMap == null && m_Actions != null && !string.IsNullOrEmpty(m_DefaultActionMap)) SwitchCurrentActionMap(m_DefaultActionMap); else m_CurrentActionMap?.Enable(); } /// /// Disable input on the player. /// /// /// Input is automatically activated when the PlayerInput component is enabled. This method can be /// used to deactivate input manually. /// /// Note that activating input will deactivate the current action map only (see ). /// /// /// public void DeactivateInput() { m_CurrentActionMap?.Disable(); m_InputActive = false; } [Obsolete("Use DeactivateInput instead.")] public void PassivateInput() { DeactivateInput(); } /// /// Switch the current control scheme to one that fits the given set of devices. /// /// A list of input devices. Note that if any of the devices is already paired to another /// player, the device will end up paired to both players. /// True if the switch was successful, false otherwise. The latter can happen, for example, if /// does not have a control scheme that fits the given set of devices. /// is null. /// has not been assigned. /// /// The player's currently paired devices (see ) will get unpaired. /// /// /// /// // Switch the first player to keyboard and mouse. /// PlayerInput.all[0] /// .SwitchCurrentControlScheme(Keyboard.current, Mouse.current); /// /// /// /// /// public bool SwitchCurrentControlScheme(params InputDevice[] devices) { if (devices == null) throw new ArgumentNullException(nameof(devices)); if (actions == null) throw new InvalidOperationException( "Must set actions on PlayerInput in order to be able to switch control schemes"); // Find control scheme matching given devices in associated action asset var scheme = InputControlScheme.FindControlSchemeForDevices(devices, actions.controlSchemes); if (!scheme.HasValue) return false; var controlScheme = scheme.Value; SwitchControlSchemeInternal(ref controlScheme, devices); return true; } ////REVIEW: these should just be SwitchControlScheme /// /// Switch the player to use the given control scheme together with the given devices. /// /// Name of the control scheme. See . /// A list of devices. /// is null -or- is /// null or empty. /// /// This method can be used to explicitly force a combination of control scheme and a specific set of /// devices. /// /// /// /// // Put player 1 on the "Gamepad" control scheme together /// // with the second gamepad. /// PlayerInput.all[0].SwitchControlScheme( /// "Gamepad", /// Gamepad.all[1]); /// /// /// /// The player's currently paired devices (see ) will get unpaired. /// /// /// public void SwitchCurrentControlScheme(string controlScheme, params InputDevice[] devices) { if (string.IsNullOrEmpty(controlScheme)) throw new ArgumentNullException(nameof(controlScheme)); if (devices == null) throw new ArgumentNullException(nameof(devices)); user.FindControlScheme(controlScheme, out InputControlScheme scheme); // throws if not found SwitchControlSchemeInternal(ref scheme, devices); } public void SwitchCurrentActionMap(string mapNameOrId) { // Must be enabled. if (!m_Enabled) { Debug.LogError($"Cannot switch to actions '{mapNameOrId}'; input is not enabled", this); return; } // Must have actions. if (m_Actions == null) { Debug.LogError($"Cannot switch to actions '{mapNameOrId}'; no actions set on PlayerInput", this); return; } // Must have map. var actionMap = m_Actions.FindActionMap(mapNameOrId); if (actionMap == null) { Debug.LogError($"Cannot find action map '{mapNameOrId}' in actions '{m_Actions}'", this); return; } currentActionMap = actionMap; } /// /// Return the Nth player. /// /// Index of the player to return. /// The player with the given player index or null if no such /// player exists. /// public static PlayerInput GetPlayerByIndex(int playerIndex) { for (var i = 0; i < s_AllActivePlayersCount; ++i) if (s_AllActivePlayers[i].playerIndex == playerIndex) return s_AllActivePlayers[i]; return null; } /// /// Find the first PlayerInput who the given device is paired to. /// /// An input device. /// The player who is paired to the given device or null if no /// PlayerInput currently is paired to . /// is null. /// /// /// /// // Find the player paired to first gamepad. /// var player = PlayerInput.FindFirstPairedToDevice(Gamepad.all[0]); /// /// /// public static PlayerInput FindFirstPairedToDevice(InputDevice device) { if (device == null) throw new ArgumentNullException(nameof(device)); for (var i = 0; i < s_AllActivePlayersCount; ++i) { if (ReadOnlyArrayExtensions.ContainsReference(s_AllActivePlayers[i].devices, device)) return s_AllActivePlayers[i]; } return null; } /// /// Instantiate a player object and set up and enable its inputs. /// /// Prefab to clone. Must contain a PlayerInput component somewhere in its hierarchy. /// Player index to assign to the player. See . /// By default will be assigned automatically based on how many players are in . /// Control scheme to activate /// /// Device to pair to the user. By default, this is null which means /// that PlayerInput will automatically pair with available, unpaired devices based on the control schemes (if any) /// present in or on the bindings therein (if no control schemes are present). /// /// is null. public static PlayerInput Instantiate(GameObject prefab, int playerIndex = -1, string controlScheme = null, int splitScreenIndex = -1, InputDevice pairWithDevice = null) { if (prefab == null) throw new ArgumentNullException(nameof(prefab)); // Set initialization data. s_InitPlayerIndex = playerIndex; s_InitSplitScreenIndex = splitScreenIndex; s_InitControlScheme = controlScheme; if (pairWithDevice != null) ArrayHelpers.AppendWithCapacity(ref s_InitPairWithDevices, ref s_InitPairWithDevicesCount, pairWithDevice); return DoInstantiate(prefab); } ////TODO: allow instantiating with an existing InputUser /// /// A wrapper around that allows instantiating a player prefab and /// automatically pair one or more specific devices to the newly created player. /// /// A player prefab containing a component in its hierarchy. /// /// /// /// /// /// /// Note that unlike , this method will always activate the resulting /// and its components. /// public static PlayerInput Instantiate(GameObject prefab, int playerIndex = -1, string controlScheme = null, int splitScreenIndex = -1, params InputDevice[] pairWithDevices) { if (prefab == null) throw new ArgumentNullException(nameof(prefab)); // Set initialization data. s_InitPlayerIndex = playerIndex; s_InitSplitScreenIndex = splitScreenIndex; s_InitControlScheme = controlScheme; if (pairWithDevices != null) { for (var i = 0; i < pairWithDevices.Length; ++i) ArrayHelpers.AppendWithCapacity(ref s_InitPairWithDevices, ref s_InitPairWithDevicesCount, pairWithDevices[i]); } return DoInstantiate(prefab); } private static PlayerInput DoInstantiate(GameObject prefab) { var destroyIfDeviceSetupUnsuccessful = s_DestroyIfDeviceSetupUnsuccessful; GameObject instance; try { instance = Object.Instantiate(prefab); instance.SetActive(true); } finally { // Reset init data. s_InitPairWithDevicesCount = 0; if (s_InitPairWithDevices != null) Array.Clear(s_InitPairWithDevices, 0, s_InitPairWithDevicesCount); s_InitControlScheme = null; s_InitPlayerIndex = -1; s_InitSplitScreenIndex = -1; s_DestroyIfDeviceSetupUnsuccessful = false; } var playerInput = instance.GetComponentInChildren(); if (playerInput == null) { DestroyImmediate(instance); Debug.LogError("The GameObject does not have a PlayerInput component", prefab); return null; } if (destroyIfDeviceSetupUnsuccessful && (!playerInput.user.valid || playerInput.hasMissingRequiredDevices)) { DestroyImmediate(instance); return null; } return playerInput; } [Tooltip("Input actions associated with the player.")] [SerializeField] internal InputActionAsset m_Actions; [Tooltip("Determine how notifications should be sent when an input-related event associated with the player happens.")] [SerializeField] internal PlayerNotifications m_NotificationBehavior; [Tooltip("UI InputModule that should have it's input actions synchronized to this PlayerInput's actions.")] #if UNITY_INPUT_SYSTEM_ENABLE_UI [SerializeField] internal InputSystemUIInputModule m_UIInputModule; [Tooltip("Event that is triggered when the PlayerInput loses a paired device (e.g. its battery runs out).")] #endif [SerializeField] internal DeviceLostEvent m_DeviceLostEvent; [SerializeField] internal DeviceRegainedEvent m_DeviceRegainedEvent; [SerializeField] internal ControlsChangedEvent m_ControlsChangedEvent; [SerializeField] internal ActionEvent[] m_ActionEvents; [SerializeField] internal bool m_NeverAutoSwitchControlSchemes; [SerializeField] internal string m_DefaultControlScheme;////REVIEW: should we have IDs for these so we can rename safely? [SerializeField] internal string m_DefaultActionMap; [SerializeField] internal int m_SplitScreenIndex = -1; [Tooltip("Reference to the player's view camera. Note that this is only required when using split-screen and/or " + "per-player UIs. Otherwise it is safe to leave this property uninitialized.")] [SerializeField] internal Camera m_Camera; // Value object we use when sending messages via SendMessage() or BroadcastMessage(). Can be ignored // by the receiver. We reuse the same object over and over to avoid allocating garbage. [NonSerialized] private InputValue m_InputValueObject; [NonSerialized] internal InputActionMap m_CurrentActionMap; [NonSerialized] private int m_PlayerIndex = -1; [NonSerialized] private bool m_InputActive; [NonSerialized] private bool m_Enabled; [NonSerialized] internal bool m_ActionsInitialized; [NonSerialized] private Dictionary m_ActionMessageNames; [NonSerialized] private InputUser m_InputUser; [NonSerialized] private Action m_ActionTriggeredDelegate; [NonSerialized] private CallbackArray> m_DeviceLostCallbacks; [NonSerialized] private CallbackArray> m_DeviceRegainedCallbacks; [NonSerialized] private CallbackArray> m_ControlsChangedCallbacks; [NonSerialized] private CallbackArray> m_ActionTriggeredCallbacks; [NonSerialized] private Action m_UnpairedDeviceUsedDelegate; [NonSerialized] private Func m_PreFilterUnpairedDeviceUsedDelegate; [NonSerialized] private bool m_OnUnpairedDeviceUsedHooked; [NonSerialized] private Action m_DeviceChangeDelegate; [NonSerialized] private bool m_OnDeviceChangeHooked; internal static int s_AllActivePlayersCount; internal static PlayerInput[] s_AllActivePlayers; private static Action s_UserChangeDelegate; // The following information is used when the next PlayerInput component is enabled. private static int s_InitPairWithDevicesCount; private static InputDevice[] s_InitPairWithDevices; private static int s_InitPlayerIndex = -1; private static int s_InitSplitScreenIndex = -1; private static string s_InitControlScheme; internal static bool s_DestroyIfDeviceSetupUnsuccessful; private void InitializeActions() { if (m_ActionsInitialized) return; if (m_Actions == null) return; // Check if we need to duplicate our actions by looking at all other players. If any // has the same actions, duplicate. for (var i = 0; i < s_AllActivePlayersCount; ++i) if (s_AllActivePlayers[i].m_Actions == m_Actions && s_AllActivePlayers[i] != this) { var oldActions = m_Actions; m_Actions = Instantiate(m_Actions); for (var actionMap = 0; actionMap < oldActions.actionMaps.Count; actionMap++) { for (var binding = 0; binding < oldActions.actionMaps[actionMap].bindings.Count; binding++) m_Actions.actionMaps[actionMap].ApplyBindingOverride(binding, oldActions.actionMaps[actionMap].bindings[binding]); } break; } #if UNITY_INPUT_SYSTEM_ENABLE_UI if (uiInputModule != null) uiInputModule.actionsAsset = m_Actions; #endif switch (m_NotificationBehavior) { case PlayerNotifications.SendMessages: case PlayerNotifications.BroadcastMessages: InstallOnActionTriggeredHook(); if (m_ActionMessageNames == null) CacheMessageNames(); break; case PlayerNotifications.InvokeCSharpEvents: InstallOnActionTriggeredHook(); break; case PlayerNotifications.InvokeUnityEvents: { // Hook up all action events. if (m_ActionEvents != null) { foreach (var actionEvent in m_ActionEvents) { var id = actionEvent.actionId; if (string.IsNullOrEmpty(id)) continue; // Find action for event. var action = m_Actions.FindAction(id); if (action == null) continue; action.performed += actionEvent.Invoke; action.canceled += actionEvent.Invoke; action.started += actionEvent.Invoke; } } break; } } m_ActionsInitialized = true; } private void UninitializeActions() { if (!m_ActionsInitialized) return; if (m_Actions == null) return; UninstallOnActionTriggeredHook(); if (m_NotificationBehavior == PlayerNotifications.InvokeUnityEvents && m_ActionEvents != null) { foreach (var actionEvent in m_ActionEvents) { var id = actionEvent.actionId; if (string.IsNullOrEmpty(id)) continue; // Find action for event. var action = m_Actions.FindAction(id); if (action != null) { ////REVIEW: really wish we had a single callback action.performed -= actionEvent.Invoke; action.canceled -= actionEvent.Invoke; action.started -= actionEvent.Invoke; } } } m_CurrentActionMap = null; m_ActionsInitialized = false; } private void InstallOnActionTriggeredHook() { if (m_ActionTriggeredDelegate == null) m_ActionTriggeredDelegate = OnActionTriggered; foreach (var actionMap in m_Actions.actionMaps) actionMap.actionTriggered += m_ActionTriggeredDelegate; } private void UninstallOnActionTriggeredHook() { if (m_ActionTriggeredDelegate != null) foreach (var actionMap in m_Actions.actionMaps) actionMap.actionTriggered -= m_ActionTriggeredDelegate; } private void OnActionTriggered(InputAction.CallbackContext context) { if (!m_InputActive) return; // We shouldn't go through this method when using UnityEvents. With events, // the callbacks should be wired up directly rather than going all to this method. Debug.Assert(m_NotificationBehavior != PlayerNotifications.InvokeUnityEvents, "OnActionTriggered callback should not be installed if notification behavior is set to InvokeUnityEvents"); switch (m_NotificationBehavior) { case PlayerNotifications.InvokeCSharpEvents: DelegateHelpers.InvokeCallbacksSafe(ref m_ActionTriggeredCallbacks, context, "PlayerInput.onActionTriggered"); break; case PlayerNotifications.BroadcastMessages: case PlayerNotifications.SendMessages: // ATM we only care about `performed` and, in the case of value actions, `canceled`. var action = context.action; if (!(context.performed || (context.canceled && action.type == InputActionType.Value))) return; // Find message name for action. if (m_ActionMessageNames == null) CacheMessageNames(); var messageName = m_ActionMessageNames[action.m_Id]; // Cache value. if (m_InputValueObject == null) m_InputValueObject = new InputValue(); m_InputValueObject.m_Context = context; // Send message. if (m_NotificationBehavior == PlayerNotifications.BroadcastMessages) BroadcastMessage(messageName, m_InputValueObject, SendMessageOptions.DontRequireReceiver); else SendMessage(messageName, m_InputValueObject, SendMessageOptions.DontRequireReceiver); // Reset context so calling Get() will result in an exception. m_InputValueObject.m_Context = null; break; } } private void CacheMessageNames() { if (m_Actions == null) return; if (m_ActionMessageNames != null) m_ActionMessageNames.Clear(); else m_ActionMessageNames = new Dictionary(); foreach (var action in m_Actions) { action.MakeSureIdIsInPlace(); var name = CSharpCodeHelpers.MakeTypeName(action.name); m_ActionMessageNames[action.m_Id] = "On" + name; } } private void ClearCaches() { } /// /// Initialize and . /// private void AssignUserAndDevices() { // If we already have a user at this point, clear out all its paired devices // to start the pairing process from scratch. if (m_InputUser.valid) m_InputUser.UnpairDevices(); // All our input goes through actions so there's no point setting // anything up if we have none. if (m_Actions == null) { // If we have devices we are meant to pair with, do so. Otherwise, don't // do anything as we don't know what kind of input to look for. if (s_InitPairWithDevicesCount > 0) { for (var i = 0; i < s_InitPairWithDevicesCount; ++i) m_InputUser = InputUser.PerformPairingWithDevice(s_InitPairWithDevices[i], m_InputUser); } else { // Make sure user is invalid. m_InputUser = new InputUser(); } return; } // If we have control schemes, try to find the one we should use. if (m_Actions.controlSchemes.Count > 0) { if (!string.IsNullOrEmpty(s_InitControlScheme)) { // We've been given a control scheme to initialize this. Try that one and // that one only. Might mean we end up with missing devices. var controlScheme = m_Actions.FindControlScheme(s_InitControlScheme); if (controlScheme == null) { Debug.LogError($"No control scheme '{s_InitControlScheme}' in '{m_Actions}'", this); } else { TryToActivateControlScheme(controlScheme.Value); } } else if (!string.IsNullOrEmpty(m_DefaultControlScheme)) { // There's a control scheme we should try by default. var controlScheme = m_Actions.FindControlScheme(m_DefaultControlScheme); if (controlScheme == null) { Debug.LogError($"Cannot find default control scheme '{m_DefaultControlScheme}' in '{m_Actions}'", this); } else { TryToActivateControlScheme(controlScheme.Value); } } // If we did not end up with a usable scheme by now but we've been given devices to pair with, // search for a control scheme matching the given devices. if (s_InitPairWithDevicesCount > 0 && (!m_InputUser.valid || m_InputUser.controlScheme == null)) { // The devices we've been given may not be all the devices required to satisfy a given control scheme so we // want to pick any one control scheme that is the best match for the devices we have regardless of whether // we'll need additional devices. TryToActivateControlScheme will take care of that. var controlScheme = InputControlScheme.FindControlSchemeForDevices( new ReadOnlyArray(s_InitPairWithDevices, 0, s_InitPairWithDevicesCount), m_Actions.controlSchemes, allowUnsuccesfulMatch: true); if (controlScheme != null) TryToActivateControlScheme(controlScheme.Value); } // If we don't have a working control scheme by now and we haven't been instructed to use // one specific control scheme, try each one in the asset one after the other until we // either find one we can use or run out of options. else if ((!m_InputUser.valid || m_InputUser.controlScheme == null) && string.IsNullOrEmpty(s_InitControlScheme)) { using (var availableDevices = InputUser.GetUnpairedInputDevices()) { var controlScheme = InputControlScheme.FindControlSchemeForDevices(availableDevices, m_Actions.controlSchemes); if (controlScheme != null) TryToActivateControlScheme(controlScheme.Value); } } } else { // There's no control schemes in the asset. If we've been given a set of devices, // we run with those (regardless of whether there's bindings for them in the actions or not). // If we haven't been given any devices, we go through all bindings in the asset and whatever // device is present that matches the binding and that isn't used by any other player, we'll // pair to the player. if (s_InitPairWithDevicesCount > 0) { for (var i = 0; i < s_InitPairWithDevicesCount; ++i) m_InputUser = InputUser.PerformPairingWithDevice(s_InitPairWithDevices[i], m_InputUser); } else { // Pair all devices for which we have a binding. using (var availableDevices = InputUser.GetUnpairedInputDevices()) { for (var i = 0; i < availableDevices.Count; ++i) { var device = availableDevices[i]; if (!HaveBindingForDevice(device)) continue; m_InputUser = InputUser.PerformPairingWithDevice(device, m_InputUser); } } } } // If we don't have a valid user at this point, we don't have any paired devices. if (m_InputUser.valid) m_InputUser.AssociateActionsWithUser(m_Actions); } private bool HaveBindingForDevice(InputDevice device) { if (m_Actions == null) return false; var actionMaps = m_Actions.actionMaps; for (var i = 0; i < actionMaps.Count; ++i) { var actionMap = actionMaps[i]; if (actionMap.IsUsableWithDevice(device)) return true; } return false; } private void UnassignUserAndDevices() { if (m_InputUser.valid) m_InputUser.UnpairDevicesAndRemoveUser(); if (m_Actions != null) m_Actions.devices = null; } private bool TryToActivateControlScheme(InputControlScheme controlScheme) { ////FIXME: this will fall apart if account management is involved and a user needs to log in on device first // Pair any devices we may have been given. if (s_InitPairWithDevicesCount > 0) { ////REVIEW: should AndPairRemainingDevices() require that there is at least one existing //// device paired to the user that is usable with the given control scheme? // First make sure that all of the devices actually work with the given control scheme. // We're fine having to pair additional devices but we don't want the situation where // we have the player grab all the devices in s_InitPairWithDevices along with a control // scheme that fits none of them and then AndPairRemainingDevices() supplying the devices // actually needed by the control scheme. for (var i = 0; i < s_InitPairWithDevicesCount; ++i) { var device = s_InitPairWithDevices[i]; if (!controlScheme.SupportsDevice(device)) return false; } // We're good. Give the devices to the user. for (var i = 0; i < s_InitPairWithDevicesCount; ++i) { var device = s_InitPairWithDevices[i]; m_InputUser = InputUser.PerformPairingWithDevice(device, m_InputUser); } } if (!m_InputUser.valid) m_InputUser = InputUser.CreateUserWithoutPairedDevices(); m_InputUser.ActivateControlScheme(controlScheme).AndPairRemainingDevices(); if (user.hasMissingRequiredDevices) { m_InputUser.ActivateControlScheme(null); m_InputUser.UnpairDevices(); return false; } return true; } private void AssignPlayerIndex() { if (s_InitPlayerIndex != -1) m_PlayerIndex = s_InitPlayerIndex; else { var minPlayerIndex = int.MaxValue; var maxPlayerIndex = int.MinValue; for (var i = 0; i < s_AllActivePlayersCount; ++i) { var playerIndex = s_AllActivePlayers[i].playerIndex; minPlayerIndex = Math.Min(minPlayerIndex, playerIndex); maxPlayerIndex = Math.Max(maxPlayerIndex, playerIndex); } if (minPlayerIndex != int.MaxValue && minPlayerIndex > 0) { // There's an index between 0 and the current minimum available. m_PlayerIndex = minPlayerIndex - 1; } else if (maxPlayerIndex != int.MinValue) { // There may be an index between the minimum and maximum available. // Search the range. If there's nothing, create a new maximum. for (var i = minPlayerIndex; i < maxPlayerIndex; ++i) { if (GetPlayerByIndex(i) == null) { m_PlayerIndex = i; return; } } m_PlayerIndex = maxPlayerIndex + 1; } else m_PlayerIndex = 0; } } private void OnEnable() { m_Enabled = true; using (InputActionRebindingExtensions.DeferBindingResolution()) { AssignPlayerIndex(); InitializeActions(); AssignUserAndDevices(); ActivateInput(); } // Split-screen index defaults to player index. if (s_InitSplitScreenIndex >= 0) m_SplitScreenIndex = splitScreenIndex; else m_SplitScreenIndex = playerIndex; // Add to global list and sort it by player index. ArrayHelpers.AppendWithCapacity(ref s_AllActivePlayers, ref s_AllActivePlayersCount, this); for (var i = 1; i < s_AllActivePlayersCount; ++i) for (var j = i; j > 0 && s_AllActivePlayers[j - 1].playerIndex > s_AllActivePlayers[j].playerIndex; --j) s_AllActivePlayers.SwapElements(j, j - 1); // If it's the first player, hook into user change notifications. if (s_AllActivePlayersCount == 1) { if (s_UserChangeDelegate == null) s_UserChangeDelegate = OnUserChange; InputUser.onChange += s_UserChangeDelegate; } // In single player, set up for automatic device switching. if (isSinglePlayer) { if (m_Actions != null && m_Actions.controlSchemes.Count == 0) { // No control schemes. We pick up whatever is compatible with the bindings // we have. StartListeningForDeviceChanges(); } else if (!neverAutoSwitchControlSchemes) { // We have control schemes so we only listen for unpaired device *input*, i.e. // actual use of an unpaired device (as opposed to it merely getting plugged in). StartListeningForUnpairedDeviceActivity(); } } HandleControlsChanged(); // Trigger join event. PlayerInputManager.instance?.NotifyPlayerJoined(this); } private void StartListeningForUnpairedDeviceActivity() { if (m_OnUnpairedDeviceUsedHooked) return; if (m_UnpairedDeviceUsedDelegate == null) m_UnpairedDeviceUsedDelegate = OnUnpairedDeviceUsed; if (m_PreFilterUnpairedDeviceUsedDelegate == null) m_PreFilterUnpairedDeviceUsedDelegate = OnPreFilterUnpairedDeviceUsed; InputUser.onUnpairedDeviceUsed += m_UnpairedDeviceUsedDelegate; InputUser.onPrefilterUnpairedDeviceActivity += m_PreFilterUnpairedDeviceUsedDelegate; ++InputUser.listenForUnpairedDeviceActivity; m_OnUnpairedDeviceUsedHooked = true; } private void StopListeningForUnpairedDeviceActivity() { if (!m_OnUnpairedDeviceUsedHooked) return; InputUser.onUnpairedDeviceUsed -= m_UnpairedDeviceUsedDelegate; InputUser.onPrefilterUnpairedDeviceActivity -= m_PreFilterUnpairedDeviceUsedDelegate; --InputUser.listenForUnpairedDeviceActivity; m_OnUnpairedDeviceUsedHooked = false; } private void StartListeningForDeviceChanges() { if (m_OnDeviceChangeHooked) return; if (m_DeviceChangeDelegate == null) m_DeviceChangeDelegate = OnDeviceChange; InputSystem.onDeviceChange += m_DeviceChangeDelegate; m_OnDeviceChangeHooked = true; } private void StopListeningForDeviceChanges() { if (!m_OnDeviceChangeHooked) return; InputSystem.onDeviceChange -= m_DeviceChangeDelegate; m_OnDeviceChangeHooked = false; } private void OnDisable() { m_Enabled = false; // Remove from global list. var index = ArrayHelpers.IndexOfReference(s_AllActivePlayers, this, s_AllActivePlayersCount); if (index != -1) ArrayHelpers.EraseAtWithCapacity(s_AllActivePlayers, ref s_AllActivePlayersCount, index); // Unhook from change notifications if we're the last player. if (s_AllActivePlayersCount == 0 && s_UserChangeDelegate != null) InputUser.onChange -= s_UserChangeDelegate; StopListeningForUnpairedDeviceActivity(); StopListeningForDeviceChanges(); // Trigger leave event. PlayerInputManager.instance?.NotifyPlayerLeft(this); ////TODO: ideally, this shouldn't have to resolve at all and instead wait for someone to need the updated setup // Avoid re-resolving bindings over and over while we disassemble // the configuration. using (InputActionRebindingExtensions.DeferBindingResolution()) { DeactivateInput(); UnassignUserAndDevices(); UninitializeActions(); } m_PlayerIndex = -1; } // ReSharper disable once UnusedMember.Global /// /// Debug helper method that can be hooked up to actions when using . /// public void DebugLogAction(InputAction.CallbackContext context) { Debug.Log(context.ToString()); } private void HandleDeviceLost() { switch (m_NotificationBehavior) { case PlayerNotifications.SendMessages: SendMessage(DeviceLostMessage, this, SendMessageOptions.DontRequireReceiver); break; case PlayerNotifications.BroadcastMessages: BroadcastMessage(DeviceLostMessage, this, SendMessageOptions.DontRequireReceiver); break; case PlayerNotifications.InvokeUnityEvents: m_DeviceLostEvent?.Invoke(this); break; case PlayerNotifications.InvokeCSharpEvents: DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceLostCallbacks, this, "onDeviceLost"); break; } } private void HandleDeviceRegained() { switch (m_NotificationBehavior) { case PlayerNotifications.SendMessages: SendMessage(DeviceRegainedMessage, this, SendMessageOptions.DontRequireReceiver); break; case PlayerNotifications.BroadcastMessages: BroadcastMessage(DeviceRegainedMessage, this, SendMessageOptions.DontRequireReceiver); break; case PlayerNotifications.InvokeUnityEvents: m_DeviceRegainedEvent?.Invoke(this); break; case PlayerNotifications.InvokeCSharpEvents: DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceRegainedCallbacks, this, "onDeviceRegained"); break; } } private void HandleControlsChanged() { switch (m_NotificationBehavior) { case PlayerNotifications.SendMessages: SendMessage(ControlsChangedMessage, this, SendMessageOptions.DontRequireReceiver); break; case PlayerNotifications.BroadcastMessages: BroadcastMessage(ControlsChangedMessage, this, SendMessageOptions.DontRequireReceiver); break; case PlayerNotifications.InvokeUnityEvents: m_ControlsChangedEvent?.Invoke(this); break; case PlayerNotifications.InvokeCSharpEvents: DelegateHelpers.InvokeCallbacksSafe(ref m_ControlsChangedCallbacks, this, "onControlsChanged"); break; } } private static void OnUserChange(InputUser user, InputUserChange change, InputDevice device) { switch (change) { case InputUserChange.DeviceLost: case InputUserChange.DeviceRegained: for (var i = 0; i < s_AllActivePlayersCount; ++i) { var player = s_AllActivePlayers[i]; if (player.m_InputUser == user) { if (change == InputUserChange.DeviceLost) player.HandleDeviceLost(); else if (change == InputUserChange.DeviceRegained) player.HandleDeviceRegained(); } } break; case InputUserChange.ControlsChanged: for (var i = 0; i < s_AllActivePlayersCount; ++i) { var player = s_AllActivePlayers[i]; if (player.m_InputUser == user) player.HandleControlsChanged(); } break; } } private static bool OnPreFilterUnpairedDeviceUsed(InputDevice device, InputEventPtr eventPtr) { // Early out if the device isn't usable with any of our control schemes. var actions = all[0].actions; return actions != null && actions.IsUsableWithDevice(device); } private void OnUnpairedDeviceUsed(InputControl control, InputEventPtr eventPtr) { // We only support automatic control scheme switching in single player mode. // OnEnable() should automatically unhook us. if (!isSinglePlayer || neverAutoSwitchControlSchemes) return; var player = all[0]; var actions = player.m_Actions; if (actions == null) return; var device = control.device; using (InputActionRebindingExtensions.DeferBindingResolution()) using (var availableDevices = InputUser.GetUnpairedInputDevices()) { // Put our device first in the list to make sure it's the first one picked for a match. if (availableDevices.Count > 1) { var indexOfDevice = availableDevices.IndexOf(device); Debug.Assert(indexOfDevice != -1, "Did not find unpaired device in list of unpaired devices"); availableDevices.SwapElements(0, indexOfDevice); } // Add all devices currently already paired to us. This avoids us preventing // control schemes switches because of devices we're looking for already being // paired to us. var currentDevices = player.devices; for (var i = 0; i < currentDevices.Count; ++i) availableDevices.Add(currentDevices[i]); // Find the best control scheme to use. if (InputControlScheme.FindControlSchemeForDevices(availableDevices, player.m_Actions.controlSchemes, out var controlScheme, out var matchResult, mustIncludeDevice: device)) { try { // First remove the currently paired devices. var userValid = player.user.valid; if (userValid) player.user.UnpairDevices(); // Then pair devices that we've picked according to the control scheme. var newDevices = matchResult.devices; Debug.Assert(newDevices.Count > 0, "Expecting to see at least one device here"); for (var i = 0; i < newDevices.Count; ++i) { player.m_InputUser = InputUser.PerformPairingWithDevice(newDevices[i], user: player.m_InputUser); if (!userValid && player.actions != null) player.m_InputUser.AssociateActionsWithUser(player.actions); } // And finally switch to the new control scheme. player.user.ActivateControlScheme(controlScheme); } finally { matchResult.Dispose(); } } } } private void OnDeviceChange(InputDevice device, InputDeviceChange change) { // If a device was added and we have no control schemes in the actions and we're in // single-player mode, pair the device to the player if it works with the bindings we have. if (change == InputDeviceChange.Added && isSinglePlayer && m_Actions != null && m_Actions.controlSchemes.Count == 0 && HaveBindingForDevice(device) && m_InputUser.valid) { InputUser.PerformPairingWithDevice(device, user: m_InputUser); } } private void SwitchControlSchemeInternal(ref InputControlScheme controlScheme, params InputDevice[] devices) { Debug.Assert(devices != null); // Note that we are doing two somwhat uncorrelated actions here: // - Switching control scheme // - Explicitly pairing with given devices regardless if making sense with respect to control scheme using (InputActionRebindingExtensions.DeferBindingResolution()) { // Unpair device previously paired but not part of given devices to pair with for (var i = user.pairedDevices.Count - 1; i >= 0; --i) { if (!devices.ContainsReference(user.pairedDevices[i])) user.UnpairDevice(user.pairedDevices[i]); } // Pair devices not previously paired but that are part of given devices to pair with foreach (var device in devices) { if (!user.pairedDevices.ContainsReference(device)) InputUser.PerformPairingWithDevice(device, user: user); } // Only activate control scheme if its a different scheme if (!user.controlScheme.HasValue || !user.controlScheme.Value.Equals(controlScheme)) user.ActivateControlScheme(controlScheme); } } [Serializable] public class ActionEvent : UnityEvent { public string actionId => m_ActionId; public string actionName => m_ActionName; [SerializeField] private string m_ActionId; [SerializeField] private string m_ActionName; public ActionEvent() { } public ActionEvent(InputAction action) { if (action == null) throw new ArgumentNullException(nameof(action)); if (action.isSingletonAction) throw new ArgumentException($"Action must be part of an asset (given action '{action}' is a singleton)"); if (action.actionMap.asset == null) throw new ArgumentException($"Action must be part of an asset (given action '{action}' is not)"); m_ActionId = action.id.ToString(); m_ActionName = $"{action.actionMap.name}/{action.name}"; } public ActionEvent(Guid actionGUID, string name = null) { m_ActionId = actionGUID.ToString(); m_ActionName = name; } } /// /// Event that is triggered when an paired to a is disconnected. /// /// [Serializable] public class DeviceLostEvent : UnityEvent { } /// /// Event that is triggered when a regains an previously lost. /// /// [Serializable] public class DeviceRegainedEvent : UnityEvent { } /// /// Event that is triggered when the set of controls used by a changes. /// /// [Serializable] public class ControlsChangedEvent : UnityEvent { } } }