I followed Koru’s lead and managed to have local multiplayer without using PlayerInput or PlayerInputManager.
Here is the relevant code for my mutliplayer Tetris clone.
To see the code in context (in a working project) check out the source.
Keep in mind that the Tetris class here basically is the player.
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Users;
public class Global : MonoBehaviour
{
public GameObject Tetrisprefab;
private bool executingGameplay = false;
[SerializeField] GameObject GameOverScreen;
[SerializeField] GameObject JoinScreen;
[SerializeField] GameObject ReadyScreen;
[SerializeField] Camera camera;
Controls Controls;
[SerializeField] int MaxPlayers = 3;
private List<Tetris> players;
private List<Tetris> newplayers;
void Awake()
{
ReadyScreen.SetActive( false );
GameOverScreen.SetActive( false );
JoinScreen.SetActive( true );
players = new List<Tetris>();
newplayers = new List<Tetris>();
// my IInputActionCollection
Controls = new Controls();
// you must enable
Controls.Enable();
// bind controls that any/every device can use
Controls.Global.StartGame.performed += context => StartGame();
// assign the callback for listening
InputUser.onUnpairedDeviceUsed += OnUnpairedDeviceUsed;
// listening will not start until InputUser.listenForUnpairedDeviceActivity > 0
BeginJoining();
}
private void OnDestroy()
{
// Avoid lingering calls after play mode has ended!!!
InputUser.onUnpairedDeviceUsed -= OnUnpairedDeviceUsed;
}
void OnUnpairedDeviceUsed( InputControl control, InputEventPtr eventPtr )
{
// Ignore anything but button presses.
if( !( control is ButtonControl ) )
return;
Debug.Log( "Unpaired device detected" + control.device.displayName );
// get a new InputUser, now paired with the device
InputUser user = InputUser.PerformPairingWithDevice( control.device );
// Create a new instance of input actions to prevent InputUser from triggering actions on another InputUser.
Controls controlsForThisUser = new Controls();
// you must enable the controls to use them
controlsForThisUser.Enable();
// the real work is done for us in InputUser
user.AssociateActionsWithUser( controlsForThisUser );
GameObject go = Instantiate( Tetrisprefab );
Tetris tetris = go.GetComponent<Tetris>();
// store the InputUser so you can unpair later
tetris.user = user;
// initialize your script with the new controls
tetris.BindControls( controlsForThisUser.Gameplay );
// You cannot do things like set the parent transform in this callback, so
// add the game object to a list of new players for processing in Update()
newplayers.Add( tetris );
InputUser.listenForUnpairedDeviceActivity--;
if( InputUser.listenForUnpairedDeviceActivity == 0 )
{
EndJoining();
ReadyScreen.SetActive( true );
}
}
void BeginJoining()
{
InputUser.listenForUnpairedDeviceActivity = MaxPlayers;
JoinScreen.SetActive( true );
}
void EndJoining()
{
InputUser.listenForUnpairedDeviceActivity = 0;
JoinScreen.SetActive( false );
}
void GameOver( Tetris loser )
{
executingGameplay = false;
GameOverScreen.SetActive( true );
Controls.Disable();
// Timer class not shown here, see the full source
new Timer( this, 3, null, () =>
{
Controls.Enable();
GameOverScreen.SetActive( false );
foreach( var player in players )
Destroy( player.gameObject );
players.Clear();
BeginJoining();
} );
}
void StartGame()
{
if( executingGameplay )
return;
EndJoining();
executingGameplay = true;
GameOverScreen.SetActive( false );
ReadyScreen.SetActive( false );
foreach( var game in players )
game.Reset();
}
void Update()
{
// add new players
foreach( var player in newplayers )
{
// this is the reason for the newplayers list. This cannot be called in
// the onpairedDeviceUsed callback above
player.transform.parent = transform;
players.Add( player );
}
newplayers.Clear();
if( executingGameplay )
for( int i = 0; i < players.Count; i++ )
players[i].ManualUpdate();
}
}
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Users;
// NOT using the callback interface because I couldn't figure out to bind it properly
public class Tetris : MonoBehaviour //, Controls.IGameplayActions
{
public InputUser user;
public Controls.GameplayActions gameplayActions;
// Controls the board with player input or AI using this struct
public struct InputStruct
{
public bool right;
public bool left;
public bool down;
public bool rotCW;
public bool rotCCW;
}
InputStruct inputState;
private void OnDestroy()
{
user.UnpairDevices();
}
public void BindControls( Controls.GameplayActions iam )
{
gameplayActions = iam;
// Bind discrete events using C# delegates
iam.RotateCW.started += ( obj ) => { inputState.rotCW = true; };
iam.RotateCCW.started += ( obj ) => { inputState.rotCCW = true; };
}
public void ManualUpdate()
{
// Use polling for continuous player actions if appropriate
inputState.down = gameplayActions.Down.phase == InputActionPhase.Started;
inputState.left = gameplayActions.Left.phase == InputActionPhase.Started;
inputState.right = gameplayActions.Right.phase == InputActionPhase.Started;
// HandleInput( inputState )
}
// NOT using the SendMessage() technique
//public void OnRight( Context ){}
}