# 2. Binding Display Strings and Device Changes
Part of [[Context-Sensitive Prompts and Onboarding]]. Prev: [[1. Prompt Design and Leave Training for Last]]. Next: [[3. Glyph Prompts in UI Toolkit]].
The Input System already knows how to print a binding. Do not maintain a parallel dictionary of "Interact = E." The official 1.20 manual is [Display bindings](https://docs.unity3d.com/Packages/
[email protected]/manual/display-bindings.html). This part wraps that API so a UI view can subscribe to it. Runtime rebinding itself lives in [[3. Runtime Rebinding]]; here we only *read* the result.
## 2.1 Display strings
```csharp
using UnityEngine.InputSystem;
public static class BindingPrompt
{
public static string ForAction(InputAction action, string controlScheme = null)
{
if (action == null) return string.Empty;
if (!string.IsNullOrEmpty(controlScheme))
{
int index = action.GetBindingIndex(InputBinding.MaskByGroup(controlScheme));
if (index >= 0)
return action.GetBindingDisplayString(index);
}
// Whole-action string: the currently active binding(s), including overrides.
return action.GetBindingDisplayString();
}
}
```
`GetBindingDisplayString()` on the action "takes into account which bindings are currently active and the actual controls bound to the action" (official wording). After `PerformInteractiveRebinding` and a saved override, this is the string you want.
Pass a control scheme name (`Keyboard&Mouse`, `Gamepad`, whatever you authored in the asset - see [[2. Control Schemes Devices and Consumption Patterns]]) when you want the prompt for *that* scheme rather than "whatever is active." A HUD that always shows both ("E / A") can call the method twice. A context prompt usually shows one: the scheme the player is using now.
## 2.2 Device layout and control path, for icons
The same manual documents an overload that also returns the device layout and the control path, so you can pick a sprite reliably (`buttonSouth` rather than a localized "A"):
```csharp
using UnityEngine;
using UnityEngine.InputSystem;
public readonly struct BindingVisual
{
public readonly string displayString;
public readonly string deviceLayout;
public readonly string controlPath;
public BindingVisual(string displayString, string deviceLayout, string controlPath)
{
this.displayString = displayString;
this.deviceLayout = deviceLayout;
this.controlPath = controlPath;
}
}
public static class BindingPrompt
{
public static BindingVisual ForBinding(InputAction action, int bindingIndex)
{
if (action == null || bindingIndex < 0)
return new BindingVisual(string.Empty, string.Empty, string.Empty);
string text = action.GetBindingDisplayString(bindingIndex, out string layout, out string path);
return new BindingVisual(text, layout, path);
}
public static BindingVisual ForScheme(InputAction action, string controlScheme)
{
int index = action.GetBindingIndex(InputBinding.MaskByGroup(controlScheme));
if (index < 0) index = 0;
return ForBinding(action, index);
}
public static bool IsGamepad(string deviceLayout)
{
return !string.IsNullOrEmpty(deviceLayout)
&& InputSystem.IsFirstLayoutBasedOnSecond(deviceLayout, "Gamepad");
}
}
```
Map `controlPath` values you care about (`buttonSouth`, `buttonWest`, `leftTrigger`, `dpad/up`, ...) onto your own sprites. There is no official Unity-supplied icon pack you are required to use; bring art, or fall back to `displayString`.
Keyboard layouts can change the printed key at runtime (official note). Subscribe to changes (below) so a French AZERTY player does not keep seeing a QWERTY prompt.
## 2.3 When to refresh
Refresh the prompt when any of these happen:
- A device is added or removed (`InputSystem.onDeviceChange`).
- The player's control scheme changes (`PlayerInput.onControlsChanged` if you use `PlayerInput`).
- A binding override is applied or cleared (after your rebind UI finishes - hook the same place [[3. Runtime Rebinding]] writes overrides).
- You enable or disable an action map.
```csharp
using System;
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// Raises Refreshed when display strings may have changed.
/// NOT compiled in Unity.
/// </summary>
public sealed class PromptBindingWatcher : MonoBehaviour
{
[SerializeField] PlayerInput playerInput; // optional
public event Action Refreshed;
void OnEnable()
{
InputSystem.onDeviceChange += OnDeviceChange;
if (playerInput != null)
playerInput.onControlsChanged += OnControlsChanged;
}
void OnDisable()
{
InputSystem.onDeviceChange -= OnDeviceChange;
if (playerInput != null)
playerInput.onControlsChanged -= OnControlsChanged;
}
void OnDeviceChange(InputDevice _, InputDeviceChange change)
{
switch (change)
{
case InputDeviceChange.Added:
case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected:
case InputDeviceChange.Reconnected:
case InputDeviceChange.UsageChanged:
case InputDeviceChange.ConfigurationChanged:
Refreshed?.Invoke();
break;
}
}
void OnControlsChanged(PlayerInput _) => Refreshed?.Invoke();
// Call this from your rebind UI when an override is saved or reset.
public void RaiseRefreshed() => Refreshed?.Invoke();
}
```
`InputDeviceChange` values used above are the long-standing Input System enum. If a value is renamed in a later 1.20 patch, the switch will fail to compile - drop the unknown case; the others are enough.
If you do not use `PlayerInput`, keep a current scheme string on your own input service and call `RaiseRefreshed` when it changes. Do not poll `GetBindingDisplayString` every frame.
## 2.4 Exercise
Print `GetBindingDisplayString()` for Interact on keyboard, then plug in a gamepad and print it again, then rebind Interact in the UI from [[5. Worked Example Remappable Player]] and print a third time. If all three strings are "E", you are not reading the live action.
## Sources
- Display bindings (1.20): https://docs.unity3d.com/Packages/
[email protected]/manual/display-bindings.html
- [[3. Runtime Rebinding]]