Skip to content
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
4 changes: 4 additions & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ however, it has to be formatted properly to pass verification tests.
### Added
- Exposed MediaPlayPause, MediaRewind, MediaForward keys on Keyboard.

### Fixed
- 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)

## [1.14.2] - 2025-08-05

### Fixed
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