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