Local Multiplayer : keyboard tirannie

Hello everyone,

I’m working on a local multiplayer game and I encounter some issue with the control of the player.
I have two scene :
1°) Players press a button on their controller to spawn on the screen. And they choose a team to play with. There are 4 teams to choose from. And when all players are ready, a second scene is loaded.
2°) The players spawn at different position regarding their team.

If the first player is keyboard controlled and the second is a gamepad controlled, after the loading of the second scene, player 1 is still controlled by the keyboard and player 2 is still controled by the gamepad.

Now lets try if the keyboard is NOT the player 1 :
a) In the first scene, if the player 1 is controlled by a gamepad and the second player by the keyboard, when the second scene is loaded, the first player is now controlled by the keyboard and the second player is now controlled by the gamepad.
And in the Input player component, I can actually see that now the keyboard controls player 1 and the joystick controls player 2. Whereas in the team selection scene it was the other way round.

b) If in the first scene the player 1 is controlled by a gamepad and the second player is controller by a second gamepad. When the second scene has loaded, the first player can be controlled by the first gamepad but the second player doesn’t move at all. Because in his player Input Player component I see that player2 is waiting for keyboard input (but as any keyboard was press in scene 1, if I press on my keyboard, the player 2 doesn’t move. So it wait for a not existing keyboard …

c) If I use the keyboard for player one, and gamepad for player 2 and gamepad for player 3, all is working very well.

I don’t know if it is my script that lead to this issue or if there is something I don’t get with Unity so here is my scripts :
PlayerConfigurationManager.cs is called everytime a player join (by pressing a controller buton) in the first scene (where players has to choose a team) and the function “HandlePlayerJoin” is used.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using System.Linq;
using TMPro;
using UnityEngine.SceneManagement;
using UnityEngine;

public class PlayerConfigurationManager : MonoBehaviour
{
    private List<PlayerConfiguration> _playerConfigs;

    [SerializeField] private int _maxPlayer = 4;
    [SerializeField] private int _minPlayer = 2;

    [SerializeField] private TextMeshProUGUI _textNumberOfPlayer;
    [SerializeField] private TextMeshProUGUI _textNumberOfJoinedPlayer;

    public static PlayerConfigurationManager Instance { get; private set; }


    private void Awake()
    {
        if (Instance != null)
        {
            Debug.Log("SINGLETON : Trying to create another instance of singleton!");
        }
        else
        {
            Instance = this;
          
            _playerConfigs = new List<PlayerConfiguration>();
          
        }
    }
    public void SetPlayerColor (int index, Material color)
    {
        _playerConfigs[index].PlayerMaterial = color;
    }

    public void SetTeam(int index, int teamNumber)
    {
        _playerConfigs[index].Team = teamNumber;
    }

    public void ReadyPlayer(int index)
    {
        _playerConfigs[index].IsReady = true;
        if (_playerConfigs.Count >= _minPlayer && _playerConfigs.All(p => p.IsReady == true))
        {
            SceneManager.LoadScene("TopDownScene");
        }
    }

    public void UnreadyPlayer(int index)
    {
        _playerConfigs[index].IsReady = false;
    }

    public void HandlePlayerJoin(UnityEngine.InputSystem.PlayerInput pi)
    {
      
        if(!_playerConfigs.Any(p => p.PlayerIndex == pi.playerIndex))
        {
            pi.transform.SetParent(transform);
            _playerConfigs.Add( new PlayerConfiguration(pi));
          
            _textNumberOfPlayer.GetComponent<PlayerNumberReadyTextScript>().SetPlayerCount(_playerConfigs.Count);
            _textNumberOfJoinedPlayer.GetComponent<TextToJoinScript>().SetJoinedPlayerCount(_playerConfigs.Count);
          
        }

      
      

    }

    public List<PlayerConfiguration> GetPlayerConfigs()
    {

        return _playerConfigs;
    }

    private void Update()
    {
      
    }
 

}

public class PlayerConfiguration
{

    public PlayerConfiguration(UnityEngine.InputSystem.PlayerInput pi)
    {
        PlayerIndex = pi.playerIndex;
        Input = pi;
    }

    public UnityEngine.InputSystem.PlayerInput Input { get; set; }

    public int PlayerIndex { get; set; }

    public bool IsReady { get; set; }

    public Material PlayerMaterial { get; set; }
 
    public int Team { get; set; }

 

}

In second scene, I have the InitializeLevel.cs. This script make the players spwan at different position regarding their team.

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

public class InitializeLevel : MonoBehaviour
{

    [SerializeField] private Transform[] _playerSpawns;

