この記事は夏のブログリレー6日目の記事です。
はじめに
こんにちは! @Haru_18 です。
突然ですが、皆さんは普段Macを使用していますか?
私は大学に入ってからMacBook Proを使っています。
Unixベースのシステム、長持ちするバッテリー、iPhoneとの連携、などなど良い点は多々あるのですが、1つ大きなデメリットを感じています。
それは、ゲームが遊べないこと。
Mac対応のタイトルが少ないのも痛いですが、厄介なのが「Macでコントローラーを使うと、かなりの割合で入力が正しく認識されない」という問題です。
クリエイティブなPCであるはずのMacでUnityのテストプレイさえまともにできないというのはどういう了見でしょうか...
というわけで今回は、Mac環境のUnityで発生する「コントローラーの入力問題」を無事に修正した、というお話です。
開発環境
- OS: macOS Tahoe 26.5.2 (Apple M4)
- Unity: 6000.5.2f1
- 入力システム: Input System 1.19.0
- コントローラー: EasySMX X05 Wireless Controller
ちなみに、純正のXboxやPlayStationなら公式にMac対応しているらしいです。
コスパを優先してサードパーティ製を買ったことが悔やまれます...
問題の特定
まずはどこで入力受付が狂っているのか、の特定を行います。
Macにコントローラーを接続した状態で、Steamやブラウザ上でデバイスの動作確認を行います。
私の場合、この段階では入力が正しく認識されていたので、UnityのInputSystemに問題があるようです。
コントローラーによってはこの段階ですでに入力が正しく認識されなかったり、そもそもMacに接続されなかったりします。そのような場合にはこの記事はあまり役に立たないかもしれません。
問題の解消
それでは、実際に問題を解決していきます。
結論から言うと、IInputStateTypeInfo という構造体を用いて独自に「生の入力データの変換方法」を定義し、Unityに教えることになります。
コードの説明
今回私が書いたコードは以下のようになりました。
コード全体をみる
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace App.Input.Fix
{
[StructLayout(LayoutKind.Explicit, Size = 17)]
public struct MyWirelessInputReport : IInputStateTypeInfo
{
public const int VendorID = 0x45E;
public const int ProductId = 0x2E0;
public const string Product = "Xbox Wireless Controller";
public readonly FourCC format => new('H', 'I', 'D');
[FieldOffset(1)]
[InputControl(name = "leftStick", layout = "Stick", format = "VEC2")]
[InputControl(name = "leftStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "leftStick/y", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,invert=true")]
public byte LeftStick;
[FieldOffset(5)]
[InputControl(name = "rightStick", layout = "Stick", format = "VEC2")]
[InputControl(name = "rightStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "rightStick/y", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,invert=true")]
public byte RightStick;
[FieldOffset(9)]
[InputControl(name = "leftTrigger", format = "BIT", sizeInBits = 10)]
public byte LeftTrigger;
[FieldOffset(11)]
[InputControl(name = "rightTrigger", format = "BIT", sizeInBits = 10)]
public byte RightTrigger;
[FieldOffset(13)]
[InputControl(name = "dpad", layout = "Dpad", sizeInBits = 4, bit = 0)]
[InputControl(name = "dpad/up", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=8,maxValue=2")]
[InputControl(name = "dpad/right", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=2,maxValue=4")]
[InputControl(name = "dpad/down", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=4,maxValue=6")]
[InputControl(name = "dpad/left", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=6,maxValue=8")]
public byte DPad;
[FieldOffset(14)]
[InputControl(name = "buttonSouth", displayName = "A", layout = "Button", format = "BIT", bit = 0)]
[InputControl(name = "buttonEast", displayName = "B", layout = "Button", format = "BIT", bit = 1)]
[InputControl(name = "buttonWest", displayName = "X", layout = "Button", format = "BIT", bit = 2)]
[InputControl(name = "buttonNorth", displayName = "Y", layout = "Button", format = "BIT", bit = 3)]
[InputControl(name = "leftShoulder", layout = "Button", format = "BIT", bit = 4)]
[InputControl(name = "rightShoulder", layout = "Button", format = "BIT", bit = 5)]
[InputControl(name = "select", layout = "Button", format = "BIT", bit = 6)]
[InputControl(name = "start", layout = "Button", format = "BIT", bit = 7)]
public byte Buttons;
}
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
[InputControlLayout(stateType = typeof(MyWirelessInputReport))]
public class MyWirelessGamepadHID : Gamepad
{
static MyWirelessGamepadHID()
{
if (Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor)
{
InputSystem.RegisterLayout<MyWirelessGamepadHID>("MyWirelessGamepadHID");
Debug.Log("Registered a custom struct layout for the wireless Xbox controller on Mac.");
}
InputSystem.onFindLayoutForDevice -= CustomLayoutHook;
InputSystem.onFindLayoutForDevice += CustomLayoutHook;
}
private static string CustomLayoutHook(ref InputDeviceDescription description, string matchedLayout, InputDeviceExecuteCommandDelegate executeCommand)
{
if (!string.IsNullOrEmpty(description.product) && description.product == MyWirelessInputReport.Product)
{
Debug.Log("<color=cyan>Conditions cleared</color> Forcibly applying MyWirelessGamepadHID.");
return "MyWirelessGamepadHID";
}
return null;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Init() { }
}
}
コードの書き方の前に、まずはコードの意味について軽く説明していきます。
StructLayout 属性
構造体のメモリ配置を指定する属性です。生の入力データはビットの位置とボタンがキッチリ紐付けられているため、C#の最適化でメモリ配置が崩されないようにします。
VendorID / ProductId
修正を行うコントローラーの固有ID (16進数, 4桁) です。UnityのHID Despector (後述) から確認できます。
public readonly FourCC format
FourCCは「Four-Character Code」の略らしいです。この構造体で設定されているデータの種類を指定します。UnityのInput Debugger (後述) から確認できます。
(今回はH, I, D, 空文字列 の4文字です。)
public byte Hoge
ボタンの入力設定を登録する変数です。入力設定は FieldOffset と InputControl の2つの属性で行います。
また、この変数自体の名前や型は何でも良いそうです。
FieldOffset 属性
その変数に登録する入力設定が、生データの何バイト目かを指定します。
InputControl 属性
生データの解釈の仕方を定義します。name で入力の種類、format でデータの型、bit でデータの何ビット目か、を指定します。
コントローラーの確認
それでは、実際にコントローラーの状態を調べながら、設定を書いていきます。
まずは、Macにコントローラーを繋いだ状態で Window > Analysis > Input Debugger を開きます。

次に、Devices の中の該当のコントローラーをダブルクリックし詳細画面を開きます。

ここで、コントローラーの詳細設定や現在の入力等を確認することができます。
私の場合、現状ではYを押すとXが反応していたり、Rであを押すとYが反応していたり、と入力がゴチャゴチャになってしまっています。
なお、この画面にある Interface が、コード内の FourCC format に入れる文字列になります。
また、 HID Despector を開くことで、VendorIDとProductIDも確認することができます。


入力設定の登録
FourCCと2つの固有IDを書き込んだら、いよいよ入力設定の登録を行っていきます。
コントローラーの詳細画面下部のEventペイン内で右クリックし、Inspectを選択します。

出てきたウィンドウで Live, Display Raw Memory, Bits/Hex をすべてオンにし、モードを Editor (Current) に切り替えます。

これが、コントローラーの現在の入力値の生データになります。私の場合は17バイトのデータになっているそうです。
このデータのバイト数が、StructLayout の size に設定する数になります。
ここでコントローラーを触ってみると、それに応じてデータの値が変動するのが分かるかと思います。ここからは、「どのボタンを押すとどのビットがどう反応するか」を1つずつ調べて、コードに書き込んでいく作業になります。
かなりめんどくさいですが、頑張って調べていきましょう。
ボタン類
例えば、私の場合Aボタンを押下すると15バイト目の一番下のビットが立つので、以下のコードを書きます。
[FieldOffset(14)] // 「15バイト目」なのでオフセットは14
[InputControl(name = "buttonSouth", displayName = "A", layout = "Button", format = "BIT", bit = 0)]
public byte ButtonSouth;
layout = "Button" で二値データ、format = "BIT", bit = 0 で一番下のビット1つを指定しています。
なお、ボタンと変数を必ずしも一対一対応させる必要はないので、FieldOffset が同じ入力は1つの変数にまとめることができます。
[FieldOffset(14)]
[InputControl(name = "buttonSouth", displayName = "A", layout = "Button", format = "BIT", bit = 0)]
[InputControl(name = "buttonEast", displayName = "B", layout = "Button", format = "BIT", bit = 1)]
[InputControl(name = "buttonWest", displayName = "X", layout = "Button", format = "BIT", bit = 2)]
[InputControl(name = "buttonNorth", displayName = "Y", layout = "Button", format = "BIT", bit = 3)]
[InputControl(name = "leftShoulder", layout = "Button", format = "BIT", bit = 4)]
[InputControl(name = "rightShoulder", layout = "Button", format = "BIT", bit = 5)]
[InputControl(name = "select", layout = "Button", format = "BIT", bit = 6)]
[InputControl(name = "start", layout = "Button", format = "BIT", bit = 7)]
public byte Buttons;
また、コードの変更を保存するとUnityのコンパイルが走って入力が表示されているウィンドウが勝手に閉じてしまうので、この作業をしている間は Cmd + S をしないことをオススメします。
トリガー
続いてトリガー (RT/LT) を設定します。
私の場合、LTは10バイト目の全ビットと11バイト目の下位2ビットが対応していました。つまり、リトルエンディアンで「10バイト目から数えて10ビット」ということになるので、RTと合わせて
[FieldOffset(9)]
[InputControl(name = "leftTrigger", format = "BIT", sizeInBits = 10)]
public byte LeftTrigger;
[FieldOffset(11)]
[InputControl(name = "rightTrigger", format = "BIT", sizeInBits = 10)]
public byte RightTrigger;
これだけ書けば、10ビットのデータを勝手に0~1のアナログ値に変換してくれるそうです。
D-pad
次はD-padです。
触ってみると、14バイト目の下位4ビットが対応していることが分かりました。
さらに調べてみると、どうやら上が 0001、右上が 0010、左上が 1000 と時計回りに1~8が割り振られているらしいです。
調べてみると InputSystemのドキュメント にちょうど参考になりそうなコードが載っていたので、これを使わせていただきます。
今回はA/B/X/Yのようにbooleanで処理することはできないので、layout = "DiscreteButton" を指定して数値として処理していくことになります。
例えば、dpad/right は入力値が2(右上)、3(真右)、4(右下)のときにオンになってほしいので、InputControl 属性に parameters = "minValue=2,maxValue=4" を追記します。
なお、dpad/up に関しては parameters = "minValue=8,maxValue=2" とすることで勝手に「8以上または2以下」と解釈してくれるらしいです。ありがたい。
最終的なd-padのコードが以下のようになりました。
[FieldOffset(13)]
[InputControl(name = "dpad", layout = "Dpad", sizeInBits = 4, bit = 0)]
[InputControl(name = "dpad/up", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=8,maxValue=2")]
[InputControl(name = "dpad/right", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=2,maxValue=4")]
[InputControl(name = "dpad/down", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=4,maxValue=6")]
[InputControl(name = "dpad/left", layout = "DiscreteButton", format = "BIT", sizeInBits = 4, bit = 0, parameters = "minValue=6,maxValue=8")]
public byte DPad;
format = "BIT", sizeInBits = 4, bit = 0 と指定することで「下位4ビット」を抽出しています。
スティック
では、最後にスティックです。
私のコントローラーでは、Lスティックをぐるぐる動かすと、2,3,4,5バイト目が変動しました。
ニュートラルだと 00 80 FF 7F、真上だと 00 80 00 00 辺り、真下は大体 00 80 FF FF くらいになります。どうやら、4,5バイト目がy軸に対応していそうです。また、数値の大きさかを見るに、データは4ビットのリトルエンディアンで上が 0x0000、下が 0xFFFF になっていると考えられます。
同様に調べてみると、2,3バイト目がx軸に対応しており、同じくリトルエンディアンで右が 0xFFFF、左が 0x0000 だと思われます。
Rスティックでも同じように調べるた結果、以下のようになりました。
[FieldOffset(1)]
[InputControl(name = "leftStick", layout = "Stick", format = "VEC2")]
[InputControl(name = "leftStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "leftStick/y", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,invert=true")]
public byte LeftStick;
[FieldOffset(5)]
[InputControl(name = "rightStick", layout = "Stick", format = "VEC2")]
[InputControl(name = "rightStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
[InputControl(name = "rightStick/y", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,invert=true")]
public byte RightStick;
format = "USHT" でushort型 (符号なし16ビット整数) を指定しています。
また、今回は parameters で normalizeZero=0.5 を指定することで 0xFFFF で 1、 0x0000 で -1 が出力されるようにしています。なお、Unityのy軸は上が正の方向なので、y方向入力だけ invert=true を追加しています。
入力設定の登録
これで、生の入力データの読み方を登録した構造体が完成しました。あとは、これをUnityに登録するだけです。
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
[InputControlLayout(stateType = typeof(MyWirelessInputReport))]
public class MyWirelessGamepadHID : Gamepad
{
static MyWirelessGamepadHID()
{
if (Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor)
{
InputSystem.RegisterLayout<MyWirelessGamepadHID>(
"MyWirelessGamepadHID",
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", MyWirelessInputReport.VendorID)
.WithCapability("productId", MyWirelessInputReport.ProductId)
);
Debug.Log("Registered a custom struct layout for my controller on Mac.");
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Init() { }
}
[InputControlLayout(stateType = typeof(MyWirelessInputReport))] で、デバイスと入力設定を紐づけています。
InputSystem.RegisterLayout で入力設定をInputSystemに登録していますが、その際、InputDeviceMatcher.WithCapability() でこの入力設定を適用する条件を指定しています。
ここまで書いたら、Unityにコンパイルさせ、一度コントローラーの接続を解除し、再接続することで、正しい入力設定が反映されるかと思います。
ちなみに、設定が反映されていると、Input Debuggerのデバイス名の表示が自作したデバイスの型名になります。

設定が反映されない場合
場合によっては、ここまでやっても自作した設定が反映されないことがあります。
Gemini曰く、「デバイスが接続されたとき、Unityは最も『一致度スコア』の高いデバイス設定を適用するが、VendorIDとProductIDだけではスコアが負けてしまっている」だそうです。
こうなってしまうともうどうしようもない気がするので、強制的に設定を適用してあげましょう。
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
[InputControlLayout(stateType = typeof(MyWirelessInputReport))]
public class MyWirelessGamepadHID : Gamepad
{
static MyWirelessGamepadHID()
{
if (Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor)
{
InputSystem.RegisterLayout<MyWirelessGamepadHID>("MyWirelessGamepadHID");
Debug.Log("Registered a custom struct layout for the wireless Xbox controller on Mac.");
}
InputSystem.onFindLayoutForDevice -= CustomLayoutHook;
InputSystem.onFindLayoutForDevice += CustomLayoutHook;
}
private static string CustomLayoutHook(ref InputDeviceDescription description, string matchedLayout, InputDeviceExecuteCommandDelegate executeCommand)
{
if (!string.IsNullOrEmpty(description.product) && description.product == MyWirelessInputReport.Product)
{
Debug.Log("<color=cyan>Conditions cleared</color> Forcibly applying MyWirelessGamepadHID.");
return "MyWirelessGamepadHID";
}
return null;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Init() { }
}
InputSystem.onFindLayoutForDevice でデバイスが接続された際のイベントを取得することができます。
まとめ
いかがだったでしょうか。
かなり根気のいる作業でしたが、私はパズル感覚でできて楽しかったです。
何にせよ、これでやっとMacでテストプレイができるようになりました。
同じ悩みを抱いていた方々の参考になれば幸いです。
夏のブログリレー、明日の担当は @Ida-ji です。お楽しみに!
参考
Unity Input SystemでXbox360コントローラーがGamepadとして認識されない件
UnityのInput SystemでUSB接続のゲームパッドを使用できるようにする手順
InputSystemで誤認されているHIDデバイスの日本語での修正マニュアル.
InputSystemリファレンス HID のサポート