Devices.md 36 KB

Devices

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.

Device descriptions

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.

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.

Matching

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.

Hijacking the matching process

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.

Device lifecycle

Device creation

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 and InputControls by manually instantiating them with new. 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",
});

Device removal

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.

Device resets

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.

Device enabling and disabling

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.

Domain reloads in the Editor

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.

Native Devices

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.

Disconnected Devices

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.

Device IDs

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.

Device usages

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.

Device commands

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.

Sending commands to Devices

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.

Adding custom device Commands

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.

Device state

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

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.

Monitoring state changes

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.

Synthesizing state

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.

Working with Devices

Monitoring Devices

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.

Adding and removing Devices

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.

Creating custom Devices

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:

  1. You have an existing API that generates input, and which you want to reflect into the Input System.
  2. You have an HID that the Input System ignores, or that the Input system auto-generates a layout for that doesn't work well enough for your needs.

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.

Step 1: The state struct

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.

Step 2: The Device class

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");
    }
}

Step 3: The Update method

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);
    }
}

Step 4: Device registration and creation

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"));
    }
}

Step 5: 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);
    }
}