Skip to content
Draft
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
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 @@ -45,6 +45,10 @@ however, it has to be formatted properly to pass verification tests.
- Fixed InputControl picker not updating correctly when the Input Actions Window was dirty. [ISXB-1221](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1221)
- Fixed formatting issues on processor documentation page

### 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 @@ public iOSGameControllerState WithAxis(iOSAxis axis, float value)
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;

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 DualShock4GampadiOS : DualShockGamepad
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