Local multiplayer with new input system

Hello :slight_smile:
I can’t get my head around handling local players. I want to have at least 2 scenes:
1st scene with player selection
2nd scene with actual gameplay
How do I handle joining players with Player Input Manager manually?
How do I migrate players between scenes with new input system?

1 Like

Get a reference to the PlayerInputManager and call
playerInputManager.JoinPlayer(....)

If you have set the PlayerInputManager component to “SendMessages” in the Editor it will call a function on all components on the same game object:

 public void OnPlayerJoined(PlayerInput playerInput) { }

The received PlayerInput instance is a MonoBehaviour on a newly spawned game object. So you can do

DontDestroyOnLoad(playerInput.gameObject);

Hope that helps!

2 Likes

Thanks for your response! Sorry for not answering but unfortunately I can’t test it right now :frowning: I also hope it helps :slight_smile:

Don’t want to leave this without response so I found some time to test this.
If I call
playerInputManager.JoinPlayer(....)
then it still needs a prefab set in PlayerInputManager
I will just Instantiate a prefab with PlayerInput and Notification Behaviour set to BroadcastMessage so I can just swap it’s children objects and change PlayerInput’s mapping to suit situation (lobby, gameplay).

Thanks! I’m gonna use this solution as well :slight_smile:

I’m trying to do a similiar thing, what are you supposed to write in the parentheses when you get a reference to the PlayerInputManager, i don’t really understand how you make it work, where are you supposed to write all the code? On a script for the spawner, or a script for the PlayerInputManager?

I have been looking for this answer everywhere, thank you so much <3 I think this will help me get started on what I’m trying to do.

You’d in theory write that in some sort of game manager object script and use that to assign devices and controls to players. I found the info for that call here https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.PlayerInputManager.html?q=join#UnityEngine_InputSystem_PlayerInputManager_JoinPlayer_System_Int32_System_Int32_System_String_UnityEngine_InputSystem_InputDevice_

I’d recommend not doing it manually though as it looks like you’ll need to select the device you want to pair it and other things when you can have the new input manager help you with that and use the sendMessages to help do things with individual devices though it depends on what you’re trying to do I suppose.

To expand on this (hope I’m not derailing, I think this is on topic), I’m confused about the Prefab that it expects, mentioned above as well. I’ve seen a tutorial or 2 on it, and sure, it seems that you plug in the game object, and when players join, it can spawn another of those game objects…but what if you don’t want your players to have the same game object? How do you have the API manage this when not all the players are the same thing?

3 Likes

Yeah I really don’t like that the prefab field is mandatory with the “Join on Action” option. I’m not sure what the “best practice” of working with the PlayerInputManager is but my way of working with it was i created a “Player UI controller” prefab and assigned it to PlayerInputManager (set to Join on Action Triggered).
The players join the game with an action from my game’s main menu and the UI controller instance created by PlayerInputManager is assigned to a UI element on the screen, and the players select their character.
I then cached the player selections and on every subsequent scene load my PlayerInputManager was set to Join Manually, and I instantiated player choices at the position of a “player spawn” game object. Join Manually doesn’t require a prefab because it expects you to instantiate from a script.

2 Likes

Thanks, that was part of what I’ve been wrapping my head around. In my case, I’m joining manually, but it took me a day to figure out how, but it’s buried in the docs under the join method. Apparently, for joining “manually,” a player is considered to have joined after a PlayerInput object has been created. For me, I had already painfully created a UI where the players could move their cursors over their player, etc… and select, so I wanted to utilize that and then join manually. I’m still fighting with it, but I have “some” idea of what I’m doing, maybe this info will help others as well. :sunglasses:

2 Likes

Wonder if you could share your solution. I also have a similar issue of spawning two different Players, each with its Inputs mapped. Helpeful to save time to many people, as usually characters are different. Thanks!

Sorry, didn’t see this post… Well, I’ll post what I have for one of my selection screens. It “works” insofar as I had multiple controllers, and each would control their respective icon on the selection screen, but I hadn’t gotten to the point of actually persisting that controller relationship to the actual gameplay. Looking at some of the samples with the new input system, I’m actually thinking of scrapping that and trying to do it “right” with the PlayerInput and the gamepad mouse cursor (gamepad mouse cursor sample scene in the input manager, you can download that)

