Geen omschrijving
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PlayerInputManager.cs 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. using System;
  2. using UnityEngine.Events;
  3. using UnityEngine.InputSystem.Controls;
  4. using UnityEngine.InputSystem.LowLevel;
  5. using UnityEngine.InputSystem.Users;
  6. using UnityEngine.InputSystem.Utilities;
  7. #if UNITY_EDITOR
  8. using UnityEditor;
  9. #endif
  10. ////REVIEW: should we automatically pool/retain up to maxPlayerCount player instances?
  11. ////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)
  12. ////TODO: add support for reacting to players missing devices
  13. namespace UnityEngine.InputSystem
  14. {
  15. /// <summary>
  16. /// Manages joining and leaving of players.
  17. /// </summary>
  18. /// <remarks>
  19. /// This is a singleton component. Only one instance is meant to be active in a game
  20. /// at any one time. To retrieve the current instance, use <see cref="instance"/>.
  21. ///
  22. /// Note that a PlayerInputManager is not strictly required to have multiple <see cref="PlayerInput"/> components.
  23. /// What PlayerInputManager provides is the implementation of specific player join mechanisms
  24. /// (<see cref="joinBehavior"/>) as well as automatic assignment of split-screen areas (<see cref="splitScreen"/>).
  25. /// However, you can always implement your own custom logic instead and simply instantiate multiple GameObjects with
  26. /// <see cref="PlayerInput"/> yourself.
  27. /// </remarks>
  28. [AddComponentMenu("Input/Player Input Manager")]
  29. [HelpURL(InputSystem.kDocUrl + "/manual/PlayerInputManager.html")]
  30. public class PlayerInputManager : MonoBehaviour
  31. {
  32. /// <summary>
  33. /// Name of the message that is sent when a player joins the game.
  34. /// </summary>
  35. public const string PlayerJoinedMessage = "OnPlayerJoined";
  36. public const string PlayerLeftMessage = "OnPlayerLeft";
  37. /// <summary>
  38. /// If enabled, each player will automatically be assigned a portion of the available screen area.
  39. /// </summary>
  40. /// <remarks>
  41. /// For this to work, each <see cref="PlayerInput"/> component must have an associated <see cref="Camera"/>
  42. /// object through <see cref="PlayerInput.camera"/>.
  43. ///
  44. /// Note that as player join, the screen may be increasingly subdivided and players may see their
  45. /// previous screen area getting resized.
  46. /// </remarks>
  47. public bool splitScreen
  48. {
  49. get => m_SplitScreen;
  50. set
  51. {
  52. if (m_SplitScreen == value)
  53. return;
  54. m_SplitScreen = value;
  55. if (!m_SplitScreen)
  56. {
  57. // Reset rects on all player cameras.
  58. foreach (var player in PlayerInput.all)
  59. {
  60. var camera = player.camera;
  61. if (camera != null)
  62. camera.rect = new Rect(0, 0, 1, 1);
  63. }
  64. }
  65. else
  66. {
  67. UpdateSplitScreen();
  68. }
  69. }
  70. }
  71. ////REVIEW: we probably need support for filling unused screen areas automatically
  72. /// <summary>
  73. /// If <see cref="splitScreen"/> is enabled, this property determines whether subdividing the screen is allowed to
  74. /// produce screen areas that have an aspect ratio different from the screen resolution.
  75. /// </summary>
  76. /// <remarks>
  77. /// By default, when <see cref="splitScreen"/> is enabled, the manager will add or remove screen subdivisions in
  78. /// steps of two. This means that when, for example, the second player is added, the screen will be subdivided into
  79. /// a left and a right screen area; the left one allocated to the first player and the right one allocated to the
  80. /// second player.
  81. ///
  82. /// This behavior makes optimal use of screen real estate but will result in screen areas that have aspect ratios
  83. /// different from the screen resolution. If this is not acceptable, this property can be set to true to enforce
  84. /// split-screen to only create screen areas that have the same aspect ratio of the screen.
  85. ///
  86. /// This results in the screen being subdivided more aggressively. When, for example, a second player is added,
  87. /// the screen will immediately be divided into a four-way split-screen setup with the lower two screen areas
  88. /// not being used.
  89. ///
  90. /// This property is irrelevant if <see cref="fixedNumberOfSplitScreens"/> is used.
  91. /// </remarks>
  92. public bool maintainAspectRatioInSplitScreen => m_MaintainAspectRatioInSplitScreen;
  93. /// <summary>
  94. /// If <see cref="splitScreen"/> is enabled, this property determines how many screen divisions there will be.
  95. /// </summary>
  96. /// <remarks>
  97. /// This is only used if <see cref="splitScreen"/> is true.
  98. ///
  99. /// By default this is set to -1 which means the screen will automatically be divided to best fit the
  100. /// current number of players i.e. the highest player index in <see cref="PlayerInput"/>
  101. /// </remarks>
  102. public int fixedNumberOfSplitScreens => m_FixedNumberOfSplitScreens;
  103. /// <summary>
  104. /// The normalized screen rectangle available for allocating player split-screens into.
  105. /// </summary>
  106. /// <remarks>
  107. /// This is only used if <see cref="splitScreen"/> is true.
  108. ///
  109. /// By default it is set to <c>(0,0,1,1)</c>, i.e. the entire screen area will be used for player screens.
  110. /// If, for example, part of the screen should display a UI/information shared by all players, this
  111. /// property can be used to cut off the area and not have it used by PlayerInputManager.
  112. /// </remarks>
  113. public Rect splitScreenArea => m_SplitScreenRect;
  114. /// <summary>
  115. /// The current number of active players.
  116. /// </summary>
  117. /// <remarks>
  118. /// This count corresponds to all <see cref="PlayerInput"/> instances that are currently enabled.
  119. /// </remarks>
  120. public int playerCount => PlayerInput.s_AllActivePlayersCount;
  121. ////FIXME: this needs to be settable
  122. /// <summary>
  123. /// Maximum number of players allowed concurrently in the game.
  124. /// </summary>
  125. /// <remarks>
  126. /// If this limit is reached, joining is turned off automatically.
  127. ///
  128. /// By default this is set to -1. Any negative value deactivates the player limit and allows
  129. /// arbitrary many players to join.
  130. /// </remarks>
  131. public int maxPlayerCount => m_MaxPlayerCount;
  132. /// <summary>
  133. /// Whether new players can currently join.
  134. /// </summary>
  135. /// <remarks>
  136. /// While this is true, new players can join via the mechanism determined by <see cref="joinBehavior"/>.
  137. /// </remarks>
  138. /// <seealso cref="EnableJoining"/>
  139. /// <seealso cref="DisableJoining"/>
  140. public bool joiningEnabled => m_AllowJoining;
  141. /// <summary>
  142. /// Determines the mechanism by which players can join when joining is enabled (<see cref="joiningEnabled"/>).
  143. /// </summary>
  144. /// <remarks>
  145. /// </remarks>
  146. public PlayerJoinBehavior joinBehavior
  147. {
  148. get => m_JoinBehavior;
  149. set
  150. {
  151. if (m_JoinBehavior == value)
  152. return;
  153. var joiningEnabled = m_AllowJoining;
  154. if (joiningEnabled)
  155. DisableJoining();
  156. m_JoinBehavior = value;
  157. if (joiningEnabled)
  158. EnableJoining();
  159. }
  160. }
  161. /// <summary>
  162. /// The input action that a player must trigger to join the game.
  163. /// </summary>
  164. /// <remarks>
  165. /// If the join action is a reference to an existing input action, it will be cloned when the PlayerInputManager
  166. /// is enabled. This avoids the situation where the join action can become disabled after the first user joins which
  167. /// can happen when the join action is the same as a player in-game action. When a player joins, input bindings from
  168. /// devices other than the device they joined with are disabled. If the join action had a binding for keyboard and one
  169. /// for gamepad for example, and the first player joined using the keyboard, the expectation is that the next player
  170. /// could still join by pressing the gamepad join button. Without the cloning behavior, the gamepad input would have
  171. /// been disabled.
  172. ///
  173. /// For more details about joining behavior, see <see cref="PlayerInput"/>.
  174. /// </remarks>
  175. public InputActionProperty joinAction
  176. {
  177. get => m_JoinAction;
  178. set
  179. {
  180. if (m_JoinAction == value)
  181. return;
  182. ////REVIEW: should we suppress notifications for temporary disables?
  183. var joinEnabled = m_AllowJoining && m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered;
  184. if (joinEnabled)
  185. DisableJoining();
  186. m_JoinAction = value;
  187. if (joinEnabled)
  188. EnableJoining();
  189. }
  190. }
  191. public PlayerNotifications notificationBehavior
  192. {
  193. get => m_NotificationBehavior;
  194. set => m_NotificationBehavior = value;
  195. }
  196. public PlayerJoinedEvent playerJoinedEvent
  197. {
  198. get
  199. {
  200. if (m_PlayerJoinedEvent == null)
  201. m_PlayerJoinedEvent = new PlayerJoinedEvent();
  202. return m_PlayerJoinedEvent;
  203. }
  204. }
  205. public PlayerLeftEvent playerLeftEvent
  206. {
  207. get
  208. {
  209. if (m_PlayerLeftEvent == null)
  210. m_PlayerLeftEvent = new PlayerLeftEvent();
  211. return m_PlayerLeftEvent;
  212. }
  213. }
  214. public event Action<PlayerInput> onPlayerJoined
  215. {
  216. add
  217. {
  218. if (value == null)
  219. throw new ArgumentNullException(nameof(value));
  220. m_PlayerJoinedCallbacks.AddCallback(value);
  221. }
  222. remove
  223. {
  224. if (value == null)
  225. throw new ArgumentNullException(nameof(value));
  226. m_PlayerJoinedCallbacks.RemoveCallback(value);
  227. }
  228. }
  229. public event Action<PlayerInput> onPlayerLeft
  230. {
  231. add
  232. {
  233. if (value == null)
  234. throw new ArgumentNullException(nameof(value));
  235. m_PlayerLeftCallbacks.AddCallback(value);
  236. }
  237. remove
  238. {
  239. if (value == null)
  240. throw new ArgumentNullException(nameof(value));
  241. m_PlayerLeftCallbacks.RemoveCallback(value);
  242. }
  243. }
  244. /// <summary>
  245. /// Reference to the prefab that the manager will instantiate when players join.
  246. /// </summary>
  247. /// <value>Prefab to instantiate for new players.</value>
  248. public GameObject playerPrefab
  249. {
  250. get => m_PlayerPrefab;
  251. set => m_PlayerPrefab = value;
  252. }
  253. /// <summary>
  254. /// Singleton instance of the manager.
  255. /// </summary>
  256. /// <value>Singleton instance or null.</value>
  257. public static PlayerInputManager instance { get; private set; }
  258. /// <summary>
  259. /// Allow players to join the game based on <see cref="joinBehavior"/>.
  260. /// </summary>
  261. /// <seealso cref="DisableJoining"/>
  262. /// <seealso cref="joiningEnabled"/>
  263. public void EnableJoining()
  264. {
  265. switch (m_JoinBehavior)
  266. {
  267. case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
  268. ValidateInputActionAsset();
  269. if (!m_UnpairedDeviceUsedDelegateHooked)
  270. {
  271. if (m_UnpairedDeviceUsedDelegate == null)
  272. m_UnpairedDeviceUsedDelegate = OnUnpairedDeviceUsed;
  273. InputUser.onUnpairedDeviceUsed += m_UnpairedDeviceUsedDelegate;
  274. m_UnpairedDeviceUsedDelegateHooked = true;
  275. ++InputUser.listenForUnpairedDeviceActivity;
  276. }
  277. break;
  278. case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
  279. // Hook into join action if we have one.
  280. if (m_JoinAction.action != null)
  281. {
  282. if (!m_JoinActionDelegateHooked)
  283. {
  284. if (m_JoinActionDelegate == null)
  285. m_JoinActionDelegate = JoinPlayerFromActionIfNotAlreadyJoined;
  286. m_JoinAction.action.performed += m_JoinActionDelegate;
  287. m_JoinActionDelegateHooked = true;
  288. }
  289. m_JoinAction.action.Enable();
  290. }
  291. else
  292. {
  293. Debug.LogError(
  294. $"No join action configured on PlayerInputManager but join behavior is set to {nameof(PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)}",
  295. this);
  296. }
  297. break;
  298. }
  299. m_AllowJoining = true;
  300. }
  301. /// <summary>
  302. /// Inhibit players from joining the game.
  303. /// </summary>
  304. /// <seealso cref="EnableJoining"/>
  305. /// <seealso cref="joiningEnabled"/>
  306. public void DisableJoining()
  307. {
  308. switch (m_JoinBehavior)
  309. {
  310. case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
  311. if (m_UnpairedDeviceUsedDelegateHooked)
  312. {
  313. InputUser.onUnpairedDeviceUsed -= m_UnpairedDeviceUsedDelegate;
  314. m_UnpairedDeviceUsedDelegateHooked = false;
  315. --InputUser.listenForUnpairedDeviceActivity;
  316. }
  317. break;
  318. case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
  319. if (m_JoinActionDelegateHooked)
  320. {
  321. var joinAction = m_JoinAction.action;
  322. if (joinAction != null)
  323. m_JoinAction.action.performed -= m_JoinActionDelegate;
  324. m_JoinActionDelegateHooked = false;
  325. }
  326. m_JoinAction.action?.Disable();
  327. break;
  328. }
  329. m_AllowJoining = false;
  330. }
  331. ////TODO
  332. /// <summary>
  333. /// Join a new player based on input on a UI element.
  334. /// </summary>
  335. /// <remarks>
  336. /// This should be called directly from a UI callback such as <see cref="Button.onClick"/>. The device
  337. /// that the player joins with is taken from the device that was used to interact with the UI element.
  338. /// </remarks>
  339. internal void JoinPlayerFromUI()
  340. {
  341. if (!CheckIfPlayerCanJoin())
  342. return;
  343. //find used device; InputSystemUIInputModule should probably make that available
  344. throw new NotImplementedException();
  345. }
  346. /// <summary>
  347. /// Join a new player based on input received through an <see cref="InputAction"/>.
  348. /// </summary>
  349. /// <param name="context"></param>
  350. /// <remarks>
  351. /// </remarks>
  352. public void JoinPlayerFromAction(InputAction.CallbackContext context)
  353. {
  354. if (!CheckIfPlayerCanJoin())
  355. return;
  356. var device = context.control.device;
  357. JoinPlayer(pairWithDevice: device);
  358. }
  359. public void JoinPlayerFromActionIfNotAlreadyJoined(InputAction.CallbackContext context)
  360. {
  361. if (!CheckIfPlayerCanJoin())
  362. return;
  363. var device = context.control.device;
  364. if (PlayerInput.FindFirstPairedToDevice(device) != null)
  365. return;
  366. JoinPlayer(pairWithDevice: device);
  367. }
  368. /// <summary>
  369. /// Spawn a new player from <see cref="playerPrefab"/>.
  370. /// </summary>
  371. /// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
  372. /// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
  373. /// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
  374. /// a split-screen index of </param>
  375. /// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
  376. /// be selected based on <paramref name="pairWithDevice"/>. If no device is given either, the first control scheme that matches
  377. /// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
  378. /// <param name="pairWithDevice">Device to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
  379. /// is not given.</param>
  380. /// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
  381. /// <remarks>
  382. /// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
  383. ///
  384. /// To pair multiple devices, use <see cref="JoinPlayer(int,int,string,InputDevice[])"/>.
  385. /// </remarks>
  386. public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, InputDevice pairWithDevice = null)
  387. {
  388. if (!CheckIfPlayerCanJoin(playerIndex))
  389. return null;
  390. PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
  391. return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
  392. controlScheme: controlScheme, pairWithDevice: pairWithDevice);
  393. }
  394. /// <summary>
  395. /// Spawn a new player from <see cref="playerPrefab"/>.
  396. /// </summary>
  397. /// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
  398. /// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
  399. /// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
  400. /// a split-screen index of </param>
  401. /// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
  402. /// be selected based on <paramref name="pairWithDevices"/>. If no device is given either, the first control scheme that matches
  403. /// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
  404. /// <param name="pairWithDevices">Devices to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
  405. /// is not given.</param>
  406. /// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
  407. /// <remarks>
  408. /// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
  409. /// </remarks>
  410. public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, params InputDevice[] pairWithDevices)
  411. {
  412. if (!CheckIfPlayerCanJoin(playerIndex))
  413. return null;
  414. PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
  415. return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
  416. controlScheme: controlScheme, pairWithDevices: pairWithDevices);
  417. }
  418. [SerializeField] internal PlayerNotifications m_NotificationBehavior;
  419. [Tooltip("Set a limit for the maximum number of players who are able to join.")]
  420. [SerializeField] internal int m_MaxPlayerCount = -1;
  421. [SerializeField] internal bool m_AllowJoining = true;
  422. [SerializeField] internal PlayerJoinBehavior m_JoinBehavior;
  423. [SerializeField] internal PlayerJoinedEvent m_PlayerJoinedEvent;
  424. [SerializeField] internal PlayerLeftEvent m_PlayerLeftEvent;
  425. [SerializeField] internal InputActionProperty m_JoinAction;
  426. [SerializeField] internal GameObject m_PlayerPrefab;
  427. [SerializeField] internal bool m_SplitScreen;
  428. [SerializeField] internal bool m_MaintainAspectRatioInSplitScreen;
  429. [Tooltip("Explicitly set a fixed number of screens or otherwise allow the screen to be divided automatically to best fit the number of players.")]
  430. [SerializeField] internal int m_FixedNumberOfSplitScreens = -1;
  431. [SerializeField] internal Rect m_SplitScreenRect = new Rect(0, 0, 1, 1);
  432. [NonSerialized] private bool m_JoinActionDelegateHooked;
  433. [NonSerialized] private bool m_UnpairedDeviceUsedDelegateHooked;
  434. [NonSerialized] private Action<InputAction.CallbackContext> m_JoinActionDelegate;
  435. [NonSerialized] private Action<InputControl, InputEventPtr> m_UnpairedDeviceUsedDelegate;
  436. [NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerJoinedCallbacks;
  437. [NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerLeftCallbacks;
  438. internal static string[] messages => new[]
  439. {
  440. PlayerJoinedMessage,
  441. PlayerLeftMessage,
  442. };
  443. private bool CheckIfPlayerCanJoin(int playerIndex = -1)
  444. {
  445. if (m_PlayerPrefab == null)
  446. {
  447. Debug.LogError("playerPrefab must be set in order to be able to join new players", this);
  448. return false;
  449. }
  450. if (m_MaxPlayerCount >= 0 && playerCount >= m_MaxPlayerCount)
  451. {
  452. Debug.LogWarning("Maximum number of supported players reached: " + maxPlayerCount, this);
  453. return false;
  454. }
  455. // If we have a player index, make sure it's unique.
  456. if (playerIndex != -1)
  457. {
  458. for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
  459. if (PlayerInput.s_AllActivePlayers[i].playerIndex == playerIndex)
  460. {
  461. Debug.LogError(
  462. $"Player index #{playerIndex} is already taken by player {PlayerInput.s_AllActivePlayers[i]}",
  463. PlayerInput.s_AllActivePlayers[i]);
  464. return false;
  465. }
  466. }
  467. return true;
  468. }
  469. private void OnUnpairedDeviceUsed(InputControl control, InputEventPtr eventPtr)
  470. {
  471. if (!m_AllowJoining)
  472. return;
  473. if (m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)
  474. {
  475. // Make sure it's a button that was actuated.
  476. if (!(control is ButtonControl))
  477. return;
  478. // Make sure it's a device that is usable by the player's actions. We don't want
  479. // to join a player who's then stranded and has no way to actually interact with the game.
  480. if (!IsDeviceUsableWithPlayerActions(control.device))
  481. return;
  482. ////REVIEW: should we log a warning or error when the actions for the player do not have control schemes?
  483. JoinPlayer(pairWithDevice: control.device);
  484. }
  485. }
  486. private void OnEnable()
  487. {
  488. if (instance == null)
  489. {
  490. instance = this;
  491. }
  492. else
  493. {
  494. Debug.LogWarning("Multiple PlayerInputManagers in the game. There should only be one PlayerInputManager", this);
  495. return;
  496. }
  497. // if the join action is a reference, clone it so we don't run into problems with the action being disabled by
  498. // PlayerInput when devices are assigned to individual players
  499. if (joinAction.reference != null && joinAction.action?.actionMap?.asset != null)
  500. {
  501. var inputActionAsset = Instantiate(joinAction.action.actionMap.asset);
  502. var inputActionReference = InputActionReference.Create(inputActionAsset.FindAction(joinAction.action.name));
  503. joinAction = new InputActionProperty(inputActionReference);
  504. }
  505. // Join all players already in the game.
  506. for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
  507. NotifyPlayerJoined(PlayerInput.s_AllActivePlayers[i]);
  508. if (m_AllowJoining)
  509. EnableJoining();
  510. }
  511. private void OnDisable()
  512. {
  513. if (instance == this)
  514. instance = null;
  515. if (m_AllowJoining)
  516. DisableJoining();
  517. }
  518. /// <summary>
  519. /// If split-screen is enabled, then for each player in the game, adjust the player's <see cref="Camera.rect"/>
  520. /// to fit the player's split screen area according to the number of players currently in the game and the
  521. /// current split-screen configuration.
  522. /// </summary>
  523. private void UpdateSplitScreen()
  524. {
  525. // Nothing to do if split-screen is not enabled.
  526. if (!m_SplitScreen)
  527. return;
  528. // Determine number of split-screens to create based on highest player index we have.
  529. var minSplitScreenCount = 0;
  530. foreach (var player in PlayerInput.all)
  531. {
  532. if (player.playerIndex >= minSplitScreenCount)
  533. minSplitScreenCount = player.playerIndex + 1;
  534. }
  535. // Adjust to fixed number if we have it.
  536. if (m_FixedNumberOfSplitScreens > 0)
  537. {
  538. if (m_FixedNumberOfSplitScreens < minSplitScreenCount)
  539. Debug.LogWarning(
  540. $"Highest playerIndex of {minSplitScreenCount} exceeds fixed number of split-screens of {m_FixedNumberOfSplitScreens}",
  541. this);
  542. minSplitScreenCount = m_FixedNumberOfSplitScreens;
  543. }
  544. // Determine divisions along X and Y. Usually, we have a square grid of split-screens so all we need to
  545. // do is make it large enough to fit all players.
  546. var numDivisionsX = Mathf.CeilToInt(Mathf.Sqrt(minSplitScreenCount));
  547. var numDivisionsY = numDivisionsX;
  548. if (!m_MaintainAspectRatioInSplitScreen && numDivisionsX * (numDivisionsX - 1) >= minSplitScreenCount)
  549. {
  550. // We're allowed to produce split-screens with aspect ratios different from the screen meaning
  551. // that we always add one more column before finally adding an entirely new row.
  552. numDivisionsY -= 1;
  553. }
  554. // Assign split-screen area to each player.
  555. foreach (var player in PlayerInput.all)
  556. {
  557. // Make sure the player's splitScreenIndex isn't out of range.
  558. var splitScreenIndex = player.splitScreenIndex;
  559. if (splitScreenIndex >= numDivisionsX * numDivisionsY)
  560. {
  561. Debug.LogError(
  562. $"Split-screen index of {splitScreenIndex} on player is out of range (have {numDivisionsX * numDivisionsY} screens); resetting to playerIndex",
  563. player);
  564. player.m_SplitScreenIndex = player.playerIndex;
  565. }
  566. // Make sure we have a camera.
  567. var camera = player.camera;
  568. if (camera == null)
  569. {
  570. Debug.LogError(
  571. "Player has no camera associated with it. Cannot set up split-screen. Point PlayerInput.camera to camera for player.",
  572. player);
  573. continue;
  574. }
  575. // Assign split-screen area based on m_SplitScreenRect.
  576. var column = splitScreenIndex % numDivisionsX;
  577. var row = splitScreenIndex / numDivisionsX;
  578. var rect = new Rect
  579. {
  580. width = m_SplitScreenRect.width / numDivisionsX,
  581. height = m_SplitScreenRect.height / numDivisionsY
  582. };
  583. rect.x = m_SplitScreenRect.x + column * rect.width;
  584. // Y is bottom-to-top but we fill from top down.
  585. rect.y = m_SplitScreenRect.y + m_SplitScreenRect.height - (row + 1) * rect.height;
  586. camera.rect = rect;
  587. }
  588. }
  589. private bool IsDeviceUsableWithPlayerActions(InputDevice device)
  590. {
  591. Debug.Assert(device != null);
  592. if (m_PlayerPrefab == null)
  593. return true;
  594. var playerInput = m_PlayerPrefab.GetComponentInChildren<PlayerInput>();
  595. if (playerInput == null)
  596. return true;
  597. var actions = playerInput.actions;
  598. if (actions == null)
  599. return true;
  600. // If the asset has control schemes, see if there's one that works with the device plus
  601. // whatever unpaired devices we have left.
  602. if (actions.controlSchemes.Count > 0)
  603. {
  604. using (var unpairedDevices = InputUser.GetUnpairedInputDevices())
  605. {
  606. if (InputControlScheme.FindControlSchemeForDevices(unpairedDevices, actions.controlSchemes,
  607. mustIncludeDevice: device) == null)
  608. return false;
  609. }
  610. return true;
  611. }
  612. // Otherwise just check whether any of the maps has bindings usable with the device.
  613. foreach (var actionMap in actions.actionMaps)
  614. if (actionMap.IsUsableWithDevice(device))
  615. return true;
  616. return false;
  617. }
  618. private void ValidateInputActionAsset()
  619. {
  620. #if DEVELOPMENT_BUILD || UNITY_EDITOR
  621. if (m_PlayerPrefab == null || m_PlayerPrefab.GetComponentInChildren<PlayerInput>() == null)
  622. return;
  623. var actions = m_PlayerPrefab.GetComponentInChildren<PlayerInput>().actions;
  624. if (actions == null)
  625. return;
  626. var isValid = true;
  627. foreach (var controlScheme in actions.controlSchemes)
  628. {
  629. if (controlScheme.deviceRequirements.Count > 0)
  630. break;
  631. isValid = false;
  632. }
  633. if (isValid) return;
  634. var assetInfo = actions.name;
  635. #if UNITY_EDITOR
  636. assetInfo = AssetDatabase.GetAssetPath(actions);
  637. #endif
  638. Debug.LogWarning($"The input action asset '{assetInfo}' in the player prefab assigned to PlayerInputManager has " +
  639. "no control schemes with required devices. The JoinPlayersWhenButtonIsPressed join behavior " +
  640. "will not work unless the expected input devices are listed as requirements in the input " +
  641. "action asset.", m_PlayerPrefab);
  642. #endif
  643. }
  644. /// <summary>
  645. /// Called by <see cref="PlayerInput"/> when it is enabled.
  646. /// </summary>
  647. /// <param name="player"></param>
  648. internal void NotifyPlayerJoined(PlayerInput player)
  649. {
  650. Debug.Assert(player != null);
  651. UpdateSplitScreen();
  652. switch (m_NotificationBehavior)
  653. {
  654. case PlayerNotifications.SendMessages:
  655. SendMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
  656. break;
  657. case PlayerNotifications.BroadcastMessages:
  658. BroadcastMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
  659. break;
  660. case PlayerNotifications.InvokeUnityEvents:
  661. m_PlayerJoinedEvent?.Invoke(player);
  662. break;
  663. case PlayerNotifications.InvokeCSharpEvents:
  664. DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerJoinedCallbacks, player, "onPlayerJoined");
  665. break;
  666. }
  667. }
  668. /// <summary>
  669. /// Called by <see cref="PlayerInput"/> when it is disabled.
  670. /// </summary>
  671. /// <param name="player"></param>
  672. internal void NotifyPlayerLeft(PlayerInput player)
  673. {
  674. Debug.Assert(player != null);
  675. UpdateSplitScreen();
  676. switch (m_NotificationBehavior)
  677. {
  678. case PlayerNotifications.SendMessages:
  679. SendMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
  680. break;
  681. case PlayerNotifications.BroadcastMessages:
  682. BroadcastMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
  683. break;
  684. case PlayerNotifications.InvokeUnityEvents:
  685. m_PlayerLeftEvent?.Invoke(player);
  686. break;
  687. case PlayerNotifications.InvokeCSharpEvents:
  688. DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerLeftCallbacks, player, "onPlayerLeft");
  689. break;
  690. }
  691. }
  692. [Serializable]
  693. public class PlayerJoinedEvent : UnityEvent<PlayerInput>
  694. {
  695. }
  696. [Serializable]
  697. public class PlayerLeftEvent : UnityEvent<PlayerInput>
  698. {
  699. }
  700. }
  701. }