Detecting The Player’s Controller Type With the Unity Input System

Hello all, this is a walkthrough of my solution for detecting the device that the player is using in a single player “gamepad” based game. In my specific case, I’m using this information to have my UI constantly represent the correct “controls” UI for the type of device my player is using. At the time of writing this, my system is specifically build to sort between PlayStation & Xbox controllers, with Xbox being the default. With that said, this system is easily extendable, and I hope that will be clear through my code & explanations. This guide is meant to help those who have routed all of their input beneath the “game pad” umbrella in the Unity Input System, but still want the specific type of controller used by the player to be accessible.

This post is using the “New” Unity Input System, NOT the Unity Legacy Input

I ran into a lot of very interesting road blocks in my quest to create an abstracted UI Image system which would live react to the type of controller being used by the player…. It sounds funny, because you would think it would be simple to detect the type of controller being used by the player, but the way that the Unity Input System deals with game pad style controllers seems to do a pretty good job of burying this data.

There were 2 ways which I thought I could approach this. The first would be to go into my actual Input Map and specify control schemes by controller type, and then have a manager which detects input from any/all control schemes. and switches state based upon the most recent input. I chose not to do this because, in my opinion, one of the biggest appeals of the Unity Input System is that it supports the most common controller types all beneath the gamepad umbrella.

It’s important to note that in my project, I have my PlayerInput Behavior set to Invoke C# Events.

My first step in this system was listening in to the InputSystem.onDeviceChange, which must be assigned to a function which takes an InputDevice and InputDeviceChange. This event will fire off every time a change to device is detected. These include a controller being: added, disconnected, removed, & reconnected. InputDeviceChange represents which one of these events was detected. Here’s what those look like:

Note: _currentController is just a basic enum meant to internally store the controller state

    private void GameManagerRegisterInput()
    {
        //Binds onDeviceChange event to InputDeviceChanged
        InputSystem.onDeviceChange += InputDeviceChanged;

    }

    //Method called  when a device change event is fired
    private void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                

                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                

                break;
                
            //Else
            default:
                break;
        }
    }//Method called  when a device change event is fired
    public void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment" && _currentController != CurrentControllerType.PlayStation)
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    _currentController = CurrentControllerType.PlayStation;
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else if(device.description.manufacturer != "Sony Interactive Entertainment" && _currentController != CurrentControllerType.Xbox)
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    _currentController = CurrentControllerType.Xbox;
                    controllerTypeChange.Invoke();
                }
                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                _currentController = CurrentControllerType.Other;
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment" && _currentController != CurrentControllerType.PlayStation)
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    _currentController = CurrentControllerType.PlayStation;
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else if(device.description.manufacturer != "Sony Interactive Entertainment" && _currentController != CurrentControllerType.Xbox)
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    _currentController = CurrentControllerType.Xbox;
                    controllerTypeChange.Invoke();
                }
                break;
                
            //Else
            default:
                break;
        }
    }    private void GameManagerRegisterInput()
    {
        //Binds onDeviceChange event to InputDeviceChanged
        InputSystem.onDeviceChange += InputDeviceChanged;

    }

    //Method called  when a device change event is fired
    private void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                

                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                

                break;
                
            //Else
            default:
                break;
        }
    }

While this was the first step I took, I was working backwards a bit. With this event being listened to, we can react to new controllers being connected, to controllers losing connection, and a bunch of other events. These events usually represent a reason to re-evaluate the current images being displayed for controls.

I wrote a fairly simple Scriptable Object which holds all the textures we will be using in our “controls” UI. So this scriptable objects holds all the images used to represent xbox and playstation controls. Additionally, it has a set of private textures which hold the “current” image for that button. Through this abstraction, I can have the Scriptable Object flip it’s set of “current” images, and have all UI read from this ScriptableObject for the image it needs to display.

Now that I had the foundation, and a way to store/represent the “controller type” state, now I just needed to determine what type of controller was being connected/added, and pass that state on to my Scriptable Object. Using the API for the type Device, which is passed on this event as a direct reference to the Device that instigated the event, I was able to determine that the field description has sub fields which are used to define the specific type of controller, the manufacturer, and more. The field product is a string which would come out something like “Dual Shock 4” or “Xbox One Elite”. While this is certainly useful, I wanted things to remain as generic as they could be. The manufacturer proved to be the most abstracted I could get, while still distinguishing between the specific types of gamepads. However, herein lies the first issue I encountered. While the device.description.manufacturer field on a Playstation controller returns a nice & neat “Sony Interactive Entertainment”, that same field for the Xbox controller is entirely empty. Since I only have to support the 2 types, I wrote these conditions into an if/else, and called it a day. But as I extend the system to include more controller types, I would make use of the device.description.product and distinguish between the remaining types of controllers. Here’s what that same chunk from above looks like with these cases written in.

