Skip to content

Commit b48ccf5

Browse files
vallerieknight-unityekcohjfreire-unityPauliusd01
authored
FIX: Correct north, east, south, west bindings for Switch Pro Controller to match the physical layout of the device (#2208)
Co-authored-by: Håkan Sidenvall <hakan.sidenvall@unity3d.com> Co-authored-by: João Freire <joao.freire@unity3d.com> Co-authored-by: Paulius Dervinis <54306142+Pauliusd01@users.noreply.github.com> Co-authored-by: Paulius Dervinis <pauliusd@unity3d.com>
1 parent e1a0db7 commit b48ccf5

10 files changed

Lines changed: 212 additions & 1 deletion

File tree

.yamato/wrench/pvp-exemptions.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@
127127
},
128128
"PVP-92-3": {
129129
"errors": [
130+
"InputSystem/Plugins/iOS/IOSGameController.cs: .Switch;",
131+
"InputSystem/Plugins/Switch/SwitchProController.cs: .Switch\n",
130132
"InputSystem/Plugins/Switch/SwitchProControllerHID.cs: .Switch.",
131133
"InputSystem/Plugins/Switch/SwitchProControllerHID.cs: .Switch\n",
132134
"InputSystem/Plugins/Switch/SwitchSupportHID.cs: .Switch\n",

Assets/Tests/InputSystem/Plugins/iOSTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using UnityEngine.InputSystem.iOS.LowLevel;
1010
using UnityEngine.InputSystem.LowLevel;
1111
using UnityEngine.InputSystem.Processors;
12+
using UnityEngine.InputSystem.Switch;
1213
using UnityEngine.InputSystem.XInput;
1314
using UnityEngine.TestTools;
1415
using UnityEngine.TestTools.Utils;
@@ -56,13 +57,70 @@ public void Devices_SupportsiOSGamePad(string product, Type deviceType, Type par
5657
Assert.That(gamepad.rightTrigger.ReadValue(), Is.EqualTo(0.456).Within(0.000001));
5758

5859
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.A), gamepad.buttonSouth);
60+
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.A), gamepad.aButton);
5961
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.X), gamepad.buttonWest);
62+
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.X), gamepad.xButton);
6063
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.Y), gamepad.buttonNorth);
64+
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.Y), gamepad.yButton);
6165
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.B), gamepad.buttonEast);
66+
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.B), gamepad.bButton);
6267
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.LeftShoulder), gamepad.leftShoulder);
6368
AssertButtonPress(gamepad, new iOSGameControllerState().WithButton(iOSButton.RightShoulder), gamepad.rightShoulder);
6469
}
6570

71+
[Test]
72+
[Category("Devices")]
73+
// this is a new test, as we need to assert the Nintendo layout (e.g. buttonSouth == B button)
74+
public void Devices_SupportsSwitchProControlleriOS()
75+
{
76+
var device = InputSystem.AddDevice(
77+
new InputDeviceDescription
78+
{
79+
interfaceName = "iOS",
80+
deviceClass = "iOSGameController",
81+
product = "Pro Controller"
82+
});
83+
Assert.That(device, Is.TypeOf(typeof(SwitchProControlleriOS)));
84+
Assert.That(device, Is.InstanceOf(typeof(SwitchProController)));
85+
Assert.That(device, Is.InstanceOf(typeof(Gamepad)));
86+
87+
var gamepad = (SwitchProControlleriOS)device;
88+
89+
InputSystem.QueueStateEvent(gamepad,
90+
new iOSGameControllerStateSwappedFaceButtons()
91+
.WithButton(iOSButton.LeftTrigger, true, 0.123f)
92+
.WithButton(iOSButton.RightTrigger, true, 0.456f)
93+
.WithAxis(iOSAxis.LeftStickX, 0.789f)
94+
.WithAxis(iOSAxis.LeftStickY, 0.987f)
95+
.WithAxis(iOSAxis.RightStickX, 0.654f)
96+
.WithAxis(iOSAxis.RightStickY, 0.321f));
97+
InputSystem.Update();
98+
99+
var leftStickDeadzone = gamepad.leftStick.TryGetProcessor<StickDeadzoneProcessor>();
100+
var rightStickDeadzone = gamepad.leftStick.TryGetProcessor<StickDeadzoneProcessor>();
101+
102+
Assert.That(gamepad.leftStick.ReadValue(), Is.EqualTo(leftStickDeadzone.Process(new Vector2(0.789f, 0.987f))));
103+
Assert.That(gamepad.rightStick.ReadValue(), Is.EqualTo(rightStickDeadzone.Process(new Vector2(0.654f, 0.321f))));
104+
Assert.That(gamepad.leftTrigger.ReadValue(), Is.EqualTo(0.123).Within(0.000001));
105+
Assert.That(gamepad.rightTrigger.ReadValue(), Is.EqualTo(0.456).Within(0.000001));
106+
// testing for Pro Controller layout...
107+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.A), gamepad.buttonEast);
108+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.A), gamepad.aButton);
109+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.X), gamepad.buttonNorth);
110+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.X), gamepad.xButton);
111+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.Y), gamepad.buttonWest);
112+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.Y), gamepad.yButton);
113+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.B), gamepad.buttonSouth);
114+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.B), gamepad.bButton);
115+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.LeftShoulder), gamepad.leftShoulder);
116+
AssertButtonPress(gamepad, new iOSGameControllerStateSwappedFaceButtons().WithButton(iOSButton.RightShoulder), gamepad.rightShoulder);
117+
Assert.Contains("Submit", gamepad.buttonEast.usages.m_Array);
118+
Assert.Contains("PrimaryAction", gamepad.aButton.usages.m_Array);
119+
Assert.Contains("Back", gamepad.bButton.usages.m_Array);
120+
Assert.Contains("Cancel", gamepad.bButton.usages.m_Array);
121+
Assert.Contains("SecondaryAction", gamepad.yButton.usages.m_Array);
122+
}
123+
66124
[Test]
67125
[Category("Devices")]
68126
[TestCase("Gravity", typeof(GravitySensor))]

