Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

Question Manual Local Multiplayer Using InputDevices

Discussion in 'Input System' started by coatline, Jun 14, 2022.

  1. coatline

    coatline

    Joined:
    Jul 31, 2019
    Posts:
    16
    Heads up: I don't know what I am doing. Tell me if I am doing it completely wrong.

    How would I go about pairing 2 devices (keyboard&mouse) to a single gameobject using the method below (onUnpairedDeviceUsed)? A control scheme called KeyboardMouse is already in my Input Action Asset. For some context, I am trying to make it to where a player joins if they press a button. (Local Multiplayer)

    I have looked around for an answer but I couldn't find one.

    Code (CSharp):
    1. int maxPlayers = 4;
    2.  
    3. // This is a c# generated class from the input asset
    4. Controls inputs;
    5.  
    6. void Awake()
    7.     {
    8.         inputs = new Controls();
    9.         inputs.Enable();
    10.         InputUser.listenForUnpairedDeviceActivity = maxPlayers;
    11.  
    12.         InputUser.onUnpairedDeviceUsed += JoinPlayer;
    13.     }
    14.  
    15. void JoinPlayer(InputControl control, InputEventPtr ptr)
    16.     {
    17.         // Ignore anything but button presses.
    18.         if (control is not ButtonControl)
    19.             return;
    20.  
    21.         // Get a new InputUser, now paired with the device
    22.         InputUser user = InputUser.PerformPairingWithDevice(control.device);
    23.  
    24.         // This is my best guess
    25.         if (user.hasMissingRequiredDevices)
    26.         {
    27.             user.UnpairDevicesAndRemoveUser();
    28.             return;
    29.         }
    30.  
    31.         Controls userInputs = new Controls();
    32.         userInputs.Enable();
    33.  
    34.         user.AssociateActionsWithUser(userInputs);
    35.  
    36.         PlayerInput.Instantiate(joinerPrefab.gameObject, pairWithDevice: control.device);
    37.  
    38.         InputUser.listenForUnpairedDeviceActivity--;
    39.     }
    Other info:

    Basically, I switched my project from using the PlayerInput components to generating a c# class and I don't really know what I am doing.

    Do I still need to use the PlayerInput component to read inputs?

    Also, do I need to create a new instance of Controls for any input to be read? Like this:
    Code (CSharp):
    1.  
    2. // This is a c# generated class from the input asset
    3. Controls inputs;
    4.  
    5. void Awake()
    6.     {
    7.         inputs = new Controls();
    8.         inputs.Enable();
    9.     }
    10.  
    Thank you in advance.

    Edit: Found out that user.controlScheme is null. Maybe I am missing a way to set the controlScheme?
     
    Last edited: Jun 15, 2022
  2. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    450
    Samyam mentions pairing in one of her videos, but I haven't dabbled with it myself.



    Go around 10:30 for the pairing.
     
  3. coatline

    coatline

    Joined:
    Jul 31, 2019
    Posts:
    16
    @dlorre, thanks for your reply. I have seen her video and it doesn't seem to have much on manual pairing. After doing a lot more research however, I managed to get something that works.

    Note: Using the method below, you do not have to use InputUser.listenForUnpairedDeviceActivity OR InputUser.onUnpairedDeviceUsed

    As far as I know, the only way to detect the control scheme is by getting the name of the device and setting it manually that way. Although I would love a more elegant solution.

    Code (CSharp):
    1. [SerializeField] HorizontalLayoutGroup horizontalLayoutGroup;
    2.     [SerializeField] LocalJoiner joinerPrefab;
    3.     [SerializeField] Color[] colors;
    4.     [SerializeField] int maxPlayers;
    5.     List<InputDevice> inputDevices;
    6.     InputAction joinAction;
    7.     int joinedCount;
    8.  
    9.     void Awake()
    10.     {
    11.         inputDevices = new List<InputDevice>();
    12.  
    13.         // Using this event, you cannot set the parent or transform of what you instantiate
    14.         //InputUser.onUnpairedDeviceUsed += JoinPlayer;
    15.  
    16.         // Bind joinAction to any button press.
    17.         joinAction = new InputAction(binding: "/*/<button>");
    18.         joinAction.started += OnJoinPressed;
    19.  
    20.         BeginJoining();
    21.     }
    22.  
    23.     void OnJoinPressed(InputAction.CallbackContext context)
    24.     {
    25.         JoinPlayer(context.control.device);
    26.     }
    27.  
    28.     void JoinPlayer(InputDevice device)
    29.     {
    30.         if (inputDevices.Contains(device))
    31.             return;
    32.  
    33.         inputDevices.Add(device);
    34.  
    35.         // Get a new InputUser, now paired with the device
    36.         InputUser user = InputUser.PerformPairingWithDevice(device);
    37.  
    38.         Controls userInputs = new Controls();
    39.  
    40.         user.AssociateActionsWithUser(userInputs);
    41.  
    42.         string controlScheme = "Gamepad";
    43.  
    44.         if (device.displayName.Contains("Mouse") || device.displayName.Contains("Keyboard"))
    45.             controlScheme = "Keyboard&Mouse";
    46.         else if (device.displayName.Contains("Joystick"))
    47.             controlScheme = "Joystick";
    48.  
    49.         if (device.displayName == "Mouse")
    50.         {
    51.             inputDevices.Add(Keyboard.current);
    52.             InputUser.PerformPairingWithDevice(Keyboard.current, user: user);
    53.         }
    54.         else if (device.displayName == "Keyboard")
    55.         {
    56.             inputDevices.Add(Mouse.current);
    57.             InputUser.PerformPairingWithDevice(Mouse.current, user: user);
    58.         }
    59.  
    60.         user.ActivateControlScheme(controlScheme);
    61.  
    62.         userInputs.Enable();
    63.  
    64.         PlayerInput p = PlayerInput.Instantiate(joinerPrefab.gameObject, -1, controlScheme: controlScheme, -1, pairWithDevices: user.pairedDevices.ToArray());
    65.         p.transform.SetParent(horizontalLayoutGroup.transform);
    66.         p.GetComponent<LocalJoiner>().Setup(colors[joined], joined + 1, userInputs.UI);
    67.  
    68.         joinedCount++;    
    69.  
    70.         if (joinedCount == maxPlayers)
    71.             EndJoining();
    72.     }
    73.  
    74.     void BeginJoining()
    75.     {
    76.          joinAction.Enable();
    77.     }
    78.  
    79.     void EndJoining()
    80.     {
    81.         joinAction.Disable();
    82.     }
    83.    
    84.     void OnDestroy()
    85.     {
    86.         joinAction.started -= OnJoinPressed;
    87.     }
    88.  
    Some links that were very helpful:
    - https://www.robotmonkeybrain.com/un...-and-mouse-to-the-same-inputuser/#comment-803
    - deeprest in https://forum.unity.com/threads/inp...oWww=1&redig=FCC6B7C70D59465C81B6DF5A017B4C04 including his link
    - https://github.com/GeneralProtectionFault/InputLocalMultiplayerTemplate/tree/master/Assets/Scripts
    - manual: https://docs.unity3d.com/Packages/c...nputSystem_Users_InputUser_controlSchemeMatch
     
    Last edited: Jun 15, 2022
  4. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    450
    Instead of checking the device name you can check the type:

    Code (csharp):
    1.  
    2.      if (device is Keyboard || device is Mouse)
    3.      {
    4.           // do something
    5.      }
    6.  
     
    coatline likes this.
  5. maximill101

    maximill101

    Joined:
    Mar 8, 2020
    Posts:
    3
    Okay first I really appreciate this entire thread its been helping me a lot. but something I cant quite get working is this line with "Controls" (Controls userInputs = new Controls(); ) what is that referencing? another script? am I just not seeing where it is in this script?. also including the using "UnityEngine.InputSystem.Users;" and other stuff could be nice for other people like myself. this is one of the only threads that has what I am looking for.
     
  6. coatline

    coatline

    Joined:
    Jul 31, 2019
    Posts:
    16
    I too had trouble finding and understanding things on this topic and I'm glad to help! I will pretty much answer every question with the prefix, "to my understanding" because I still don't really know what I am talking about.

    First question, what is the "'Controls' class?" This class can be generated by selecting your input action asset and checking the "Generate C# Class" checkbox. It is generated through the editor by selecting your input action asset like so:

    WhereToGenerateClass.PNG

    It can be named anything but I think it defaults to whatever your input action asset is named.

    To use it, you just need to make an instance of the class like:

    Code (CSharp):
    1. Controls controls = new Controls();
    I believe this is just another way to read input from your input action asset, instead of using the PlayerInput components. It is less reliant on GameObjects in this sense. So the instance that you create is kind of how you read the inputs into use. For example, to read input from this instance of controls that can be "paired" to an InputUser by using something like this:

    Code (CSharp):
    1. InputUser user = InputUser.PerformPairingWithDevice(device);
    2. Controls userControls = new Controls();
    3. user.AssociateActionsWithUser(userControls);
    The variable device is just an InputDevice which you can grab from a number of places, including:

    Code (CSharp):
    1. InputSystem.devices[]
    2. InputUser.GetUnpairedInputDevices();
    3.  
    4. /*The parameter InputControl gives the control that is pressed and you can get the device from which it originated by using InputControl.device*/
    5.  
    6. System. Action<InputControl, InputEventPtr> InputUser.onUnpairedDeviceUsed
    Anyways, an example of how this generated input action asset class can read input is like so:

    Code (CSharp):
    1. void Start()
    2.     {
    3.          Controls userControls = new Controls();
    4.          userControls.Gameplay.Jump.Performed += OnJump;
    5.     }
    6.  
    7. void OnJump(InputAction.CallbackContext value)
    8.     {
    9.         // Jump
    10.     }
    There is a bit to explain. The Gameplay property I referenced is an action map in my input action asset. Here:

    actionmap.PNG

    As far as I know, the class updates when you update the asset. "Performed" can also be "Canceled" or "Started." I honestly do not fully know the difference between these except that the order of calling is Started, Performed, and Canceled. Of course, you are going to want to unsubscribe later in OnDisable or OnDestroy. Otherwise you will have very annoying errors and various problems.

    Second question: what is the UnityEngine.InputSystem.Users, InputUser API, all about? Well, this is hard to explain, partly because I still don't completely understand either. But one thing that really helped me to understand is to go into the input debugger (Window/Analysis/Input Debugger) and take a look at the Users tab once you create one. It contains all active InputUsers in the project. Here is what a user paired to the keyboard and mouse looks like to me:

    inputuserindebugger.PNG

    So I guess that an InputUser just contains all of that information. One thing thought is that if you read the docs: https://docs.unity3d.com/Packages/c...nputSystem.Users.InputUser.html?q=getunpaired it can have something to do with accounts relating to the platform it is on. Really the docs tell you all you need to know about InputUsers but I haven't looked into other things in the namespace UnityEngine.InputSystem.Users like:

    Anyway, I've gotten my local joining code down to where I am happier with it. I will post it here, hopefully it is even more helpful. Note that InputUserManager is a Singleton. Also, sorry if it is hard to read, I know it is a lot and I tried to simplify it.

    Code (CSharp):
    1. public class InputUserManager : Singleton<InputUserManager>
    2. {
    3.     Dictionary<InputDevice, InputUser> deviceToUser;
    4.     Dictionary<InputUser, Controls> userToControls;
    5.     List<InputDevice> inputDevices;
    6.  
    7.     protected override void Awake()
    8.     {
    9.         inputDevices = new();
    10.         deviceToUser = new();
    11.         userToControls = new();
    12.  
    13.         base.Awake();
    14.     }
    15.  
    16.     public Controls GetControlsFromUser(InputUser u) => userToControls[u];
    17.     public bool DeviceUsed(InputDevice device) => inputDevices.Contains(device);
    18.     // UserInputData is a class that contains important information regarding an InputUser
    19.     public UserInputData GetInputData(InputUser user)
    20.     {
    21.         Controls controls = GetControlsFromUser(user);
    22.         string controlScheme = user.controlScheme.Value.name;
    23.         return new UserInputData(controlScheme, user.pairedDevices.ToArray(), controls, user);
    24.     }
    25.  
    26.     public void DisconnectDevices(InputDevice[] devices)
    27.     {
    28.         InputUser user = deviceToUser[devices[0]];
    29.  
    30.         foreach (InputDevice device in devices)
    31.         {
    32.             userToControls.Remove(user);
    33.             deviceToUser.Remove(device);
    34.             inputDevices.Remove(device);
    35.         }
    36.  
    37.         user.UnpairDevicesAndRemoveUser();
    38.     }
    39.  
    40.     public InputUser CreateInputUser(InputDevice device)
    41.     {
    42.         List<InputDevice> devices = new();
    43.  
    44.         string controlScheme = "Gamepad";
    45.  
    46.         if (device is Mouse || device is Keyboard)
    47.         {
    48.             controlScheme = "Keyboard&Mouse";
    49.  
    50.             devices.Add(Keyboard.current);
    51.             devices.Add(Mouse.current);
    52.         }
    53.         else
    54.             devices.Add(device);
    55.  
    56.         InputUser user = InputUser.PerformPairingWithDevice(devices[0]);
    57.  
    58.         foreach (InputDevice dev in devices)
    59.         {
    60.             if (dev != devices[0])
    61.                 InputUser.PerformPairingWithDevice(dev, user: user);
    62.  
    63.             inputDevices.Add(dev);
    64.             deviceToUser.Add(dev, user);
    65.         }
    66.  
    67.         Controls userControls = new Controls();
    68.  
    69.         user.AssociateActionsWithUser(userControls);
    70.         user.ActivateControlScheme(controlScheme);
    71.  
    72.         userControls.Enable();
    73.         userToControls.Add(user, userControls);
    74.  
    75.         return user;
    76.     }
    77. }
    78.  
    Code (CSharp):
    1. public class LocalJoiner : MonoBehaviour
    2. {
    3.     [SerializeField] UnityEvent<LocalUser> PlayerJoined;
    4.     [SerializeField] LocalUser userPrefab;
    5.     [SerializeField] int maxPlayers;
    6.  
    7.     public List<LocalUser> LocalUsers { get; private set; }
    8.  
    9.     void Start()
    10.     {
    11.         LocalUsers = new();
    12.         IDToJoinable = new();
    13.  
    14.         InputUser.onUnpairedDeviceUsed += JoinPressed;
    15.         InputUser.listenForUnpairedDeviceActivity += maxPlayers;
    16.         BeginJoining();
    17.         JoinInitial();
    18.     }
    19.  
    20.     void JoinPressed(InputControl inputControl, InputEventPtr inputEventPtr) => OnJoinPressed(inputControl);
    21.  
    22.     void OnJoinPressed(InputControl control)
    23.     {
    24.         if (LocalUsers.Count == maxPlayers)
    25.         {
    26.             print("max players");
    27.             return;
    28.         }
    29.  
    30.         InputDevice device = control.device;
    31.  
    32.         if (InputUserManager.I.DeviceUsed(device))
    33.             return;
    34.  
    35.         InputUser user = InputUserManager.I.CreateInputUser(device);
    36.         JoinPlayer(user);
    37.     }
    38.  
    39.     void JoinPlayer(InputUser user)
    40.     {
    41.         UserInputData inputData = InputUserManager.I.GetInputData(user);
    42.  
    43.         LocalUser localUser = Instantiate(userPrefab, transform);
    44.  
    45.         Color color = LocalJoinManager.I.GetColor(joinable.LocalSlotIndex);
    46.         localUser.Setup(LocalUsers.Count, joinable.LocalSlotIndex, color, inputData);
    47.         InputUser.listenForUnpairedDeviceActivity--;
    48.  
    49.         LocalUsers.Add(localUser);
    50.         PlayerJoined?.Invoke(localUser);
    51.     }
    52. }
    If you have any questions feel free to ask, not that I am guarenteed to know. :D
     
  7. maximill101

    maximill101

    Joined:
    Mar 8, 2020
    Posts:
    3
    Thank you so vary much for that response it was vary helpful. My local multi player game is now finally working after 3 months mostly because of this thread!
     
    Last edited: Jul 26, 2022
    coatline and dlorre like this.