Skip to content

FIX: Correct north, east, south, west bindings for Switch Pro Controller to match the physical layout of the device #2208

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

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
52 changes: 52 additions & 0 deletions Assets/Tests/InputSystem/Plugins/iOSTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,65 @@ public void Devices_SupportsiOSGamePad(string product, Type deviceType, Type par
Assert.That(gamepad.rightTrigger.ReadValue(), Is.EqualTo(0.456).Within(0.000001));

AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.A), gamepad.buttonSouth);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.A), gamepad.aButton);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.X), gamepad.buttonWest);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.X), gamepad.xButton);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.Y), gamepad.buttonNorth);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.Y), gamepad.yButton);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.B), gamepad.buttonEast);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.B), gamepad.bButton);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.LeftShoulder), gamepad.leftShoulder);
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.RightShoulder), gamepad.rightShoulder);
}

[Test]
[Category("Devices")]
// this is a new test, as we need to assert the Nintendo layout (e.g. buttonSouth == B button)
public void Devices_SupportsSwitchProControlleriOS()
{
var device = InputSystem.AddDevice(
new InputDeviceDescription
{
interfaceName = "iOS",
deviceClass = "iOSGameController",
product = "Pro Controller"
});
Assert.That(device, Is.TypeOf(typeof(SwitchProControlleriOS)));
Assert.That(device, Is.InstanceOf(typeof(SwitchProController)));
Assert.That(device, Is.InstanceOf(typeof(Gamepad)));

var gamepad = (SwitchProControlleriOS)device;

InputSystem.QueueStateEvent(gamepad,
new iOSGameControllerStateSwappedFaceButtons()
.WithButton(iOSButton.LeftTrigger, true, 0.123f)
.WithButton(iOSButton.RightTrigger, true, 0.456f)
.WithAxis(iOSAxis.LeftStickX, 0.789f)
.WithAxis(iOSAxis.LeftStickY, 0.987f)
.WithAxis(iOSAxis.RightStickX, 0.654f)
.WithAxis(iOSAxis.RightStickY, 0.321f));
InputSystem.Update();

var leftStickDeadzone = gamepad.leftStick.TryGetProcessor<StickDeadzoneProcessor>();
var rightStickDeadzone = gamepad.leftStick.TryGetProcessor<StickDeadzoneProcessor>();

Assert.That(gamepad.leftStick.ReadValue(), Is.EqualTo(leftStickDeadzone.Process(new Vector2(0.789f, 0.987f))));
Assert.That(gamepad.rightStick.ReadValue(), Is.EqualTo(rightStickDeadzone.Process(new Vector2(0.654f, 0.321f))));
Assert.That(gamepad.leftTrigger.ReadValue(), Is.EqualTo(0.123).Within(0.000001));
Assert.That(gamepad.rightTrigger.ReadValue(), Is.EqualTo(0.456).Within(0.000001));
// testing for Pro Controller layout...
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.A), gamepad.buttonEast);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.A), gamepad.aButton);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.X), gamepad.buttonNorth);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.X), gamepad.xButton);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.Y), gamepad.buttonWest);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.Y), gamepad.yButton);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.B), gamepad.buttonSouth);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.B), gamepad.bButton);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.LeftShoulder), gamepad.leftShoulder);
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.RightShoulder), gamepad.rightShoulder);
}

[Test]
[Category("Devices")]
[TestCase("Gravity", typeof(GravitySensor))]
Expand Down
5 changes: 5 additions & 0 deletions Assets/Tests/InputSystem/SwitchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ public void Devices_SupportsHIDNpad()
Assert.That(currentRight, Is.EqualTo(expectedRight).Using(new Vector2EqualityComparer(0.01f)));

AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.A), controller.buttonEast);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.A), controller.aButton);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.B), controller.buttonSouth);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.B), controller.bButton);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.X), controller.buttonNorth);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.X), controller.xButton);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Y), controller.buttonWest);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Y), controller.yButton);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.StickL), controller.leftStickButton);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.StickR), controller.rightStickButton);
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.L), controller.leftShoulder);
Expand Down Expand Up @@ -168,6 +172,7 @@ public void Devices_SupportsSwitchLikeControllers(int vendorId, int productId)
});

