Skip to content

Commit fbb5577

Browse files
NEW: Support Xbox controllers over USB using native macOS support [ISXB-1547] (#2179)
1 parent 1f9414b commit fbb5577

File tree

5 files changed

+205
-5
lines changed

5 files changed

+205
-5
lines changed

Assets/Tests/InputSystem/Plugins/XInputTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,82 @@ public void Devices_SupportXboxControllerOnOSX()
150150
AssertButtonPress(gamepad, new XInputControllerOSXState().WithButton(XInputControllerOSXState.Button.Select), gamepad.selectButton);
151151
}
152152

153+
[Test]
154+
[Category("Devices")]
155+
public void Devices_SupportXboxControllerUsingOSDriverOSX()
156+
{
157+
// Native support kicks in when a device is named "Controller"
158+
// This is what macOS names the controller
159+
var device = InputSystem.AddDevice(new InputDeviceDescription
160+
{
161+
interfaceName = "HID",
162+
product = "Controller",
163+
manufacturer = "Microsoft"
164+
});
165+
166+
Assert.That(device, Is.AssignableTo<XInputController>());
167+
Assert.That(device, Is.AssignableTo<XboxGamepadMacOSNative>());
168+
var gamepad = (XboxGamepadMacOSNative)device;
169+
170+
// macOS reports the same way we do for the Y axis; e.g. up = 1, down = -1
171+
// As such, our input data from the controller doesn't need to be inverted
172+
// This is unlike our approach for the 360Controller device
173+
InputSystem.QueueStateEvent(gamepad,
174+
new XInputControllerNativeOSXState()
175+
{
176+
leftStickX = 32767,
177+
leftStickY = 32767,
178+
rightStickX = 32767,
179+
rightStickY = 32767,
180+
leftTrigger = 255,
181+
rightTrigger = 255,
182+
});
183+
184+
InputSystem.Update();
185+
186+
Assert.That(gamepad.leftStick.x.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
187+
Assert.That(gamepad.leftStick.y.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
188+
Assert.That(gamepad.leftStick.up.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
189+
Assert.That(gamepad.leftStick.down.ReadValue(), Is.EqualTo(0.0).Within(0.001));
190+
Assert.That(gamepad.leftStick.right.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
191+
Assert.That(gamepad.leftStick.left.ReadValue(), Is.EqualTo(0.0).Within(0.001));
192+
193+
Assert.That(gamepad.rightStick.x.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
194+
Assert.That(gamepad.rightStick.y.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
195+
Assert.That(gamepad.rightStick.up.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
196+
Assert.That(gamepad.rightStick.down.ReadValue(), Is.EqualTo(0.0).Within(0.001));
197+
Assert.That(gamepad.rightStick.right.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
198+
Assert.That(gamepad.rightStick.left.ReadValue(), Is.EqualTo(0.0).Within(0.001));
199+
200+
Assert.That(gamepad.leftTrigger.ReadValue(), Is.EqualTo(1));
201+
Assert.That(gamepad.rightTrigger.ReadValue(), Is.EqualTo(1));
202+
203+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.A), gamepad.aButton);
204+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.A), gamepad.buttonSouth);
205+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.B), gamepad.bButton);
206+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.B), gamepad.buttonEast);
207+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.X), gamepad.xButton);
208+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.X), gamepad.buttonWest);
209+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Y), gamepad.yButton);
210+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Y), gamepad.buttonNorth);
211+
212+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadDown), gamepad.dpad.down);
213+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadUp), gamepad.dpad.up);
214+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadLeft), gamepad.dpad.left);
215+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadRight), gamepad.dpad.right);
216+
217+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.LeftThumbstickPress), gamepad.leftStickButton);
218+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.RightThumbstickPress), gamepad.rightStickButton);
219+
220+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.LeftShoulder), gamepad.leftShoulder);
221+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.RightShoulder), gamepad.rightShoulder);
222+
223+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Start), gamepad.menu);
224+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Start), gamepad.startButton);
225+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Select), gamepad.view);
226+
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Select), gamepad.selectButton);
227+
}
228+
153229
[TestCase(0x045E, 0x02E0, 16, 11)] // Xbox One Wireless Controller
154230
[TestCase(0x045E, 0x0B20, 10, 11)] // Xbox Series X|S Wireless Controller
155231
// This test is used to establish the correct button map layout based on the PID and VIDs. The usual difference

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ however, it has to be formatted properly to pass verification tests.
1010

