Skip to content

NEW: Support Xbox controllers over USB using native macOS support #2178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions Assets/Tests/InputSystem/Plugins/XInputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,83 @@ public void Devices_SupportXboxControllerOnOSX()
AssertButtonPress(gamepad, new XInputControllerOSXState().WithButton(XInputControllerOSXState.Button.Select), gamepad.view);
AssertButtonPress(gamepad, new XInputControllerOSXState().WithButton(XInputControllerOSXState.Button.Select), gamepad.selectButton);
}

[Test]
[Category("Devices")]
public void Devices_SupportXboxControllerUsingOSDriverOSX()
{
// Native support kicks in when a device is named "Controller"
// This is what macOS names the controller
var device = InputSystem.AddDevice(new InputDeviceDescription
{
interfaceName = "HID",
product = "Controller",
manufacturer = "Microsoft"
});

Assert.That(device, Is.AssignableTo<XInputController>());
Assert.That(device, Is.AssignableTo<XboxGamepadMacOSNative>());
var gamepad = (XboxGamepadMacOSNative)device;

// macOS reports the same way we do for the Y axis; e.g. up = 1, down = -1
// As such, our input data from the controller doesn't need to be inverted
// This is unlike our approach for the 360Controller device
InputSystem.QueueStateEvent(gamepad,
new XInputControllerNativeOSXState()
{
leftStickX = 32767,
leftStickY = 32767,
rightStickX = 32767,
rightStickY = 32767,
leftTrigger = 255,
rightTrigger = 255,
});

InputSystem.Update();

Assert.That(gamepad.leftStick.x.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.leftStick.y.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.leftStick.up.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.leftStick.down.ReadValue(), Is.EqualTo(0.0).Within(0.001));
Assert.That(gamepad.leftStick.right.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.leftStick.left.ReadValue(), Is.EqualTo(0.0).Within(0.001));

Assert.That(gamepad.rightStick.x.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.rightStick.y.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.rightStick.up.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.rightStick.down.ReadValue(), Is.EqualTo(0.0).Within(0.001));
Assert.That(gamepad.rightStick.right.ReadValue(), Is.EqualTo(0.9999).Within(0.001));
Assert.That(gamepad.rightStick.left.ReadValue(), Is.EqualTo(0.0).Within(0.001));

Assert.That(gamepad.leftTrigger.ReadValue(), Is.EqualTo(1));
Assert.That(gamepad.rightTrigger.ReadValue(), Is.EqualTo(1));

AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.A), gamepad.aButton);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.A), gamepad.buttonSouth);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.B), gamepad.bButton);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.B), gamepad.buttonEast);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.X), gamepad.xButton);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.X), gamepad.buttonWest);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Y), gamepad.yButton);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Y), gamepad.buttonNorth);

AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadDown), gamepad.dpad.down);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadUp), gamepad.dpad.up);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadLeft), gamepad.dpad.left);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.DPadRight), gamepad.dpad.right);

AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.LeftThumbstickPress), gamepad.leftStickButton);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.RightThumbstickPress), gamepad.rightStickButton);

AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.LeftShoulder), gamepad.leftShoulder);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.RightShoulder), gamepad.rightShoulder);

AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Start), gamepad.menu);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Start), gamepad.startButton);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Select), gamepad.view);
AssertButtonPress(gamepad, new XInputControllerNativeOSXState().WithButton(XInputControllerNativeOSXState.Button.Select), gamepad.selectButton);
}

[TestCase(0x045E, 0x02E0, 16, 11)] // Xbox One Wireless Controller
[TestCase(0x045E, 0x0B20, 10, 11)] // Xbox Series X|S Wireless Controller
// This test is used to establish the correct button map layout based on the PID and VIDs. The usual difference
Expand Down
4 changes: 4 additions & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ however, it has to be formatted properly to pass verification tests.

## [Unreleased] - yyyy-mm-dd

### Added