Assert.That(device, Is.TypeOf<SwitchProControllerHID>());
Assert.That(device, Is.InstanceOf(typeof(SwitchProController)));
}

#endif
Expand Down
2 changes: 2 additions & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ however, it has to be formatted properly to pass verification tests.

### Fixed
- Fixed an issue where using Pen devices on Android tablets would result in double clicks for UI interactions. [ISXB-1456](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1456)
- Fixed `buttonSouth` returning the state of the east button (and so on for all the compass named buttons) when using a Nintendo Switch Pro Controller on iOS [ISXB-1632](issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1632)
- Fixed `aButton` returning the state of the east button (and so on for all the letter named buttons) when using a Nintendo Switch Pro Controller on Standalone & iOS [ISXB-1632](issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1632)



Expand Down
35 changes: 35 additions & 0 deletions Packages/com.unity.inputsystem/InputSystem/Devices/Gamepad.cs
Original file line number Diff line number Diff line change
Expand Up @@ -806,4 +806,39 @@ public virtual void SetMotorSpeeds(float lowFrequency, float highFrequency)
private static int s_GamepadCount;
private static Gamepad[] s_Gamepads;
}

/// <summary>
/// Base class for Nintendo Switch Pro Controllers that provides the correct button mappings for Nintendo's face button layout where A is east, B is south, X is north, and Y is west.
/// If you use InputSystem.GetDevice and the ABXY properties to represent the labels on the device, you must query for this class
/// </summary>
public abstract class SwitchProController : Gamepad
{
/// <summary>
/// A Button for a Nintendo Switch Pro Controller.
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
/// The gamepad class will return the state of buttonSouth, whereas this class returns the state of buttonEast
/// </summary>
public new ButtonControl aButton => buttonEast;

/// <summary>
/// B Button for a Nintendo Switch Pro Controller.
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
/// The gamepad class will return the state of buttonEast, whereas this class returns the state of buttonSouth
/// </summary>
public new ButtonControl bButton => buttonSouth;

/// <summary>
/// Y Button for a Nintendo Switch Pro Controller.
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
/// The gamepad class will return the state of buttonNorth, whereas this class returns the state of buttonWest
/// </summary>
public new ButtonControl yButton => buttonWest;

/// <summary>
/// X Button for a Nintendo Switch Pro Controller.
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
/// The gamepad class will return the state of buttonWest, whereas this class returns the state of buttonNorth
/// </summary>
public new ButtonControl xButton => buttonNorth;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ namespace UnityEngine.InputSystem.Switch
/// A Nintendo Switch Pro controller connected to a desktop mac/windows PC using the HID interface.
/// </summary>
[InputControlLayout(stateType = typeof(SwitchProControllerHIDInputState), displayName = "Switch Pro Controller")]
public class SwitchProControllerHID : Gamepad, IInputStateCallbackReceiver, IEventPreProcessor
public class SwitchProControllerHID : SwitchProController, IInputStateCallbackReceiver, IEventPreProcessor
{
[InputControl(name = "capture", displayName = "Capture")]
public ButtonControl captureButton { get; protected set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if UNITY_EDITOR || UNITY_IOS || UNITY_TVOS || UNITY_VISIONOS || PACKAGE_DOCS_GENERATION
using System.Runtime.InteropServices;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.DualShock;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
Expand Down Expand Up @@ -95,6 +96,66 @@
return this;
}
}

/// <summary>
/// State for iOS Gamepads using a layout where B button is south, A is east, X is north, and Y is west
/// This layout is typically seen on Nintendo gamepads, such as the Switch Pro Controller.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct iOSGameControllerStateSwappedFaceButtons : IInputStateTypeInfo
{
public static FourCC kFormat = new FourCC('I', 'G', 'C', ' ');
public const int MaxButtons = (int)iOSButton.Select + 1;
public const int MaxAxis = (int)iOSAxis.RightStickY + 1;

[InputControl(name = "dpad")]
[InputControl(name = "dpad/up", bit = (uint)iOSButton.DpadUp)]
[InputControl(name = "dpad/right", bit = (uint)iOSButton.DpadRight)]
[InputControl(name = "dpad/down", bit = (uint)iOSButton.DpadDown)]
[InputControl(name = "dpad/left", bit = (uint)iOSButton.DpadLeft)]
[InputControl(name = "buttonSouth", bit = (uint)iOSButton.B)]
[InputControl(name = "buttonWest", bit = (uint)iOSButton.Y)]
[InputControl(name = "buttonNorth", bit = (uint)iOSButton.X)]
[InputControl(name = "buttonEast", bit = (uint)iOSButton.A)]
[InputControl(name = "leftStickPress", bit = (uint)iOSButton.LeftStick)]
[InputControl(name = "rightStickPress", bit = (uint)iOSButton.RightStick)]
[InputControl(name = "leftShoulder", bit = (uint)iOSButton.LeftShoulder)]
[InputControl(name = "rightShoulder", bit = (uint)iOSButton.RightShoulder)]
[InputControl(name = "start", bit = (uint)iOSButton.Start)]
[InputControl(name = "select", bit = (uint)iOSButton.Select)]
public uint buttons;

[InputControl(name = "leftTrigger", offset = sizeof(uint) + sizeof(float) * (uint)iOSButton.LeftTrigger)]
[InputControl(name = "rightTrigger", offset = sizeof(uint) + sizeof(float) * (uint)iOSButton.RightTrigger)]
public fixed float buttonValues[MaxButtons];

private const uint kAxisOffset = sizeof(uint) + sizeof(float) * MaxButtons;
[InputControl(name = "leftStick", offset = (uint)iOSAxis.LeftStickX * sizeof(float) + kAxisOffset)]
[InputControl(name = "rightStick", offset = (uint)iOSAxis.RightStickX * sizeof(float) + kAxisOffset)]
public fixed float axisValues[MaxAxis];

public FourCC format => kFormat;

public iOSGameControllerStateSwappedFaceButtons WithButton(iOSButton button, bool value = true, float rawValue = 1.0f)
{
buttonValues[(int)button] = rawValue;

Debug.Assert((int)button < 32, $"Expected button < 32, so we fit into the 32 bit wide bitmask");
var bit = 1U << (int)button;
if (value)
buttons |= bit;
else
buttons &= ~bit;

Check warning on line 148 in Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/IOSGameController.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/IOSGameController.cs#L148

Added line #L148 was not covered by tests

return this;
}

public iOSGameControllerStateSwappedFaceButtons WithAxis(iOSAxis axis, float value)
{
axisValues[(int)axis] = value;
return this;
}
}
}