But I hadn’t gotten to a “solution” or anything just yet, and I was referring to just beginning to understand the new system. That said, I’m fine posting one of the scripts here if it does pose any interest to anyone. Please note, I don’t take full credit for this, the basic cursor behavior and idea of overriding the PointerInputModule is something I got from a post on here somewhere years ago, which I have no idea as to the location of :-P.

Basically, what I’m doing is that, plus putting the actual game object that the player will use behind/child of a UI/canvas image. It works, but I want to incorporate PlayerInput, now that it’s there, because I never really felt comfortable wrapping my head around the notion of keeping the player # associated with the game object, and it just gets awfully confusing. Anyhoo, script:

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;

// Attach this script on your canvas's EventSystem game object
// It glitches the StandaloneInputModule if both are active at the same time

public class CarSelectionScript : PointerInputModule
{

    // Use this for initialization
    void Start()
    {

        // Disable the car controls so the cars don't move while trying to select them! (Re-enable afterwards)
        foreach (var car in GameObject.FindGameObjectsWithTag("Car"))
        {
            var physicsCarObject = car.GetComponent<Rigidbody>();
            physicsCarObject.constraints = RigidbodyConstraints.FreezeAll;
        }


        for (int i = 0; i < PersistentManagerScript.Instance.cursorsActive.Count; i++)
        {
            // Set cursors all to false initially in case selection screen is revisted w/ different # of players
            PersistentManagerScript.Instance.cursorsActive[i] = false;
        }
           
       
       
        for (int i = 0; i < PersistentManagerScript.Instance.numberOfPlayers; i++)
        {
            // However many players are selected, activate their cursors
            // Associate with player by index (+1)
            PersistentManagerScript.Instance.cursorsActive[i] = true;

           
        }


        // (PointerInputModule)
        base.Start();

        if (cursorObjects == null || eventSystem == null)
        {
            Debug.LogError("Set the game objects in the cursor module.");
            GameObject.Destroy(gameObject);
        }

        // This is only necessary if players already selected cars, but came back to the screen to pick again.
        // We need to destroy the player car game objects so they can be created afresh
        var destroyMe = Resources.FindObjectsOfTypeAll<GameObject>();

        foreach (var item in destroyMe)
        {
            if (item.name.Contains("Player") && item.name.Contains("Car"))
                Destroy(item);
        }

       

        // INPUT EVENTS
        PersistentManagerScript.Instance.controls.Generic.start.performed += OnStartButton;
       
    }

    private void OnDestroy()
    {
        base.OnDestroy();
        PersistentManagerScript.Instance.controls.Generic.start.performed -= OnStartButton;

        // Re-enable car control scripts
        foreach (var car in GameObject.FindGameObjectsWithTag("Car"))
        {
            var physicsCarObject = car.GetComponent<Rigidbody>();
            physicsCarObject.constraints = RigidbodyConstraints.None;

            // Re-enable the constraints we do want in game
            physicsCarObject.constraints = RigidbodyConstraints.FreezeRotationX;
            physicsCarObject.constraints = RigidbodyConstraints.FreezeRotationZ;
            physicsCarObject.constraints = RigidbodyConstraints.FreezePositionY;
        }
    }




    private void OnStartButton(InputAction.CallbackContext ctx)
    {
        // Check that all players have selected a car :)
        for (var i = 0; i < PersistentManagerScript.Instance.numberOfPlayers; i++)
        {
            if (PersistentManagerScript.Instance.PlayerCarsAssigned[i] == false)
                return;
        }

        SceneManager.LoadScene("CharacterSelection");
    }



   


    // The same event system used on the Canvas
    [SerializeField] EventSystem eventSystem;

    // A list of cursor objects inside the canvas
    // It moves only on X and Y axis
    [SerializeField] List<GameObject> cursorObjects;

    private Vector2 auxVec2;
    private PointerEventData pointer;


