Auto-switching between keyboard and gamepad with the hints manager that resolves action IDs into the right display string and prompt icon for whichever device the player just touched.
Summary
This page covers the two sub-managers that together answer the question “the player just touched a gamepad – what should the on-screen prompts now say?”. IControlSchemeManager (reached through ScyllaInput.Instance.ControlSchemeManager) tracks which control scheme is active and switches it automatically when the player moves between keyboard/mouse and gamepad. IInputHintsManager (reached through ScyllaInput.Instance.HintsManager) resolves an action ID into a UI-ready InputHint value containing the display text (“E”, “A”, “Square”) and the matching sprite (“kb_E”, “xbox_A”, “ps_square”) for the currently active scheme. Together they replace the “I checked IsGamepadConnected once at startup and forgot about it” pattern that ships with most projects.
There is always a moment in playtesting when a player picks up a gamepad mid-session, looks at the “Press [E] to interact” prompt above the chest, and stops because the on-screen text does not match what is in their hands. Scylla solves this with two coordinated behaviors. The control-scheme manager samples device input each frame, applies a small set of activity thresholds (analog stick magnitude, trigger pressure, mouse delta, keyboard key) to filter out noise, and switches the active scheme when significant input arrives from a different device family than the current one. The switch fires OnSchemeChanged and publishes a ControlSchemeChangedEvent on the SEX (Scylla Event eXchange) bus. The hints manager subscribes to that event, invalidates its internal cache, and the next GetHint("Interact") call returns the gamepad variant.
The hints manager is icon-set-driven. An InputIconSet is a ScriptableObject (or a runtime-constructed object) that maps Unity Input System control paths to display strings and sprites. Your project typically ships one icon set per device family it wants distinct prompts for (Keyboard/Mouse, Xbox, PlayStation, Switch, Steam Deck, generic gamepad), assigns each to a ControlSchemeType or per-GamepadType slot, and then never thinks about icons in gameplay code again. The hints manager handles the lookup: when GetHint("Interact") is called, it resolves the action’s current binding for the active scheme, looks up the binding’s control path in the icon set assigned to that scheme, and returns the InputHint ready for UI consumption. Results are cached and invalidated on binding changes and scheme switches.
The two managers are layered intentionally. The control-scheme manager owns the “what device is the player using” decision and exposes it as the ActiveSchemeType plus DetectedGamepadType pair. The hints manager owns the “given an active scheme, what does this action’s prompt look like” decision. Projects that want to swap UI without rendering icon sprites (a text-only prompt that just says “Press [Triangle]”) can use the hints manager’s GetActionDisplayText; projects that want full icon rendering use GetActionIcon or the combined GetHint. Projects that only care about the scheme (a HUD that hides crosshair UI when the gamepad becomes active) can subscribe directly to ControlSchemeChangedEvent without touching the hints layer at all.
Use this page for:
- Auto-switching the active scheme when the player picks up a gamepad mid-session.
- Tuning the analog thresholds (stick deadzone, trigger sensitivity, mouse delta) for project-specific feel.
- Forcing the scheme manually from a settings menu (a “Force Keyboard Prompts” accessibility option).
- Authoring
InputIconSetScriptableObjects per device family (Keyboard, Xbox, PlayStation, Switch). - Resolving “Press [X] to Interact” prompts dynamically against the player’s current bindings and current device.
- Subscribing to
ControlSchemeChangedEvent/InputHintChangedEventso UI code never polls for “did the prompts change”. - Differentiating gamepad sub-types (Xbox vs PlayStation vs Nintendo) for vendor-correct face button labels.
Features
- Auto-detected scheme switching.
Update()samples device input each frame; significant input on a different device family flipsActiveSchemeand fires the change events. Thresholds (GamepadStickThreshold,GamepadTriggerThreshold,MouseDeltaThreshold) and aSwitchCooldownkeep the switching stable. - Programmatic override.
SetActiveScheme(string)andSetActiveScheme(ControlSchemeType)switch instantly, bypassing thresholds and cooldown. The right shape for a settings-menu override or a debug hotkey. - Two built-in schemes plus custom.
InitializeregistersKeyboardMouseandGamepaddefault schemes;RegisterSchemeadds custom schemes (Touch, Steam Deck profile, Steam Input remap profile) withControlSchemeType.Customso they coexist with the built-ins. - Gamepad sub-type detection.
DetectedGamepadTypeclassifies the connected gamepad asXbox,PlayStation,Nintendo,Steam, orGeneric. Re-firesOnGamepadTypeChangedwhen the active gamepad changes vendor. Used by the hints manager to pick vendor-correct face button labels. - Two layered events.
OnSchemeChanged(C# event on the manager, payload is the previous and newControlScheme) for callers that hold the manager reference;ControlSchemeChangedEventon the SEX bus (payload includes both scheme objects, bothControlSchemeTypevalues, and theTriggeringDevice) for global subscribers. InputIconSetmapping per device family. Each icon set carries anIconSetID, aPrimarySchemeType, an optionalGamepadType, and the binding-path-to-sprite mappings. Register multiple sets and assign one per scheme viaSetIconSetForScheme; the active scheme picks the right set automatically.- Three hint retrieval shapes.
GetActionIcon(sprite only),GetActionDisplayText(string only),GetHint(fullInputHintwith both plus binding path and scheme metadata). Per-scheme overloads available for explicit scheme queries. - Automatic cache invalidation. The hints manager subscribes to
InputRebindCompletedEvent,InputBindingsLoadedEvent, and the scheme manager’sOnSchemeChanged; cached entries flush automatically when bindings or the active scheme change. UI code never has to callInvalidateCachein the normal flow. - Hint change events.
OnHintChanged(actionID)on the manager andInputHintChangedEventon the bus, fired when an action’s hint becomes stale (after a rebind, after a scheme switch). UI components subscribed to a specific action’s hint refresh without polling.
Concepts
The vocabulary used throughout the page. Each row is a type, an enum, or a recurring shape from the snippets below.
| Concept | Type / shape | Notes |
|---|---|---|
IControlSchemeManager | interface, ScyllaInput.Instance.ControlSchemeManager | Owns scheme registry, active-scheme decision, auto-detection. Samples device input in Update. Publishes ControlSchemeChangedEvent on every switch. |
ControlScheme | class, fluent-built | SchemeID (string), DisplayName, SchemeType (enum), optional GamepadType, plus the icon-set ID. Constructed via ControlScheme.Create(id, type) then .WithDisplayName(...).WithGamepadType(...).WithIconSetID(...). |
ControlSchemeType | enum | Unknown, KeyboardMouse, Gamepad, Touch, Custom. The category dimension for built-in lookups (GetSchemeByType) and the hints manager’s per-scheme icon-set assignment. |
GamepadType | enum | Generic, Xbox, PlayStation, Nintendo, Steam. Detected from the device’s name/layout strings. Drives vendor-correct face button labels when the active scheme is Gamepad. |
| Auto-detection thresholds | floats on IControlSchemeManager | GamepadStickThreshold (default 0.2), GamepadTriggerThreshold (default 0.1), MouseDeltaThreshold (default 5 px), SwitchCooldown (default 0.3 s). All clamped to non-negative. |
IInputHintsManager | interface, ScyllaInput.Instance.HintsManager | Owns icon set registry and the action-to-hint lookup. Caches per (actionID, schemeType). Auto-invalidates on rebind and scheme change. |
InputIconSet | ScriptableObject (or runtime instance) | IconSetID, DisplayName, PrimarySchemeType, optional GamepadType, plus a list of (controlPath, displayString, sprite) entries. One set per device family. |
InputHint | readonly struct | ActionID, DisplayText, Icon (Sprite), BindingPath, SchemeType, IconSetID, IsValid. InputHint.Invalid is the sentinel for “no match”. |
ControlSchemeChangedEvent | ScyllaEvent subtype | Fired on every scheme switch. Carries PreviousScheme, CurrentScheme, PreviousType, CurrentType, TriggeringDevice. |
InputHintChangedEvent | ScyllaEvent subtype | Fired per-action when its hint becomes stale (post-rebind, post-scheme-switch). Carries ActionID, OldHint, NewHint. |
Recipes
Each recipe below is self-contained. They share the same two managers, so if you are new to the API, start with the first recipe to wire up the scheme listener; the others assume that plumbing is in place. Recipes that deal with bindings assume you have already set up actions as described in Actions and Bindings.
Show the right button prompts for the active device
Problem. Your HUD has an “Interact” prompt above every chest and door. When the player is on keyboard it should say “Press [E]”; when they pick up a gamepad it should say “Press [A]”. You want that swap to happen automatically without polling every frame from UI code.
Solution. Subscribe to ControlSchemeChangedEvent once from your HUD controller, then call GetHint to re-resolve every prompt on each switch. The control-scheme manager auto-detects from the moment the module initializes; AutoDetectionEnabled is true by default, so you do not need to do anything to start the detection.
using Scylla.Core;
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
public sealed class HUDPromptRefresher : MonoBehaviour
{
private ScyllaEventSubscription _schemeSubscription;
private void Start()
{
ScyllaCore.Instance.RunWhenReady(() =>
{
/* The control scheme manager auto-detects from the moment the
* module initialises. AutoDetectionEnabled is true by default. */
var schemes = ScyllaInput.Instance.ControlSchemeManager;
Debug.Log($"Initial scheme: {schemes.ActiveSchemeType}");
/* Subscribe to the bus event for global refreshes. The handler
* fires on every scheme switch (auto or manual). */
_schemeSubscription = ScyllaEvents.Listen<ControlSchemeChangedEvent>(
subscriber: this,
handler: OnSchemeChanged,
priority: ScyllaEventPriority.Medium
);
});
}
private void OnDestroy()
{
_schemeSubscription?.Dispose();
}
private void OnSchemeChanged(ControlSchemeChangedEvent evt)
{
/* The payload carries both ControlScheme objects (use these for
* the IconSetID and DisplayName) and both ControlSchemeType
* values (use these for the routing decision). TriggeringDevice
* is null when the switch was programmatic. */
Debug.Log(
$"Scheme switched: {evt.PreviousType} -> {evt.CurrentType} " +
$"(triggered by {evt.TriggeringDevice?.displayName ?? "code"})");
RefreshAllPrompts(evt.CurrentType);
}
private void RefreshAllPrompts(ControlSchemeType newType)
{
/* Your HUD iterates its prompt widgets and re-resolves each one
* via the hints manager. See the "Resolve a binding to a display
* string or icon" recipe below for the per-prompt shape. */
}
}
Resolve a binding to a display string or icon
Problem. Your interact prompt widget needs to show “Press [E]” on keyboard and “Press [A]” on gamepad, and it needs to update automatically when the player rebinds keys. You want one call that returns the right text and sprite for whatever device is active right now, without writing your own binding-path lookup.
Solution. Register your InputIconSet assets at startup (one per device family), then call GetHint from each prompt widget. The hints manager resolves the action’s current binding for the active scheme, looks up the control path in the assigned icon set, and returns an InputHint with DisplayText and Icon ready for your UI. The cache makes repeat calls on the same frame cheap; the framework invalidates it automatically on rebinds and scheme switches.
using Scylla.Core;
using Scylla.Input;
using UnityEngine;
public sealed class HintsBootstrap : MonoBehaviour
{
/* Author these as ScriptableObjects via Create > Scylla > Input >
* Icon Set in the Project window. Each asset carries the binding-path-
* to-sprite mappings for one device family. */
[SerializeField] private InputIconSet _keyboardIcons;
[SerializeField] private InputIconSet _xboxIcons;
[SerializeField] private InputIconSet _playstationIcons;
[SerializeField] private InputIconSet _genericGamepadIcons;
private void Start()
{
ScyllaCore.Instance.RunWhenReady(RegisterIconSets);
}
private void RegisterIconSets()
{
var hints = ScyllaInput.Instance.HintsManager;
/* Register each icon set. The PrimarySchemeType on the asset
* auto-assigns the set as the default for that scheme when no
* explicit assignment has been made yet. */
hints.RegisterIconSet(_keyboardIcons);
hints.RegisterIconSet(_xboxIcons);
hints.RegisterIconSet(_playstationIcons);
hints.RegisterIconSet(_genericGamepadIcons);
/* Explicit per-scheme assignment. Keyboard scheme always uses the
* keyboard set. Gamepad scheme uses the generic gamepad set by
* default; vendor-correct sets are swapped in the recipe below. */
hints.SetIconSetForScheme(ControlSchemeType.KeyboardMouse, _keyboardIcons.IconSetID);
hints.SetIconSetForScheme(ControlSchemeType.Gamepad, _genericGamepadIcons.IconSetID);
}
}
public sealed class InteractPromptLabel : MonoBehaviour
{
[SerializeField] private TMPro.TextMeshProUGUI _text;
[SerializeField] private UnityEngine.UI.Image _icon;
[SerializeField] private string _actionID = "Interact";
public void Refresh()
{
var hints = ScyllaInput.Instance.HintsManager;
/* GetHint returns the full payload for the active scheme. The
* cache makes repeat calls cheap; the framework invalidates on
* rebinds and scheme switches automatically. */
var hint = hints.GetHint(_actionID);
if (!hint.IsValid)
{
_text.text = "?";
_icon.sprite = null;
return;
}
/* DisplayText is the short label ("E", "A", "Square").
* Icon is the sprite (may be null when no sprite is mapped). */
_text.text = hint.DisplayText;
_icon.sprite = hint.Icon;
/* For text-only prompts, GetActionDisplayText is the shorthand
* that skips the icon lookup entirely:
* _text.text = hints.GetActionDisplayText(_actionID);
*/
}
}
Swap on-screen glyphs when the player switches devices
Problem. The player starts a session with a PlayStation controller, then unplugs it mid-game and grabs an Xbox controller. The prompts should show Cross/Circle/Square/Triangle for PlayStation and A/B/X/Y for Xbox, not just a generic gamepad glyph. You need the icon swap to happen the moment the new controller is identified, not only on the next full scheme switch.
Solution. Subscribe to OnGamepadTypeChanged on the control scheme manager. The handler fires whenever DetectedGamepadType changes (including when the player connects a controller for the first time after startup), and you respond by calling SetIconSetForScheme(ControlSchemeType.Gamepad, ...) with the matching vendor icon set. The hints manager invalidates its cache and publishes OnIconSetChanged automatically; your prompt widgets subscribed to that event refresh.
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
public sealed class VendorIconSwitcher : MonoBehaviour
{
[SerializeField] private InputIconSet _xboxIcons;
[SerializeField] private InputIconSet _playstationIcons;
[SerializeField] private InputIconSet _nintendoIcons;
[SerializeField] private InputIconSet _genericGamepadIcons;
private void Start()
{
Scylla.Core.ScyllaCore.Instance.RunWhenReady(() =>
{
var schemes = ScyllaInput.Instance.ControlSchemeManager;
/* Apply the right icon set for whatever gamepad is currently
* connected (covers the case where a gamepad was plugged in
* before the framework initialised). */
ApplyGamepadIcons(schemes.DetectedGamepadType);
/* Subscribe to changes. The handler fires when the player
* unplugs an Xbox controller and plugs in a PlayStation one
* (or when a gamepad is first connected after startup). */
schemes.OnGamepadTypeChanged += ApplyGamepadIcons;
});
}
private void OnDestroy()
{
if (ScyllaInput.Instance == null) return;
var schemes = ScyllaInput.Instance.ControlSchemeManager;
if (schemes != null) schemes.OnGamepadTypeChanged -= ApplyGamepadIcons;
}
private void ApplyGamepadIcons(GamepadType detectedType)
{
var hints = ScyllaInput.Instance.HintsManager;
/* Pick the matching icon set; fall back to the generic gamepad
* set when the vendor is unknown or not handled. */
InputIconSet set = detectedType switch
{
GamepadType.Xbox => _xboxIcons,
GamepadType.PlayStation => _playstationIcons,
GamepadType.Nintendo => _nintendoIcons,
_ => _genericGamepadIcons,
};
if (set == null) return;
/* Swap the icon set assigned to the Gamepad scheme. The hints
* manager invalidates its cache and publishes OnIconSetChanged
* automatically; UI code subscribed to that event refreshes. */
hints.SetIconSetForScheme(ControlSchemeType.Gamepad, set.IconSetID);
}
}
Detect the active control scheme
Problem. Your HUD has a crosshair that only makes sense on keyboard/mouse (the player aims with the mouse), and you want to hide it when the player switches to a gamepad. You need to know the current scheme at any point and react to changes, without keeping your own copy of the state.
Solution. Read ActiveSchemeType for a one-off check, or subscribe to ControlSchemeChangedEvent for ongoing reactions. Both are available immediately after ScyllaCore.RunWhenReady fires.
using Scylla.Core;
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
public sealed class CrosshairVisibility : MonoBehaviour
{
[SerializeField] private GameObject _crosshair;
private ScyllaEventSubscription _schemeSubscription;
private void Start()
{
ScyllaCore.Instance.RunWhenReady(() =>
{
/* One-off check on startup. */
var schemes = ScyllaInput.Instance.ControlSchemeManager;
UpdateCrosshair(schemes.ActiveSchemeType);
/* Subscribe for ongoing changes. */
_schemeSubscription = ScyllaEvents.Listen<ControlSchemeChangedEvent>(
subscriber: this,
handler: evt => UpdateCrosshair(evt.CurrentType),
priority: ScyllaEventPriority.Medium
);
});
}
private void OnDestroy()
{
_schemeSubscription?.Dispose();
}
private void UpdateCrosshair(ControlSchemeType scheme)
{
/* Show the crosshair only when the player is on keyboard/mouse.
* The gamepad player aims with the right stick, so the crosshair
* is distracting and visually incorrect. */
_crosshair.SetActive(scheme == ControlSchemeType.KeyboardMouse);
}
}
Tune detection thresholds for your project
Problem. The default thresholds work well for most setups, but you are seeing spurious scheme switches: a player with a drifting stick keeps toggling prompts, or a sit-and-aim player twitches the mouse by two pixels and the scheme switches back to keyboard. The thresholds and cooldown are tunable per-project and you want them tighter.
Solution. The four threshold properties on IControlSchemeManager are all settable after initialization. Tune them once in a startup component or drive them from your ScyllaInputConfiguration asset so QA can adjust them without a rebuild.
using Scylla.Input;
using UnityEngine;
public sealed class SchemeDetectionTuning : MonoBehaviour
{
private void Start()
{
Scylla.Core.ScyllaCore.Instance.RunWhenReady(ConfigureThresholds);
}
private void ConfigureThresholds()
{
var schemes = ScyllaInput.Instance.ControlSchemeManager;
/* Raise the stick threshold for players reporting drift-induced
* spurious switches. The default 0.2 already filters most
* controller drift; 0.35 is a strict setting that requires a
* deliberate stick movement. */
schemes.GamepadStickThreshold = 0.35f;
/* Lower the mouse delta threshold for sit-and-aim playstyles
* where small mouse movements should count as "still using mouse".
* Default is 5 pixels per frame; 2 px is more responsive. */
schemes.MouseDeltaThreshold = 2f;
/* Tighten the cooldown for fast device switching (debug or
* accessibility scenarios). Default 0.3 s feels right for shipped
* games; 0.1 s for a "show me both prompt types fast" debug mode. */
schemes.SwitchCooldown = 0.1f;
/* Trigger threshold for projects that bind gameplay triggers
* (driving games, racing wheels emulated as gamepad triggers).
* Default 0.1 filters hair-trigger sensitivity; 0.05 is a
* sensitive setting for analog-pressure-dependent gameplay. */
schemes.GamepadTriggerThreshold = 0.05f;
}
}
Force a scheme or disable auto-detection
Problem. There are accessibility cases and competitive cases where auto-detection is the wrong default: a player who sets up keyboard prompts on purpose and does not want them swapped because they touched the gamepad to scroll, a tournament setup that should always display the same prompts regardless of what device a streamer happens to wave at the rig, a debug menu that should force gamepad prompts for screenshot work. You want to lock the scheme programmatically.
Solution. Set AutoDetectionEnabled = false to stop the per-frame polling. Call SetActiveScheme(string) or SetActiveScheme(ControlSchemeType) to switch immediately, bypassing the cooldown and thresholds.
using Scylla.Input;
public sealed class AccessibilityScheme
{
/* Settings UI: lock prompts to keyboard regardless of device activity. */
public void OnLockToKeyboardClicked()
{
var schemes = ScyllaInput.Instance.ControlSchemeManager;
schemes.AutoDetectionEnabled = false;
schemes.SetActiveScheme(ControlSchemeType.KeyboardMouse);
}
/* Settings UI: switch back to default auto-detect behavior. */
public void OnUnlockClicked()
{
ScyllaInput.Instance.ControlSchemeManager.AutoDetectionEnabled = true;
}
/* Debug hotkey: force gamepad prompts even if no gamepad is connected
* (useful for screenshot work). SetActiveScheme returns false when no
* scheme of the requested type is registered. */
public void OnDebugForceGamepad()
{
var schemes = ScyllaInput.Instance.ControlSchemeManager;
if (!schemes.SetActiveScheme(ControlSchemeType.Gamepad))
{
Debug.LogWarning("No gamepad scheme registered");
}
}
/* Switch by string ID when multiple schemes share a type (the
* type-keyed overload only finds the first-registered scheme of
* that type; the string overload picks an exact registered scheme). */
public void OnSelectSteamDeckProfile()
{
ScyllaInput.Instance.ControlSchemeManager.SetActiveScheme("SteamDeck");
}
}
API Sketch
The two manager surfaces and the relevant payload and value types.
namespace Scylla.Input
{
public interface IControlSchemeManager
{
/* Active state. */
ControlScheme ActiveScheme { get; }
ControlSchemeType ActiveSchemeType { get; }
GamepadType DetectedGamepadType { get; }
bool IsInitialized { get; }
/* Auto-detection tuning. */
bool AutoDetectionEnabled { get; set; } /* default true */
float SwitchCooldown { get; set; } /* default 0.3 s */
float GamepadStickThreshold { get; set; } /* default 0.2 */
float GamepadTriggerThreshold { get; set; } /* default 0.1 */
float MouseDeltaThreshold { get; set; } /* default 5 px */
/* Scheme registry. */
bool RegisterScheme (ControlScheme scheme);
bool UnregisterScheme(string schemeID);
ControlScheme GetScheme (string schemeID);
ControlScheme GetSchemeByType (ControlSchemeType schemeType);
IReadOnlyList<ControlScheme> GetAllSchemes ();
bool HasScheme (string schemeID);
/* Scheme switching (bypasses cooldown and thresholds). */
bool SetActiveScheme(string schemeID);
bool SetActiveScheme(ControlSchemeType schemeType);
/* C# events. Bus events also published; see below. */
event Action<ControlScheme, ControlScheme> OnSchemeChanged; /* (previous, current) */
event Action<GamepadType> OnGamepadTypeChanged;
}
public class ControlScheme
{
public string SchemeID { get; }
public string DisplayName { get; }
public ControlSchemeType SchemeType { get; }
public GamepadType GamepadType { get; set; }
public string IconSetID { get; }
public static ControlScheme Create(string schemeID, ControlSchemeType schemeType);
/* Fluent setters (return this). */
public ControlScheme WithDisplayName(string displayName);
public ControlScheme WithGamepadType (GamepadType gamepadType);
public ControlScheme WithIconSetID (string iconSetID);
}
public enum ControlSchemeType { Unknown = 0, KeyboardMouse = 1, Gamepad = 2, Touch = 3, Custom = 4 }
public enum GamepadType { Generic = 0, Xbox = 1, PlayStation = 2, Nintendo = 3, Steam = 4 }
public interface IInputHintsManager
{
string ActiveIconSetID { get; }
bool IsInitialized { get; }
/* Hint retrieval. The scheme-less overloads use the active scheme. */
Sprite GetActionIcon (string actionID);
Sprite GetActionIcon (string actionID, ControlSchemeType schemeType);
Sprite GetActionIcon (string actionID, string iconSetID);
string GetActionDisplayText(string actionID);
InputHint GetHint (string actionID);
InputHint GetHint (string actionID, ControlSchemeType schemeType);
/* Icon set registry. */
bool RegisterIconSet (InputIconSet iconSet);
bool UnregisterIconSet(string iconSetID);
InputIconSet GetIconSet (string iconSetID);
IReadOnlyList<InputIconSet> GetAllIconSets ();
bool HasIconSet (string iconSetID);
/* Icon set assignment. */
void SetActiveIconSet (string iconSetID);
void SetIconSetForScheme(ControlSchemeType schemeType, string iconSetID);
string GetIconSetForScheme(ControlSchemeType schemeType);
/* Cache. */
void InvalidateCache();
void InvalidateCache(string actionID);
/* C# events. Bus events also published; see below. */
event Action<string> OnHintChanged; /* actionID */
event Action<string> OnIconSetChanged; /* iconSetID (may be null) */
}
public readonly struct InputHint
{
public string ActionID { get; }
public string DisplayText { get; }
public Sprite Icon { get; }
public string BindingPath { get; }
public ControlSchemeType SchemeType { get; }
public string IconSetID { get; }
public bool IsValid { get; }
public static InputHint Invalid { get; }
}
/* ScriptableObject typically, but can be constructed at runtime. */
public class InputIconSet : UnityEngine.ScriptableObject
{
public string IconSetID;
public string DisplayName;
public ControlSchemeType PrimarySchemeType;
public GamepadType GamepadType;
public List<InputIconMapping> Mappings;
}
/* Bus event payloads. */
public class ControlSchemeChangedEvent : Scylla.Core.Events.ScyllaEvent
{
public ControlScheme PreviousScheme { get; set; }
public ControlScheme CurrentScheme { get; set; }
public ControlSchemeType PreviousType { get; set; }
public ControlSchemeType CurrentType { get; set; }
public UnityEngine.InputSystem.InputDevice TriggeringDevice { get; set; }
}
public class InputHintChangedEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public InputHint OldHint { get; set; }
public InputHint NewHint { get; set; }
}
}
See the API reference for the full surface of InputIconSet, InputIconMapping, and the InputIconSetChangedEvent payload.
ControlSchemeType and GamepadType reference
The enum values and what they classify.
ControlSchemeType | Detected when | Typical icon set |
|---|---|---|
Unknown | Sentinel. Returned before initialization, after every scheme has been unregistered, or as the previous-type on the first switch. | None. |
KeyboardMouse | A keyboard key press, a mouse button press, or mouse delta above MouseDeltaThreshold. | One icon set with keyboard glyphs (“E”, “Space”) and mouse glyphs (“LMB”, “Scroll”). |
Gamepad | A gamepad button press, stick magnitude above GamepadStickThreshold, or trigger pressure above GamepadTriggerThreshold. | One icon set per GamepadType your project supports (Xbox, PlayStation, Nintendo, generic). |
Touch | A touch device produces a tap, drag, or pinch (project-supplied; not auto-registered by Initialize). | One icon set with touch glyphs (“Tap”, “Swipe”, “Pinch”). |
Custom | Project-defined. Use for Steam Input remapping profiles, accessibility schemes, or one-off device families. | Per-project. |
GamepadType | Detected from device name / layout strings containing | Typical icon set |
|---|---|---|
Generic | None of the patterns below (or no gamepad connected). | Neutral / abstract face-button labels (“A1”, “A2”) or omitted. |
Xbox | "xbox", "xinput", "microsoft". | A/B/X/Y, LB/RB/LT/RT, Menu/View. |
PlayStation | "dualshock", "dualsense", "playstation", "sony". | Cross/Circle/Square/Triangle, L1/R1/L2/R2, Options/Share. |
Nintendo | "nintendo", "switch", "pro controller", "joy-con". | A/B/X/Y (mirrored layout), L/R/ZL/ZR, +/-. |
Steam | "steam", "steamdeck", "valve". | Steam Deck face buttons or remap-specific glyphs. |
Best Practices
- Subscribe to
ControlSchemeChangedEventonce, not per-prompt. A single HUD controller that re-resolves every prompt in response to the event is faster and easier to reason about than every prompt widget subscribing individually. Per-actionInputHintChangedEventexists for the rare case where a single prompt has a lifetime that does not match the HUD. - Register icon sets at module startup, not per-screen. The hints manager caches across sessions; setting up the icon registry in
RunWhenReadyand leaving it alone is the right shape. Re-registering icon sets per screen creates churn and invalidates the cache unnecessarily. - Set
PrimarySchemeTypeon everyInputIconSet. Without it, the auto-assignment that runs duringRegisterIconSetdoes not pick a default for the scheme, andSetIconSetForSchemecalls become mandatory. With it, the registration call doubles as the assignment. - Use the
GetHintoverload over theGetActionIcon+GetActionDisplayTextpair.GetHintreturns the full struct in one cache lookup; the two-call version performs the binding resolve twice. Negligible per-call, noticeable on a HUD with 30 prompts being rebuilt on a scheme switch. - Tune thresholds in the configuration, not in code. The settings should live on
ScyllaInputConfigurationso QA and accessibility can adjust them without a rebuild. The values shown in the threshold-tuning recipe are starting points; your final values come from playtest reports. - Programmatic
SetActiveSchemecalls bypass the cooldown. Reach for them from settings menus and debug hotkeys; do not call them every frame inUpdate(that defeats the cooldown’s purpose and produces a thrashing change-event stream). - Fall back to
GamepadType.Genericicons when no vendor-specific set is registered. Detection can produce values for which your project does not ship icons (a Nintendo Pro Controller on a project that only ships Xbox and PlayStation prompts). The fallback to a generic gamepad icon set is the cleanest path; the alternative is a blankIconfield on theInputHint. - Treat
InputHint.IsValidas the gate for UI rendering. Afalseresult means the action is not registered, the binding has no entry in any registered icon set, or the manager is not initialized. UI code that renders the empty hint anyway shows blank prompts; gating onIsValidlets the UI show a “?” placeholder or hide the prompt entirely. - Subscribe to
OnGamepadTypeChanged, notOnSchemeChanged, for vendor-icon swaps. The vendor change can occur while the active scheme is still keyboard (the player plugs in a Sony controller mid-mouse-session); the type-change event fires immediately, so the next gamepad switch already shows the right icons. The scheme-change event fires later, after auto-detection or a manual switch.
Pitfalls
Related
- Scylla Input
- Actions and Bindings
- Rebinding
- API reference:
Scylla.Input.IControlSchemeManager - API reference:
Scylla.Input.ControlScheme - API reference:
Scylla.Input.ControlSchemeType - API reference:
Scylla.Input.GamepadType - API reference:
Scylla.Input.IInputHintsManager - API reference:
Scylla.Input.InputHint - API reference:
Scylla.Input.InputIconSet - API reference:
Scylla.Input.ControlSchemeChangedEvent - API reference:
Scylla.Input.InputHintChangedEvent