Skip to content

Commit 6e00488

Browse files
author
Rene Damm
authored
FIX: Layout overrides corrupting layout state (#888).
1 parent 7156361 commit 6e00488

File tree

18 files changed

+390
-25
lines changed

18 files changed

+390
-25
lines changed

Assets/Samples/CustomDeviceUsages.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"displayName": "Custom Device Usages",
3+
"description": "Shows how to tag devices with custom usage strings that can be used, for example, to distinguish multiple instances of the same type of device (e.g. 'Gamepad') based on how the device is used (e.g. 'Player1' vs 'Player2' or 'LeftHand' vs 'RightHand')."
4+
}

Assets/Samples/CustomDeviceUsages/.sample.json.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.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using UnityEngine;
2+
using UnityEngine.InputSystem;
3+
#if UNITY_EDITOR
4+
using UnityEditor;
5+
#endif
6+
7+
// Say you want to distinguish a device not only by its type (e.g. "PS4 Controller")
8+
// but also by the way it is used. This is a common scenario for VR controllers, for
9+
// example, where the same type of controller may be used once in the left hand and
10+
// once in the right hand. However, the need for distinguishing devices in a similar
11+
// manner can pop up in a variety of situations. For example, on Switch it is used
12+
// to distinguish the current orientation of the Joy-Con controller ("Horizontal" vs.
13+
// "Vertical") allowing you to take orientation into account when binding actions.
14+
//
15+
// The input system allows you to distinguish devices based on the "usages" assigned
16+
// to them. This is a generic mechanism that can be used to tag devices with arbitrary
17+
// custom usages.
18+
//
19+
// To make this more concrete, let's say we have a game where two players control
20+
// the game together each one using a gamepad but each receiving control over half
21+
// the actions in the game.
22+
//
23+
// NOTE: What we do here is only one way to achieve this kind of setup. We could
24+
// alternatively go and just create one control scheme for the first player
25+
// and one control scheme for the second one and then have two PlayerInputs
26+
// each using one of the two.
27+
//
28+
// So, what we'd like to do is tag one gamepad with "Player1" and one gamepad with
29+
// with "Player2". Then, in the actions we can set up a binding scheme specifically
30+
// for this style of play and bind actions such that are driven either from the
31+
// first player's gamepad or from the second player's gamepad (or from either).
32+
//
33+
// The first bit we need for this is to tell the input system that "Player1" and
34+
// "Player2" are usages that we intend to apply to gamepads. For this, we need
35+
// to modify the "Gamepad" layout. We do so by applying what's called a "layout
36+
// override". This needs to happen during initialization so here we go:
37+
#if UNITY_EDITOR
38+
[InitializeOnLoad]
39+
#endif
40+
public static class InitCustomDeviceUsages
41+
{
42+
static InitCustomDeviceUsages()
43+
{
44+
Initialize();
45+
}
46+
47+
[RuntimeInitializeOnLoadMethod]
48+
private static void Initialize()
49+
{
50+
// Here we register the layout override with the system.
51+
//
52+
// The layout override is just a fragment of layout information
53+
// in JSON format.
54+
//
55+
// The key property here is "commonUsages" which tells the system
56+
// that "Player1" and "Player2" are possible usages applied to devices
57+
// using the given layout ("Gamepad" in our case).
58+
InputSystem.RegisterLayoutOverride(@"
59+
{
60+
""name"" : ""GamepadPlayerUsageTags"",
61+
""extend"" : ""Gamepad"",
62+
""commonUsages"" : [
63+
""Player1"", ""Player2""
64+
]
65+
}
66+
");
67+
68+
// Now that we have done this, you will see that when using the
69+
// control picker in the action editor, that there is now a
70+
// "Gamepad (Player1)" and "Gamepad (Player2)" entry underneath
71+
// "Gamepad". When you select those, you can bind specifically
72+
// to a gamepad with the respective device usage.
73+
//
74+
// Also, you will now be able to *require* a device with the
75+
// given usage in a control scheme. So, when creating a control
76+
// scheme representing the shared Player1+Player2 controls,
77+
// you can add one "Gamepad (Player1)" and one "Gamepad (Player2)"
78+
// requirement.
79+
//
80+
// You can see an example setup for how this would look in an
81+
// .inputactions file in the TwoPlayerControls.inputactions file
82+
// that is part of this sample.
83+
}
84+
}
85+
86+
// However, we are still missing a piece. At runtime, no gamepad will
87+
// receive either the "Player1" or the "Player2" usage assignment yet.
88+
// So none of the bindings will work yet.
89+
//
90+
// To assign the usage tags to the devices, we need to call
91+
// InputSystem.AddDeviceUsage or SetDeviceUsage.
92+
//
93+
// We could set this up any which way. As a demonstration, let's create
94+
// a MonoBehaviour here that simply associates a specific tag with a
95+
// specific gamepad index.
96+
//
97+
// In practice, you would probably want to do the assignment in a place
98+
// where you handle your player setup/joining.
99+
public class CustomDeviceUsages : MonoBehaviour
100+
{
101+
public int gamepadIndex;
102+
public string usageTag;
103+
104+
private Gamepad m_Gamepad;
105+
106+
protected void OnEnable()
107+
{
108+
if (gamepadIndex >= 0 && gamepadIndex < Gamepad.all.Count)
109+
{
110+
m_Gamepad = Gamepad.all[gamepadIndex];
111+
InputSystem.AddDeviceUsage(m_Gamepad, usageTag);
112+
}
113+
}
114+
115+
protected void OnDisable()
116+
{
117+
// If we grabbed a gamepad and it's still added to the system,
118+
// remove the usage tag we added.
119+
if (m_Gamepad != null && m_Gamepad.added)
120+
InputSystem.RemoveDeviceUsage(m_Gamepad, usageTag);
121+
m_Gamepad = null;
122+
}
123+
}

Assets/Samples/CustomDeviceUsages/CustomDeviceUsages.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This sample shows how to tag devices with custom "usages" and how to bind actions specifically to devices with only those usages.
2+
3+
This is useful if you have the same type of device that appears in multiple different roles that you want to distinguish when binding to the device. For example, when a device may appear in both the left and the right hand or may appear held in different orientations (say, horizontal vs vertical).

Assets/Samples/CustomDeviceUsages/README.md.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "TwoPlayerControls",
3+
"maps": [
4+
{
5+
"name": "TwoPlayers",
6+
"id": "0e22eb1c-8c6e-4cec-8364-2a9f0e3ef769",
7+
"actions": [
8+
{
9+
"name": "Move",
10+
"type": "Button",
11+
"id": "d35725fa-073a-4e1c-9052-2c45b8ef0b4c",
12+
"expectedControlType": "",
13+
"processors": "",
14+
"interactions": ""
15+
},
16+
{
17+
"name": "Look",
18+
"type": "Button",
19+
"id": "249187b5-59c4-459f-84d0-731cb510c536",
20+
"expectedControlType": "",
21+
"processors": "",
22+
"interactions": ""
23+
}
24+
],
25+
"bindings": [
26+
{
27+
"name": "",
28+
"id": "3e67c7ae-ca6c-4345-8f61-8df16fb87fec",
29+
"path": "<Gamepad>{Player1}/leftStick",
30+
"interactions": "",
31+
"processors": "",
32+
"groups": "TwoPlayers",
33+
"action": "Move",
34+
"isComposite": false,
35+
"isPartOfComposite": false
36+
},
37+
{
38+
"name": "",
39+
"id": "46cadfba-140f-4895-8d9b-184da46b6010",
40+
"path": "<Gamepad>{Player2}/rightStick",
41+
"interactions": "",
42+
"processors": "",
43+
"groups": "TwoPlayers",
44+
"action": "Look",
45+
"isComposite": false,
46+
"isPartOfComposite": false
47+
}
48+
]
49+
}
50+
],
51+
"controlSchemes": [
52+
{
53+
"name": "TwoPlayers",
54+
"bindingGroup": "TwoPlayers",
55+
"devices": [
56+
{
57+
"devicePath": "<Gamepad>{Player1}",
58+
"isOptional": false,
59+
"isOR": false
60+
},
61+
{
62+
"devicePath": "<Gamepad>{Player2}",
63+
"isOptional": false,
64+
"isOR": false
65+
}
66+
]
67+
}
68+
]
69+
}