    private int _offSet1 = 0;
    private int _offSet2 = 0;
    private int _offSet3 = 0;
    private int _offSet4 = 0;
    private Vector3 _OffSetVector = new Vector3(3.0f, 0f, 0f);

    [SerializeField] private GameObject _playerPrefab;
    // Start is called before the first frame update
    void Start()
    {
        var playerConfigs = PlayerConfigurationManager.Instance.GetPlayerConfigs().ToArray();
        for (int i = 0; i < playerConfigs.Length; i++)
        {
            Debug.Log("Player Index : " + playerConfigs[i].PlayerIndex + "is from Team : " + playerConfigs[i].Team);
            switch (playerConfigs[i].Team)
            {
                case 1:
                    _offSet1++;
                    var player1 = Instantiate(_playerPrefab, _playerSpawns[playerConfigs[i].Team-1].position + _offSet1 * _OffSetVector, Quaternion.identity, gameObject.transform);
                    player1.GetComponent<PlayerInputHandler>().InitializePlayer(playerConfigs[i]);
                    break;
              
                case 2:
                    _offSet2++;
                    var player2 = Instantiate(_playerPrefab, _playerSpawns[playerConfigs[i].Team-1].position + _offSet2 * _OffSetVector, Quaternion.identity, gameObject.transform);
                    player2.GetComponent<PlayerInputHandler>().InitializePlayer(playerConfigs[i]);
                    break;
              
                case 3:
                    _offSet3++;
                    var player3 = Instantiate(_playerPrefab, _playerSpawns[playerConfigs[i].Team-1].position + _offSet3 * _OffSetVector, Quaternion.identity, gameObject.transform);
                    player3.GetComponent<PlayerInputHandler>().InitializePlayer(playerConfigs[i]);
                    break;
              
                case 4:
                    _offSet4++;
                    var player4 = Instantiate(_playerPrefab, _playerSpawns[playerConfigs[i].Team-1].position + _offSet4 * _OffSetVector, Quaternion.identity, gameObject.transform);
                    player4.GetComponent<PlayerInputHandler>().InitializePlayer(playerConfigs[i]);
                    break;
              
              
              
            }

          
        }

    }

}

And each player have the script “PlayerInputHandler.cs”. This script links the correct information between the player and their character (in-game).So therefore correctly distributes the information contained in the playersConfigs.

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

public class PlayerInputHandler : MonoBehaviour
{
    private PlayerConfiguration _playerConfig;
    private CharacterStateMachine _characterStateMachine;

    [SerializeField] private GameObject _playerMesh;

    private Player_Input _controls;
 

    private void Awake()
    {
      
        _characterStateMachine = GetComponent<CharacterStateMachine>();
        _controls = new Player_Input();
    }

    public void InitializePlayer(PlayerConfiguration pc)
    {
        _playerConfig = pc;
        _playerMesh.GetComponentInChildren<Renderer>().material = pc.PlayerMaterial;
        _playerConfig.Input.onActionTriggered += Input_OnActionTriggered;
    }

    private void Input_OnActionTriggered(InputAction.CallbackContext obj)
    {
        if (obj.action.name == _controls.CharaControls.Move.name)
        {
            OnMove(obj);
        }
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        if (_characterStateMachine != null)
        {
            _characterStateMachine.OnMovementInput(context);
        }
    }
}

It seems that unity NEEDS a player controlled by a keyboard and it NEEDS to be the player 1 (index 0) : "
The Keyboard Tirannie
"

Can someone help me please ?

So it’s a little hard to tell everything that’s going on (one big point is if you have it set to Join Players Manually or not), but a couple things I would look at:

For one thing, there’s more than just the player index alone. There’s the device. I see you’re using Instantiate for the players, but there’s a PlayerInput.Instantiate method as well, in which you pass in the object, scheme, etc… and the device. It might be that the PC picks up the devices in a certain order, and you’re not enforcing that in the 2nd scene.

This between-scene stuff is annoying in Unity, to be sure. I made this tutorial a while back and I think it may be of some help to you:

It’s for a Smash Bros.-style selection screen, but the nuts & bolts center around persisting that controller-player relationship between scenes, so I think it would help you.

Hello @Holonet .

Your scripts helped me understand what’s going on with mine.
So currently, I record the players index, their team, their color (which changes depending on the team) and their input (the inputDevice and controlScheme).
To retrieve its information in the second scene, I use

"playerConfigs = PlayerConfigurationManager.Instance.GetPlayerConfigs().ToArray();"

which is a line found in “initializeLevel.cs”
And “GetPlayerConfigs” function is located in “PlayerconfigurationManager.cs”