Assets/Tests/InputSystem/SwitchTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,13 @@ public void Devices_SupportsHIDNpad()
5656
Assert.That(currentRight, Is.EqualTo(expectedRight).Using(new Vector2EqualityComparer(0.01f)));
5757

5858
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.A), controller.buttonEast);
59+
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.A), controller.aButton);
5960
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.B), controller.buttonSouth);
61+
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.B), controller.bButton);
6062
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.X), controller.buttonNorth);
63+
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.X), controller.xButton);
6164
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Y), controller.buttonWest);
65+
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Y), controller.yButton);
6266
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.StickL), controller.leftStickButton);
6367
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.StickR), controller.rightStickButton);
6468
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.L), controller.leftShoulder);
@@ -67,6 +71,12 @@ public void Devices_SupportsHIDNpad()
6771
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.ZR), controller.rightTrigger);
6872
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Plus), controller.startButton);
6973
AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Minus), controller.selectButton);
74+
75+
Assert.Contains("Submit", controller.buttonEast.usages.m_Array);
76+
Assert.Contains("PrimaryAction", controller.aButton.usages.m_Array);
77+
Assert.Contains("Back", controller.bButton.usages.m_Array);
78+
Assert.Contains("Cancel", controller.bButton.usages.m_Array);
79+
Assert.Contains("SecondaryAction", controller.yButton.usages.m_Array);
7080
}
7181

7282
private static SwitchProControllerHIDInputState StateWithButton(SwitchProControllerHIDInputState.Button button)
@@ -168,6 +178,7 @@ public void Devices_SupportsSwitchLikeControllers(int vendorId, int productId)
168178
});
169179

170180
Assert.That(device, Is.TypeOf<SwitchProControllerHID>());
181+
Assert.That(device, Is.InstanceOf(typeof(SwitchProController)));
171182
}
172183

