In an era where online games dominate the multiplayer market, the simple joy of huddling around the family PC with your siblings and friends, and playing a local multiplayer game on a single, shared keyboard seems like a distant memory, but at Not A Robot, we’re in the business of joy, and old-school is the new cool.
Today, we’re going to solve a problem with Unity’s Input System package that has left many stumped, and led others to implement a mish-mash of weird and wonderful workarounds.
You can find the GitHub gist for the classes created in this post here.
What’s The Problem?
Implementing local multiplayer is simple in Unity, with the PlayerInputManager component. This component is part of the ‘new’ Input System package, which replaces the built-in input system. It allows you to quickly set up a player prefab and define a ‘join game’ mechanism, and it takes care of split-screen behaviour.
It’s an incredibly useful component, and for most use cases, it’s perfectly suitable.
Unfortunately, Unity’s developers made a very modern assumption - that there’s a 1:1 relationship between connected devices and players - which is decidedly un-retro of them.
The problem is as follows: when a player is added to the system via one of the automatic Join Behaviour mechanisms, the PlayerInputManager does one of the following:
If the player is joining because they pressed a button on an unassigned device, then that device is registered to the new player, which means the device is now assigned, and will no longer trigger new player joins.
If the player joins via an input action, then the system checks whether the device triggering this action has already been assigned to a player. If it hasn’t, then the device is assigned to the new player, but if it is, then the join request is ignored.
In both of these cases, the device that triggered the join request is prevented from ever triggering another join request, so it’s simply not possible to share one device between multiple players using the automatic joining method.
In most cases, this isn’t a problem. Many, if not most, games today support controllers, and you can support a wide range of controllers and input devices easily with the Input System package. It’s not completely unreasonable to expect your players to use separate controllers to play your game, but back in my day, before USB was even invented, we had to share a single keyboard, and we were grateful.
Let’s Get Lazy
Like all good programmers, you want to do the least work possible in order to achieve your goals. You could implement dual user keyboard and split-screen support yourself relatively easily, but that feels a little like going against the grain, and manually triggering the player join via code requires a lot of additional management of devices, player IDs, and more. We’re lazy - we’re not doing all of that rubbish!
As much as possible, we want to lean on the work the Unity developers have already done for us, and simply add support for shared devices into PlayerInputManager - which is the recommended way to handle players joining and leaving your game.
Thankfully, this is actually a very easy process, and though the solution is not ideal, it’s a fairly painless way to work around PlayerInputManager’s limitations.
Assuming you’re already using the Input System package, we first need to copy the package’s directory out of your project’s Library/PackageCache folder, and dump it into your Packages folder instead:
This allows us to make a few tiny modifications to PlayerInputManager, letting us bypass the device registration check when an Input Action is performed.
Open your project in your IDE of choice, and find PlayerInputManager.cs.
Here, find the JoinPlayerFromActionIfNotAlreadyJoined method, and make it virtual:
This will let us override this method in a custom class a little later.
While you’re in PlayerInputManager.cs, also find the CheckIfPlayerCanJoin method, and make it protected.
Now that we’ve modified PlayerInputManager, we can create a new class, which inherits from it, and overrides the JoinPlayerFromActionIfNotAlreadyJoined method.
The key here is to intercept incoming players who would be assigned the keyboard, and skip the check that would ordinarily prevent it from being used by more than one player.
We also add an additional step - RebindPlayer - which lets us map a particular control scheme onto the incoming player. For example, we might want our game to support two players, with one player using the WASD keys on the keyboard, and the other using the directional arrows:
And that’s pretty much all there is to it!
Let’s assign our new component in the editor:
Well, functionally we’ve got everything we need, but since we’re now using an inherited class, we’ve lost the nice inspector layout we see on the standard PlayerInputManager component, so let’s quickly fix that up.
Back in your IDE, find PlayerInputManagerEditor.cs and change it from internal to public:
Now, create a class in an Editor folder in your project called SharedDeviceInputManagerEditor. All we need to do here is tell Unity to draw the inspector for a PlayerInputManager instead of a SharedDeviceInputManager:
Now we flick back over to Unity, and take another look at our SharedDeviceInputManager component:
And finally, we’re done! This component can now be configured the same way we would the standard PlayerInputManager component from earlier, but now, instead of mandating a 1:1 relationship between a device and a player, we allow the keyboard to be shared. All other functionality should be as expected - split-screen will automatically work, and your player instances will receive the correct input events based on their control scheme, even if the players are sharing the keyboard.
Bring It All Together
Now all that’s left to do is test. Assign a Join Action Reference and a Player Prefab to your SharedDeviceInputManager, and make sure you have two control schemes in your Input Actions Asset: ‘WASD’ and ‘Arrows’.
Make sure your Player Prefab has a Player Input component, and that the correct Actions asset is assigned:
Obviously, your player prefabs will need to actually do something with whatever input they receive, and that’s beyond the scope of this guide, but, all being well, you should shortly be in a position to enter play mode and try out your new, dual user keyboard capabilities!
With a little extra work, and with the help of some lovely free assets from Kenney - behold, a cosy, 2-player split-screen gladiator game, with both players sharing a single keyboard:
Conclusion
In this guide, we’ve seen how we can lightly modify the Input System package to give ourselves additional functionality when building a local multiplayer game. The solution shown here isn’t ideal: we would much prefer not to have to modify the Input System package at all to ensure we can upgrade to any future version with ease, but the modifications required are minimal, and we’re still able to take advantage of existing functionality provided by the package.
Though the market may not exactly be screaming out for games that support multiple users on a single keyboard, it does seem a shame that the current version of the PlayerInputManager doesn’t support it by default. Local multiplayer today may well be neglected in the mainstream, in the shadow of huge online games, but there’s nothing quite like the enjoyment you can get from playing together in the same physical space.
You can find the GitHub gist for the classes created in this post here.
I hope you’ve found this guide helpful, and if it’s given you any ideas for your next game, then do let me know!
If you’ve enjoyed this post, check out my previous Unity tutorial, and feel free to subscribe and share with your community!