- Support for Xbox controllers over USB on macOS, using macOS's default driver. [ISXB-1548]

### Fixed
- 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)
- 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)
Expand Down
4 changes: 3 additions & 1 deletion Packages/com.unity.inputsystem/Documentation~/Gamepad.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ Xbox controllers are well supported on different Devices. The Input System imple

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

* [`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).
* [`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.

* [`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`

* [`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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@ public static void Initialize()
matches: new InputDeviceMatcher().WithInterface("XInput"));
#endif
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
// Legacy support when a user is using the 360Controller driver on macOS <= 10.15
InputSystem.RegisterLayout<XboxGamepadMacOS>(
matches: new InputDeviceMatcher().WithInterface("HID")
.WithProduct("Xbox.*Wired Controller"));


// Matches macOS native support for Xbox Controllers
// macOS reports all Xbox controllers as "Controller" with manufacter Microsoft
InputSystem.RegisterLayout<XboxGamepadMacOSNative>(
matches: new InputDeviceMatcher().WithInterface("HID")
.WithProduct("Controller").WithManufacturer("Microsoft"));

// Matching older Xbox One controllers that have different View and Share buttons than the newer Xbox Series
// controllers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,84 @@ public XInputControllerOSXState WithButton(Button button)
}
}

// macOS's native bit mapping for Xbox Controllers connected via USB
[StructLayout(LayoutKind.Explicit)]
internal struct XInputControllerNativeOSXState : IInputStateTypeInfo
{
public static FourCC kFormat => new FourCC('H', 'I', 'D');

public enum Button
{
Start = 2,
Select = 3,
A = 4,
B = 5,
X = 6,
Y = 7,
DPadUp = 8,
DPadDown = 9,
DPadLeft = 10,
DPadRight = 11,
LeftShoulder = 12,
RightShoulder = 13,
LeftThumbstickPress = 14,
RightThumbstickPress = 15,
}

[InputControl(name = "buttonSouth", bit = (uint)Button.A, displayName = "A")]
[InputControl(name = "buttonEast", bit = (uint)Button.B, displayName = "B")]
[InputControl(name = "buttonWest", bit = (uint)Button.X, displayName = "X")]
[InputControl(name = "buttonNorth", bit = (uint)Button.Y, displayName = "Y")]
[InputControl(name = "start", bit = (uint)Button.Start, displayName = "Start")]
[InputControl(name = "select", bit = (uint)Button.Select, displayName = "Select")]
[InputControl(name = "dpad", layout = "Dpad", sizeInBits = 4, bit = 0)]
[InputControl(name = "dpad/up", bit = (uint)Button.DPadUp)]
[InputControl(name = "dpad/down", bit = (uint)Button.DPadDown)]
[InputControl(name = "dpad/left", bit = (uint)Button.DPadLeft)]
[InputControl(name = "dpad/right", bit = (uint)Button.DPadRight)]
[InputControl(name = "leftStickPress", bit = (uint)Button.LeftThumbstickPress)]
[InputControl(name = "rightStickPress", bit = (uint)Button.RightThumbstickPress)]
[InputControl(name = "leftShoulder", bit = (uint)Button.LeftShoulder)]
[InputControl(name = "rightShoulder", bit = (uint)Button.RightShoulder)]
[FieldOffset(4)]
public ushort buttons;

[InputControl(name = "leftTrigger", format = "BYTE")]
[FieldOffset(6)] public byte leftTrigger;
[InputControl(name = "rightTrigger", format = "BYTE")]
[FieldOffset(8)] public byte rightTrigger;


[InputControl(name = "leftStick", layout = "Stick", format = "VC2S")]
[InputControl(name = "leftStick/x", offset = 0, format = "SHRT", parameters = "")]
[InputControl(name = "leftStick/left", offset = 0, format = "SHRT", parameters = "")]
[InputControl(name = "leftStick/right", offset = 0, format = "SHRT", parameters = "")]
[InputControl(name = "leftStick/y", offset = 2, format = "SHRT", parameters = "")]
[InputControl(name = "leftStick/up", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=0,clampMax=1,invert=false")]
[InputControl(name = "leftStick/down", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert=true")]
[FieldOffset(10)] public short leftStickX;
[FieldOffset(12)] public short leftStickY;

[InputControl(name = "rightStick", layout = "Stick", format = "VC2S")]
[InputControl(name = "rightStick/x", offset = 0, format = "SHRT", parameters = "")]
[InputControl(name = "rightStick/left", offset = 0, format = "SHRT", parameters = "")]
[InputControl(name = "rightStick/right", offset = 0, format = "SHRT", parameters = "")]
[InputControl(name = "rightStick/y", offset = 2, format = "SHRT", parameters = "")]
[InputControl(name = "rightStick/up", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=0,clampMax=1,invert=false")]
[InputControl(name = "rightStick/down", offset = 2, format = "SHRT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert=true")]
[FieldOffset(14)] public short rightStickX;
[FieldOffset(16)] public short rightStickY;

public FourCC format => kFormat;

public XInputControllerNativeOSXState WithButton(Button button)
{
Debug.Assert((int)button < 16, $"A maximum of 16 buttons is supported for this layout.");
buttons |= (ushort)(1U << (int)button);
return this;
}
}

[StructLayout(LayoutKind.Explicit)]
internal struct XInputControllerWirelessOSXState : IInputStateTypeInfo
{
Expand Down Expand Up @@ -296,22 +374,37 @@ namespace UnityEngine.InputSystem.XInput
/// </summary>
/// <remarks>
/// An Xbox 360 or Xbox one wired gamepad connected to a mac.
/// These controllers don't work on a mac out of the box, but require a driver like https://github.com/360Controller/
/// to work.
/// This layout is used for macOS versions when https://github.com/360Controller/ was required
/// On modern macOS versions, you will instead get a device with class XboxGamepadMacOSNative
/// </remarks>
[InputControlLayout(displayName = "Xbox Controller", stateType = typeof(XInputControllerOSXState), hideInUI = true)]
public class XboxGamepadMacOS : XInputController
{
}

/// <summary>
/// A wired Xbox Gamepad connected to a macOS computer
/// </summary>
/// <remarks>
/// An Xbox 360 or Xbox One wired gamepad connected ot a Mac.
/// This layout is used on modern macOS systems. It is different from <see cref="XboxGamepadMacOS"/>, due to that working with older
/// systems that are using the 360Controller driver.
/// macOS's native controller support provides a bit mapping which is different to 360Controller's mapping
/// As such this is a new device, in order to not break existing projects.
/// </remarks>
[InputControlLayout(displayName = "Xbox Controller", stateType = typeof(XInputControllerNativeOSXState), hideInUI = true)]
public class XboxGamepadMacOSNative : XInputController
{
}

/// <summary>
/// A wireless Xbox One Gamepad connected to a macOS computer.
/// </summary>
/// <remarks>
/// An Xbox One wireless gamepad connected to a mac using Bluetooth.
/// Note: only the latest version of Xbox One wireless gamepads support Bluetooth. Older models only work
/// with a proprietary Xbox wireless protocol, and cannot be used on a Mac.
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on macOS.
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on older macOS versions
/// </remarks>
[InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXState), hideInUI = true)]
public class XboxOneGampadMacOSWireless : XInputController
Expand All @@ -328,7 +421,7 @@ public class XboxOneGampadMacOSWireless : XInputController
/// that some Xbox One and Xbox Series controller share the same mappings so this combines them all.
/// Note: only the latest version of Xbox One wireless gamepads support Bluetooth. Older models only work
/// with a proprietary Xbox wireless protocol, and cannot be used on a Mac.
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on macOS.
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on older macOS versions
/// </remarks>
[InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXStateV2), hideInUI = true)]
public class XboxGamepadMacOSWireless : XInputController
Expand Down