173184
#endif

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1616
- Fixed caching for InputControlPath display name [ISX-2501](https://jira.unity3d.com/browse/ISX-2501)
1717
- Fixed editor closing and not saving the input asset when clicking cancel in the dialog prompt [UUM-134748](https://jira.unity3d.com/browse/UUM-134748)
1818
- Fixed the input actions editor window entering a corrupt state when restarting Unity after the asset was edited. [UUM-134737](https://jira.unity3d.com/browse/UUM-134737)
19+
- 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)
20+
- 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)
1921

2022
### Changed
2123

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using UnityEngine.InputSystem.Controls;
2+
3+
namespace UnityEngine.InputSystem.Switch
4+
{
5+
/// <summary>
6+
/// 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.
7+
/// If you use InputSystem.GetDevice and the ABXY properties to represent the labels on the device, you must query for this class
8+
/// </summary>
9+
public abstract class SwitchProController : Gamepad
10+
{
11+
/// <summary>
12+
/// A Button for a Nintendo Switch Pro Controller.
13+
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
14+
/// The gamepad class will return the state of buttonSouth, whereas this class returns the state of buttonEast
15+
/// </summary>
16+
public new ButtonControl aButton => buttonEast;
17+
18+
/// <summary>
19+
/// B Button for a Nintendo Switch Pro Controller.
20+
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
21+
/// The gamepad class will return the state of buttonEast, whereas this class returns the state of buttonSouth
22+
/// </summary>
23+
public new ButtonControl bButton => buttonSouth;
24+
25+
/// <summary>
26+
/// Y Button for a Nintendo Switch Pro Controller.
27+
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
28+
/// The gamepad class will return the state of buttonNorth, whereas this class returns the state of buttonWest
29+
/// </summary>
30+
public new ButtonControl yButton => buttonWest;
31+
32+
/// <summary>
33+
/// X Button for a Nintendo Switch Pro Controller.
34+
/// If querying via script, ensure you cast the device to a Switch Pro Controller class, rather than using the Gamepad class.
35+
/// The gamepad class will return the state of buttonWest, whereas this class returns the state of buttonNorth
36+
/// </summary>
37+
public new ButtonControl xButton => buttonNorth;
38+
}
39+
}

Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProController.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ namespace UnityEngine.InputSystem.Switch
147147
/// A Nintendo Switch Pro controller connected to a desktop mac/windows PC using the HID interface.
148148
/// </summary>
149149
[InputControlLayout(stateType = typeof(SwitchProControllerHIDInputState), displayName = "Switch Pro Controller")]
150-
public class SwitchProControllerHID : Gamepad, IInputStateCallbackReceiver, IEventPreProcessor
150+
public class SwitchProControllerHID : SwitchProController, IInputStateCallbackReceiver, IEventPreProcessor
151151
{
152152
[InputControl(name = "capture", displayName = "Capture")]
153153
public ButtonControl captureButton { get; protected set; }

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using UnityEngine.InputSystem.Layouts;
55
using UnityEngine.InputSystem.LowLevel;
66
using UnityEngine.InputSystem.iOS.LowLevel;
7+
using UnityEngine.InputSystem.Switch;
78
using UnityEngine.InputSystem.Utilities;
89

910
namespace UnityEngine.InputSystem.iOS.LowLevel
@@ -95,6 +96,66 @@ public iOSGameControllerState WithAxis(iOSAxis axis, float value)
9596
return this;
9697
}
9798
}
99+
100+
/// <summary>
101+
/// State for iOS Gamepads using a layout where B button is south, A is east, X is north, and Y is west
102+
/// This layout is typically seen on Nintendo gamepads, such as the Switch Pro Controller.
103+
/// </summary>
104+
[StructLayout(LayoutKind.Sequential)]
105+
internal unsafe struct iOSGameControllerStateSwappedFaceButtons : IInputStateTypeInfo
106+
{
107+
public static FourCC kFormat = new FourCC('I', 'G', 'C', ' ');
108+
public const int MaxButtons = (int)iOSButton.Select + 1;
109+
public const int MaxAxis = (int)iOSAxis.RightStickY + 1;
110+
111+
[InputControl(name = "dpad")]
112+
[InputControl(name = "dpad/up", bit = (uint)iOSButton.DpadUp)]
113+
[InputControl(name = "dpad/right", bit = (uint)iOSButton.DpadRight)]
114+
[InputControl(name = "dpad/down", bit = (uint)iOSButton.DpadDown)]
115+
[InputControl(name = "dpad/left", bit = (uint)iOSButton.DpadLeft)]
116+
[InputControl(name = "buttonSouth", bit = (uint)iOSButton.B, displayName = "B", shortDisplayName = "B", usages = new[] { "Back", "Cancel" })]
117+
[InputControl(name = "buttonWest", bit = (uint)iOSButton.Y, displayName = "Y", shortDisplayName = "Y", usage = "SecondaryAction")]
118+
[InputControl(name = "buttonNorth", bit = (uint)iOSButton.X, displayName = "X", shortDisplayName = "X")]
119+
[InputControl(name = "buttonEast", bit = (uint)iOSButton.A, displayName = "A", shortDisplayName = "A", usages = new[] { "PrimaryAction", "Submit" })]
120+
[InputControl(name = "leftStickPress", bit = (uint)iOSButton.LeftStick)]
121+
[InputControl(name = "rightStickPress", bit = (uint)iOSButton.RightStick)]
122+
[InputControl(name = "leftShoulder", bit = (uint)iOSButton.LeftShoulder)]
123+
[InputControl(name = "rightShoulder", bit = (uint)iOSButton.RightShoulder)]
124+
[InputControl(name = "start", bit = (uint)iOSButton.Start)]
125+
[InputControl(name = "select", bit = (uint)iOSButton.Select)]
126+
public uint buttons;
127+
128+
[InputControl(name = "leftTrigger", offset = sizeof(uint) + sizeof(float) * (uint)iOSButton.LeftTrigger)]
129+
[InputControl(name = "rightTrigger", offset = sizeof(uint) + sizeof(float) * (uint)iOSButton.RightTrigger)]
130+
public fixed float buttonValues[MaxButtons];
131+
132+
private const uint kAxisOffset = sizeof(uint) + sizeof(float) * MaxButtons;
133+
[InputControl(name = "leftStick", offset = (uint)iOSAxis.LeftStickX * sizeof(float) + kAxisOffset)]
134+
[InputControl(name = "rightStick", offset = (uint)iOSAxis.RightStickX * sizeof(float) + kAxisOffset)]
135+
public fixed float axisValues[MaxAxis];
136+
137+
public FourCC format => kFormat;
138+
139+
public iOSGameControllerStateSwappedFaceButtons WithButton(iOSButton button, bool value = true, float rawValue = 1.0f)
140+
{
141+
buttonValues[(int)button] = rawValue;
142+
143+
Debug.Assert((int)button < 32, $"Expected button < 32, so we fit into the 32 bit wide bitmask");
144+
var bit = 1U << (int)button;
145+
if (value)
146+
buttons |= bit;
147+
else
148+
buttons &= ~bit;
149+
150+
return this;
151+
}
152+
153+
public iOSGameControllerStateSwappedFaceButtons WithAxis(iOSAxis axis, float value)
154+
{
155+
axisValues[(int)axis] = value;
156+
return this;
157+
}
158+
}
98159
}
99160