public List<PlayerConfiguration> GetPlayerConfigs()
     {
         Debug.Log("GetPlayerConfgis 0: " + _playerConfigs[0].Input);
         Debug.Log("GetPlayerConfgis 0: " + _playerConfigs[0].PlayerMaterial);
         Debug.Log("GetPlayerConfgis 1: " + _playerConfigs[1].Input);
         return _playerConfigs;
     }

But as we can see I call PlayerConfigurationManage.Instance but this instance no longer exists between the two scenes.
However playersConfigs[index].Color returns me the color chosen in scene 1 and the same thing with the Team and the index. It seems that the only information that is lost between the two scenes is the input.
Indeed :
playerConfigs[0].PlayerIndex returns me the correct value
playerConfigs[0].PlayerMaterial returns me the correct value
playerConfigs[0].Team returns me the correct value
playerConfigs[0].Input is NOT returning the correct value to me

I looked in your Holonet script but mine doesn’t change that much from yours except that you save the input in a dictionary and I do in a list (which links the index to the input). But I don’t understand why some data is not erased from one scene to another and why other data is erased…
And how do you, Holonet, make your dictionary keep the data from one scene to another?

So on the topic of preserving between scenes, you can either have an object in the scene and have a script that calls DontDestroyOnLoad(this/whatever), or you can simply make it a static class if you don’t need any of Unity’s game object dross.

So, without diving in too deep, I think your problem is you’re passing in a PlayerInput object, as in the component. I’m not sure if that can work (you would think so, but this is Unity we’re talking about), but it’s not how I did it.

List or dictionary, what I do is store the devices, not the PlayerInput component. When the selection is confirmed, I add the first device of the PlayerInput component like so:

playerControllers.Add(playerIndex, playerInputComponent.devices[0]);

…then when the next scene stats, like I said, there’s a PlayerInput.Instantiate method, not just vanilla Instantiate. I’m not sure how you’re doing it, but I had a PlayerInput component disabled by default on the prefab, then set that to active, then explicitly pair the InputUser to the device. Don’t ask me what the Unity devs were thinking here because I don’t know :hushed:, but probably it’s very extensible and the tradeoff being a PITA to work with. Here’s the code from that project that does that actual pairing:

foreach (var player in PlayerObjectHandler.playerControllers)
{
    var playerController = PlayerObjectHandler.playerControllers[player.Key];
    var playerObjectName = PlayerObjectHandler.playerSelectionNames[player.Key];
    var playerControlScheme = PlayerObjectHandler.playerControlSchemes[player.Key];

    GameObject parentPlayerObject = new GameObject();

    for (int i = 0; i < playerObjectName.Count; i++)
    {
        var currentObject = Resources.Load<GameObject>(playerObjectName[i]);
       
        // Only activate PlayerInput component on the first object (it defines the "player"
        if (i == 0)
        {
            parentPlayerObject = currentObject;
            PlayerInput playerInput = PlayerInput.Instantiate(currentObject, player.Key, playerControlScheme, -1, playerController);
           
            // Activates the player input component on the prefab we just instantiated
            // We have the component disabled by default, otherwise it could not be a "selectable object" independent of the PlayerInput component on the cursor
            // in the selection screen
            currentObject.GetComponent<PlayerControls>().SetPlayerInputActive(true, playerInput);

            //  *** It seems...that the above Instantiation doesn't exactly work... I'm assuming, because the PlayerInput component on the prefab is starting off
            // disabled, that it...doesn't work.  This code here will force it to keep the device/scheme/etc... that we tried to assign the wretch above!
            var inputUser = playerInput.user;
            playerInput.SwitchCurrentControlScheme(playerControlScheme);
            InputUser.PerformPairingWithDevice(playerController, inputUser, InputUserPairingOptions.UnpairCurrentDevicesFromUser);
        }

        // If not the first object (sword/vehicle/etc...) just instantiate, don't associate a PlayerInput
        else
        {
            Instantiate(currentObject, parentPlayerObject.transform);
        }



    }
}

So, notice the PlayerInput component has a user and a device, and the PlayerInput.Instantiate method passes in the DEVICE. Also, the InputUser, I manually pair with the device. As commented, I think that’s necessary in my case because I have the PlayerInput component disabled by default. This is so I could have the game object be in the scene for selection, but not be associated with a player until I wanted it to be.

Ok thanks a lot @Holonet ! I’ve understand my issue and why it didn’t work = Player input can’t be passed from one scene to another as simple as other variable. Using the controlScheme is one solution; using don’t destroy on load is the second one.
I’ve use some lines of your scripts so thanks a lot.

1 Like