Multiplayer Controller Input Manager

Since Skeletons In Hats is a local multiplayer game, I need a system in place to handle multiple controllers and assign them to players. Some features I want this system to include are the ability to handle multiple kinds of controllers(both PlayStation and Xbox), the ability to handle controllers connecting and disconnecting, and to be able to assign controllers to specific controllable entities based on the player they belong to. I also want to work on tuning the input on the analog sticks of controllers by creating special dead zones.

My plan is to make a controller objects that will handle the controller input, a multiplayer controller manager that will handle giving controllers to players, a player object that will handle giving its assigned controller to controllable entities, and a character input component which will allow objects to be assigned a controller and allow other components to read the input of that specific controllable entity.

ControllerDiagram.png

Joystick Keycodes

Before getting started with the controllers, I needed an easier way to deal with axes. In Unity, a common way to get input for buttons is through a line of code such as Input.GetKey(KeyCode.somekey). Unity does not have any equivalent to this for axes such as joysticks and the mouse. Instead, you must set up an axis in the Unity input manager, give it a name, and use a line such as Input.GetAxis(“someaxis”). The main issue with this is you can’t get any sort of auto complete for your axis and must remember the string name for every axis you set up.

Setting up axis codes isn’t very difficult, so this section will be brief. First you set up all your axes in the Unity Input Manager like normal, making sure to give them intuitive names. The AxisCode script just consists of properties for each axis such as public static string Joystick2AxisX {get{return "Joystick2AxisX";}}. After all the AxisCodes are set up, I can use AxisCodes just like KeyCodes, making input much more consistent. Here is an example of using AxisCodes; Input.GetAxis(AxisCode.Joystick1AxisX).


Controllers

Since I want to support multiple types of controllers, I will have a controller class, and sub classes for every controller I want to support. The controller will have Keycode variables for every button, and string variables for every axis. The subclasses decide what these variables are initialized as. The controller class has properties for each of these buttons that returns the correct input, for example; public bool faceButton1Down { get{return Input.GetKeyDown(face1);}}, where face1 is initialized in the subclass PS4 using the line; face1 = KeyCode.Joystick1Button1.

Setting up axes requires a bit more work. For these properties, they will return a Vector2 containing both the X and Y axis. Similarly to the buttons, I start off by getting the values using Vector2 stickInput = new Vector2(Input.GetAxisRaw(leftStickX), Input.GetAxisRaw(leftStickY)). Then I just return stickInput, right? Nope. Due to how Unity handles axes, if I point the joystick to the top right, it will return (1,1), which has a magnitude greater than 1. If I point the stick directly to the right, it returns (1,0). This means that Unity is treating the joystick like it has a square of movement space, rather than a circle. This is actually an easy fix, simply normalize the vector if the magnitude is greater than 1.

I could leave it at that and return, but I also want to add a deadzone to this. First, I give controller a float variable, deadZoneLeft. This is the deadzone for the left joystick, after later testing I eventually decided to make this 0.3. If the magnitude of the input is less than the deadzone, return Vector2.zero. If the value is not less than the deadzone, I need to set it up so the magnitude starts at zero when it is at the deadzone, instead of it starting at 0.3. To do this, I get the direction of the stick, dir = stickInput.normalized. The new magnitude will be ((stickInput.magnitude-deadZoneLeft)/(1-deadZoneLeft)). If the magnitude is 1, the numerator and denominator will be equal and evaluate to 1. If the the magnitude is the deadzone, the numerator will be 0 and evaluate to 0. Now I just multiply the direction by thge new magnitude and return.

That is enough for a simple stick input, but after reading this article, I decided I wanted to implement something a bit more impressive. The bottom of the article mentions that some FPS games use what they call the “bowtie” deadzone. Basically it makes it easier to look in a straight line horizontally and vertically without having slight imperfections in direction through you off.

To start, I duplicated the code I just made and changed the name, this way I can still get the stick value without the “bowtie” smoothing applied to it. I added a float variable maxBowtieDeadZone, which will be used per axis. I need to scale this value so it is less effective when the joystick is closer to the center, float bowTieValue = maxBowtieDeadZone * mag. Since the deadzone gets smaller toward the center, you can visualize this looking like a bowtie. Starting with the x axis, if the x value is less than bowTieValue, set the dir.x = 0. Otherwise, dir.x = dir.x * ((Mathf.Abs(dir.x)-bowTieValue)/(1-bowTieValue)). This is the same math used for the previous deadzone, except I are applying it to 1 axis. After doing the same with dir.y, I normalize the direction and return the input the old magnitude * the new direction.

Players and Controllables

After finishing the Controller class and setting up 1 or 2 controller subclasses, I created the Player class. A player has a Controller, Controllable, a player number, and a boolean for weather or not they are playing. The Player constructor takes a controller and player number as parameters, and sets the Controllable to null and isPlaying boolean to false. The player also has public functions for giving the player a new Controller and giving the player a new Controllable.

The Controllable has Controller and a Player. It contains public functions ConnectToPlayer() and DisconnectFromPlayer(). When a player is given a controllable, the ConnectToPlayer() function is called, so that the controllable has a reference to the Player and the Controller. Here is an example of how this is eventually used in my JumpComponent, if(myInput.myController.faceButton1Down){//jump}, where myInput is a controllable. I am not a fan of the name, “Controllable”, and will probably rename it to “InputComponent” sometime down the road.

Manager

Looking back at the diagram I made, the only thing left is the Manager. [Section in progress]