Human Interface Device (HID) is a specification to describe peripheral user input devices connected to computers via USB or Bluetooth. HID is commonly used to implement devices such as gamepads, joysticks, or racing wheels.
The Input System directly supports HID (connected via both USB and Bluetooth) on Windows, MacOS, and the Universal Windows Platform (UWP). The system might support HID on other platforms, but not deliver input through HID-specific APIs. For example, on Linux, the system supports gamepad and joystick HIDs through SDL, but doesn't support other HIDs.
Every HID comes with a device descriptor. To browse through the descriptor of an HID from the Input Debugger, click the HID Descriptor button in the device debugger window. To specify the type of the device, the HID descriptor reports entry numbers in the HID usage tables, and a list of all controls on the device, along with their data ranges and usages.
The Input System handles HIDs in one of two ways:
By default, the Input System creates layouts and Device representations for any HID which reports its usage as GenericDesktop/Joystick
, GenericDesktop/Gamepad
, or GenericDesktop/MultiAxisController
(see the HID usage table specifications for more information). To change the list of supported usages, set HIDSupport.supportedHIDUsages
.
When the Input System automatically creates a layout for an HID, it always reports these Devices as Joysticks
, represented by the Joystick
device class. The first elements with a reported HID usage of GenericDesktop/X
and GenericDesktop/Y
together form the joystick's stick
Control. The system then adds Controls for all further HID axis or button elements, using the Control names reported by the HID specification. The Input System assigns the first control with an HID usage of Button/Button 1
to the joystick's trigger
Control.
The auto-generated layouts represent a "best effort" on the part of the Input System. The way Human Interface Devices describe themselves in accordance with the HID standard is too ambiguous in practice, so generated layouts might lead to Controls that don't work as expected. For example, while the layout builder can identify hat switches and D-pads, it can often only make guesses as to which direction represents which. The same goes for individual buttons, which generally aren't assigned any meaning in HID.
The best way to resolve the situation of HIDs not working as expected is to add a custom layout, which bypasses auto-generation altogether. See Overriding the HID fallback for details.
HIDs can support output (for example, to toggle lights or force feedback motors on a gamepad). Unity controls output by sending HID Output Report commands to a Device. Output reports use Device-specific data formats. To use HID Output Reports, call InputDevice.ExecuteCommand
to send a command struct with the typeStatic
property set as "HIDO"
to a Device. The command struct contains the Device-specific data sent out to the HID.
Often, when using the layouts auto-generated for HIDs, the result isn't ideal. Controls don't receive proper names specific to the Device, some Controls might not work as expected, and some Controls that use vendor-specific formats might not appear altogether.
The best way to deal with this is to set up a custom Device layout specifically for your Device. This overrides the default auto-generation and gives you full control over how the Device is exposed.
The following example assumes that the Input System doesn't already have a custom layout for the PS4 DualShock controller, and that you want to add such a layout.
In this example, you want to expose the controller as a Gamepad
and you roughly know the HID data format used by the Device.
Tip: If you don't know the format of a given HID you want to support, you can open the Input Debugger with the Device plugged in and pop up both the debugger view for the Device and the window showing the HID descriptor. Then, you can go through the Controls one by one, see what happens in the debug view, and correlate that to the Controls in the HID descriptor. You can also double-click individual events and compare the raw data coming in from the Device. If you select two events in the event trace, you can then right-click them and choose Compare to open a window that shows only the differences between the two events.
The first step is to describe in detail what format that input data for the Device comes in, as well as the InputControl
instances that should read out individual pieces of information from that data.
The HID input reports from the PS4 controller look approximately like this:
struct PS4InputReport
{
byte reportId; // #0
byte leftStickX; // #1
byte leftStickY; // #2
byte rightStickX; // #3
byte rightStickY; // #4
byte dpad : 4; // #5 bit #0 (0=up, 2=right, 4=down, 6=left)
byte squareButton : 1; // #5 bit #4
byte crossButton : 1; // #5 bit #5
byte circleButton : 1; // #5 bit #6
byte triangleButton : 1; // #5 bit #7
byte leftShoulder : 1; // #6 bit #0
byte rightShoulder : 1; // #6 bit #1
byte leftTriggerButton : 2;// #6 bit #2
byte rightTriggerButton : 2;// #6 bit #3
byte shareButton : 1; // #6 bit #4
byte optionsButton : 1; // #6 bit #5
byte leftStickPress : 1; // #6 bit #6
byte rightStickPress : 1; // #6 bit #7
byte psButton : 1; // #7 bit #0
byte touchpadPress : 1; // #7 bit #1
byte padding : 6;
byte leftTrigger; // #8
byte rightTrigger; // #9
}
You can translate this into a C# struct:
// We receive data as raw HID input reports. This struct
// describes the raw binary format of such a report.
[StructLayout(LayoutKind.Explicit, Size = 32)]
struct DualShock4HIDInputReport : IInputStateTypeInfo
{
// Because all HID input reports are tagged with the 'HID ' FourCC,
// this is the format we need to use for this state struct.
public FourCC format => new FourCC('H', 'I', 'D');
// HID input reports can start with an 8-bit report ID. It depends on the device
// whether this is present or not. On the PS4 DualShock controller, it is
// present. We don't really need to add the field, but let's do so for the sake of
// completeness. This can also help with debugging.
[FieldOffset(0)] public byte reportId;
// The InputControl annotations here probably look a little scary, but what we do
// here is relatively straightforward. The fields we add we annotate with
// [FieldOffset] to force them to the right location, and then we add InputControl
// to attach controls to the fields. Each InputControl attribute can only do one of
// two things: either it adds a new control or it modifies an existing control.
// Given that our layout is based on Gamepad, almost all the controls here are
// inherited from Gamepad, and we just modify settings on them.
[InputControl(name = "leftStick", layout = "Stick", format = "VC2B")]
[InputControl(name = "leftStick/x", offset = 0, format = "BYTE",
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "leftStick/left", offset = 0, format = "BYTE",
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0,clampMax=0.5,invert")]
[InputControl(name = "leftStick/right", offset = 0, format = "BYTE",
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0.5,clampMax=1")]
[InputControl(name = "leftStick/y", offset = 1, format = "BYTE",
parameters = "invert,normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "leftStick/up", offset = 1, format = "BYTE",
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0,clampMax=0.5,invert")]
[InputControl(name = "leftStick/down", offset = 1, format = "BYTE",
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0.5,clampMax=1,invert=false")]
[FieldOffset(1)] public byte leftStickX;
[FieldOffset(2)] public byte leftStickY;
[InputControl(name = "rightStick", layout = "Stick", format = "VC2B")]
[InputControl(name = "rightStick/x", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "rightStick/left", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0,clampMax=0.5,invert")]
[InputControl(name = "rightStick/right", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0.5,clampMax=1")]
[InputControl(name = "rightStick/y", offset = 1, format = "BYTE", parameters = "invert,normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "rightStick/up", offset = 1, format = "BYTE", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0,clampMax=0.5,invert")]
[InputControl(name = "rightStick/down", offset = 1, format = "BYTE", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp,clampMin=0.5,clampMax=1,invert=false")]
[FieldOffset(3)] public byte rightStickX;
[FieldOffset(4)] public byte rightStickY;
[InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4, defaultState = 8)]
[InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=7,maxValue=1,nullValue=8,wrapAtValue=7", bit = 0, sizeInBits = 4)]
[InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=1,maxValue=3", bit = 0, sizeInBits = 4)]
[InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=3,maxValue=5", bit = 0, sizeInBits = 4)]
[InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=5, maxValue=7", bit = 0, sizeInBits = 4)]
[InputControl(name = "buttonWest", displayName = "Square", bit = 4)]
[InputControl(name = "buttonSouth", displayName = "Cross", bit = 5)]
[InputControl(name = "buttonEast", displayName = "Circle", bit = 6)]
[InputControl(name = "buttonNorth", displayName = "Triangle", bit = 7)]
[FieldOffset(5)] public byte buttons1;
[InputControl(name = "leftShoulder", bit = 0)]
[InputControl(name = "rightShoulder", bit = 1)]
[InputControl(name = "leftTriggerButton", layout = "Button", bit = 2)]
[InputControl(name = "rightTriggerButton", layout = "Button", bit = 3)]
[InputControl(name = "select", displayName = "Share", bit = 4)]
[InputControl(name = "start", displayName = "Options", bit = 5)]
[InputControl(name = "leftStickPress", bit = 6)]
[InputControl(name = "rightStickPress", bit = 7)]
[FieldOffset(6)] public byte buttons2;
[InputControl(name = "systemButton", layout = "Button", displayName = "System", bit = 0)]
[InputControl(name = "touchpadButton", layout = "Button", displayName = "Touchpad Press", bit = 1)]
[FieldOffset(7)] public byte buttons3;
[InputControl(name = "leftTrigger", format = "BYTE")]
[FieldOffset(8)] public byte leftTrigger;
[InputControl(name = "rightTrigger", format = "BYTE")]
[FieldOffset(9)] public byte rightTrigger;
[FieldOffset(30)] public byte batteryLevel;
}
Next, you need an InputDevice
to represent your device. Because you're dealing with a gamepad, you must create a new subclass of Gamepad
.
For simplicity, this example ignores the fact that there is a DualShockGamepad
class that the actual DualShockGamepadHID
is based on.
// Using InputControlLayoutAttribute, we tell the system about the state
// struct we created, which includes where to find all the InputControl
// attributes that we placed on there. This is how the Input System knows
// what controls to create and how to configure them.
[InputControlLayout(stateType = typeof(DualShock4HIDInputReport)]
public DualShock4GamepadHID : Gamepad
{
}
The last step is to register your new type of Device and set up the Input System so that when a PS4 controller is connected, the Input System generates your custom Device instead of using the default HID fallback.
This only requires a call to InputSystem.RegisterLayout<T>
, giving it an InputDeviceMatcher
that matches the description for a PS4 DualShock HID. In theory, you can place this call anywhere, but the best point for registering layouts is generally during startup. Doing so ensures that your custom layout is visible to the Unity Editor and therefore exposed, for example, in the Input Control picker.
You can insert your registration into the startup sequence by modifying the code for your DualShock4GamepadHID
Device as follows:
[InputControlLayout(stateType = typeof(DualShock4HIDInputReport)]
#if UNITY_EDITOR
[InitializeOnLoad] // Make sure static constructor is called during startup.
#endif
public DualShock4GamepadHID : Gamepad
{
static DualShock4GamepadHID()
{
// This is one way to match the Device.
InputSystem.RegisterLayout<DualShock4GamepadHID>(
new InputDeviceMatcher()
.WithInterface("HID")
.WithManufacturer("Sony.+Entertainment")
.WithProduct("Wireless Controller"));
// Alternatively, you can also match by PID and VID, which is generally
// more reliable for HIDs.
InputSystem.RegisterLayout<DualShock4GamepadHID>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment.
.WithCapability("productId", 0x9CC)); // Wireless controller.
}
// In the Player, to trigger the calling of the static constructor,
// create an empty method annotated with RuntimeInitializeOnLoadMethod.
[RuntimeInitializeOnLoadMethod]
static void Init() {}
}
Your custom layout now picks up any Device that matches the manufacturer and product name strings, or the vendor and product IDs in its HID descriptor. The Input System now represents a DualShock4GamepadHID
Device instance.
For more information, you can also read the Device matching documentation.