Assets/Samples/CustomDeviceUsages/TwoPlayerControls.inputactions.meta

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

Assets/Tests/InputSystem/CoreTests_Layouts.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,29 @@ public void Layouts_CannotBeBasedOnMultipleLayouts()
681681
Throws.Exception.TypeOf<NotSupportedException>());
682682
}
683683

684+
[Test]
685+
[Category("Layouts")]
686+
public void Layouts_CanLoadLayoutOverride()
687+
{
688+
const string json = @"
689+
{
690+
""name"" : ""Overrides"",
691+
""extend"" : ""Gamepad"",
692+
""controls"" : [
693+
{ ""name"" : ""extraControl"", ""layout"" : ""Button"" }
694+
]
695+
}
696+
";
697+
698+
InputSystem.RegisterLayoutOverride(json);
699+
700+
var layout = InputSystem.LoadLayout("Overrides");
701+
702+
Assert.That(layout, Is.Not.Null);
703+
Assert.That(layout.isOverride, Is.True);
704+
Assert.That(layout.controls, Has.Count.EqualTo(1)); // Should not have merged base layout.
705+
}
706+
684707
[Test]
685708
[Category("Layouts")]
686709
public void Layouts_CanApplyOverridesToExistingLayouts()
@@ -748,6 +771,37 @@ public void Layouts_CanApplyOverridesToControlLayouts()
748771
Assert.That(Gamepad.current.leftStick["extraControl"], Is.TypeOf<ButtonControl>());
749772
}
750773

