Physically, Input Devices represent devices attached to the computer, which a user can use to control the app. Logically, Input Devices are the top-level container for Controls. The InputDevice
class is itself a specialization of InputControl
. See supported Devices to see what kind of Devices the Input System currently supports.
To query the set of all currently present Devices, you can use InputSystem.devices
.
An InputDeviceDescription
describes a Device. The Input System uses this primarily during the Device discovery process. When a new Device is reported (by the runtime or by the user), the report contains a Device description. Based on the description, the system then attempts to find a Device layout that matches the description. This process is based on Device matchers.
After a Device has been created, you can retrieve the description it was created from through the InputDevice.description
property.
Every description has a set of standard fields:
Field | Description |
---|---|
interfaceName |
Identifier for the interface/API that is making the Device available. In many cases, this corresponds to the name of the platform, but there are several more specific interfaces that are commonly used: HID, RawInput, XInput. This field is required. |
deviceClass |
A broad categorization of the Device. For example, "Gamepad" or "Keyboard". |
product |
Name of the product as reported by the Device/driver itself. |
manufacturer |
Name of the manufacturer as reported by the Device/driver itself. |
version |
If available, provides the version of the driver or hardware for the Device. |
serial |
If available, provides the serial number for the Device. |
capabilities |
A string in JSON format that describes Device/interface-specific capabilities. See the section on capabilities. |
Aside from a number of standardized fields, such as product
and manufacturer
, a Device description can contain a capabilities
string in JSON format. This string describes characteristics which help the Input System to interpret the data from a Device, and map it to Control representations. Not all Device interfaces report Device capabilities. Examples of interface-specific Device capabilities are HID descriptors. WebGL, Android, and Linux use similar mechanisms to report available Controls on connected gamepads.
InputDeviceMatcher
instances handle matching an InputDeviceDescription
to a registered layout. Each matcher loosely functions as a kind of regular expression. Each field in the description can be independently matched with either a plain string or regular expression. Matching is not case-sensitive. For a matcher to apply, all of its individual expressions have to match.
To matchers to any layout, call InputSystem.RegisterLayoutMatcher
. You can also supply them when you register a layout.
// Register a new layout and supply a matcher for it.
InputSystem.RegisterLayoutMatcher<MyDevice>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithProduct("MyDevice.*")
.WithManufacturer("MyBrand");
// Register an alternate matcher for an already registered layout.
InputSystem.RegisterLayoutMatcher<MyDevice>(
new InputDeviceMatcher()
.WithInterface("HID")
If multiple matchers are matching the same InputDeviceDescription
, the Input System chooses the matcher that has the larger number of properties to match against.
You can overrule the internal matching process from outside to select a different layout for a Device than the system would normally choose. This also makes it possible to quickly build new layouts. To do this, add a custom handler to the InputSystem.onFindControlLayoutForDevice
event. If your handler returns a non-null layout string, then the Input System uses this layout.
Once the system has chosen a layout for a device, it instantiates an InputDevice
and populates it with InputControls
as the layout dictates. This process is internal and happens automatically.
Note: You can't create valid
InputDevices
andInputControls
by manually instantiating them withnew
. To guide the creation process, you must use layouts.
After the Input System assembles the InputDevice
, it calls FinishSetup
on each control of the device and on the device itself. Use this to finalize the setup of the Controls.
After an InputDevice
is fully assembled, the Input System adds it to the system. As part of this process, the Input System calls MakeCurrent
on the Device, and signals InputDeviceChange.Added
on InputSystem.onDeviceChange
. The Input System also calls InputDevice.OnAdded
.
Once added, the InputDevice.added
flag is set to true.
To add devices manually, you can call one of the InputSystem.AddDevice
methods such as InputSystem.AddDevice(layout)
.
// Add a gamepad. This bypasses the matching process and creates a device directly
// with the Gamepad layout.
InputSystem.AddDevice<Gamepad>();
// Add a device such that matching process is employed:
InputSystem.AddDevice(new InputDeviceDescription
{
interfaceName = "XInput",
product = "Xbox Controller",
});
When a Device is disconnected, it is removed from the system. A notification appears for InputDeviceChange.Removed
(sent via InputSystem.onDeviceChange
) and the Devices are removed from the devices
list. The system also calls InputDevice.OnRemoved
.
The InputDevice.added
flag is reset to false in the process.
Note that Devices are not destroyed when removed. Device instances remain valid and you can still access them in code. However, trying to read values from the controls of these Devices leads to exceptions.
In the player, Devices are reset when the window loses focus. Each reset sets the state of a Device back to its default state. An exception to this are noisy controls which are left at their current value, based on the assumption that they represent sensor readings that should be left at the last sample rather than snapped back to default values.
The reset happens from within Application.focusChanged
. The resets are observable state changes that trigger state change monitors
, and therefore also cancel ongoing Actions tied to the respective input state.
When Application.runInBackground
(not supported on all platforms) is enabled in the Unity player settings, devices that are marked as able to run in the background via InputDevice.canRunInBackground
are left alone and are not reset. This allows Devices such as HMDs and VR controllers to continuously provide input to a Unity application, even if the application does not have focus.
When a Device is added, the Input System sends it an initial QueryEnabledStateCommand
to find out whether the device is currently enabled or not. The result of this is reflected in the InputDevice.enabled
property.
When disabled, no events are processed for a Device, even if they are sent.
A Device can be manually disabled and re-enabled via InputSystem.DisableDevice
and InputSystem.EnableDevice
respectively.
Note that sensors start in a disabled state by default, and you need to enable them in order for them to generate events.
The Editor reloads the C# application domain whenever it reloads and recompiles scripts, or when the Editor goes into Play mode. This requires the Input System to reinitialize itself after each domain reload. During this process, the Input System attempts to recreate devices that were instantiated before the domain reload. However, the state of each Device doesn't carry across, which means that Devices reset to their default state on domain reloads.
Note that layout registrations do not persist across domain reloads. Instead, the Input System relies on all registrations to become available as part of the initialization process (for example, by using [InitializeOnLoad]
to run registration as part of the domain startup code in the Editor). This allows you to change registrations and layouts in script, and the change to immediately take effect after a domain reload.
Devices that the native backend reports are considered native (as opposed to Devices created from script code). To identify these Devices, you can check the InputDevice.native
property.
The Input System remembers native Devices. For example, if the system has no matching layout when the Device is first reported, but a layout which matches the device is registered later, the system uses this layout to recreate the Device.
If you want to get notified when Input Devices disconnect, subscribe to the InputSystem.onDeviceChange
event, and look for events of type InputDeviceChange.Disconnected
.
The Input System keeps track of disconnected Devices in InputSystem.disconnectedDevices
. If one of these Devices reconnects later, the Input System can detect that the Device was connected before, and reuses its InputDevice
instance. This allows the PlayerInputManager
to reassign the Device to the same user again.
Each Device that is created receives a unique numeric ID. You can access this ID through InputDevice.deviceId
.
All IDs are only used once per Unity session.
Like any InputControl
, a Device can have usages associated with it. You can query usages with the usages
property, and useInputSystem.SetDeviceUsage()
to set them. Usages can be arbitrary strings with arbitrary meanings. One common case where the Input System assigns Devices usages is the handedness of XR controllers, which are tagged with the "LeftHand" or "RightHand" usages.
While input events deliver data from a Device, commands send data back to the Device. The Input System uses these to retrieve specific information from the Device, to trigger functions on the Device (such as rumble effects), and for a variety of other needs.
The Input System sends commands to the Device through InputDevice.ExecuteCommand<TCommand>
. To monitor Device commands, use InputSystem.onDeviceCommand
.
Each Device command implements the IInputDeviceCommandInfo
interface, which only requires the typeStatic
property to identify the type of the command. The native implementation of the Device should then understand how to handle that command. One common case is the "HIDO"
command type which is used to send HID output reports to HIDs.
To create custom Device commands (for example, to support some functionality for a specific HID), create a struct
that contains all the data to be sent to the Device, and add a typeStatic
property to make that struct implement the IInputDeviceCommandInfo
interface. To send data to a HID, this property should return "HIDO"
.
You can then create an instance of this struct and populate all its fields, then use InputDevice.ExecuteCommand<TCommand>
to send it to the Device. The data layout of the struct must match the native representation of the data as the device interprets it.
Like any other type of Control, each Device has a block of memory allocated to it which stores the state of all the Controls associated with the Device.
State changes are usually initiated through state events from the native backend, but you can use InputControl<>.WriteValueIntoState()
to manually overwrite the state of any Control.
You can use InputState.AddChangeMonitor()
to register a callback to be called whenever the state of a Control changes. The Input System uses the same mechanism to implement input Actions.
The Input System can synthesize a new state from an existing state. An example of such a synthesized state is the press
button Control that Touchscreen
inherits from Pointer
. Unlike a mouse, which has a physical button, for Touchscreen
this is a synthetic Control that doesn't correspond to actual data coming in from the Device backend. Instead, the Input System considers the button to be pressed if any touch is currently ongoing, and released otherwise.
To do this, the Input System uses InputState.Change
, which allows feeding arbitrary state changes into the system without having to run them through the input event queue. The Input System incorporates state changes directly and synchronously. State change monitors still trigger as expected.
To be notified when new Devices are added or existing Devices are removed, use InputSystem.onDeviceChange
.
InputSystem.onDeviceChange +=
(device, change) =>
{
switch (change)
{
case InputDeviceChange.Added:
Debug.Log("New device added: " + device);
break;
case InputDeviceChange.Removed:
Debug.Log("Device removed: " + device);
break;
}
};
InputSystem.onDeviceChange
delivers notifications for other device-related changes as well. See the InputDeviceChange
enum for more information.
To manually add and remove Devices through the API, use InputSystem.AddDevice()
and InputSystem.RemoveDevice()
.
This allows you to create your own Devices, which can be useful for testing purposes, or for creating virtual Input Devices which synthesize input from other events. As an example, see the on-screen Controls that the Input System provides. The Input Devices used for on-screen Controls are created entirely in code and have no native representation.
Note: This example deals only with Devices that have fixed layouts (that is, you know the specific model or models that you want to implement). This is different from an interface such as HID, where Devices can describe themselves through the interface and take on a wide variety of forms. A fixed Device layout can't cover self-describing Devices, so you need to use a layout builder to build Device layouts from information you obtain at runtime.
There are two main situations in which you might need to create a custom Device:
For the second scenario, see Overriding the HID Fallback.
The steps below deal with the first scenario, where you want to create a new Input Device entirely from scratch and provide input to it from a third-party API.
The first step is to create a C# struct
that represents the form in which the system receives and stores input, and also describes the InputControl
instances that the Input System must create for the Device in order to retrieve its state.
// A "state struct" describes the memory format that a Device uses. Each Device can
// receive and store memory in its custom format. InputControls then connect to
// the individual pieces of memory and read out values from them.
//
// If it's important for the memory format to match 1:1 at the binary level
// to an external representation, it's generally advisable to use
// LayoutLind.Explicit.
[StructLayout(LayoutKind.Explicit, Size = 32)]
public struct MyDeviceState : IInputStateTypeInfo
{
// You must tag every state with a FourCC code for type
// checking. The characters can be anything. Choose something that allows
// you to easily recognize memory that belongs to your own Device.
public FourCC format => return new FourCC('M', 'Y', 'D', 'V');
// InputControlAttributes on fields tell the Input System to create Controls
// for the public fields found in the struct.
// Assume a 16bit field of buttons. Create one button that is tied to
// bit #3 (zero-based). Note that buttons don't need to be stored as bits.
// They can also be stored as floats or shorts, for example. The
// InputControlAttribute.format property determines which format the
// data is stored in. If omitted, the system generally infers it from the value
// type of the field.
[InputControl(name = "button", layout = "Button", bit = 3)]
public ushort buttons;
// Create a floating-point axis. If a name is not supplied, it is taken
// from the field.
[InputControl(layout = "Axis")]
public short axis;
}
The Input System's layout mechanism uses InputControlAttribute
annotations to add Controls to the layout of your Device. For details, see the layout system documentation.
With the state struct in place, you now have a way to send input data to the Input System and store it there. The next thing you need is an InputDevice
that uses your custom state struct and represents your custom Device.
Next, you need a class derived from one of the InputDevice
base classes. You can either base your Device directly on InputDevice
, or you can pick a more specific Device type, like Gamepad
.
This example assumes that your Device doesn't fit into any of the existing Device classes, so it derives directly from InputDevice
.
// InputControlLayoutAttribute attribute is only necessary if you want
// to override the default behavior that occurs when you register your Device
// as a layout.
// The most common use of InputControlLayoutAttribute is to direct the system
// to a custom "state struct" through the `stateType` property. See below for details.
[InputControlLayout(displayName = "My Device", stateType = typeof(MyDeviceState))]
public class MyDevice : InputDevice
{
// In the state struct, you added two Controls that you now want to
// surface on the Device, for convenience. The Controls
// get added to the Device either way. When you expose them as properties,
// it is easier to get to the Controls in code.
public ButtonControl button { get; private set; }
public AxisControl axis { get; private set; }
// The Input System calls this method after it constructs the Device,
// but before it adds the device to the system. Do any last-minute setup
// here.
protected override void FinishSetup()
{
base.FinishSetup();
// NOTE: The Input System creates the Controls automatically.
// This is why don't do `new` here but rather just look
// the Controls up.
button = GetChildControl<ButtonControl>("button");
axis = GetChildControl<AxisControl>("axis");
}
}
You now have a Device in place along with its associated state format. You can call the following method to create a fully set-up Device with your two Controls on it:
InputSystem.AddDevice<MyDevice>();
However, this Device doesn't receive input yet, because you haven't added any code that generates input. To do that, you can use InputSystem.QueueStateEvent
or InputSystem.QueueDeltaStateEvent
from anywhere, including from a thread. The following example uses IInputUpdateCallbackReceiver
, which, when implemented by any InputDevice
, adds an OnUpdate()
method that automatically gets called during InputSystem.onBeforeUpdate
and provides input events to the current input update.
Note: If you already have a place where input for your device becomes available, you can skip this step and queue input events from there instead of using
IInputUpdateCallbackReceiver
.
public class MyDevice : InputDevice, IInputUpdateCallbackReceiver
{
//...
public void OnUpdate()
{
// In practice, this would read out data from an external
// API. This example uses some empty input.
var state = new MyDeviceState();
InputSystem.QueueStateEvent(this, state);
}
}
You now have a functioning device, but you haven't registered it (added it to the system) yet. This means you can't see the device when, for example, you create bindings in the Action editor.
You can register your device type with the system from within the code that runs automatically as part of Unity's startup. To do so, modify the definition of MyDevice
like so:
// Add the InitializeOnLoad attribute to automatically run the static
// constructor of the class after each C# domain load.
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
public class MyDevice : InputDevice, IInputUpdateCallbackReceiver
{
//...
static MyDevice()
{
// RegisterLayout() adds a "Control layout" to the system.
// These can be layouts for individual Controls (like sticks)
// or layouts for entire Devices (which are themselves
// Controls) like in our case.
InputSystem.RegisterLayout<MyDevice>();
}
// You still need a way to trigger execution of the static constructor
// in the Player. To do this, you can add the RuntimeInitializeOnLoadMethod
// to an empty method.
[RuntimeInitializeOnLoadMethod]
private static void InitializeInPlayer() {}
}
This registers the Device type with the system and makes it available in the Control picker. However, you still need a way to add an instance of the Device when it is connected.
In theory, you could call InputSystem.AddDevice<MyDevice>()
somewhere, but in a real-world setup you likely have to correlate the Input Devices you create with their identities in the third-party API.
It might be tempting to do something like this:
public class MyDevice : InputDevice, IInputUpdateCallbackReceiver
{
//...
// This does NOT work correctly.
public ThirdPartyAPI.DeviceId externalId { get; set; }
}
and then set that on the Device after calling AddDevice<MyDevice>
. However, this doesn't work as expected in the Editor, because the Input System requires Devices to be created solely from their InputDeviceDescription
in combination with the chosen layout (and layout variant). In addition, the system supports a fixed set of mutable per-device properties such as device usages (that is, InputSystem.SetDeviceUsage()
and related methods). This allows the system to easily recreate Devices after domain reloads in the Editor, as well as to create replicas of remote Devices when connecting to a Player. To comply with this requirement, you must cast that information provided by the third-party API into an InputDeviceDescription
and then use an InputDeviceMatcher
to match the description to our custom MyDevice
layout.
This example assumes that the third-party API has two callbacks, like this:
public static ThirdPartyAPI
{
// This example assumes that the argument is a string that
// contains the name of the Device, and that no two Devices
// have the same name in the external API.
public static Action<string> deviceAdded;
public static Action<string> deviceRemoved;
}
You can hook into those callbacks and create and destroy devices in response.
// This example uses a MonoBehaviour with [ExecuteInEditMode]
// on it to run the setup code. You can do this many other ways.
[ExecuteInEditMode]
public class MyDeviceSupport : MonoBehaviour
{
protected void OnEnable()
{
ThirdPartyAPI.deviceAdded += OnDeviceAdded;
ThirdPartyAPI.deviceRemoved += OnDeviceRemoved;
}
protected void OnDisable()
{
ThirdPartyAPI.deviceAdded -= OnDeviceAdded;
ThirdPartyAPI.deviceRemoved -= OnDeviceRemoved;
}
private void OnDeviceAdded(string name)
{
// Feed a description of the Device into the system. In response, the
// system matches it to the layouts it has and creates a Device.
InputSystem.AddDevice(
new InputDeviceDescription
{
interfaceName = "ThirdPartyAPI",
product = name
});
}
private void OnDeviceRemoved(string name)
{
var device = InputSystem.devices.FirstOrDefault(
x => x.description == new InputDeviceDescription
{
interfaceName = "ThirdPartyAPI",
product = name,
});
if (device != null)
InputSystem.RemoveDevice(device);
}
// Move the registration of MyDevice from the
// static constructor to here, and change the
// registration to also supply a matcher.
protected void Awake()
{
// Add a match that catches any Input Device that reports its
// interface as "ThirdPartyAPI".
InputSystem.RegisterLayout<MyDevice>(
matches: new InputDeviceMatcher()
.WithInterface("ThirdPartyAPI"));
}
}
current
and all
(optional)For convenience, you can quickly access the last used device of a given type, or list all devices of a specific type. To do this, add support for a current
and for an all
getter to the API of MyDevice
.
public class MyDevice : InputDevice, IInputCallbackReceiver
{
//...
public static MyDevice current { get; private set; }
public static IReadOnlyList<MyDevice> all => s_AllMyDevices;
private static List<MyDevice> s_AllMyDevices = new List<MyDevice>();
public override void MakeCurrent()
{
base.MakeCurrent();
current = this;
}
protected override void OnAdded()
{
base.OnAdded();
s_AllMyDevices.Add(this);
}
protected override void OnRemoved()
{
base.OnRemoved();
s_AllMyDevices.Remove(this);
}
}