diff --git a/Assets/Tests/InputSystem/Plugins/XInputTests.cs b/Assets/Tests/InputSystem/Plugins/XInputTests.cs index 1e122c0466..8fd76f474b 100644 --- a/Assets/Tests/InputSystem/Plugins/XInputTests.cs +++ b/Assets/Tests/InputSystem/Plugins/XInputTests.cs @@ -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()); + Assert.That(device, Is.AssignableTo()); + 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 diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index bf42c7171d..110c17ee4f 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -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) diff --git a/Packages/com.unity.inputsystem/Documentation~/Gamepad.md b/Packages/com.unity.inputsystem/Documentation~/Gamepad.md index ad4acf19cf..fbf9fd7209 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Gamepad.md +++ b/Packages/com.unity.inputsystem/Documentation~/Gamepad.md @@ -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. diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs index 06a3e37d94..2e947b9b8d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs @@ -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( 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( + 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. diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs index 5cdec5258f..aaefa1c685 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs @@ -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 { @@ -296,14 +374,29 @@ namespace UnityEngine.InputSystem.XInput /// /// /// 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 /// [InputControlLayout(displayName = "Xbox Controller", stateType = typeof(XInputControllerOSXState), hideInUI = true)] public class XboxGamepadMacOS : XInputController { } + /// + /// A wired Xbox Gamepad connected to a macOS computer + /// + /// + /// An Xbox 360 or Xbox One wired gamepad connected ot a Mac. + /// This layout is used on modern macOS systems. It is different from , 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. + /// + [InputControlLayout(displayName = "Xbox Controller", stateType = typeof(XInputControllerNativeOSXState), hideInUI = true)] + public class XboxGamepadMacOSNative : XInputController + { + } + /// /// A wireless Xbox One Gamepad connected to a macOS computer. /// @@ -311,7 +404,7 @@ public class XboxGamepadMacOS : XInputController /// 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 /// [InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXState), hideInUI = true)] public class XboxOneGampadMacOSWireless : XInputController @@ -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 /// [InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXStateV2), hideInUI = true)] public class XboxGamepadMacOSWireless : XInputController