A note: currentImageScheme is my Scriptable Object described above. This scriptable object provides the texture to raw image prefabs in my UI. controllerTypeChange is an event which is listened to by the UI. The UI will react to this event by setting their texture to the “current” texture stored in the ScriptableObject

//Method called  when a device change event is fired
    public void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment")
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    controllerTypeChange.Invoke();
                }
                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment")
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    controllerTypeChange.Invoke();
                }
                break;
                
            //Else
            default:
                break;
        }
    }

So remember when I said I was working backwards? At this point, my system detects and reacts to big controller events, but at the start of the program has no idea what controller is being used. This is where I encountered the majority of my hardship, but I’m very proud of the end result!

Something I learned quickly is that the Unity Input System stores all known input devices, regardless of them being currently connected or not. So if my player is playing on their xbox controller, it’s batteries die, and they switch to their PS controller, InputSystem.devices still stores both devices. Additionally, while the type Device has a property enabled, this returns true for all devices registered to the Unity Input System. So all of this results in perhaps the most notable roadblock I encountered with this System: I could not find a way in which the Unity Input System distinguishes between the currently connected/in use controllers and those which are simply known by the Unity Input System, ie not in use at all. Devices can easily be individually assigned, and switched between, and detected, but there is no property of the Type Device (that I could find) which represents whether an individual device is actively connected. I pray I’m wrong about that, but the closest I could get was InputDevice.lastUpdateTime, which InputSystem inherits from. Even that field returns extremely unreliable values, as on PS it counts gyroscopic data as input, every frame, and in my tests, was returning the same value for all my devices, connected or not.

TlDr; I could not find a way to distinguish between connected and non connected input devices.

My response to this, however, is where I’m most proud of this code! I found that if I remove all the stored devices on game start, the controller being used by the player instantly re-constructs itself, and is quickly the only device stored in InputSystem.devices[]. From here, it’s easy to just check InputSystem.devices[0].manufacturer, and we have the same check on game start as we do when a controller event happens. Here’s that code!

Once again, currentImageScheme is my SO

//Called in GameManagerGameStart() to set the UI initially
    private void UIImageSchemeInitialSet()
    {
        //Disables all devices currently read by InputSystem
        for (int rep = 0; rep < InputSystem.devices.Count - 1; rep++)
        {
            InputSystem.RemoveDevice(InputSystem.devices[rep]);
        }

        if (InputSystem.devices[0] == null) return;
        
        //Checks the first slot of the InputSystem devices list for controller type
        if (InputSystem.devices[0].description.manufacturer == "Sony Interactive Entertainment")
        {
            //Sets UI scheme to PS
            Debug.Log("Playstation Controller Detected");
            currentImageScheme.SetImagesToPlaystation();
            _currentController = CurrentControllerType.PlayStation;
            controllerTypeChange.Invoke();
        }
        else
        {
            //Sets UI scheme to XB
            Debug.Log("Xbox Controller Detected");
            currentImageScheme.SetImagesToXbox();
            _currentController = CurrentControllerType.Xbox;
            controllerTypeChange.Invoke();
        }
    }

From there, to complete my system, all I had to write was a MonoBehaviour which stores a reference to my SO, and listens to my controllerTypeChange event, reacting by just setting its image to the corresponding image stored in the Scriptable Object. Here’s that code, for those who are curious!

public class ControllerImageScript : MonoBehaviour
{
    public UIImageSchemeSO uiImageScheme;
    
    public enum buttonImage
    {
        southButton,
        eastButton,
        northButton,
        westButton,
        rightTrigger,
        leftTrigger
    }

    public buttonImage myButtonImage;

    private RawImage myImage;
    
    // Start is called before the first frame update
    void Start()
    {
        myImage = gameObject.GetComponent<RawImage>();
        SetButtonImage();
    }

    public void SetButtonImage()
    {
        if (myButtonImage == buttonImage.southButton)
        {
            myImage.texture = uiImageScheme.GetSouthButton();
        }
        else if (myButtonImage == buttonImage.eastButton)
        {
            myImage.texture = uiImageScheme.GetEastButton();
        }
        else if (myButtonImage == buttonImage.northButton)
        {
            myImage.texture = uiImageScheme.GetNorthButton();
        }
        else if (myButtonImage == buttonImage.westButton)
        {
            myImage.texture = uiImageScheme.GetWestButton();
        }
        else if (myButtonImage == buttonImage.leftTrigger)
        {
            myImage.texture = uiImageScheme.GetLeftTrigger();
        }
        else if (myButtonImage == buttonImage.rightTrigger)
        {
            myImage.texture = uiImageScheme.GetRightTrigger();
        }
    }
}

I spent all day on this code, and I found it a surprisingly underdocumented topic. I hope that this proves helpful to those who are in my situation, just as all the wonderful discussions/threads online helped me!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s