    // Process is called once per tick
    public override void Process()
    {


        // For each player                                  // Hopefully joysticks too!!
        for (int i = 0; i < PersistentManagerScript.Instance.PlayerObjects.Count; i++)
        {
            // Get the player # according to the InputSystem API
            var playerIndex = PersistentManagerScript.Instance.PlayerObjects[i].GetComponent<PlayerInput>().playerIndex;

            // Getting objects related to player (i+1)
            GameObject cursorObject = cursorObjects[playerIndex];

            // Add step to make player selection cursor visible *******
            if (!cursorObject.GetComponentInChildren<Renderer>().enabled)
                cursorObject.GetComponentInChildren<Renderer>().enabled = true;



            GetPointerData(playerIndex, out pointer, true);
       

            // Converting the 3D-coords to Screen-coords
            // This gets the position of the cursor OBJECT
            Vector3 screenPos = Camera.main.WorldToScreenPoint(cursorObject.transform.position);


            auxVec2.x = screenPos.x;
            auxVec2.y = screenPos.y;

            // This sets the pointer itself (PointerEventData) to the position of the OBJECT we have above :)
            pointer.position = auxVec2;
            // Raycasting
            eventSystem.RaycastAll(pointer, this.m_RaycastResultCache);
            RaycastResult raycastResult = FindFirstRaycast(this.m_RaycastResultCache);
            pointer.pointerCurrentRaycast = raycastResult;
            this.ProcessMove(pointer);


            pointer.clickCount = 0;


        // Cursor click - adapt for detect input for player (i+1) only
        // if (Input.GetButtonDown("Boost_P0" + (i+1).ToString()))


        if (Gamepad.all[playerIndex].buttonSouth.isPressed)
        {
                Debug.Log(Gamepad.all[playerIndex] + " buttonSouth pressed! " + playerIndex);

                pointer.pressPosition = auxVec2;
                pointer.clickTime = Time.unscaledTime;
                pointer.pointerPressRaycast = raycastResult;

                pointer.clickCount = 1;
                pointer.eligibleForClick = true;

               

                // If the player pressed the confirm button over a car
                if (this.m_RaycastResultCache.Count > 0 && PersistentManagerScript.Instance.cursorsActive[i])
                {
                    Debug.Log("Player " + (playerIndex + 1) + " selected a car!");
                    pointer.selectedObject = raycastResult.gameObject;
                    pointer.pointerPress = ExecuteEvents.ExecuteHierarchy(raycastResult.gameObject, pointer, ExecuteEvents.submitHandler);
                    pointer.rawPointerPress = raycastResult.gameObject;

                    // DEBUGGING
                    // Debug.Log("The game object selected is " + pointer.selectedObject.transform.GetChild(0).gameObject);
                    // Debug.Log("Raycast count is " + m_RaycastResultCache.Count);

                    // Lock the player's cursor after selection
                    if (pointer.selectedObject.transform.GetChild(0).gameObject.tag == "Car")
                    {
                        Debug.Log(pointer.selectedObject.transform.GetChild(0).gameObject);
                        // PersistentManagerScript.PlayerCars[i] = pointer.selectedObject.transform.GetChild(0).gameObject;  
                        PersistentManagerScript.Instance.PlayerCarsAssigned[playerIndex] = true;

                        // Actually create new Game OBJECT, not object within list
                        var currentCar = Instantiate(pointer.selectedObject.transform.GetChild(0).gameObject);

                        // var currentCar = pointer.selectedObject.transform.GetChild(0).gameObject;

                        // Assign the selected car to the newly created GameObject
                        currentCar.name = "Player" + (playerIndex + 1).ToString() + "Car";
                       
                        // Make invisible/inactive until we place it/spawn it in the arena
                        currentCar.SetActive(false);
                       
                        DontDestroyOnLoad(currentCar);

                        // Add this car as a property to the PlayerObject in the dictionary
                        if (PersistentManagerScript.Instance.PlayerObjects[playerIndex] != null)
                        {
                            var playerObject = PersistentManagerScript.Instance.PlayerObjects[playerIndex];
                            // Move the PlayerInput, etc... object to the same spot so the child (the car) doesn't move/disappear from the menu
                            playerObject.transform.position = currentCar.transform.position;

                            // Make the car the parent so it can be found by GameObject.Find later (camera tracking)
                            playerObject.transform.parent = currentCar.transform;

                        }
                        // Set taunt according to character here (Character as prefab--get taunt component?)

                        PersistentManagerScript.Instance.cursorsActive[playerIndex] = false;  //  Freeze cursor
                    }
                }
                else
                {
                    pointer.selectedObject = null;
                    pointer.pointerPress = null;
                    pointer.rawPointerPress = null;
                }
            } // End of "if the confirm (south) button was pressed"


            // Add cancel button condition to deselect car and free up pointer motion
            if (Gamepad.all[playerIndex].buttonWest.isPressed)
            {
                PersistentManagerScript.Instance.PlayerCarsAssigned[playerIndex] = false;  //  Unassign previously selected car
                PersistentManagerScript.Instance.cursorsActive[playerIndex] = true;   //  Free up the player's cursor again

                // This garbage is necessary because the simple GameObject.Find method does not return inactive objects!
                var destroyMe = Resources.FindObjectsOfTypeAll<GameObject>();

                foreach(var item in destroyMe)
                {
                    if (item.name == "Player" + (playerIndex + 1).ToString() + "Car")
                        Destroy(item);
                }
            }



            else
            {
                pointer.clickCount = 0;
                pointer.eligibleForClick = false;
                pointer.pointerPress = null;
                pointer.rawPointerPress = null;
            }
        } // for( int i; i < cursorObjects.Count; i++ )

    }
}

