Skip to content

Commit 060b562

Browse files
authored
CHANGE: ISX-2377: Rebinding sample improvements (Mouse sensitivity + Swap sticks), FIX: ISXB-1721 ApplyParameterOverride doesn't work if binding name is empty string. (#2241)
1 parent 5ef4387 commit 060b562

20 files changed

+1590
-676
lines changed

Assets/Samples/RebindingUI/ActionLabelEditor.cs

Lines changed: 4 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,17 @@ public class ActionLabelEditor : UnityEditor.Editor
1515
{
1616
protected void OnEnable()
1717
{
18-
m_ActionProperty = serializedObject.FindProperty("m_Action");
19-
m_BindingIdProperty = serializedObject.FindProperty("m_BindingId");
2018
m_BindingTextProperty = serializedObject.FindProperty("m_BindingText");
21-
m_DisplayStringOptionsProperty = serializedObject.FindProperty("m_DisplayStringOptions");
2219
m_UpdateBindingUIEventProperty = serializedObject.FindProperty("m_UpdateBindingUIEvent");
23-
24-
RefreshBindingOptions();
20+
m_BindingUI = new BindingUI(serializedObject);
2521
}
2622

2723
public override void OnInspectorGUI()
2824
{
2925
EditorGUI.BeginChangeCheck();
3026

3127
// Binding section.
32-
EditorGUILayout.LabelField(m_BindingLabel, Styles.boldLabel);
33-
using (new EditorGUI.IndentLevelScope())
34-
{
35-
EditorGUILayout.PropertyField(m_ActionProperty);
36-
37-
var newSelectedBinding = EditorGUILayout.Popup(m_BindingLabel, m_SelectedBindingOption, m_BindingOptions);
38-
if (newSelectedBinding != m_SelectedBindingOption)
39-
{
40-
var bindingId = m_BindingOptionValues[newSelectedBinding];
41-
m_BindingIdProperty.stringValue = bindingId;
42-
m_SelectedBindingOption = newSelectedBinding;
43-
}
44-
45-
var optionsOld = (InputBinding.DisplayStringOptions)m_DisplayStringOptionsProperty.intValue;
46-
var optionsNew = (InputBinding.DisplayStringOptions)EditorGUILayout.EnumFlagsField(m_DisplayOptionsLabel, optionsOld);
47-
if (optionsOld != optionsNew)
48-
m_DisplayStringOptionsProperty.intValue = (int)optionsNew;
49-
}
28+
m_BindingUI.Draw();
5029

5130
// UI section.
5231
EditorGUILayout.Space();
@@ -67,91 +46,16 @@ public override void OnInspectorGUI()
6746
if (EditorGUI.EndChangeCheck())
6847
{
6948
serializedObject.ApplyModifiedProperties();
70-
RefreshBindingOptions();
71-
}
72-
}
73-
74-
protected void RefreshBindingOptions()
75-
{
76-
var actionReference = (InputActionReference)m_ActionProperty.objectReferenceValue;
77-
var action = actionReference?.action;
78-
79-
if (action == null)
80-
{
81-
m_BindingOptions = new GUIContent[0];
82-
m_BindingOptionValues = new string[0];
83-
m_SelectedBindingOption = -1;
84-
return;
85-
}
86-
87-
var bindings = action.bindings;
88-
var bindingCount = bindings.Count;
89-
90-
m_BindingOptions = new GUIContent[bindingCount];
91-
m_BindingOptionValues = new string[bindingCount];
92-
m_SelectedBindingOption = -1;
93-
94-
var currentBindingId = m_BindingIdProperty.stringValue;
95-
for (var i = 0; i < bindingCount; ++i)
96-
{
97-
var binding = bindings[i];
98-
var bindingId = binding.id.ToString();
99-
var haveBindingGroups = !string.IsNullOrEmpty(binding.groups);
100-
101-
// If we don't have a binding groups (control schemes), show the device that if there are, for example,
102-
// there are two bindings with the display string "A", the user can see that one is for the keyboard
103-
// and the other for the gamepad.
104-
var displayOptions =
105-
InputBinding.DisplayStringOptions.DontUseShortDisplayNames | InputBinding.DisplayStringOptions.IgnoreBindingOverrides;
106-
if (!haveBindingGroups)
107-
displayOptions |= InputBinding.DisplayStringOptions.DontOmitDevice;
108-
109-
// Create display string.
110-
var displayString = action.GetBindingDisplayString(i, displayOptions);
111-
112-
// If binding is part of a composite, include the part name.
113-
if (binding.isPartOfComposite)
114-
displayString = $"{ObjectNames.NicifyVariableName(binding.name)}: {displayString}";
115-
116-
// Some composites use '/' as a separator. When used in popup, this will lead to to submenus. Prevent
117-
// by instead using a backlash.
118-
displayString = displayString.Replace('/', '\\');
119-
120-
// If the binding is part of control schemes, mention them.
121-
if (haveBindingGroups)
122-
{
123-
var asset = action.actionMap?.asset;
124-
if (asset != null)
125-
{
126-
var controlSchemes = string.Join(", ",
127-
binding.groups.Split(InputBinding.Separator)
128-
.Select(x => asset.controlSchemes.FirstOrDefault(c => c.bindingGroup == x).name));
129-
130-
displayString = $"{displayString} ({controlSchemes})";
131-
}
132-
}
133-
134-
m_BindingOptions[i] = new GUIContent(displayString);
135-
m_BindingOptionValues[i] = bindingId;
136-
137-
if (currentBindingId == bindingId)
138-
m_SelectedBindingOption = i;
49+
m_BindingUI.Refresh();
13950
}
14051
}
14152

142-
private SerializedProperty m_ActionProperty;
143-
private SerializedProperty m_BindingIdProperty;
14453
private SerializedProperty m_BindingTextProperty;
14554
private SerializedProperty m_UpdateBindingUIEventProperty;
146-
private SerializedProperty m_DisplayStringOptionsProperty;
14755

148-
private GUIContent m_BindingLabel = new GUIContent("Binding");
149-
private GUIContent m_DisplayOptionsLabel = new GUIContent("Display Options");
15056
private GUIContent m_UILabel = new GUIContent("UI");
15157
private GUIContent m_EventsLabel = new GUIContent("Events");
152-
private GUIContent[] m_BindingOptions;
153-
private string[] m_BindingOptionValues;
154-
private int m_SelectedBindingOption;
58+
private BindingUI m_BindingUI;
15559

15660
private static class Styles
15761
{
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#if UNITY_EDITOR
2+
3+
using System;
4+
using System.Linq;
5+
using UnityEditor;
6+
7+
namespace UnityEngine.InputSystem.Samples.RebindUI
8+
{
9+
/// <summary>
10+
/// Common binding UI helper to allow editor composition.
11+
/// </summary>
12+
internal class BindingUI
13+
{
14+
private readonly SerializedProperty m_ActionProperty;
15+
private readonly SerializedProperty m_BindingIdProperty;
16+
private readonly SerializedProperty m_DisplayStringOptionsProperty;
17+
18+
public BindingUI(SerializedObject serializedObject)
19+
: this(serializedObject.FindProperty("m_Action"), serializedObject.FindProperty("m_BindingId"),
20+
serializedObject.FindProperty("m_DisplayStringOptions"))
21+
{}
22+
23+
public BindingUI(SerializedProperty actionProperty, SerializedProperty bindingIdProperty,
24+
SerializedProperty displayStringOptionsProperty = null)
25+
{
26+
m_ActionProperty = actionProperty;
27+
m_BindingIdProperty = bindingIdProperty;
28+
m_DisplayStringOptionsProperty = displayStringOptionsProperty;
29+
30+
Reset();
31+
Refresh();
32+
}
33+
34+
private void Reset()
35+
{
36+
bindingOptions = Array.Empty<GUIContent>();
37+
bindingOptionValues = Array.Empty<string>();
38+
selectedBindingIndex = -1;
39+
}
40+
41+
public void Draw()
42+
{
43+
// Binding section.
44+
EditorGUILayout.LabelField(m_BindingLabel);
45+
using (new EditorGUI.IndentLevelScope())
46+
{
47+
EditorGUILayout.PropertyField(m_ActionProperty);
48+
49+
var newSelectedBinding = EditorGUILayout.Popup(m_BindingLabel, selectedBindingIndex, bindingOptions);
50+
if (newSelectedBinding != selectedBindingIndex)
51+
{
52+
var id = bindingOptionValues[newSelectedBinding];
53+
m_BindingIdProperty.stringValue = id;
54+
selectedBindingIndex = newSelectedBinding;
55+
}
56+
57+
if (m_DisplayStringOptionsProperty != null)
58+
{
59+
var optionsOld = (InputBinding.DisplayStringOptions)m_DisplayStringOptionsProperty.intValue;
60+
var optionsNew = (InputBinding.DisplayStringOptions)EditorGUILayout.EnumFlagsField(m_DisplayOptionsLabel, optionsOld);
61+
if (optionsOld != optionsNew)
62+
m_DisplayStringOptionsProperty.intValue = (int)optionsNew;
63+
}
64+
}
65+
}
66+
67+
public bool Refresh()
68+
{
69+
if (action == null)
70+
{
71+
Reset();
72+
return false;
73+
}
74+
75+
var bindings = action.bindings;
76+
var bindingCount = bindings.Count;
77+
78+
bindingOptions = new GUIContent[bindingCount];
79+
bindingOptionValues = new string[bindingCount];
80+
selectedBindingIndex = -1;
81+
82+
var currentBindingId = m_BindingIdProperty.stringValue;
83+
for (var i = 0; i < bindingCount; ++i)
84+
{
85+
var binding = bindings[i];
86+
var id = binding.id.ToString();
87+
var haveBindingGroups = !string.IsNullOrEmpty(binding.groups);
88+
89+
// If we don't have a binding groups (control schemes), show the device that if there are, for example,
90+
// there are two bindings with the display string "A", the user can see that one is for the keyboard
91+
// and the other for the gamepad.
92+
var displayOptions =
93+
InputBinding.DisplayStringOptions.DontUseShortDisplayNames | InputBinding.DisplayStringOptions.IgnoreBindingOverrides;
94+
if (!haveBindingGroups)
95+
displayOptions |= InputBinding.DisplayStringOptions.DontOmitDevice;
96+
97+
// Create display string.
98+
var displayString = action.GetBindingDisplayString(i, displayOptions);
99+
100+
// If binding is part of a composite, include the part name.
101+
if (binding.isPartOfComposite)
102+
displayString = $"{ObjectNames.NicifyVariableName(binding.name)}: {displayString}";
103+
104+
// Some composites use '/' as a separator. When used in popup, this will lead to to submenus. Prevent
105+
// by instead using a backlash.
106+
displayString = displayString.Replace('/', '\\');
107+
108+
// If the binding is part of control schemes, mention them.
109+
if (haveBindingGroups)
110+
{
111+
var asset = action.actionMap?.asset;
112+
if (asset != null)
113+
{
114+
var controlSchemes = string.Join(", ",
115+
binding.groups.Split(InputBinding.Separator)
116+
.Select(x => asset.controlSchemes.FirstOrDefault(c => c.bindingGroup == x).name));
117+
118+
displayString = $"{displayString} ({controlSchemes})";
119+
}
120+
}
121+
122+
bindingOptions[i] = new GUIContent(displayString);
123+
bindingOptionValues[i] = id;
124+
125+
if (currentBindingId == id)
126+
selectedBindingIndex = i;
127+
}
128+
129+
return true;
130+
}
131+
132+
public string bindingId => m_BindingIdProperty.stringValue;
133+
public int bindingIndex => action.FindBindingById(m_BindingIdProperty.stringValue);
134+
135+
public InputAction action => ((InputActionReference)m_ActionProperty.objectReferenceValue)?.action;
136+
137+
private GUIContent[] bindingOptions { get; set; }
138+
private string[] bindingOptionValues { get; set; }
139+
private int selectedBindingIndex { get; set; }
140+
141+
private readonly GUIContent m_BindingLabel = new GUIContent("Binding");
142+
private readonly GUIContent m_DisplayOptionsLabel = new GUIContent("Display Options");
143+
}
144+
}
145+
146+
#endif // UNITY_EDITOR

Assets/Samples/RebindingUI/BindingUI.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.

Assets/Samples/RebindingUI/Game/GameplayManager.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ internal bool TryTeleportOrthographicExtents(Vector3 position, out Vector3 resul
191191

192192
private void Awake()
193193
{
194+
// This game is designed for landscape orientation, so make sure we use it.
195+
Screen.orientation = ScreenOrientation.LandscapeLeft;
196+
194197
m_FeedbackController = GetComponent<FeedbackController>();
195198

196199
m_EnemyPool = new ObjectPool<Enemy>(
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
3+
namespace UnityEngine.InputSystem.Samples.RebindUI
4+
{
5+
/// <summary>
6+
/// Extension methods to reduce code bloat of this example.
7+
/// </summary>
8+
public static class InputActionExtensions
9+
{
10+
/// <summary>
11+
/// Attempts to find an action binding using its binding ID (GUID).
12+
/// </summary>
13+
/// <param name="action">The action instance, may be null.</param>
14+
/// <param name="bindingId">The binding ID (GUID) represented by a string.</param>
15+
/// <returns>Zero-based index of the binding or -1 if not found.</returns>
16+
public static int FindBindingById(this InputAction action, string bindingId)
17+
{
18+
if (action == null || string.IsNullOrEmpty(bindingId))
19+
return -1;
20+
var id = new Guid(bindingId);
21+
return action.bindings.IndexOf(x => x.id == id);
22+
}
23+
}
24+
}

Assets/Samples/RebindingUI/InputActionExtensions.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.

Assets/Samples/RebindingUI/InputActionIndicator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ public class InputActionIndicator : MonoBehaviour
3434
private void OnEnable()
3535
{
3636
if (action != null && action.action != null)
37+
{
3738
action.action.performed += OnPerformed;
39+
}
3840
}
3941

4042
private void OnDisable()
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1-
This sample demonstrates how to use the Input System APIs to set up a rebinding UI. The main file is [RebindActionUI](./RebindActionUI.cs) which, aside from serving as an example, contains a reusable `MonoBehaviour` component for composing rebinding UIs. The [RebindUIPrefab](./RebindUIPrefab.prefab) contains a ready-made prefab that can be used as a simple drop-in setup for rebinding an individual action.
1+
This sample demonstrates how to use the Input System APIs to set up a rebinding UI. The main file is
2+
[RebindActionUI](./RebindActionUI.cs) which, aside from serving as an example, contains a reusable `MonoBehaviour`
3+
component for composing rebinding UIs. The [RebindUIPrefab](./RebindUIPrefab.prefab) contains a ready-made prefab that
4+
can be used as a simple drop-in setup for rebinding an individual action.
25

3-
To demonstrate how to use images instead of textual display strings, take a look at [GamepadIconsExample](./GamepadIconsExample.cs).
6+
To demonstrate how to use images instead of textual display strings, take a look at
7+
[GamepadIconsExample](./GamepadIconsExample.cs).
48

5-
To demonstrate how to show dynamic texts based on input action bindings, see [ActionLabel](./ActionLabel.cs).
9+
To demonstrate how to show dynamic UI texts based on input action bindings, see [ActionLabel](./ActionLabel.cs).
610

7-
Finally, the [RebindSaveLoad](./RebindSaveLoad.cs) script demonstrates how to persist user rebinds in `PlayerPrefs` and how to restore them from there.
11+
[InputActionIndicator](./InputActionIndicator.cs) and [InputActionIndicator.prefab](./InputActionIndicator.prefab)
12+
shows how to make a simple UI indicator that shows whether an associated input action is enabled, disabled or
13+
performed. This behavior has been added to this sample to add observability of actions triggered within gameplay,
14+
menu and rebind contexts.
815

9-
The icons used in the sample are taken from [Free Prompts Pack v4.0](https://opengameart.org/content/free-keyboard-and-controllers-prompts-pack) created by, and made available to public domain by Nicolae Berbece.
10-
Icons are licensed under [Creative Commons CC0](https://creativecommons.org/publicdomain/zero/1.0/).
16+
The [RebindSaveLoad](./RebindSaveLoad.cs) script demonstrates how to persist user rebinds in `PlayerPrefs` and how
17+
to restore them.
18+
19+
In this sample, keyboard bindings for "Move" (default WASD) is rebound as a single composite. This means that
20+
indivudual parts will get assigned one after the other. Another way of doing this is to set it up as four individual
21+
button bindings and assign them individually as four partial bindings.
22+
23+
In this sample it is possible to directly rebind gamepad sticks in the gamepad control scheme. In practice, you
24+
probably don't want to set up rebinding the sticks like this but rather have a "swap sticks" kind of toggle instead.
25+
In this sample we have both variants for demonstration purposes. See [RebindActionUI.SwapBinding](./RebindActionUI.cs)
26+
for a method that swaps two bindings of similar type.
27+
28+
The icons used in the sample are taken from
29+
[Free Prompts Pack v4.0](https://opengameart.org/content/free-keyboard-and-controllers-prompts-pack) created by,
30+
and made available to public domain by Nicolae Berbece.
31+
Icons are licensed under [Creative Commons CC0](https://creativecommons.org/publicdomain/zero/1.0/).

0 commit comments

Comments
 (0)