2023-03-28 13:24:16 -04:00
using System ;
using UnityEngine.Events ;
using UnityEngine.InputSystem.Controls ;
using UnityEngine.InputSystem.LowLevel ;
using UnityEngine.InputSystem.Users ;
using UnityEngine.InputSystem.Utilities ;
#if UNITY_EDITOR
using UnityEditor ;
#endif
////REVIEW: should we automatically pool/retain up to maxPlayerCount player instances?
////REVIEW: the join/leave messages should probably give a *GameObject* rather than the PlayerInput component (which can be gotten to via a simple GetComponent(InChildren) call)
////TODO: add support for reacting to players missing devices
namespace UnityEngine.InputSystem
{
/// <summary>
/// Manages joining and leaving of players.
/// </summary>
/// <remarks>
/// This is a singleton component. Only one instance is meant to be active in a game
/// at any one time. To retrieve the current instance, use <see cref="instance"/>.
///
/// Note that a PlayerInputManager is not strictly required to have multiple <see cref="PlayerInput"/> components.
/// What PlayerInputManager provides is the implementation of specific player join mechanisms
/// (<see cref="joinBehavior"/>) as well as automatic assignment of split-screen areas (<see cref="splitScreen"/>).
/// However, you can always implement your own custom logic instead and simply instantiate multiple GameObjects with
/// <see cref="PlayerInput"/> yourself.
/// </remarks>
[AddComponentMenu("Input/Player Input Manager")]
2023-05-07 18:43:11 -04:00
[HelpURL(InputSystem.kDocUrl + "/manual/PlayerInputManager.html")]
2023-03-28 13:24:16 -04:00
public class PlayerInputManager : MonoBehaviour
{
/// <summary>
/// Name of the message that is sent when a player joins the game.
/// </summary>
public const string PlayerJoinedMessage = "OnPlayerJoined" ;
public const string PlayerLeftMessage = "OnPlayerLeft" ;
/// <summary>
/// If enabled, each player will automatically be assigned a portion of the available screen area.
/// </summary>
/// <remarks>
/// For this to work, each <see cref="PlayerInput"/> component must have an associated <see cref="Camera"/>
/// object through <see cref="PlayerInput.camera"/>.
///
/// Note that as player join, the screen may be increasingly subdivided and players may see their
/// previous screen area getting resized.
/// </remarks>
public bool splitScreen
{
get = > m_SplitScreen ;
set
{
if ( m_SplitScreen = = value )
return ;
m_SplitScreen = value ;
if ( ! m_SplitScreen )
{
// Reset rects on all player cameras.
foreach ( var player in PlayerInput . all )
{
var camera = player . camera ;
if ( camera ! = null )
camera . rect = new Rect ( 0 , 0 , 1 , 1 ) ;
}
}
else
{
UpdateSplitScreen ( ) ;
}
}
}
////REVIEW: we probably need support for filling unused screen areas automatically
/// <summary>
/// If <see cref="splitScreen"/> is enabled, this property determines whether subdividing the screen is allowed to
/// produce screen areas that have an aspect ratio different from the screen resolution.
/// </summary>
/// <remarks>
/// By default, when <see cref="splitScreen"/> is enabled, the manager will add or remove screen subdivisions in
/// steps of two. This means that when, for example, the second player is added, the screen will be subdivided into
/// a left and a right screen area; the left one allocated to the first player and the right one allocated to the
/// second player.
///
/// This behavior makes optimal use of screen real estate but will result in screen areas that have aspect ratios
/// different from the screen resolution. If this is not acceptable, this property can be set to true to enforce
/// split-screen to only create screen areas that have the same aspect ratio of the screen.
///
/// This results in the screen being subdivided more aggressively. When, for example, a second player is added,
/// the screen will immediately be divided into a four-way split-screen setup with the lower two screen areas
/// not being used.
///
/// This property is irrelevant if <see cref="fixedNumberOfSplitScreens"/> is used.
/// </remarks>
public bool maintainAspectRatioInSplitScreen = > m_MaintainAspectRatioInSplitScreen ;
/// <summary>
/// If <see cref="splitScreen"/> is enabled, this property determines how many screen divisions there will be.
/// </summary>
/// <remarks>
/// This is only used if <see cref="splitScreen"/> is true.
///
/// By default this is set to -1 which means the screen will automatically be divided to best fit the
/// current number of players i.e. the highest player index in <see cref="PlayerInput"/>
/// </remarks>
public int fixedNumberOfSplitScreens = > m_FixedNumberOfSplitScreens ;
/// <summary>
/// The normalized screen rectangle available for allocating player split-screens into.
/// </summary>
/// <remarks>
/// This is only used if <see cref="splitScreen"/> is true.
///
/// By default it is set to <c>(0,0,1,1)</c>, i.e. the entire screen area will be used for player screens.
/// If, for example, part of the screen should display a UI/information shared by all players, this
/// property can be used to cut off the area and not have it used by PlayerInputManager.
/// </remarks>
public Rect splitScreenArea = > m_SplitScreenRect ;
/// <summary>
/// The current number of active players.
/// </summary>
/// <remarks>
/// This count corresponds to all <see cref="PlayerInput"/> instances that are currently enabled.
/// </remarks>
public int playerCount = > PlayerInput . s_AllActivePlayersCount ;
////FIXME: this needs to be settable
/// <summary>
/// Maximum number of players allowed concurrently in the game.
/// </summary>
/// <remarks>
/// If this limit is reached, joining is turned off automatically.
///
/// By default this is set to -1. Any negative value deactivates the player limit and allows
/// arbitrary many players to join.
/// </remarks>
public int maxPlayerCount = > m_MaxPlayerCount ;
/// <summary>
/// Whether new players can currently join.
/// </summary>
/// <remarks>
/// While this is true, new players can join via the mechanism determined by <see cref="joinBehavior"/>.
/// </remarks>
/// <seealso cref="EnableJoining"/>
/// <seealso cref="DisableJoining"/>
public bool joiningEnabled = > m_AllowJoining ;
/// <summary>
/// Determines the mechanism by which players can join when joining is enabled (<see cref="joiningEnabled"/>).
/// </summary>
/// <remarks>
/// </remarks>
public PlayerJoinBehavior joinBehavior
{
get = > m_JoinBehavior ;
set
{
if ( m_JoinBehavior = = value )
return ;
var joiningEnabled = m_AllowJoining ;
if ( joiningEnabled )
DisableJoining ( ) ;
m_JoinBehavior = value ;
if ( joiningEnabled )
EnableJoining ( ) ;
}
}
/// <summary>
/// The input action that a player must trigger to join the game.
/// </summary>
/// <remarks>
/// If the join action is a reference to an existing input action, it will be cloned when the PlayerInputManager
/// is enabled. This avoids the situation where the join action can become disabled after the first user joins which
/// can happen when the join action is the same as a player in-game action. When a player joins, input bindings from
/// devices other than the device they joined with are disabled. If the join action had a binding for keyboard and one
/// for gamepad for example, and the first player joined using the keyboard, the expectation is that the next player
/// could still join by pressing the gamepad join button. Without the cloning behavior, the gamepad input would have
/// been disabled.
///
/// For more details about joining behavior, see <see cref="PlayerInput"/>.
/// </remarks>
public InputActionProperty joinAction
{
get = > m_JoinAction ;
set
{
if ( m_JoinAction = = value )
return ;
////REVIEW: should we suppress notifications for temporary disables?
var joinEnabled = m_AllowJoining & & m_JoinBehavior = = PlayerJoinBehavior . JoinPlayersWhenJoinActionIsTriggered ;
if ( joinEnabled )
DisableJoining ( ) ;
m_JoinAction = value ;
if ( joinEnabled )
EnableJoining ( ) ;
}
}
public PlayerNotifications notificationBehavior
{
get = > m_NotificationBehavior ;
set = > m_NotificationBehavior = value ;
}
public PlayerJoinedEvent playerJoinedEvent
{
get
{
if ( m_PlayerJoinedEvent = = null )
m_PlayerJoinedEvent = new PlayerJoinedEvent ( ) ;
return m_PlayerJoinedEvent ;
}
}
public PlayerLeftEvent playerLeftEvent
{
get
{
if ( m_PlayerLeftEvent = = null )
m_PlayerLeftEvent = new PlayerLeftEvent ( ) ;
return m_PlayerLeftEvent ;
}
}
public event Action < PlayerInput > onPlayerJoined
{
add
{
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
m_PlayerJoinedCallbacks . AddCallback ( value ) ;
}
remove
{
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
m_PlayerJoinedCallbacks . RemoveCallback ( value ) ;
}
}
public event Action < PlayerInput > onPlayerLeft
{
add
{
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
m_PlayerLeftCallbacks . AddCallback ( value ) ;
}
remove
{
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
m_PlayerLeftCallbacks . RemoveCallback ( value ) ;
}
}
/// <summary>
/// Reference to the prefab that the manager will instantiate when players join.
/// </summary>
/// <value>Prefab to instantiate for new players.</value>
public GameObject playerPrefab
{
get = > m_PlayerPrefab ;
set = > m_PlayerPrefab = value ;
}
/// <summary>
/// Singleton instance of the manager.
/// </summary>
/// <value>Singleton instance or null.</value>
public static PlayerInputManager instance { get ; private set ; }
/// <summary>
/// Allow players to join the game based on <see cref="joinBehavior"/>.
/// </summary>
/// <seealso cref="DisableJoining"/>
/// <seealso cref="joiningEnabled"/>
public void EnableJoining ( )
{
switch ( m_JoinBehavior )
{
case PlayerJoinBehavior . JoinPlayersWhenButtonIsPressed :
ValidateInputActionAsset ( ) ;
if ( ! m_UnpairedDeviceUsedDelegateHooked )
{
if ( m_UnpairedDeviceUsedDelegate = = null )
m_UnpairedDeviceUsedDelegate = OnUnpairedDeviceUsed ;
InputUser . onUnpairedDeviceUsed + = m_UnpairedDeviceUsedDelegate ;
m_UnpairedDeviceUsedDelegateHooked = true ;
+ + InputUser . listenForUnpairedDeviceActivity ;
}
break ;
case PlayerJoinBehavior . JoinPlayersWhenJoinActionIsTriggered :
// Hook into join action if we have one.
if ( m_JoinAction . action ! = null )
{
if ( ! m_JoinActionDelegateHooked )
{
if ( m_JoinActionDelegate = = null )
m_JoinActionDelegate = JoinPlayerFromActionIfNotAlreadyJoined ;
m_JoinAction . action . performed + = m_JoinActionDelegate ;
m_JoinActionDelegateHooked = true ;
}
m_JoinAction . action . Enable ( ) ;
}
else
{
Debug . LogError (
$"No join action configured on PlayerInputManager but join behavior is set to {nameof(PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)}" ,
this ) ;
}
break ;
}
m_AllowJoining = true ;
}
/// <summary>
/// Inhibit players from joining the game.
/// </summary>
/// <seealso cref="EnableJoining"/>
/// <seealso cref="joiningEnabled"/>
public void DisableJoining ( )
{
switch ( m_JoinBehavior )
{
case PlayerJoinBehavior . JoinPlayersWhenButtonIsPressed :
if ( m_UnpairedDeviceUsedDelegateHooked )
{
InputUser . onUnpairedDeviceUsed - = m_UnpairedDeviceUsedDelegate ;
m_UnpairedDeviceUsedDelegateHooked = false ;
- - InputUser . listenForUnpairedDeviceActivity ;
}
break ;
case PlayerJoinBehavior . JoinPlayersWhenJoinActionIsTriggered :
if ( m_JoinActionDelegateHooked )
{
var joinAction = m_JoinAction . action ;
if ( joinAction ! = null )
m_JoinAction . action . performed - = m_JoinActionDelegate ;
m_JoinActionDelegateHooked = false ;
}
m_JoinAction . action ? . Disable ( ) ;
break ;
}
m_AllowJoining = false ;
}
////TODO
/// <summary>
/// Join a new player based on input on a UI element.
/// </summary>
/// <remarks>
/// This should be called directly from a UI callback such as <see cref="Button.onClick"/>. The device
/// that the player joins with is taken from the device that was used to interact with the UI element.
/// </remarks>
internal void JoinPlayerFromUI ( )
{
if ( ! CheckIfPlayerCanJoin ( ) )
return ;
//find used device; InputSystemUIInputModule should probably make that available
throw new NotImplementedException ( ) ;
}
/// <summary>
/// Join a new player based on input received through an <see cref="InputAction"/>.
/// </summary>
/// <param name="context"></param>
/// <remarks>
/// </remarks>
public void JoinPlayerFromAction ( InputAction . CallbackContext context )
{
if ( ! CheckIfPlayerCanJoin ( ) )
return ;
var device = context . control . device ;
JoinPlayer ( pairWithDevice : device ) ;
}
public void JoinPlayerFromActionIfNotAlreadyJoined ( InputAction . CallbackContext context )
{
if ( ! CheckIfPlayerCanJoin ( ) )
return ;
var device = context . control . device ;
if ( PlayerInput . FindFirstPairedToDevice ( device ) ! = null )
return ;
JoinPlayer ( pairWithDevice : device ) ;
}
/// <summary>
/// Spawn a new player from <see cref="playerPrefab"/>.
/// </summary>
/// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
/// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
/// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
/// a split-screen index of </param>
/// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
/// be selected based on <paramref name="pairWithDevice"/>. If no device is given either, the first control scheme that matches
/// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
/// <param name="pairWithDevice">Device to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
/// is not given.</param>
/// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
/// <remarks>
/// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
///
/// To pair multiple devices, use <see cref="JoinPlayer(int,int,string,InputDevice[])"/>.
/// </remarks>
public PlayerInput JoinPlayer ( int playerIndex = - 1 , int splitScreenIndex = - 1 , string controlScheme = null , InputDevice pairWithDevice = null )
{
if ( ! CheckIfPlayerCanJoin ( playerIndex ) )
return null ;
PlayerInput . s_DestroyIfDeviceSetupUnsuccessful = true ;
return PlayerInput . Instantiate ( m_PlayerPrefab , playerIndex : playerIndex , splitScreenIndex : splitScreenIndex ,
controlScheme : controlScheme , pairWithDevice : pairWithDevice ) ;
}
/// <summary>
/// Spawn a new player from <see cref="playerPrefab"/>.
/// </summary>
/// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
/// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
/// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
/// a split-screen index of </param>
/// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
/// be selected based on <paramref name="pairWithDevices"/>. If no device is given either, the first control scheme that matches
/// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
/// <param name="pairWithDevices">Devices to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
/// is not given.</param>
/// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
/// <remarks>
/// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
/// </remarks>
public PlayerInput JoinPlayer ( int playerIndex = - 1 , int splitScreenIndex = - 1 , string controlScheme = null , params InputDevice [ ] pairWithDevices )
{
if ( ! CheckIfPlayerCanJoin ( playerIndex ) )
return null ;
PlayerInput . s_DestroyIfDeviceSetupUnsuccessful = true ;
return PlayerInput . Instantiate ( m_PlayerPrefab , playerIndex : playerIndex , splitScreenIndex : splitScreenIndex ,
controlScheme : controlScheme , pairWithDevices : pairWithDevices ) ;
}
[SerializeField] internal PlayerNotifications m_NotificationBehavior ;
[Tooltip("Set a limit for the maximum number of players who are able to join.")]
[SerializeField] internal int m_MaxPlayerCount = - 1 ;
[SerializeField] internal bool m_AllowJoining = true ;
[SerializeField] internal PlayerJoinBehavior m_JoinBehavior ;
[SerializeField] internal PlayerJoinedEvent m_PlayerJoinedEvent ;
[SerializeField] internal PlayerLeftEvent m_PlayerLeftEvent ;
[SerializeField] internal InputActionProperty m_JoinAction ;
[SerializeField] internal GameObject m_PlayerPrefab ;
[SerializeField] internal bool m_SplitScreen ;
[SerializeField] internal bool m_MaintainAspectRatioInSplitScreen ;
[Tooltip("Explicitly set a fixed number of screens or otherwise allow the screen to be divided automatically to best fit the number of players.")]
[SerializeField] internal int m_FixedNumberOfSplitScreens = - 1 ;
[SerializeField] internal Rect m_SplitScreenRect = new Rect ( 0 , 0 , 1 , 1 ) ;
[NonSerialized] private bool m_JoinActionDelegateHooked ;
[NonSerialized] private bool m_UnpairedDeviceUsedDelegateHooked ;
[NonSerialized] private Action < InputAction . CallbackContext > m_JoinActionDelegate ;
[NonSerialized] private Action < InputControl , InputEventPtr > m_UnpairedDeviceUsedDelegate ;
[NonSerialized] private CallbackArray < Action < PlayerInput > > m_PlayerJoinedCallbacks ;
[NonSerialized] private CallbackArray < Action < PlayerInput > > m_PlayerLeftCallbacks ;
internal static string [ ] messages = > new [ ]
{
PlayerJoinedMessage ,
PlayerLeftMessage ,
} ;
private bool CheckIfPlayerCanJoin ( int playerIndex = - 1 )
{
if ( m_PlayerPrefab = = null )
{
Debug . LogError ( "playerPrefab must be set in order to be able to join new players" , this ) ;
return false ;
}
if ( m_MaxPlayerCount > = 0 & & playerCount > = m_MaxPlayerCount )
{
Debug . LogError ( "Have reached maximum player count of " + maxPlayerCount , this ) ;
return false ;
}
// If we have a player index, make sure it's unique.
if ( playerIndex ! = - 1 )
{
for ( var i = 0 ; i < PlayerInput . s_AllActivePlayersCount ; + + i )
if ( PlayerInput . s_AllActivePlayers [ i ] . playerIndex = = playerIndex )
{
Debug . LogError (
$"Player index #{playerIndex} is already taken by player {PlayerInput.s_AllActivePlayers[i]}" ,
PlayerInput . s_AllActivePlayers [ i ] ) ;
return false ;
}
}
return true ;
}
private void OnUnpairedDeviceUsed ( InputControl control , InputEventPtr eventPtr )
{
if ( ! m_AllowJoining )
return ;
if ( m_JoinBehavior = = PlayerJoinBehavior . JoinPlayersWhenButtonIsPressed )
{
// Make sure it's a button that was actuated.
if ( ! ( control is ButtonControl ) )
return ;
// Make sure it's a device that is usable by the player's actions. We don't want
// to join a player who's then stranded and has no way to actually interact with the game.
if ( ! IsDeviceUsableWithPlayerActions ( control . device ) )
return ;
////REVIEW: should we log a warning or error when the actions for the player do not have control schemes?
JoinPlayer ( pairWithDevice : control . device ) ;
}
}
private void OnEnable ( )
{
if ( instance = = null )
{
instance = this ;
}
else
{
Debug . LogWarning ( "Multiple PlayerInputManagers in the game. There should only be one PlayerInputManager" , this ) ;
return ;
}
// if the join action is a reference, clone it so we don't run into problems with the action being disabled by
// PlayerInput when devices are assigned to individual players
if ( joinAction . reference ! = null & & joinAction . action ? . actionMap ? . asset ! = null )
{
var inputActionAsset = Instantiate ( joinAction . action . actionMap . asset ) ;
var inputActionReference = InputActionReference . Create ( inputActionAsset . FindAction ( joinAction . action . name ) ) ;
joinAction = new InputActionProperty ( inputActionReference ) ;
}
// Join all players already in the game.
for ( var i = 0 ; i < PlayerInput . s_AllActivePlayersCount ; + + i )
NotifyPlayerJoined ( PlayerInput . s_AllActivePlayers [ i ] ) ;
if ( m_AllowJoining )
EnableJoining ( ) ;
}
private void OnDisable ( )
{
if ( instance = = this )
instance = null ;
if ( m_AllowJoining )
DisableJoining ( ) ;
}
/// <summary>
/// If split-screen is enabled, then for each player in the game, adjust the player's <see cref="Camera.rect"/>
/// to fit the player's split screen area according to the number of players currently in the game and the
/// current split-screen configuration.
/// </summary>
private void UpdateSplitScreen ( )
{
// Nothing to do if split-screen is not enabled.
if ( ! m_SplitScreen )
return ;
// Determine number of split-screens to create based on highest player index we have.
var minSplitScreenCount = 0 ;
foreach ( var player in PlayerInput . all )
{
if ( player . playerIndex > = minSplitScreenCount )
minSplitScreenCount = player . playerIndex + 1 ;
}
// Adjust to fixed number if we have it.
if ( m_FixedNumberOfSplitScreens > 0 )
{
if ( m_FixedNumberOfSplitScreens < minSplitScreenCount )
Debug . LogWarning (
$"Highest playerIndex of {minSplitScreenCount} exceeds fixed number of split-screens of {m_FixedNumberOfSplitScreens}" ,
this ) ;
minSplitScreenCount = m_FixedNumberOfSplitScreens ;
}
// Determine divisions along X and Y. Usually, we have a square grid of split-screens so all we need to
// do is make it large enough to fit all players.
var numDivisionsX = Mathf . CeilToInt ( Mathf . Sqrt ( minSplitScreenCount ) ) ;
var numDivisionsY = numDivisionsX ;
if ( ! m_MaintainAspectRatioInSplitScreen & & numDivisionsX * ( numDivisionsX - 1 ) > = minSplitScreenCount )
{
// We're allowed to produce split-screens with aspect ratios different from the screen meaning
// that we always add one more column before finally adding an entirely new row.
numDivisionsY - = 1 ;
}
// Assign split-screen area to each player.
foreach ( var player in PlayerInput . all )
{
// Make sure the player's splitScreenIndex isn't out of range.
var splitScreenIndex = player . splitScreenIndex ;
if ( splitScreenIndex > = numDivisionsX * numDivisionsY )
{
Debug . LogError (
$"Split-screen index of {splitScreenIndex} on player is out of range (have {numDivisionsX * numDivisionsY} screens); resetting to playerIndex" ,
player ) ;
player . m_SplitScreenIndex = player . playerIndex ;
}
// Make sure we have a camera.
var camera = player . camera ;
if ( camera = = null )
{
Debug . LogError (
"Player has no camera associated with it. Cannot set up split-screen. Point PlayerInput.camera to camera for player." ,
player ) ;
continue ;
}
// Assign split-screen area based on m_SplitScreenRect.
var column = splitScreenIndex % numDivisionsX ;
var row = splitScreenIndex / numDivisionsX ;
var rect = new Rect
{
width = m_SplitScreenRect . width / numDivisionsX ,
height = m_SplitScreenRect . height / numDivisionsY
} ;
rect . x = m_SplitScreenRect . x + column * rect . width ;
// Y is bottom-to-top but we fill from top down.
rect . y = m_SplitScreenRect . y + m_SplitScreenRect . height - ( row + 1 ) * rect . height ;
camera . rect = rect ;
}
}
private bool IsDeviceUsableWithPlayerActions ( InputDevice device )
{
Debug . Assert ( device ! = null ) ;
if ( m_PlayerPrefab = = null )
return true ;
var playerInput = m_PlayerPrefab . GetComponentInChildren < PlayerInput > ( ) ;
if ( playerInput = = null )
return true ;
var actions = playerInput . actions ;
if ( actions = = null )
return true ;
// If the asset has control schemes, see if there's one that works with the device plus
// whatever unpaired devices we have left.
if ( actions . controlSchemes . Count > 0 )
{
using ( var unpairedDevices = InputUser . GetUnpairedInputDevices ( ) )
{
if ( InputControlScheme . FindControlSchemeForDevices ( unpairedDevices , actions . controlSchemes ,
mustIncludeDevice : device ) = = null )
return false ;
}
return true ;
}
// Otherwise just check whether any of the maps has bindings usable with the device.
foreach ( var actionMap in actions . actionMaps )
if ( actionMap . IsUsableWithDevice ( device ) )
return true ;
return false ;
}
private void ValidateInputActionAsset ( )
{
#if DEVELOPMENT_BUILD | | UNITY_EDITOR
if ( m_PlayerPrefab = = null | | m_PlayerPrefab . GetComponentInChildren < PlayerInput > ( ) = = null )
return ;
var actions = m_PlayerPrefab . GetComponentInChildren < PlayerInput > ( ) . actions ;
if ( actions = = null )
return ;
var isValid = true ;
foreach ( var controlScheme in actions . controlSchemes )
{
if ( controlScheme . deviceRequirements . Count > 0 )
break ;
isValid = false ;
}
if ( isValid ) return ;
var assetInfo = actions . name ;
#if UNITY_EDITOR
assetInfo = AssetDatabase . GetAssetPath ( actions ) ;
#endif
Debug . LogWarning ( $"The input action asset '{assetInfo}' in the player prefab assigned to PlayerInputManager has " +
"no control schemes with required devices. The JoinPlayersWhenButtonIsPressed join behavior " +
"will not work unless the expected input devices are listed as requirements in the input " +
"action asset." , m_PlayerPrefab ) ;
#endif
}
/// <summary>
/// Called by <see cref="PlayerInput"/> when it is enabled.
/// </summary>
/// <param name="player"></param>
internal void NotifyPlayerJoined ( PlayerInput player )
{
Debug . Assert ( player ! = null ) ;
UpdateSplitScreen ( ) ;
switch ( m_NotificationBehavior )
{
case PlayerNotifications . SendMessages :
SendMessage ( PlayerJoinedMessage , player , SendMessageOptions . DontRequireReceiver ) ;
break ;
case PlayerNotifications . BroadcastMessages :
BroadcastMessage ( PlayerJoinedMessage , player , SendMessageOptions . DontRequireReceiver ) ;
break ;
case PlayerNotifications . InvokeUnityEvents :
m_PlayerJoinedEvent ? . Invoke ( player ) ;
break ;
case PlayerNotifications . InvokeCSharpEvents :
DelegateHelpers . InvokeCallbacksSafe ( ref m_PlayerJoinedCallbacks , player , "onPlayerJoined" ) ;
break ;
}
}
/// <summary>
/// Called by <see cref="PlayerInput"/> when it is disabled.
/// </summary>
/// <param name="player"></param>
internal void NotifyPlayerLeft ( PlayerInput player )
{
Debug . Assert ( player ! = null ) ;
UpdateSplitScreen ( ) ;
switch ( m_NotificationBehavior )
{
case PlayerNotifications . SendMessages :
SendMessage ( PlayerLeftMessage , player , SendMessageOptions . DontRequireReceiver ) ;
break ;
case PlayerNotifications . BroadcastMessages :
BroadcastMessage ( PlayerLeftMessage , player , SendMessageOptions . DontRequireReceiver ) ;
break ;
case PlayerNotifications . InvokeUnityEvents :
m_PlayerLeftEvent ? . Invoke ( player ) ;
break ;
case PlayerNotifications . InvokeCSharpEvents :
DelegateHelpers . InvokeCallbacksSafe ( ref m_PlayerLeftCallbacks , player , "onPlayerLeft" ) ;
break ;
}
}
[Serializable]
public class PlayerJoinedEvent : UnityEvent < PlayerInput >
{
}
[Serializable]
public class PlayerLeftEvent : UnityEvent < PlayerInput >
{
}
}
}