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}