How to know which device the player is actually holding, react when a controller is unplugged mid-fight, and swap the layer that talks to the hardware.
Summary
Two related things live on this page. The first is devices: which physical thing the player is using right now, when that changes, and what to do when a controller vanishes mid-fight. The second is the bridge, which is the layer between Scylla and whatever is actually reading the hardware.
Device tracking is the part most projects need. ScyllaInput.Instance.LastUsedDevice and LastUsedDeviceType report what the player touched most recently, and that’s what switches the prompt glyphs from keyboard to gamepad the moment someone picks up a controller. Two events fire around that, one when a device connects and one when it disconnects, and the control scheme manager publishes ControlSchemeChangedEvent when the player moves between keyboard/mouse and gamepad. The disconnect case matters most, because a controller running out of battery mid-fight is exactly the scenario console certification requires you to handle.
The bridge is the extension point. IScyllaInputBridge defines what a source of input has to provide: a device list, a last-used device, key and mouse queries, and connect and disconnect events. UnityInputSystemBridge is the implementation that ships, wrapping Unity’s Input System. Because it is an interface, a second bridge can sit alongside it, and IInputBridgeManager decides how several of them combine: one at a time, in priority order, routed by device type, or all at once.
Most games never touch the bridge directly. It matters when you need input from somewhere Unity’s Input System does not reach: a replay file, a network stream driving a remote player, an arcade cabinet’s custom hardware, or an automated test harness pressing buttons. The bridge feeds the module’s direct device queries, last-used device tracking and control scheme detection. Actions read Unity’s Input System directly, so a bridge does not drive them.
Use devices and bridges for:
- Switching prompt glyphs the instant the player moves between keyboard and gamepad.
- Pausing the game and showing a reconnect prompt when a controller disconnects.
- Telling keyboard, gamepad, touchscreen and pen apart, from a device or from a binding path.
- Feeding recorded or synthetic input through the same path as live input, for replays and tests.
- Supporting hardware Unity’s Input System does not know about.
Features
- Last-used device tracking.
LastUsedDeviceandLastUsedDeviceTypeon the module report what the player actually touched most recently, which is the signal a prompt-glyph system needs. - Device events.
InputDeviceConnectedEventandInputDeviceDisconnectedEventare published on the event bus, each carrying the device, its type, and its ID.InputDeviceChangedEventis published when the last-used device changes, carrying the previous and current device and their types. It also fires when the last-used device is unplugged and tracking falls back to another device. For prompt glyphs,ControlSchemeChangedEventis usually the better signal, since it only fires when the scheme category changes. - Module lifecycle events.
InputEnabledEventandInputDisabledEventfire when input as a whole is switched on or off. - Device classification.
InputDeviceUtilsorts a device into keyboard, mouse, gamepad, touchscreen, joystick or pen, and can do the same from a binding path string without a live device. - A direct-callback listener.
IInputListenerreceives the same low-level device and enable events through a single method. Register the object withScyllaInputand the callbacks are synchronous, on the main thread. - A swappable input backend.
IScyllaInputBridgeis the contract for anything that supplies input, so Unity’s Input System is one implementation rather than a hard dependency. - Multiple bridges at once.
IInputBridgeManagerregisters several bridges and combines them in one of four query modes, with per-bridge priorities. - Per-device-type routing. A bridge can be nominated for one device type, so a custom gamepad driver handles pads while the standard bridge keeps the keyboard.
Device types
InputDeviceType is the classification everything else keys off. It is deliberately coarse: what matters for glyphs and prompts is the family, not the exact model.
| Value | Covers | Notes |
|---|---|---|
Unknown | Anything unclassified | What you get from LastUsedDeviceType before the player has touched anything, and from a device the bridge does not recognize. Treat it as “show the default prompts” rather than as an error. |
Keyboard | Keyboard | Paired with Mouse in practice. InputDeviceUtil.IsKeyboardOrMouse exists because the two almost always want the same prompt set. |
Mouse | Mouse | Separate from keyboard at the device level, though a control scheme usually treats them as one. |
Gamepad | Standard gamepads | The common console and PC pad. IsGamepadOrJoystick groups this with Joystick for the “show button glyphs” decision. |
Touchscreen | Touch input | IsTouchDevice is the check. Worth branching on for control layout as well as prompts, since touch usually needs on-screen controls rather than glyph hints. |
Joystick | Flight sticks and other joysticks | Distinct from Gamepad because the control layout differs, even though both want button prompts rather than key names. |
Pen | Stylus and pen input | Rare in games, present for completeness and for tablet-targeted tools. |
InputDeviceUtil.GetDeviceTypeFromPath does the same classification from a binding path string such as <Gamepad>/buttonSouth, so you can classify a binding rather than a live device, for example while building a rebinding screen before any device has been used.
Bridge query modes
When more than one bridge is registered, InputBridgeQueryMode decides how the manager combines them. With a single bridge, which is the default state, the mode does not matter.
| Mode | Behavior | Notes |
|---|---|---|
SingleBridge | Only the active bridge answers | The simplest arrangement and the right default. Whichever bridge is active handles everything; the others are registered but idle. Use this when a second bridge is a replacement rather than a supplement, such as a replay bridge that takes over entirely during playback. |
PriorityChain | Bridges are asked in priority order until one answers | Each bridge gets a chance, highest priority first, and the first to report input wins. Suits a setup where a specialized bridge handles what it knows and falls through to the standard one for everything else. |
PerInputType | Each device type is routed to a nominated bridge | Set with SetBridgeForInputType. The arrangement for custom hardware: a bespoke bridge owns Gamepad while UnityInputSystemBridge keeps keyboard and mouse. Types with no nomination fall back to the active bridge. |
CombineAll | Every bridge is queried and the results merged | All bridges contribute, which suits two genuine sources of input that should both work at once, such as a local player on a pad and a networked player on a stream. Costs the most, since every query touches every bridge. |
InputConflictResolution on the manager decides what happens when two bridges report the same input in the combining modes.
Recipes
Each recipe stands alone. The first three cover what most projects need; the rest are for the cases where the bridge is worth reaching for.
Switch prompt glyphs when the player picks up a controller
Problem. Your tutorial says “press Space to jump” and the player just put down the keyboard and picked up a pad. The prompt needs to become a button glyph without the player doing anything to tell you.
Solution. LastUsedDeviceType reports what was touched most recently, and ControlSchemeChangedEvent fires when the player moves between keyboard/mouse and gamepad.
private ScyllaEventSubscription _schemeChanged;
private void OnEnable()
{
/* Fires when the active control scheme switches, carrying the previous and current
scheme types and the device that triggered the switch. */
_schemeChanged = ScyllaEvents.Listen<ControlSchemeChangedEvent>(this, OnSchemeChanged);
/* Set the initial state too. The event only fires on a change, so a player who
started on a pad has not raised one yet. */
RefreshPrompts(ScyllaInput.Instance.LastUsedDeviceType);
}
private void OnDisable()
{
_schemeChanged?.Dispose();
}
private void OnSchemeChanged(ControlSchemeChangedEvent evt)
{
/* TriggeringDevice is null for a switch made from code, so fall back to the
module's own last-used device in that case. */
var deviceType = evt.TriggeringDevice != null
? InputDeviceUtil.GetDeviceType(evt.TriggeringDevice)
: ScyllaInput.Instance.LastUsedDeviceType;
RefreshPrompts(deviceType);
}
private void RefreshPrompts(InputDeviceType deviceType)
{
/* A glyph decision wants the coarse classification: a flight stick and a pad
both show button prompts, and Unknown means fall back to the default set. */
_promptSet = deviceType switch
{
InputDeviceType.Gamepad or InputDeviceType.Joystick => _gamepadPrompts,
InputDeviceType.Touchscreen => _touchPrompts,
_ => _keyboardPrompts,
};
}
Pause the game when a controller disconnects
Problem. A controller runs out of battery in the middle of a boss fight. The player cannot do anything, and the boss carries on hitting them. Console certification requires you to handle this, and players notice on every platform.
Solution. InputDeviceDisconnectedEvent carries the device that went away, so you can check whether it was the one being used.
private void OnEnable()
{
_disconnected = ScyllaEvents.Listen<InputDeviceDisconnectedEvent>(this, OnDeviceLost);
_connected = ScyllaEvents.Listen<InputDeviceConnectedEvent>(this, OnDeviceFound);
}
private void OnDeviceLost(InputDeviceDisconnectedEvent evt)
{
/* DeviceID identifies which device went, so a second pad disconnecting in a
local-multiplayer game does not pause player one. */
if (evt.DeviceID != _activeDeviceID)
{
return;
}
/* Pause through the framework's time subsystem so everything else honors it. */
ScyllaTime.Instance.Pause();
_reconnectPrompt.Show($"Please reconnect your {evt.DeviceType}.");
}
private void OnDeviceFound(InputDeviceConnectedEvent evt)
{
/* Do not auto-resume: the player may have set the pad down deliberately. Show
that it is back and let them dismiss the prompt. */
if (evt.DeviceType == InputDeviceType.Gamepad)
{
_reconnectPrompt.ShowResumeButton();
}
}
Tell devices apart from a binding path
Problem. You are building a rebinding screen and want a gamepad glyph next to gamepad bindings, but the player has not touched a pad yet so there is no live device to inspect.
Solution. GetDeviceTypeFromPath classifies the binding string itself.
foreach (var binding in action.bindings)
{
/* Works on the path string, so no device needs to be connected. "<Gamepad>/
buttonSouth" classifies as Gamepad, "<Keyboard>/space" as Keyboard. */
var deviceType = InputDeviceUtil.GetDeviceTypeFromPath(binding.path);
row.Icon = _glyphs.ForDeviceType(deviceType); /* your own glyph lookup */
}
/* The live-device forms, for when you do have one. The grouped checks exist because
most decisions actually want the grouping, not the exact device type. */
var device = ScyllaInput.Instance.LastUsedDevice;
if (InputDeviceUtil.IsKeyboardOrMouse(device))
{
ShowKeyboardLayoutHint();
}
else if (InputDeviceUtil.IsGamepadOrJoystick(device))
{
ShowButtonLayoutHint();
}
else if (InputDeviceUtil.IsTouchDevice(device))
{
EnableOnScreenControls();
}
/* And the precise classification when the family is not enough. */
var type = InputDeviceUtil.GetDeviceType(device);
React to device events without managing subscriptions
Problem. Several systems each care about device connect and disconnect, and each one is carrying its own subscribe, unsubscribe and dispose boilerplate.
Solution. IInputListener is a single-method interface receiving the same low-level events. Implement it, register the object once, and one method handles them all instead of one subscription each.
/* One method, one enum, one device. InputEventType declares DeviceConnected,
DeviceDisconnected, DeviceChanged, InputEnabled and InputDisabled. DeviceChanged
arrives with the new last-used device whenever the player switches devices. */
public class ControllerWatcher : MonoBehaviour, IInputListener
{
private void OnEnable()
{
/* Implementing the interface is not enough on its own: the object has to be
registered. Duplicate registrations are ignored, so this is safe to repeat. */
ScyllaInput.Instance.RegisterListener(this);
}
private void OnDisable()
{
/* The module holds the reference, so an unregistered listener on a destroyed
object is a stale callback waiting to happen. */
ScyllaInput.Instance.UnregisterListener(this);
}
public void OnInputEvent(InputEventType eventType, InputDevice device)
{
/* device is null for InputEnabled and InputDisabled, which are not tied to any
particular piece of hardware. Guard before dereferencing it. */
switch (eventType)
{
case InputEventType.DeviceDisconnected:
_hud.ShowControllerWarning(device);
break;
case InputEventType.DeviceConnected:
_hud.ClearControllerWarning();
break;
case InputEventType.InputDisabled:
/* Input as a whole was switched off, during a cutscene or a load.
Distinct from a device going away. */
_hud.HidePrompts();
break;
}
}
}
Know when input is switched off entirely
Problem. A cutscene disables input, and your HUD keeps showing “press A to interact” prompts for controls that currently do nothing.
Solution. InputEnabledEvent and InputDisabledEvent bracket the module being switched on and off. Neither carries a payload, because there is nothing to say beyond the fact itself.
_inputDisabled = ScyllaEvents.Listen<InputDisabledEvent>(this, _ => _prompts.Hide());
_inputEnabled = ScyllaEvents.Listen<InputEnabledEvent>(this, _ => _prompts.Show());
Feed recorded input through the live input path
Problem. You want a replay system, or an automated test that plays through a level, and you do not want a second code path where recorded input is handled differently from real input. Anything that only happens on the real path is a bug you will not catch.
Solution. Implement IScyllaInputBridge and register it. Everything that reads through the bridge sees it as ordinary input: the module’s direct queries such as ScyllaInput.Instance.IsKeyPressed and MousePosition, last-used device tracking, and control scheme detection. Actions, and therefore contexts and buffering, read Unity’s Input System directly, so recorded input that should drive actions has to be fed through the Input System itself.
/* A bridge supplies devices, a last-used device, key and mouse state, and the two
device events. Implementing it puts your source on the same footing as Unity's. */
public class ReplayBridge : IScyllaInputBridge
{
public string BridgeID => "Replay";
public int Priority => 100; /* Higher wins in PriorityChain mode. */
public bool IsAvailable => _playing;
public event Action<InputDevice> OnDeviceConnected;
public event Action<InputDevice> OnDeviceDisconnected;
public void Initialize() { _frames = LoadRecording(); }
public void Shutdown() { _frames = null; }
public void Update()
{
/* Advance the recording by one frame. Everything downstream reads the state
this sets, so the replay drives the game exactly as a player would. */
_current = _frames[_frameIndex++];
}
public bool IsKeyPressed(Key key) => _current.Keys.Contains(key);
public bool WasKeyPressedThisFrame(Key key) => _current.Pressed.Contains(key);
/* ... the rest of the contract ... */
}
/* Register it, then decide how it combines with the standard bridge. SingleBridge
means the replay takes over completely while it is active. */
var manager = ScyllaInput.Instance.BridgeManager;
manager.RegisterBridge(new ReplayBridge());
manager.QueryMode = InputBridgeQueryMode.SingleBridge;
Route one device type to custom hardware
Problem. Your arcade cabinet has a custom controller board that Unity’s Input System knows nothing about, but the keyboard and mouse should keep working normally for the operator menu.
Solution. PerInputType mode routes each device type to a nominated bridge.
var manager = ScyllaInput.Instance.BridgeManager;
manager.RegisterBridge(new CabinetHardwareBridge());
/* Each device type can name a bridge. Anything not nominated falls through to the
active bridge, so the keyboard keeps working with no extra configuration. */
manager.QueryMode = InputBridgeQueryMode.PerInputType;
manager.SetBridgeForInputType(InputDeviceType.Gamepad, "CabinetHardware");
manager.SetBridgeForInputType(InputDeviceType.Joystick, "CabinetHardware");
/* The routing is inspectable and reversible at runtime. */
if (manager.HasBridgeForInputType(InputDeviceType.Gamepad))
{
var owner = manager.GetBridgeForInputType(InputDeviceType.Gamepad);
}
manager.ClearBridgeForInputType(InputDeviceType.Joystick);
/* Priority matters in PriorityChain mode and can be changed after registration. */
manager.SetBridgePriority("CabinetHardware", 200);
Inspect what is currently supplying input
Problem. Something is wrong with input on one machine and you need to know which bridge is answering before you can debug further.
Solution. The manager exposes the active bridge and the full registration list.
var manager = ScyllaInput.Instance.BridgeManager;
/* Which one is answering right now. */
Log.Info($"Active bridge: {manager.ActiveBridge?.BridgeID ?? "none"}", LogCategory.Input);
/* Every registered bridge and whether it is currently able to supply input.
IsAvailable is the bridge's own report, so a replay bridge says false when it is
not playing back. */
foreach (var bridge in manager.Bridges)
{
Log.Info($" {bridge.BridgeID} priority={bridge.Priority} available={bridge.IsAvailable}",
LogCategory.Input);
}
/* Look one up by ID, for tooling that targets a known bridge. */
if (manager.HasBridge("Replay"))
{
var replay = manager.GetBridge("Replay");
}
/* And remove one when it is finished, such as a replay bridge after playback ends. */
manager.UnregisterBridge("Replay");
API Sketch
Device access hangs off the module; the bridge contracts are the extension point.
/* --- On the module --- */
InputDevice ScyllaInput.Instance.LastUsedDevice { get; }
InputDeviceType ScyllaInput.Instance.LastUsedDeviceType { get; }
IInputBridgeManager ScyllaInput.Instance.BridgeManager { get; }
/* --- Device classification --- */
static class InputDeviceUtil {
InputDeviceType GetDeviceType(InputDevice device);
bool IsKeyboardOrMouse(InputDevice device);
bool IsGamepadOrJoystick(InputDevice device);
bool IsTouchDevice(InputDevice device);
InputDeviceType GetDeviceTypeFromPath(string bindingPath);
}
enum InputDeviceType { Unknown, Keyboard, Mouse, Gamepad, Touchscreen, Joystick, Pen }
/* --- Events, published on the SEX (Scylla Event eXchange) bus --- */
class InputDeviceConnectedEvent { InputDevice Device; InputDeviceType DeviceType; int DeviceID; }
class InputDeviceDisconnectedEvent { InputDevice Device; InputDeviceType DeviceType; int DeviceID; }
/* Published when the last-used device changes, including the fallback after it is unplugged. */
class InputDeviceChangedEvent { InputDevice PreviousDevice, CurrentDevice;
InputDeviceType PreviousDeviceType, CurrentDeviceType; }
class InputEnabledEvent { } /* no payload */
class InputDisabledEvent { } /* no payload */
/* --- Direct callback alternative --- */
interface IInputListener {
void OnInputEvent(InputEventType eventType, InputDevice device); /* device may be null */
}
/* Implementing is not enough; the object has to be registered. */
void ScyllaInput.Instance.RegisterListener(IInputListener listener);
void ScyllaInput.Instance.UnregisterListener(IInputListener listener);
enum InputEventType { DeviceConnected, DeviceDisconnected, DeviceChanged,
InputEnabled, InputDisabled }
/* --- The bridge contract --- */
interface IScyllaInputBridge {
string BridgeID { get; }
int Priority { get; }
bool IsAvailable { get; }
void Initialize();
void Shutdown();
void Update();
IReadOnlyList<InputDevice> GetDevices();
InputDevice GetLastUsedDevice();
InputDeviceType GetDeviceType(InputDevice device);
event Action<InputDevice> OnDeviceConnected;
event Action<InputDevice> OnDeviceDisconnected;
bool IsKeyPressed(Key key);
bool WasKeyPressedThisFrame(Key key);
bool WasKeyReleasedThisFrame(Key key);
bool IsAnyKeyPressed();
Vector2 GetMousePosition();
Vector2 GetMouseDelta();
Vector2 GetMouseScrollDelta();
bool IsMouseButtonPressed(int button);
bool WasMouseButtonPressedThisFrame(int button);
bool WasMouseButtonReleasedThisFrame(int button);
bool IsMouseConnected();
bool IsGamepadConnected();
Gamepad GetCurrentGamepad();
bool IsGamepadButtonPressed(GamepadButton button);
bool WasGamepadButtonPressedThisFrame(GamepadButton button);
bool WasGamepadButtonReleasedThisFrame(GamepadButton button);
Vector2 GetGamepadLeftStick();
Vector2 GetGamepadRightStick();
float GetGamepadLeftTrigger();
float GetGamepadRightTrigger();
}
/* --- The manager --- */
interface IInputBridgeManager {
IScyllaInputBridge ActiveBridge { get; }
IReadOnlyList<IScyllaInputBridge> Bridges { get; }
InputBridgeQueryMode QueryMode { get; set; }
InputConflictResolution ConflictResolution { get; set; }
void RegisterBridge(IScyllaInputBridge bridge);
void UnregisterBridge(string bridgeID);
IScyllaInputBridge GetBridge(string bridgeID);
bool HasBridge(string bridgeID);
void SetBridgePriority(string bridgeID, int priority);
int GetBridgePriority(string bridgeID);
void SetBridgeForInputType(InputDeviceType inputType, string bridgeID);
string GetBridgeForInputType(InputDeviceType inputType);
void ClearBridgeForInputType(InputDeviceType inputType);
bool HasBridgeForInputType(InputDeviceType inputType);
void Initialize();
void Shutdown();
void Update();
/* Device management and the same input queries as a bridge, answered through
the bridges according to QueryMode. */
IReadOnlyList<InputDevice> GetAllDevices();
InputDevice GetLastUsedDevice();
InputDeviceType GetDeviceType(InputDevice device);
event Action<InputDevice> OnDeviceConnected;
event Action<InputDevice> OnDeviceDisconnected;
}
enum InputBridgeQueryMode { SingleBridge, PriorityChain, PerInputType, CombineAll }
/* The shipped implementation, wrapping Unity's Input System. */
class UnityInputSystemBridge : IScyllaInputBridge { }
See the API reference for full signatures, remarks, and exception contracts on every member.
Best Practices
- Drive prompts from
LastUsedDeviceType, not from what is connected. A player with a pad plugged in who is typing on the keyboard wants keyboard prompts, and connection state cannot tell you that. - Set the initial prompt state as well as subscribing.
ControlSchemeChangedEventonly fires on a change, so a player who started on a gamepad has raised nothing yet. - Handle disconnect, and do not auto-resume on reconnect. The pause is required on console and welcome everywhere; resuming without the player asking drops them back into whatever was hitting them.
- Check
DeviceIDbefore reacting to a disconnect. In local multiplayer, one pad going flat should not pause the other player. - Use the grouped checks rather than switching on every enum value.
IsGamepadOrJoystickexists because a flight stick and a pad want the same prompts, and a switch that forgetsJoystickis a bug waiting for the one player who owns one. - Reach for
GetDeviceTypeFromPathwhen there is no live device. A rebinding screen has bindings before it has devices, and the path carries the classification already. - Treat
Unknownas “show defaults”, not as an error. It is the honest answer before the player has touched anything, and it will occur on the first frame of every session. - Leave the bridge alone unless you need a second input source. The shipped Unity bridge covers everything a normal game does; the extension point exists for replays, networked input and custom hardware.
- Give a bridge a stable
BridgeID. Registration, lookup, priority and per-type routing all key on that string, so changing it later breaks every call site that named it. - Snapshot bridge state once per
Update. Everything above the bridge assumes its answers are consistent within a frame; a bridge that reads live hardware per query produces bugs that look like they live in the action layer. - Prefer
SingleBridgeunless you genuinely need combining. It is the cheapest, and a replay or test bridge usually wants to replace live input rather than supplement it. - Unregister a bridge when its job ends. A replay bridge left registered after playback keeps being queried, and
IsAvailablereturning false is a weaker guarantee than not being in the list.
Pitfalls
Related
- 01 Scylla Input
- 07 Input – Control Schemes and Hints
- 03 Input – Actions and Bindings
- 09 Input – Configuration
- API reference:
Scylla.Input.IScyllaInputBridge - API reference:
Scylla.Input.IInputBridgeManager - API reference:
Scylla.Input.InputDeviceUtil - API reference:
Scylla.Input.InputDeviceType - API reference:
Scylla.Input.IInputListener