namespace UnityEngine.InputSystem.iOS
Expand Down Expand Up @@ -134,5 +195,14 @@
public class DualSenseGampadiOS : DualShockGamepad
{
}

/// <summary>
/// A Switch Pro Controller connected to an iOS device.
/// If you use InputSystem.GetDevice, you must query for SwitchProControlleriOS rather than Gamepad in order for aButton, bButton, yButton and xButton to be correct
/// </summary>
[InputControlLayout(stateType = typeof(iOSGameControllerStateSwappedFaceButtons), displayName = "iOS Switch Pro Controller Gamepad")]
public class SwitchProControlleriOS : SwitchProController
{
}
}
#endif // UNITY_EDITOR || UNITY_IOS || UNITY_TVOS || UNITY_VISIONOS
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public static void Initialize()
.WithDeviceClass("iOSGameController")
.WithProduct("DualSense Wireless Controller"));

InputSystem.RegisterLayout<SwitchProControlleriOS>("SwitchProGamepadiOS",
matches: new InputDeviceMatcher()
.WithInterface("iOS")
.WithDeviceClass("iOSGameController")
.WithProduct("Pro Controller"));

InputSystem.RegisterLayoutMatcher("GravitySensor",
new InputDeviceMatcher()
.WithInterface("iOS")
Expand Down