1111
## [Unreleased] - yyyy-mm-dd
1212

13+
### Added
14+
15+
- Support for Xbox controllers over USB on macOS, using macOS's default driver. [ISXB-1548]
16+
1317
### Fixed
1418
- Fixed an analytics event being invoked twice when the Save button in the Actions view was pressed. [ISXB-1378](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1378)
1519
- Fixed an issue causing a number of errors to be displayed when using `InputTestFixture` in playmode tests with domain reloading disabled on playmode entry. [ISXB-1446](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1446)

Packages/com.unity.inputsystem/Documentation~/Gamepad.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ Xbox controllers are well supported on different Devices. The Input System imple
177177

178178
On other platforms Unity, uses derived classes to represent Xbox controllers:
179179

180-
* [`XboxGamepadMacOS`](../api/UnityEngine.InputSystem.XInput.XboxGamepadMacOS.html): Any Xbox or compatible gamepad connected to a Mac via USB using the [Xbox Controller Driver for macOS](https://github.com/360Controller/360Controller).
180+
* [`XboxGamepadMacOS`](../api/UnityEngine.InputSystem.XInput.XboxGamepadMacOS.html): Any Xbox or compatible gamepad connected to a Mac via USB using the [Xbox Controller Driver for macOS](https://github.com/360Controller/360Controller). This class is only used when the `360Controller` driver is in use, and as such you shouldn't see it in use on modern versions of macOS - it is provided primarily for legacy reasons, and for scenarios where macOS 10.15 may still be used.
181+
182+
* [`XboxGamepadMacOSNative`](../api/UnityEngine.InputSystem.XInput.XboxGamepadMacOSNative.html): Any Xbox gamepad connected to a Mac (macOS 11.0 or higher) via USB. On modern macOS versions, you will get this class instead of `XboxGamepadMacOS`
181183

182184
* [`XboxOneGampadMacOSWireless`](../api/UnityEngine.InputSystem.XInput.XboxOneGampadMacOSWireless.html): An Xbox One controller connected to a Mac via Bluetooth. Only the latest generation of Xbox One controllers supports Bluetooth. These controllers don't require any additional drivers in this scenario.
183185

Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,18 @@ public static void Initialize()
2525
matches: new InputDeviceMatcher().WithInterface("XInput"));
2626
#endif
2727
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
28+
// Legacy support when a user is using the 360Controller driver on macOS <= 10.15
2829
InputSystem.RegisterLayout<XboxGamepadMacOS>(
2930
matches: new InputDeviceMatcher().WithInterface("HID")
3031
.WithProduct("Xbox.*Wired Controller"));
3132

33+
34+
// Matches macOS native support for Xbox Controllers
35+
// macOS reports all Xbox controllers as "Controller" with manufacter Microsoft
36+
InputSystem.RegisterLayout<XboxGamepadMacOSNative>(
37+
matches: new InputDeviceMatcher().WithInterface("HID")
38+
.WithProduct("Controller").WithManufacturer("Microsoft"));
39+
3240
// Matching older Xbox One controllers that have different View and Share buttons than the newer Xbox Series
3341
// controllers.
3442
// Reported inhttps://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1264

Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,101 @@ public XInputControllerOSXState WithButton(Button button)
103103
}
104104
}
105105