100161
namespace UnityEngine.InputSystem.iOS
@@ -134,5 +195,14 @@ public class DualShock4GampadiOS : DualShockGamepad
134195
public class DualSenseGampadiOS : DualShockGamepad
135196
{
136197
}
198+
199+
/// <summary>
200+
/// A Switch Pro Controller connected to an iOS device.
201+
/// If you use InputSystem.GetDevice, you must query for SwitchProControlleriOS rather than Gamepad in order for aButton, bButton, yButton and xButton to be correct
202+
/// </summary>
203+
[InputControlLayout(stateType = typeof(iOSGameControllerStateSwappedFaceButtons), displayName = "iOS Switch Pro Controller Gamepad")]
204+
public class SwitchProControlleriOS : SwitchProController
205+
{
206+
}
137207
}
138208
#endif // UNITY_EDITOR || UNITY_IOS || UNITY_TVOS || UNITY_VISIONOS

Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/iOSSupport.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public static void Initialize()
3737
.WithDeviceClass("iOSGameController")
3838
.WithProduct("DualSense Wireless Controller"));
3939

40+
InputSystem.RegisterLayout<SwitchProControlleriOS>("SwitchProGamepadiOS",
41+
matches: new InputDeviceMatcher()
42+
.WithInterface("iOS")
43+
.WithDeviceClass("iOSGameController")
44+
.WithProduct("Pro Controller"));
45+
4046
InputSystem.RegisterLayoutMatcher("GravitySensor",
4147
new InputDeviceMatcher()
4248
.WithInterface("iOS")

Packages/com.unity.inputsystem/ValidationExceptions.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@
1515
"ValidationTest": "API Validation",
1616
"ExceptionMessage": "Additions require a new minor or major version.",
1717
"PackageVersion": "1.19.1"
18+
},
19+
{
20+
"ValidationTest": "NDA Keyword Validation",
21+
"ExceptionMessage": "File 'InputSystem/Plugins/iOS/IOSGameController.cs' contains keyword pattern '(?<!(BuildTarget|BuildTargetGroup|BuildSettings|AddressablesPlatform|RuntimePlatform|TargetPlatform|OpCodes|GraphicsDeviceType))\\.switch\\W' which indicates it is specific to restricted targets '-Switch'. It must not be published",
22+
"PackageVersion": "1.19.1"
23+
},
24+
{
25+
"ValidationTest": "NDA Keyword Validation",
26+
"ExceptionMessage": "using UnityEngine.InputSystem.Switch;",
27+
"PackageVersion": "1.19.1"
28+
},
29+
{
30+
"ValidationTest": "NDA Keyword Validation",
31+
"ExceptionMessage": "File 'InputSystem/Plugins/Switch/SwitchProController.cs' contains keyword pattern '(?<!(BuildTarget|BuildTargetGroup|BuildSettings|AddressablesPlatform|RuntimePlatform|TargetPlatform|OpCodes|GraphicsDeviceType))\\.switch\\W' which indicates it is specific to restricted targets '-Switch'. It must not be published",
32+
"PackageVersion": "1.19.1"
33+
},
34+
{
35+
"ValidationTest": "NDA Keyword Validation",
36+
"ExceptionMessage": "namespace UnityEngine.InputSystem.Switch",
37+
"PackageVersion": "1.19.1"
1838
}
1939
],
2040
"WarningExceptions": []

0 commit comments

Comments
 (0)