Firstborn/Library/PackageCache/com.unity.inputsystem@1.4.4/Documentation~/Devices.md
Schaken-Mods b486678290 Library -Artifacts
Library -Artifacts
2023-03-28 12:24:16 -05:00

44 KiB

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 the matching process is employed:
InputSystem.AddDevice(new InputDeviceDescription
{
    interfaceName = "XInput",
    product = "Xbox Controller",
});

When a device is added, the Input System automatically issues a sync request on the device. This instructs the device to send an event representing its current state. Whether this request succeeds depends on the whether the given device supports the sync command.

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

Resetting a Device resets its Controls to their default state. You can do this manually using InputSystem.ResetDevice:

    InputSystem.ResetDevice(Gamepad.current);

There are two types of resets as determined by the second parameter to InputSystem.ResetDevice:

Type Description
"Soft" Resets This is the default. With this type, only controls that are not marked as dontReset are reset to their default value. This excludes controls such as Pointer.position from resets and thus prevents mouse positions resetting to (0,0).
"Hard" Resets In this type, all controls are reset to their default value regardless of whether they have dontReset set or not.

Resetting Controls this way is visible on Actions. If you reset a Device that is currently driving one or more Action, the Actions are cancelled. This cancellation is different from sending an event with default state. Whereas the latter may inadvertently perform Actions (e.g. a button that was pressed would not appear to have been released), a reset will force clean cancellation.

Resets may be triggered automatically by the Input System depending on application focus.

Device syncs

A Device may be requested to send an event with its current state through RequestSyncCommand. It depends on the platform and type of Device whether this is supported or not.

A synchronization request can be explicitly sent using InputSystem.TrySyncDevice. If the device supports sync requests, the method returns true and an InputEvent will have been queued on the device for processing in the next update.

Synchronization requests are also automatically sent by the Input System in certain situations. See Background and focus change behavior for more details.

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 other than removal (DeviceRemoveEvent) and configuration change (DeviceConfigurationEvent) 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 Input System may automatically disable and re-enable Devices in certain situations, as detailed in the next section.

Background and focus change behavior

In general, input is tied to application focus. This means that Devices do not receive input while the application is not in the foreground and thus no Actions will receive input either. When the application comes back into focus, all devices will receive a sync request to have them send their current state (which may have changed while the application was in the background) to the application. Devices that do not support sync requests will see a soft reset that resets all Controls not marked as dontReset to their default state.

On platforms such as iOS and Android, that do not support running Unity applications in the background, this is the only supported behavior.

If the application is configured to run while in the background (that is, not having focus), input behavior can be selected from several options. This is supported in two scenarios:

  • In Unity's Player Settings you can explicity enable Run In Background for specific players that support it (such as Windows or Mac standalone players). Note that in these players this setting is always enabled automatically in development players.
  • In the editor, application focus is tied to focus on the Game View. If no Game View is focused, the application is considered to be running in the background. However, while in play mode, the editor will always keep running the player loop regardless of focus on the Game View window. This means that in the editor, Run In Background is considered to always be enabled.

If the application is configured this way to keep running while in the background, the player loop and thus the Input System, too, will keep running even when the application does not have focus. What happens with respect to input then depends on two factors:

  1. On the ability of individual devices to receive input while the application is not running in the foreground. This is only supported by a small subset of devices and platforms. VR devices (TrackedDevice) such as HMDs and VR controllers generally support this.

    To find out whether a specific device supports this, you can query the InputDevice.canRunInBackground property. This property can also be forced to true or false via a Device's layout.
  2. On two settings you can find in the project-wide Input Settings. Specifically, InputSettings.backgroundBehavior:
    Background Behavior Setting
    and InputSettings.editorInputBehaviorInPlayMode:
    Play Mode Input Behavior
    The table below shows a detailed breakdown of how input behaviors vary based on these two settings and in relation to the Run In Background player setting in Unity.

Note: InputDevice.canRunInBackground is overridden by the editor in certain situations (see table below). In general, the value of the property does not have to be the same between the editor and the player and depends on the specific platform and device.

The following table shows the full matrix of behaviors according to the Input Settings and whether the game is running in the editor or in the player.

Focus Behavior

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 => 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(RuntimeInitializeLoadType.BeforeSceneLoad)]
    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);
    }
}

Step 6: Device Commands (Optional)

A final, but optional, step is to add support for Device commands. A "device command" is that opposite of input. In other words, it consists of data traveling to the input device, which might also return data as part of the operation (much like a function call). You can use this to communicate with the backend of the device in order to query configuration, or to initiate effects such as haptics. At the moment there isn't a proper interface available for this, however there are still some scenarios that can be solved with the current interfaces.

E.g. the following shows, when implementing a non-hardware-backed device (simulated device), how to simulate hardware reporting that the device can be run in the background and supports sync commands. This is useful to prevent the device from cancelling Actions when application focus is lost and restored. For more info see Device syncs

public class MyDevice : InputDevice, IInputCallbackReceiver
{
    //...

    protected override unsafe long ExecuteCommand(InputDeviceCommand* commandPtr)
    {
        var type = commandPtr->type;
        if (type == RequestSyncCommand.Type)
        {
            // Report that the device supports the sync command and has handled it.
            // This will prevent device reset during focus changes.
            result = InputDeviceCommand.GenericSuccess;
            return true;
        }

        if (type == QueryCanRunInBackground.Type)
        {
            // Notify that the device supports running in the background.
            ((QueryCanRunInBackground*)commandPtr)->canRunInBackground = true;
            result = InputDeviceCommand.GenericSuccess;
            return true;
        }

        result = default;
        return false;
    }
}