106+
// macOS's native bit mapping for Xbox Controllers connected via USB
107+
[StructLayout(LayoutKind.Explicit)]
108+
internal struct XInputControllerNativeOSXState : IInputStateTypeInfo
109+
{
110+
public static FourCC kFormat => new FourCC('H', 'I', 'D');
111+
112+
public enum Button
113+
{
114+
Start = 2,
115+
Select = 3,
116+
A = 4,
117+
B = 5,
118+
X = 6,
119+
Y = 7,
120+
DPadUp = 8,
121+
DPadDown = 9,
122+
DPadLeft = 10,
123+
DPadRight = 11,
124+
LeftShoulder = 12,
125+
RightShoulder = 13,
126+
LeftThumbstickPress = 14,
127+
RightThumbstickPress = 15,
128+
}
129+
130+
// IL2CPP on 2021 doesn't respect the FieldOffsets - as such, we need some padding fields
131+
#if UNITY_2021 && ENABLE_IL2CPP
132+
[FieldOffset(0)]
133+
private uint padding;
134+
#endif
135+
136+
[InputControl(name = "buttonSouth", bit = (uint)Button.A, displayName = "A")]
137+
[InputControl(name = "buttonEast", bit = (uint)Button.B, displayName = "B")]
138+
[InputControl(name = "buttonWest", bit = (uint)Button.X, displayName = "X")]
139+
[InputControl(name = "buttonNorth", bit = (uint)Button.Y, displayName = "Y")]
140+
[InputControl(name = "start", bit = (uint)Button.Start, displayName = "Start")]
141+
[InputControl(name = "select", bit = (uint)Button.Select, displayName = "Select")]
142+
[InputControl(name = "dpad", layout = "Dpad", sizeInBits = 4, bit = 0)]
143+
[InputControl(name = "dpad/up", bit = (uint)Button.DPadUp)]
144+
[InputControl(name = "dpad/down", bit = (uint)Button.DPadDown)]
145+
[InputControl(name = "dpad/left", bit = (uint)Button.DPadLeft)]
146+
[InputControl(name = "dpad/right", bit = (uint)Button.DPadRight)]
147+
[InputControl(name = "leftStickPress", bit = (uint)Button.LeftThumbstickPress)]
148+
[InputControl(name = "rightStickPress", bit = (uint)Button.RightThumbstickPress)]
149+
[InputControl(name = "leftShoulder", bit = (uint)Button.LeftShoulder)]
150+
[InputControl(name = "rightShoulder", bit = (uint)Button.RightShoulder)]
151+
[FieldOffset(4)]
152+
public ushort buttons;
153+
154+
[InputControl(name = "leftTrigger", format = "BYTE")]
155+
[FieldOffset(6)] public byte leftTrigger;
156+
157+
#if UNITY_2021 && ENABLE_IL2CPP
158+
[FieldOffset(7)]
159+
private byte triggerPadding;
160+
#endif
161+
162+
[InputControl(name = "rightTrigger", format = "BYTE")]
163+
[FieldOffset(8)] public byte rightTrigger;
164+
165+
#if UNITY_2021 && ENABLE_IL2CPP
166+
[FieldOffset(9)]
167+
private byte triggerPadding2;
168+
#endif
169+
170+
171+
[InputControl(name = "leftStick", layout = "Stick", format = "VC2S")]
172+
[InputControl(name = "leftStick/x", offset = 0, format = "SHRT", parameters = "")]
173+
[InputControl(name = "leftStick/left", offset = 0, format = "SHRT", parameters = "")]
174+
[InputControl(name = "leftStick/right", offset = 0, format = "SHRT", parameters = "")]
175+
[InputControl(name = "leftStick/y", offset = 2, format = "SHRT", parameters = "")]
176+
[InputControl(name = "leftStick/up", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=0,clampMax=1,invert=false")]
177+
[InputControl(name = "leftStick/down", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert=true")]
178+
[FieldOffset(10)] public short leftStickX;
179+
[FieldOffset(12)] public short leftStickY;
180+
181+
[InputControl(name = "rightStick", layout = "Stick", format = "VC2S")]
182+
[InputControl(name = "rightStick/x", offset = 0, format = "SHRT", parameters = "")]
183+
[InputControl(name = "rightStick/left", offset = 0, format = "SHRT", parameters = "")]
184+
[InputControl(name = "rightStick/right", offset = 0, format = "SHRT", parameters = "")]
185+
[InputControl(name = "rightStick/y", offset = 2, format = "SHRT", parameters = "")]
186+
[InputControl(name = "rightStick/up", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=0,clampMax=1,invert=false")]
187+
[InputControl(name = "rightStick/down", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert=true")]
188+
[FieldOffset(14)] public short rightStickX;
189+
[FieldOffset(16)] public short rightStickY;
190+
191+
public FourCC format => kFormat;
192+
193+
public XInputControllerNativeOSXState WithButton(Button button)
194+
{
195+
Debug.Assert((int)button < 16, $"A maximum of 16 buttons is supported for this layout.");
196+
buttons |= (ushort)(1U << (int)button);
197+
return this;
198+
}
199+
}
200+
106201
[StructLayout(LayoutKind.Explicit)]
107202
internal struct XInputControllerWirelessOSXState : IInputStateTypeInfo
108203
{
@@ -296,22 +391,37 @@ namespace UnityEngine.InputSystem.XInput
296391
/// </summary>
297392
/// <remarks>
298393
/// An Xbox 360 or Xbox one wired gamepad connected to a mac.
299-
/// These controllers don't work on a mac out of the box, but require a driver like https://github.com/360Controller/
300-
/// to work.
394+
/// This layout is used for macOS versions when https://github.com/360Controller/ was required
395+
/// On modern macOS versions, you will instead get a device with class XboxGamepadMacOSNative
301396
/// </remarks>
302397
[InputControlLayout(displayName = "Xbox Controller", stateType = typeof(XInputControllerOSXState), hideInUI = true)]
303398
public class XboxGamepadMacOS : XInputController
304399
{
305400
}
306401

402+
/// <summary>
403+
/// A wired Xbox Gamepad connected to a macOS computer
404+
/// </summary>
405+
/// <remarks>
406+
/// An Xbox 360 or Xbox One wired gamepad connected ot a Mac.
407+
/// This layout is used on modern macOS systems. It is different from <see cref="XboxGamepadMacOS"/>, due to that working with older
408+
/// systems that are using the 360Controller driver.
409+
/// macOS's native controller support provides a bit mapping which is different to 360Controller's mapping
410+
/// As such this is a new device, in order to not break existing projects.
411+
/// </remarks>
412+
[InputControlLayout(displayName = "Xbox Controller", stateType = typeof(XInputControllerNativeOSXState), hideInUI = true)]
413+
public class XboxGamepadMacOSNative : XInputController
414+
{
415+
}
416+
307417
/// <summary>
308418
/// A wireless Xbox One Gamepad connected to a macOS computer.
309419
/// </summary>
310420
/// <remarks>
311421
/// An Xbox One wireless gamepad connected to a mac using Bluetooth.
312422
/// Note: only the latest version of Xbox One wireless gamepads support Bluetooth. Older models only work
313423
/// with a proprietary Xbox wireless protocol, and cannot be used on a Mac.
314-
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on macOS.
424+
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on older macOS versions
315425
/// </remarks>
316426
[InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXState), hideInUI = true)]
317427
public class XboxOneGampadMacOSWireless : XInputController
@@ -328,7 +438,7 @@ public class XboxOneGampadMacOSWireless : XInputController
328438
/// that some Xbox One and Xbox Series controller share the same mappings so this combines them all.
329439
/// Note: only the latest version of Xbox One wireless gamepads support Bluetooth. Older models only work
330440
/// with a proprietary Xbox wireless protocol, and cannot be used on a Mac.
331-
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on macOS.
441+
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on older macOS versions
332442
/// </remarks>
333443
[InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXStateV2), hideInUI = true)]
334444
public class XboxGamepadMacOSWireless : XInputController

0 commit comments

Comments
 (0)