HID のサポート
ヒューマンインターフェースデバイス (HID) とは、USB または Bluetooth 経由でコンピューターに接続されるユーザー入力周辺機器を記述する 仕様 です。HID は、ゲームパッド、ジョイスティック、レーシングホイールなどのデバイスを実装するためによく使用されます。
Input System では、Windows、MacOS、およびユニバーサル Windows プラットフォーム (UWP) で HID (USB 接続と Bluetooth 接続の両方) を直接サポートしています。他のプラットフォームでも HID をサポートしている場合がありますが、HID 固有の API 経由での入力は提供していません。例えば、Linux では、SDL を通じてゲームパッドとジョイスティックの HID がサポートされますが、その他の HID はサポートされません。
すべての HID にはデバイス記述子が含まれています。Input Debugger から HID の記述子を参照するには、デバイスのデバッガーウィンドウで HID Descriptor ボタンをクリックします。デバイスのタイプを指定するために、HID 記述子によって、HID 使用法テーブル 内のエントリ番号、デバイス上のすべてのコントロール、およびそれらのデータ範囲と使用法が報告されます。
Input System では、以下の 2 つの方法のいずれかで HID が処理されます。
- 特定の HID については、既知のレイアウトが用意されています。
- 既知のレイアウトがない場合は、その HID のレイアウトが自動生成されます。
レイアウトの自動生成
Input System ではデフォルトで、使用法を GenericDesktop/Joystick
、GenericDesktop/Gamepad
、または GenericDesktop/MultiAxisController
として報告する任意の HID に対して、レイアウトと Device 表現が作成されます (詳細については、HID 使用法テーブルの仕様 を参照してください)。サポートされる使用法のリストを変更するには、HIDSupport.supportedHIDUsages
を設定します。
HID のレイアウトが自動生成されるときは常に、これらの Device が、Joystick
デバイスクラス で表される Joystick
として報告されます。GenericDesktop/X
および GenericDesktop/Y
の、報告される HID 使用法の最初の要素の組み合わせで、ジョイスティックの stick
Control が形成されます。次に、HID の他のすべての軸またはボタン要素に対応する Control が、HID 仕様で報告される Control 名を使用して追加されます。Input System によって、HID 使用法が Button/Button 1
である最初のコントロールが、ジョイスティックの trigger
Control に割り当てられます。
自動生成されたレイアウトは、Input System による "ベストエフォート" を表します。HID 標準に従ったヒューマンインターフェースデバイスの自己記述は、実際にはあいまいな部分が多く、生成されたレイアウトでは Control が意図したとおりに動作しないことがあります。例えば、レイアウトビルダーでは、ハットスイッチと D パッドを識別できますが、どれがどの方向を表すかについては推測するのみである場合がよくあります。個々のボタンについても同様で、HID では通常、ボタンには意味が割り当てられません。
HID が期待どおりに動作しない状況を解決する最善策は、カスタムレイアウトを加えることです。これにより、自動生成のプロセス全体がバイパスされます。詳細については、HID フォールバックのオーバーライド を参照してください。
HID 出力
HID は出力をサポートすることができます (例えば、ゲームパッドのライトをトグルしたり、フィードバックモーターを強制的に作動させたりできます)。Unity では、HID 出力レポートコマンドを Device に送信することで出力が制御されます。出力レポートでは、Device 固有のデータ形式が使用されます。HID 出力レポートを使用するには、InputDevice.ExecuteCommand
を呼び出して、typeStatic
プロパティを "HIDO"
に設定したコマンド構造体を Device に送信します。コマンド構造体には、HID に送信される Device 固有のデータが含まれます。
HID フォールバックのオーバーライド
HID のために自動生成されたレイアウトを使用すると、理想的な結果にならないことがよくあります。Device 固有の正しい名前が Control に設定されなかったり、一部の Control が期待どおりに動作しなかったり、ベンダー固有の形式を使用する一部の Control がまったく認識されなかったりする場合があります。
このような場合の最善の対応策は、その Device 専用のカスタム Device レイアウトを設定することです。これにより、デフォルトの自動生成をオーバーライドし、Device の開放方法を完全に制御できます。
ワークフローの例
以下の例では、PS4 DualShock コントローラー用のカスタムレイアウトがまだ Input System に存在しておらず、そのレイアウトを手動で加えることを想定しています。
この例では、コントローラーを Gamepad
として開放しようとしており、デバイスで使用される HID データ形式が大まかにわかっているものとします。
ヒント: サポートしようとしている特定の HID の形式がわからない場合は、Device を接続した状態で Input Debugger を開くと、Device のデバッガービューと、HID 記述子を表示するウィンドウの両方がポップアップ表示されます。その状態で Control を 1 つずつ操作し、デバッガービューに表示される結果を確認して、Control を HID 記述子と関連付けることができます。個々のイベントをダブルクリックし、Device から入力された未加工のデータを比較することもできます。イベントトレースで 2 つのイベントを選択し、右クリックして Compare を選択すると、2 つのイベントの相違点だけを表示するウィンドウが開きます。
ステップ 1: 状態構造体
最初のステップでは、Device の入力データが送信される形式と、そのデータから各部分の情報を読み出す必要のある InputControl
インスタンスを詳細に記述します。
PS4 コントローラーから報告される HID 入力は、以下のようになります。
struct PS4InputReport
{
byte reportId; // #0
byte leftStickX; // #1
byte leftStickY; // #2
byte rightStickX; // #3
byte rightStickY; // #4
byte dpad : 4; // #5 ビット #0 (0 = 上、2 = 右、4 = 下、6 = 左)
byte squareButton : 1; // #5 ビット #4
byte crossButton : 1; // #5 ビット #5
byte circleButton : 1; // #5 ビット #6
byte triangleButton : 1; // #5 ビット #7
byte leftShoulder : 1; // #6 ビット #0
byte rightShoulder : 1; // #6 ビット #1
byte leftTriggerButton : 2;// #6 ビット #2
byte rightTriggerButton : 2;// #6 ビット #3
byte shareButton : 1; // #6 ビット #4
byte optionsButton : 1; // #6 ビット #5
byte leftStickPress : 1; // #6 ビット #6
byte rightStickPress : 1; // #6 ビット #7
byte psButton : 1; // #7 ビット #0
byte touchpadPress : 1; // #7 ビット #1
byte padding : 6;
byte leftTrigger; // #8
byte rightTrigger; // #9
}
これは、以下のような C# 構造体に変換できます。
// データは未加工の HID 入力レポートとして受信されます。この構造体では、
// レポートの未加工のバイナリ形式を記述します。
[StructLayout(LayoutKind.Explicit, Size = 32)]
struct DualShock4HIDInputReport : IInputStateTypeInfo
{
// すべての HID 入力は、"HID " という FourCC でタグ付けされるため、
// この状態構造体でも同じ形式を使用します。
public FourCC format => new FourCC('H', 'I', 'D');
// HID 入力レポートは、8 ビットのレポート ID で始めることができます。これが
// 存在するかどうかはデバイスによって異なります。PS4 DualShock コントローラーでは
// 存在します。このフィールドは必ずしも加える必要はありませんが、ここでは、完全を期すために
// 加えておきます。これは、デバッグに役立つ可能性もあります。
[FieldOffset(0)] public byte reportId;
// 以下の InputControl の注釈は難解に見えるかもしれませんが、ここで
// 行っていることは比較的簡単です。作成するフィールドを適切な位置に配置する
// ために [FieldOffset] で注釈を付け、InputControl を追加してフィールドに
// コントロールをアタッチします。各 InputControl 属性では、新しいコントロールの
// 作成と、既存のコントロールの変更のどちらかのみを行うことができます。
// ここでは、レイアウトがゲームパッドに基づいており、ほぼすべてのコントロールが
// ゲームパッドから継承されるため、それらの設定を変更するだけです。
[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;
}
ステップ 2: InputDevice
次に、デバイスを表す InputDevice
が必要です。ここではゲームパッドを対象としているため、Gamepad
の新しいサブクラスを作成する必要があります。
実際の DualShockGamepadHID
は DualShockGamepad
クラスに基づいていますが、以下の例では、わかりやすくするためにそのことを無視しています。
// InputControlLayoutAttribute を使用して、作成した状態構造体をシステムに
// 知らせます。ここには、すべての InputControl 属性の配置位置を
// 特定できる情報が含まれています。このようにして、Input System では、
// 作成するコントロールとそれらの設定方法が認識されます。
[InputControlLayout(stateType = typeof(DualShock4HIDInputReport)]
public DualShock4GamepadHID : Gamepad
{
}
ステップ 3: Device の登録
最後のステップでは、新しい Device タイプを登録します。そして、PS4 コントローラーが接続されたときに、デフォルトの HID フォールバックを使用するのではなく、カスタムの Device を生成するように Input System を設定します。
必要な処理は、InputSystem.RegisterLayout<T>
を呼び出して、PS4 DualShock HID の説明に一致する InputDeviceMatcher
を指定することだけです。理論上は、この呼び出しを任意の時点で行うことができますが、通常、レイアウトの登録に最適なのは起動時です。そうすることで、カスタムレイアウトが確実に Unity エディターに認識されるため、Input Control ピッカーなどで参照できるようになります。
起動シーケンスに登録処理を加えるには、DualShock4GamepadHID
Device のコードを以下のように変更します。
[InputControlLayout(stateType = typeof(DualShock4HIDInputReport)]
# if UNITY_EDITOR
[InitializeOnLoad] // 起動時に静的コンストラクターが呼び出されるようにします。
# endif
public DualShock4GamepadHID : Gamepad
{
static DualShock4GamepadHID()
{
// 以下は、Device に一致させる方法の 1 つです。
InputSystem.RegisterLayout<DualShock4GamepadHID>(
new InputDeviceMatcher()
.WithInterface("HID")
.WithManufacturer("Sony.+Entertainment")
.WithProduct("Wireless Controller"));
// 別の方法として、PID と VID で一致させることもできます。一般に、HID では
// こちらの方が確実です。
InputSystem.RegisterLayout<DualShock4GamepadHID>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment。
.WithCapability("productId", 0x9CC)); // ワイヤレスコントローラー。
}
// プレイヤーで静的コンストラクターの呼び出しをトリガーするために、
// RuntimeInitializeOnLoadMethod で注釈を付けた空のメソッドを作成します。
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Init() {}
}
これで、製造元と製品名の文字列、または HID 記述子のベンダー ID と製品 ID が一致する任意の Device が、このカスタムレイアウトによって選択されるようになります。Input System では DualShock4GamepadHID
Device インスタンスとして表されます。
詳細については、Device の照合 に関するドキュメントも参照してください。