A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEngine.Events;
4using UnityEngine.UI;
5
6////TODO: localization support
7
8////TODO: deal with composites that have parts bound in different control schemes
9
10namespace UnityEngine.InputSystem.Samples.RebindUI
11{
12 /// <summary>
13 /// A reusable component with a self-contained UI for rebinding a single action.
14 /// </summary>
15 public class RebindActionUI : MonoBehaviour
16 {
17 /// <summary>
18 /// Reference to the action that is to be rebound.
19 /// </summary>
20 public InputActionReference actionReference
21 {
22 get => m_Action;
23 set
24 {
25 m_Action = value;
26 UpdateActionLabel();
27 UpdateBindingDisplay();
28 }
29 }
30
31 /// <summary>
32 /// ID (in string form) of the binding that is to be rebound on the action.
33 /// </summary>
34 /// <seealso cref="InputBinding.id"/>
35 public string bindingId
36 {
37 get => m_BindingId;
38 set
39 {
40 m_BindingId = value;
41 UpdateBindingDisplay();
42 }
43 }
44
45 public InputBinding.DisplayStringOptions displayStringOptions
46 {
47 get => m_DisplayStringOptions;
48 set
49 {
50 m_DisplayStringOptions = value;
51 UpdateBindingDisplay();
52 }
53 }
54
55 /// <summary>
56 /// Text component that receives the name of the action. Optional.
57 /// </summary>
58 public Text actionLabel
59 {
60 get => m_ActionLabel;
61 set
62 {
63 m_ActionLabel = value;
64 UpdateActionLabel();
65 }
66 }
67
68 /// <summary>
69 /// Text component that receives the display string of the binding. Can be <c>null</c> in which
70 /// case the component entirely relies on <see cref="updateBindingUIEvent"/>.
71 /// </summary>
72 public Text bindingText
73 {
74 get => m_BindingText;
75 set
76 {
77 m_BindingText = value;
78 UpdateBindingDisplay();
79 }
80 }
81
82 /// <summary>
83 /// Optional text component that receives a text prompt when waiting for a control to be actuated.
84 /// </summary>
85 /// <seealso cref="startRebindEvent"/>
86 /// <seealso cref="rebindOverlay"/>
87 public Text rebindPrompt
88 {
89 get => m_RebindText;
90 set => m_RebindText = value;
91 }
92
93 /// <summary>
94 /// Optional UI that is activated when an interactive rebind is started and deactivated when the rebind
95 /// is finished. This is normally used to display an overlay over the current UI while the system is
96 /// waiting for a control to be actuated.
97 /// </summary>
98 /// <remarks>
99 /// If neither <see cref="rebindPrompt"/> nor <c>rebindOverlay</c> is set, the component will temporarily
100 /// replaced the <see cref="bindingText"/> (if not <c>null</c>) with <c>"Waiting..."</c>.
101 /// </remarks>
102 /// <seealso cref="startRebindEvent"/>
103 /// <seealso cref="rebindPrompt"/>
104 public GameObject rebindOverlay
105 {
106 get => m_RebindOverlay;
107 set => m_RebindOverlay = value;
108 }
109
110 /// <summary>
111 /// Event that is triggered every time the UI updates to reflect the current binding.
112 /// This can be used to tie custom visualizations to bindings.
113 /// </summary>
114 public UpdateBindingUIEvent updateBindingUIEvent
115 {
116 get
117 {
118 if (m_UpdateBindingUIEvent == null)
119 m_UpdateBindingUIEvent = new UpdateBindingUIEvent();
120 return m_UpdateBindingUIEvent;
121 }
122 }
123
124 /// <summary>
125 /// Event that is triggered when an interactive rebind is started on the action.
126 /// </summary>
127 public InteractiveRebindEvent startRebindEvent
128 {
129 get
130 {
131 if (m_RebindStartEvent == null)
132 m_RebindStartEvent = new InteractiveRebindEvent();
133 return m_RebindStartEvent;
134 }
135 }
136
137 /// <summary>
138 /// Event that is triggered when an interactive rebind has been completed or canceled.
139 /// </summary>
140 public InteractiveRebindEvent stopRebindEvent
141 {
142 get
143 {
144 if (m_RebindStopEvent == null)
145 m_RebindStopEvent = new InteractiveRebindEvent();
146 return m_RebindStopEvent;
147 }
148 }
149
150 /// <summary>
151 /// When an interactive rebind is in progress, this is the rebind operation controller.
152 /// Otherwise, it is <c>null</c>.
153 /// </summary>
154 public InputActionRebindingExtensions.RebindingOperation ongoingRebind => m_RebindOperation;
155
156 /// <summary>
157 /// Return the action and binding index for the binding that is targeted by the component
158 /// according to
159 /// </summary>
160 /// <param name="action"></param>
161 /// <param name="bindingIndex"></param>
162 /// <returns></returns>
163 public bool ResolveActionAndBinding(out InputAction action, out int bindingIndex)
164 {
165 bindingIndex = -1;
166
167 action = m_Action?.action;
168 if (action == null)
169 return false;
170
171 if (string.IsNullOrEmpty(m_BindingId))
172 return false;
173
174 // Look up binding index.
175 var bindingId = new Guid(m_BindingId);
176 bindingIndex = action.bindings.IndexOf(x => x.id == bindingId);
177 if (bindingIndex == -1)
178 {
179 Debug.LogError($"Cannot find binding with ID '{bindingId}' on '{action}'", this);
180 return false;
181 }
182
183 return true;
184 }
185
186 /// <summary>
187 /// Trigger a refresh of the currently displayed binding.
188 /// </summary>
189 public void UpdateBindingDisplay()
190 {
191 var displayString = string.Empty;
192 var deviceLayoutName = default(string);
193 var controlPath = default(string);
194
195 // Get display string from action.
196 var action = m_Action?.action;
197 if (action != null)
198 {
199 var bindingIndex = action.bindings.IndexOf(x => x.id.ToString() == m_BindingId);
200 if (bindingIndex != -1)
201 displayString = action.GetBindingDisplayString(bindingIndex, out deviceLayoutName, out controlPath, displayStringOptions);
202 }
203
204 // Set on label (if any).
205 if (m_BindingText != null)
206 m_BindingText.text = displayString;
207
208 // Give listeners a chance to configure UI in response.
209 m_UpdateBindingUIEvent?.Invoke(this, displayString, deviceLayoutName, controlPath);
210 }
211
212 /// <summary>
213 /// Remove currently applied binding overrides.
214 /// </summary>
215 public void ResetToDefault()
216 {
217 if (!ResolveActionAndBinding(out var action, out var bindingIndex))
218 return;
219
220 if (action.bindings[bindingIndex].isComposite)
221 {
222 // It's a composite. Remove overrides from part bindings.
223 for (var i = bindingIndex + 1; i < action.bindings.Count && action.bindings[i].isPartOfComposite; ++i)
224 action.RemoveBindingOverride(i);
225 }
226 else
227 {
228 action.RemoveBindingOverride(bindingIndex);
229 }
230 UpdateBindingDisplay();
231 }
232
233 /// <summary>
234 /// Initiate an interactive rebind that lets the player actuate a control to choose a new binding
235 /// for the action.
236 /// </summary>
237 public void StartInteractiveRebind()
238 {
239 if (!ResolveActionAndBinding(out var action, out var bindingIndex))
240 return;
241
242 // If the binding is a composite, we need to rebind each part in turn.
243 if (action.bindings[bindingIndex].isComposite)
244 {
245 var firstPartIndex = bindingIndex + 1;
246 if (firstPartIndex < action.bindings.Count && action.bindings[firstPartIndex].isPartOfComposite)
247 PerformInteractiveRebind(action, firstPartIndex, allCompositeParts: true);
248 }
249 else
250 {
251 PerformInteractiveRebind(action, bindingIndex);
252 }
253 }
254
255 private void PerformInteractiveRebind(InputAction action, int bindingIndex, bool allCompositeParts = false)
256 {
257 m_RebindOperation?.Cancel(); // Will null out m_RebindOperation.
258
259 void CleanUp()
260 {
261 m_RebindOperation?.Dispose();
262 m_RebindOperation = null;
263 action.Enable();
264 }
265
266 //Fixes the "InvalidOperationException: Cannot rebind action x while it is enabled" error
267 action.Disable();
268
269 // Configure the rebind.
270 m_RebindOperation = action.PerformInteractiveRebinding(bindingIndex)
271 .OnCancel(
272 operation =>
273 {
274 m_RebindStopEvent?.Invoke(this, operation);
275 if (m_RebindOverlay != null)
276 m_RebindOverlay.SetActive(false);
277 UpdateBindingDisplay();
278 CleanUp();
279 })
280 .OnComplete(
281 operation =>
282 {
283 if (m_RebindOverlay != null)
284 m_RebindOverlay.SetActive(false);
285 m_RebindStopEvent?.Invoke(this, operation);
286 UpdateBindingDisplay();
287 CleanUp();
288
289 // If there's more composite parts we should bind, initiate a rebind
290 // for the next part.
291 if (allCompositeParts)
292 {
293 var nextBindingIndex = bindingIndex + 1;
294 if (nextBindingIndex < action.bindings.Count && action.bindings[nextBindingIndex].isPartOfComposite)
295 PerformInteractiveRebind(action, nextBindingIndex, true);
296 }
297 });
298
299 // If it's a part binding, show the name of the part in the UI.
300 var partName = default(string);
301 if (action.bindings[bindingIndex].isPartOfComposite)
302 partName = $"Binding '{action.bindings[bindingIndex].name}'. ";
303
304 // Bring up rebind overlay, if we have one.
305 m_RebindOverlay?.SetActive(true);
306 if (m_RebindText != null)
307 {
308 var text = !string.IsNullOrEmpty(m_RebindOperation.expectedControlType)
309 ? $"{partName}Waiting for {m_RebindOperation.expectedControlType} input..."
310 : $"{partName}Waiting for input...";
311 m_RebindText.text = text;
312 }
313
314 // If we have no rebind overlay and no callback but we have a binding text label,
315 // temporarily set the binding text label to "<Waiting>".
316 if (m_RebindOverlay == null && m_RebindText == null && m_RebindStartEvent == null && m_BindingText != null)
317 m_BindingText.text = "<Waiting...>";
318
319 // Give listeners a chance to act on the rebind starting.
320 m_RebindStartEvent?.Invoke(this, m_RebindOperation);
321
322 m_RebindOperation.Start();
323 }
324
325 protected void OnEnable()
326 {
327 if (s_RebindActionUIs == null)
328 s_RebindActionUIs = new List<RebindActionUI>();
329 s_RebindActionUIs.Add(this);
330 if (s_RebindActionUIs.Count == 1)
331 InputSystem.onActionChange += OnActionChange;
332 }
333
334 protected void OnDisable()
335 {
336 m_RebindOperation?.Dispose();
337 m_RebindOperation = null;
338
339 s_RebindActionUIs.Remove(this);
340 if (s_RebindActionUIs.Count == 0)
341 {
342 s_RebindActionUIs = null;
343 InputSystem.onActionChange -= OnActionChange;
344 }
345 }
346
347 // When the action system re-resolves bindings, we want to update our UI in response. While this will
348 // also trigger from changes we made ourselves, it ensures that we react to changes made elsewhere. If
349 // the user changes keyboard layout, for example, we will get a BoundControlsChanged notification and
350 // will update our UI to reflect the current keyboard layout.
351 private static void OnActionChange(object obj, InputActionChange change)
352 {
353 if (change != InputActionChange.BoundControlsChanged)
354 return;
355
356 var action = obj as InputAction;
357 var actionMap = action?.actionMap ?? obj as InputActionMap;
358 var actionAsset = actionMap?.asset ?? obj as InputActionAsset;
359
360 for (var i = 0; i < s_RebindActionUIs.Count; ++i)
361 {
362 var component = s_RebindActionUIs[i];
363 var referencedAction = component.actionReference?.action;
364 if (referencedAction == null)
365 continue;
366
367 if (referencedAction == action ||
368 referencedAction.actionMap == actionMap ||
369 referencedAction.actionMap?.asset == actionAsset)
370 component.UpdateBindingDisplay();
371 }
372 }
373
374 [Tooltip("Reference to action that is to be rebound from the UI.")]
375 [SerializeField]
376 private InputActionReference m_Action;
377
378 [SerializeField]
379 private string m_BindingId;
380
381 [SerializeField]
382 private InputBinding.DisplayStringOptions m_DisplayStringOptions;
383
384 [Tooltip("Text label that will receive the name of the action. Optional. Set to None to have the "
385 + "rebind UI not show a label for the action.")]
386 [SerializeField]
387 private Text m_ActionLabel;
388
389 [Tooltip("Text label that will receive the current, formatted binding string.")]
390 [SerializeField]
391 private Text m_BindingText;
392
393 [Tooltip("Optional UI that will be shown while a rebind is in progress.")]
394 [SerializeField]
395 private GameObject m_RebindOverlay;
396
397 [Tooltip("Optional text label that will be updated with prompt for user input.")]
398 [SerializeField]
399 private Text m_RebindText;
400
401 [Tooltip("Event that is triggered when the way the binding is display should be updated. This allows displaying "
402 + "bindings in custom ways, e.g. using images instead of text.")]
403 [SerializeField]
404 private UpdateBindingUIEvent m_UpdateBindingUIEvent;
405
406 [Tooltip("Event that is triggered when an interactive rebind is being initiated. This can be used, for example, "
407 + "to implement custom UI behavior while a rebind is in progress. It can also be used to further "
408 + "customize the rebind.")]
409 [SerializeField]
410 private InteractiveRebindEvent m_RebindStartEvent;
411
412 [Tooltip("Event that is triggered when an interactive rebind is complete or has been aborted.")]
413 [SerializeField]
414 private InteractiveRebindEvent m_RebindStopEvent;
415
416 private InputActionRebindingExtensions.RebindingOperation m_RebindOperation;
417
418 private static List<RebindActionUI> s_RebindActionUIs;
419
420 // We want the label for the action name to update in edit mode, too, so
421 // we kick that off from here.
422 #if UNITY_EDITOR
423 protected void OnValidate()
424 {
425 UpdateActionLabel();
426 UpdateBindingDisplay();
427 }
428
429 #endif
430
431 private void UpdateActionLabel()
432 {
433 if (m_ActionLabel != null)
434 {
435 var action = m_Action?.action;
436 m_ActionLabel.text = action != null ? action.name : string.Empty;
437 }
438 }
439
440 [Serializable]
441 public class UpdateBindingUIEvent : UnityEvent<RebindActionUI, string, string, string>
442 {
443 }
444
445 [Serializable]
446 public class InteractiveRebindEvent : UnityEvent<RebindActionUI, InputActionRebindingExtensions.RebindingOperation>
447 {
448 }
449 }
450}