123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801 |
- 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")]
- [HelpURL(InputSystem.kDocUrl + "/manual/PlayerInputManager.html")]
- 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.LogWarning("Maximum number of supported players reached: " + 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>
- {
- }
- }
- }
|