Inside the override for “Process()” and the for loop inside there is the crux of the cursor objects.

1 Like

Thanks, I am also following in the forum the Player Input component thread https://discussions.unity.com/t/782666 that also has some suggestions on “doing it right”.
I will study also your code. Thanks for contributing.

I didn’t want to use SendMessage(), UnityEvents, or have PlayerInputManager spawn prefabs.
There is a way “roll your own” using tips from Rene-Damm in the other local multiplayer thread .
Hope someone else finds it useful. There’s a link to the source there, too.
NOTE: I was not able to test with multiple gamepads (I only have one), and did not include device connectivity callbacks. I’m sure PlayerInput provides a lot of useful stuff that’s missing from my code… but for a simple game it worked for me.

2 Likes

Saw a little activity on this thread, so I thought I’d close the loop I opened. I went about this local selection bit a different way, and I finally got what I was trying for. I created a thread here with a Github link for sharing in case it helps anyone with this specific issue: Local Multiplayer Selection Screen Solution! - Unity Engine - Unity Discussions

2 Likes

Hi, I know this forum is rather old, and there seems to be solutions to these problems, but I still don’t quite understand how this all works. My situation is very similar to dziemo’s initial problem. I want to have a character select screen that proceeds into gameplay on another scene.
The main issue, however, is that I can’t figure out how to save separate players’ choices. Basically, I have it so that the player will choose a character in a scene, and it stores that chosen character as an int using a “playerPref”, which in this case, I named “selectedCharacter”. Once the gameplay scene loads, the player’s car script references the selectedCharacter, and activates one of many child objects and deactivates the rest (those child objects are just the different character models). Fortunately, this works for one player, but I can’t seem to differentiate between multiple players. I’m really stuck, and I haven’t worked with the new input system very much, so any help would be greatly, greatly appreciated. I’m right on the verge of understanding how this works, but I can’t seem to get it working for my split-screen situation. Can someone help me?

I’m not sure, especially with no code to look at, but it sounds like you’re just storing an arbitrary relationship between the player # and your game objects. It doesn’t sound like you’re doing anything to handle the controllers.

In the post just before yours, I linked to a demo project that does exactly this. Also, recently, in this thread:
https://forum.unity.com/threads/local-multiplayer-keyboard-tirannie.1489306/#post-9300236
I posted the relevant part of the code that persists the relationship to the controllers between scenes.

Okay, thank you! I think this is enough info to kick start me in the right direction. I appreciate your help!

1 Like