774+
[Test]
775+
[Category("Layouts")]
776+
public void Layouts_CanApplyOverridesToOverrides()
777+
{
778+
const string json1 = @"
779+
{
780+
""name"" : ""Overrides1"",
781+
""extend"" : ""Gamepad"",
782+
""controls"" : [
783+
{ ""name"" : ""extraControl"", ""layout"" : ""Button"" }
784+
]
785+
}
786+
";
787+
const string json2 = @"
788+
{
789+
""name"" : ""Overrides2"",
790+
""extend"" : ""Overrides1"",
791+
""controls"" : [
792+
{ ""name"" : ""extraControl"", ""layout"" : ""Axis"" }
793+
]
794+
}
795+
";
796+
797+
InputSystem.RegisterLayoutOverride(json1);
798+
InputSystem.RegisterLayoutOverride(json2);
799+
800+
var device = InputSystem.AddDevice<Gamepad>();
801+
802+
Assert.That(device["extraControl"].layout, Is.EqualTo("Axis"));
803+
}
804+
751805
[Test]
752806
[Category("Layouts")]
753807
public void Layouts_CanOverrideCommonUsagesOnExistingLayout()
@@ -765,10 +819,14 @@ public void Layouts_CanOverrideCommonUsagesOnExistingLayout()
765819

766820
var layout = InputSystem.LoadLayout("Gamepad");
767821

822+
Assert.That(layout.appliedOverrides, Is.EquivalentTo(new[] {new InternedString("Overrides")}));
768823
Assert.That(layout.commonUsages.Count, Is.EqualTo(3));
769824
Assert.That(layout.commonUsages, Has.Exactly(1).EqualTo(new InternedString("A")));
770825
Assert.That(layout.commonUsages, Has.Exactly(1).EqualTo(new InternedString("B")));
771826
Assert.That(layout.commonUsages, Has.Exactly(1).EqualTo(new InternedString("C")));
827+
828+
// Applying the override should not have created a cycle.
829+
Assert.That(layout.baseLayouts, Does.Not.Contains(new InternedString("Gamepad")));
772830
}
773831

774832
[Test]

0 commit comments

Comments
 (0)