Everything you need to let players remap controls at runtime, from the first ‘press any key’ prompt to conflict resolution, profile persistence, and a full options-screen data model.
Summary
This page covers everything between “the player clicks Rebind Jump in the options menu” and “the new binding survives a relaunch with the right key still mapped”. Two layers of API support that journey, and most projects use both. The low-level IInputBindingManager – accessed through ScyllaInput.Instance.BindingManager – gives you single-binding rebind operations, conflict detection, override read/write, and the storage layer. The higher-level InputRebindManager is a concrete class you instantiate per options screen; it builds a complete, UI-agnostic data model organized by control-scheme columns and action rows, with batch operations and auto-resolve helpers baked in.
There is always a moment in production when QA reports that rebinding works, except pressing the same key in a new slot silently double-binds it without telling anyone, or the save file has the new binding but the next launch shows the default again, or the player gets stuck in a “press any key” prompt that never closes because the cancel key is also a valid bind target. The binding subsystem is shaped to make those bugs hard to write. GetConflicts returns every existing binding that occupies a candidate path before the rebind commits; saves and loads run through a pluggable IBindingStorage so the persistence path is one swap away from a JSON file or a PlayerPrefs entry; rebind operations have an explicit cancel path, a configurable timeout, and a completion cooldown so a mouse-button rebind cannot immediately re-trigger another rebind through the same click that finished the first one.
Bindings in the framework are overrides. The authoritative defaults live in the project’s InputActionAsset; only the deviations from those defaults are saved to disk, loaded back at startup, and reset when the player asks for it. This means a patch can change a default binding without breaking saved profiles, and “Reset to Defaults” is a single call (ResetAllToDefaults) that drops every override at once.
When you need to build a full configuration screen, InputRebindManager is the right shape. You hand it the action manager, binding manager, and optionally the context manager in its constructor, then declare which control-scheme groups appear as columns (WASD, Gamepad, IJKL, etc.). The output is a flat per-action data model that drops directly into UI: one row per action, one cell per scheme group, with conflict state, override state, display strings, and rebind start/cancel helpers all pre-computed. The bundled ConfigScreen demo is the canonical reference for how the two layers compose into a player-facing screen.
Use this page for:
- Triggering a single-binding rebind from a button click (“Rebind Jump”).
- Showing the player a conflict warning before committing a new binding.
- Persisting binding overrides per-profile, with JSON-on-disk or PlayerPrefs storage.
- Switching storage backends at runtime (a JSON exporter for modding, a PlayerPrefs fallback for consoles).
- Building a full ConfigScreen-style remapping UI with scheme columns, conflict highlights, and reset-to-defaults.
- Reacting to rebind events on the SEX (Scylla Event eXchange) bus to update HUD prompts and re-cache button labels.
- Importing or exporting binding profiles as JSON for cloud-save or cross-device flows.
Features
- Two-layer API.
IInputBindingManagerfor single-binding operations (the low level:StartRebind,GetConflicts,SaveBindings,LoadBindings).InputRebindManager(concrete class, instantiated by you) for a full ConfigScreen data model (the high level: scheme groups, batch reset, auto-resolve, post-completion cooldown). - Override-only persistence. The
InputActionAssetdefaults are authoritative; only the player’s deviations are serialised. A patch that changes a default does not invalidate saved profiles. - Pluggable storage. Default
PlayerPrefsBindingStoragefor the zero-config path;JSONFileBindingStoragewrites one file per profile underApplication.persistentDataPath/Scylla/Input/. Swap viaBindingManager.SetStorage(...). Custom backends implementIBindingStorage. - Profile system.
CurrentProfileID(default"default") is the implicit target forSaveBindings/LoadBindings.SetCurrentProfile,GetAllProfiles, andDeleteProfilecover the profile UI. Saved profiles persist across runs as long as the storage backend keeps the data. - Conflict detection.
GetConflicts(path, excludeActionID)returns every existing binding that occupies the candidate path.BindingConflictcarries the conflicting action ID, map ID, binding index, control path, and a display string ready for a UI label. Conflict detection respects context exclusion groups when the context manager is supplied. - Configurable rebind operations.
InputRebindOperationhas an explicit cancel control path (<Keyboard>/escapeby default), a timeout (5 seconds by default), and callbacks onOnConflictDetectedandOnComplete. The fluent surface allows tweaks per call site. - SEX bus events.
InputRebindStartedEvent,InputRebindCompletedEvent,InputRebindCancelledEvent,InputBindingsSavedEvent,InputBindingsLoadedEventfor every meaningful transition. HUD code subscribes to the saved/loaded pair to refresh button prompts after a rebind without polling. - JSON export/import.
ExportBindingsAsJSONandImportBindingsFromJSONround-trip the override set as a pretty-printed JSON document with version, profile ID, timestamp, and the override array. Same payload the storage backends save. - Post-completion cooldown.
InputRebindManagerenforces a 200 ms cooldown between a rebind completing and the next rebind starting, which prevents the mouse-button press that finished one rebind from immediately re-triggering another via Unity UI’s pointer-upOnClick. - Auto-load at initialize.
IInputBindingManager.Initializechecks the storage backend for the"default"profile and applies it before initialization completes; the game starts up with the player’s bindings already restored. - Auto-resolve conflicts.
InputRebindManager.AutoResolveConflicts()walks the data model and remaps colliding bindings to available keys, returning anAutoResolveResultthat lists every change made and how many conflicts remain.
Concepts
The vocabulary used throughout the page.
| Concept | Type / shape | Notes |
|---|---|---|
IInputBindingManager | interface, reached via ScyllaInput.Instance.BindingManager | The low-level surface: rebind operations, conflict detection, override read/write, profile persistence, JSON export/import. Owned by the module; do not instantiate directly. |
InputRebindManager | concrete class, instantiated by user code | The high-level UI-agnostic facade for a full ConfigScreen. Takes the three Input sub-managers in its constructor; call Initialize(schemeGroups) to build the data model. Disposable. |
| Override | per-binding overridePath | The player’s deviation from the default. Stored separately from defaults; cleared by ClearBindingOverride or ResetToDefaults. Only overrides are saved. |
InputRebindOperation | per-rebind session object | Returned by BindingManager.StartRebind(actionID, bindingIndex). Listens for a new input, validates against the cancel control path, fires OnConflictDetected and OnComplete. Single active operation per BindingManager. |
BindingConflict | immutable record | One entry returned by GetConflicts(path). Carries ConflictingActionID, ConflictingMapID, ConflictingBindingIndex, ConflictingPath, DisplayString. Pass the action + index back to ClearBindingOverride to resolve. |
| Profile | named string ID | Groups overrides for the same player or controller setup. Default is "default". Switched via SetCurrentProfile; each profile saves under its own key in the storage backend. |
IBindingStorage | pluggable backend interface | Decouples the binding manager from any specific persistence mechanism. Methods: SaveBindings(profile, json), LoadBindings(profile) -> json, DeleteBindings, HasBindings, GetAllProfiles. |
PlayerPrefsBindingStorage | default storage backend | Stores under PlayerPrefs keys. Zero configuration; cross-platform; subject to PlayerPrefs size limits on some platforms. |
JSONFileBindingStorage | file-based backend | Writes one file per profile under Application.persistentDataPath/Scylla/Input/. Human-readable; suitable for modding and external editing. Custom base paths are supported via the constructor. |
ConfigSchemeGroup | configuration value for InputRebindManager.Initialize | Declares which control scheme appears as a column in the data model. A project that supports keyboard, IJKL alternate, and gamepad would pass three scheme groups. |
Recipes
These recipes cover the full rebinding surface from a single “Rebind Jump” button up to a complete options screen. Each one is self-contained – you can jump straight to the one that matches what you are building. The first recipe (“Let a player rebind a single action”) shows the foundational setup that the later recipes assume is in place.
Let a player rebind a single action
Problem. Your options menu has a “Rebind Jump” button. The player clicks it, the game waits for a keypress, and the new binding sticks for the rest of the session and across relaunches. That is the entire flow for the vast majority of games that offer any remapping at all.
Solution. One StartRebind call plus a completion handler. The binding manager rejects concurrent rebinds and applies the override to the live action by the time the handler fires, so success means the change is already active.
using Scylla.Core;
using Scylla.Input;
using UnityEngine;
public sealed class RebindButton : MonoBehaviour
{
[SerializeField] private string _actionID = "Jump";
[SerializeField] private int _bindingIndex = 0;
public void OnClick()
{
var binding = ScyllaInput.Instance.BindingManager;
/* Reject concurrent rebinds at the UI layer too. The manager would
* also reject, but matching the gate in UI gives the click a clean
* no-op outcome rather than a warning log. */
if (binding.IsRebinding) return;
/* StartRebind returns null when the rebind cannot start: manager not
* initialized, action not registered, bindingIndex out of range,
* or another rebind already running. */
var op = binding.StartRebind(_actionID, _bindingIndex);
if (op == null)
{
Debug.LogWarning($"Could not start rebind for {_actionID}[{_bindingIndex}]");
return;
}
/* OnComplete fires whether the operation succeeded, was cancelled
* (Escape pressed by default), or timed out.
* Parameters: success flag, new path (null on cancel/timeout). */
op.OnComplete += (success, newPath) =>
{
if (success)
{
/* The override is already applied to the Unity action.
* Persist it so the next launch starts with the new binding. */
binding.SaveBindings();
Debug.Log($"Rebound {_actionID} to {newPath}");
}
else
{
Debug.Log("Rebind cancelled or timed out.");
}
};
}
}
Detect and resolve binding conflicts before committing
Problem. There is always a player who tries to rebind two actions to the same key by accident. The cheap path is to detect the conflict, surface it as a UI prompt (“Rebinding to Space will replace Crouch”), and let the player either back out or confirm the replacement. You want this check to happen before the rebind commits, not after, so the UI can respond while there is still a choice to make.
Solution. OnConflictDetected fires after the player presses a valid input but before the rebind commits. You get the candidate path and the full list of BindingConflict entries that share it. Calling CancelRebind in the handler backs out; letting the handler return without cancelling auto-applies.
using Scylla.Input;
using System.Collections.Generic;
using UnityEngine;
public sealed class ConfigCellController : MonoBehaviour
{
[SerializeField] private string _actionID;
[SerializeField] private int _bindingIndex;
public void OnRebindClicked()
{
var binding = ScyllaInput.Instance.BindingManager;
if (binding.IsRebinding) return;
var op = binding.StartRebind(_actionID, _bindingIndex);
if (op == null) return;
/* OnConflictDetected fires after the player presses a valid input
* but before the rebind commits. The argument is the list of every
* BindingConflict that shares the candidate path. */
op.OnConflictDetected += (conflicts) =>
{
/* Show a modal: "Rebinding to Space will replace Crouch.
* Continue? [Replace] [Cancel]" */
ShowConflictModal(
conflicts,
onReplace: () =>
{
/* Clear every conflicting binding so the candidate path
* becomes uniquely owned by this action. The operation
* auto-applies after OnConflictDetected returns when the
* caller does not cancel it. */
foreach (var conflict in conflicts)
{
binding.ClearBindingOverride(
conflict.ConflictingActionID,
conflict.ConflictingBindingIndex);
}
},
onCancel: () =>
{
binding.CancelRebind();
});
};
op.OnComplete += (success, newPath) =>
{
if (success) binding.SaveBindings();
};
}
private void ShowConflictModal(
IReadOnlyList<BindingConflict> conflicts,
System.Action onReplace,
System.Action onCancel)
{
/* UI modal omitted for brevity. Each BindingConflict carries the
* conflicting action ID, map ID, binding index, control path, and
* display string ready to paste into the modal's body text. */
}
}
Save, load, and reset bindings
Problem. You want the player’s custom bindings to survive a relaunch and to support an “Undo my changes” button that reverts to whatever was last saved. You also want a “Reset All” button that takes everything back to the shipped defaults.
Solution. Persistence is three calls: SaveBindings(profileID = null) writes the override set; LoadBindings(profileID = null) reads it back and applies; ResetAllToDefaults() drops every override. The defaults in the InputActionAsset are never touched by the storage layer.
using Scylla.Input;
using UnityEngine;
public sealed class BindingsPersistencePanel : MonoBehaviour
{
/* Save the current profile. SaveBindings(null) is equivalent. */
public void OnSaveClicked()
{
ScyllaInput.Instance.BindingManager.SaveBindings();
}
/* Reload the current profile (an "undo my changes since last save" button).
* LoadBindings calls ResetAllToDefaults internally before applying the
* stored payload, so you always start from a clean slate. */
public void OnReloadClicked()
{
ScyllaInput.Instance.BindingManager.LoadBindings();
}
/* Reset every binding to its default. Does not auto-save; call
* SaveBindings afterwards to make the reset stick across relaunches. */
public void OnResetAllClicked()
{
var binding = ScyllaInput.Instance.BindingManager;
binding.ResetAllToDefaults();
binding.SaveBindings();
}
/* Reset one specific action. ResetToDefaults(actionID) removes every
* override on that action without touching others. */
public void OnResetActionClicked(string actionID)
{
var binding = ScyllaInput.Instance.BindingManager;
binding.ResetToDefaults(actionID);
binding.SaveBindings();
}
/* Switch to a different profile and load its overrides. The two calls
* are deliberately separate: SetCurrentProfile only changes the active
* ID, LoadBindings reads the stored payload. */
public void OnSelectProfile(string profileID)
{
var binding = ScyllaInput.Instance.BindingManager;
binding.SetCurrentProfile(profileID);
if (binding.HasSavedBindings(profileID))
{
binding.LoadBindings();
}
else
{
binding.ResetAllToDefaults();
}
}
}
Switch storage backends per platform
Problem. You want players on desktop to be able to inspect and share their binding files, but you also want the game to work cleanly on console and mobile without touching the file system. The zero-config PlayerPrefsBindingStorage works everywhere, but the JSON alternative is dramatically better for desktop modding and community-shared control profiles.
Solution. Call SetStorage once at startup, gated on a platform #if block. Swapping the backend does not migrate existing data, so check HasSavedBindings after the swap and handle the empty-backend case.
using Scylla.Core;
using Scylla.Input;
using UnityEngine;
public sealed class StorageBackendSelector : MonoBehaviour
{
private void Start()
{
ScyllaCore.Instance.RunWhenReady(ConfigureStorage);
}
private void ConfigureStorage()
{
var binding = ScyllaInput.Instance.BindingManager;
#if UNITY_STANDALONE || UNITY_EDITOR
/* Desktop: prefer JSON files. Players can inspect, share, and edit
* the file at Application.persistentDataPath/Scylla/Input/.
* The default base path can be overridden via the constructor
* when a custom location is required (e.g. portable installs). */
binding.SetStorage(new JSONFileBindingStorage());
#else
/* Other platforms: stay on PlayerPrefs. Console and mobile builds
* typically do not benefit from JSON-on-disk and may forbid
* arbitrary file writes outside platform-specific APIs. */
binding.SetStorage(new PlayerPrefsBindingStorage());
#endif
/* Swapping the backend does not migrate existing saved data.
* If the new backend has no profile saved, reset to defaults
* (or load from a different source) before continuing. */
if (!binding.HasSavedBindings("default"))
{
binding.ResetAllToDefaults();
}
else
{
binding.LoadBindings("default");
}
}
}
Build a full options screen with InputRebindManager
Problem. A single “Rebind Jump” button is enough for some games. But if you are building a standard options screen with one row per action, one column per control scheme (keyboard, gamepad, alternate layout), conflict highlights, and a Reset All button, the single-binding API requires a lot of bookkeeping. You want a data model you can iterate over and drive directly from UI without tracking every action/index pair yourself.
Solution. InputRebindManager builds that data model for you. Construct it with the three sub-managers, call Initialize(schemeGroups) to declare your columns, then walk the entries to instantiate UI rows. The manager owns the rebind lifecycle, the cooldown, and the conflict tracking; your UI layer only calls StartRebind, CancelRebind, and ResetAll.
using Scylla.Input;
using UnityEngine;
public sealed class InputConfigScreen : MonoBehaviour
{
private InputRebindManager _config;
private void Start()
{
Scylla.Core.ScyllaCore.Instance.RunWhenReady(InitializeConfigScreen);
}
private void InitializeConfigScreen()
{
var input = ScyllaInput.Instance;
/* Construct with the three sub-managers. The context manager is
* optional but required for exclusion-group-aware conflict detection. */
_config = new InputRebindManager(
input.ActionManager,
input.BindingManager,
input.ContextManager
);
/* Declare which control schemes get a column in the data model.
* A typical action game: keyboard primary, gamepad primary,
* keyboard alternate. Column order matches the scheme group order. */
_config.Initialize(
new ConfigSchemeGroup("WASD"),
new ConfigSchemeGroup("Gamepad", "<Gamepad>"),
new ConfigSchemeGroup("IJKL")
);
BuildUI();
}
private void BuildUI()
{
/* For each map, walk the entries and instantiate UI rows.
* ConfigActionEntry is one row; its Bindings list is one cell per
* scheme group. Each cell exposes DisplayText, IsRebindable,
* HasOverride, and the BindingIndex needed to start a rebind. */
foreach (var mapID in _config.MapIDs)
{
var entries = _config.GetEntriesForMap(mapID);
foreach (var entry in entries)
{
/* InstantiateRow is project-specific. The data model carries
* everything the UI needs; the project supplies the widget layer.
* See the ConfigScreen demo for a full reference implementation. */
InstantiateRow(entry);
}
}
}
/* IsInCooldown is true for 200 ms after a rebind completes. UI rebind
* buttons should check it before opening their mid-rebind state to
* prevent the pointer-up click that finished the rebind from
* immediately re-triggering another one. */
public void OnCellRebindClicked(ConfigActionEntry entry, ConfigBindingEntry bindingEntry)
{
if (_config.IsRebinding || _config.IsInCooldown) return;
if (!bindingEntry.IsRebindable || bindingEntry.BindingIndex < 0) return;
_config.StartRebind(entry.ActionID, bindingEntry.BindingIndex, bindingEntry.SchemeGroup);
}
/* Reset all overrides in the data model, then re-save. */
public void OnResetAllClicked()
{
_config.ResetAll();
ScyllaInput.Instance.BindingManager.SaveBindings();
}
/* Let the manager auto-remap any colliding bindings to free keys,
* then save the resolved state. */
public void OnAutoResolveClicked()
{
var result = _config.AutoResolveConflicts();
if (result.ConflictsResolved > 0)
{
ScyllaInput.Instance.BindingManager.SaveBindings();
}
}
private void OnDestroy()
{
/* InputRebindManager is IDisposable; dispose explicitly so its
* subscriptions and the data model are released cleanly. */
_config?.Dispose();
}
private void InstantiateRow(ConfigActionEntry entry) { /* project-specific */ }
}
API Sketch
The low-level binding manager surface and the high-level rebind manager surface, with the relevant payload types.
namespace Scylla.Input
{
/* Low-level: reached via ScyllaInput.Instance.BindingManager. */
public interface IInputBindingManager
{
bool IsInitialized { get; }
bool IsRebinding { get; }
InputRebindOperation CurrentRebindOperation { get; }
string CurrentProfileID { get; }
/* Storage backend swap. Default is PlayerPrefsBindingStorage. */
void SetStorage(IBindingStorage storage);
/* Rebind. Single active operation at a time. */
InputRebindOperation StartRebind(string actionID, int bindingIndex = 0);
void CancelRebind();
void UpdateRebind();
/* Binding queries. */
string GetBindingPath (string actionID, int bindingIndex = 0);
string GetBindingDisplayString (string actionID, int bindingIndex = 0);
bool HasBindingOverride (string actionID, int bindingIndex = 0);
/* Binding modification (programmatic; bypasses interactive rebind). */
void SetBindingOverride (string actionID, int bindingIndex, string path);
void ClearBindingOverride (string actionID, int bindingIndex);
void ResetToDefaults (string actionID = null);
void ResetAllToDefaults ();
/* Conflict detection. excludeActionID excludes one action by name. */
IReadOnlyList<BindingConflict> GetConflicts(string path, string excludeActionID = null);
bool HasConflict (string path, string excludeActionID = null);
/* Persistence. profileID = null falls back to CurrentProfileID. */
void SaveBindings (string profileID = null);
void LoadBindings (string profileID = null);
bool HasSavedBindings(string profileID = null);
void DeleteProfile (string profileID);
IReadOnlyList<string> GetAllProfiles();
void SetCurrentProfile(string profileID);
/* JSON round-trip. Same payload shape the storage backends save. */
string ExportBindingsAsJSON();
void ImportBindingsFromJSON(string json);
/* C# events on the manager. SEX events also published (see below). */
event Action<InputRebindOperation> OnRebindStarted;
event Action<bool, string> OnRebindCompleted; /* (success, newPath) */
event Action<string> OnBindingsLoaded; /* profileID */
event Action<string> OnBindingsSaved; /* profileID */
}
/* High-level: instantiated by user code; one per config screen. */
public class InputRebindManager : IDisposable
{
public InputRebindManager(
IInputActionManager actionManager,
IInputBindingManager bindingManager,
IInputContextManager contextManager = null);
public IReadOnlyList<ConfigSchemeGroup> SchemeGroups { get; }
public IReadOnlyList<string> MapIDs { get; }
public bool IsInitialized { get; }
public bool IsRebinding { get; }
public bool IsInCooldown { get; } /* true for 200 ms after rebind completion */
public string ClearBindingPath { get; set; } /* default "<Keyboard>/delete" */
public int ActionMapCount { get; }
public int ActionCount { get; } /* counts only the first scheme group */
public int BindingCountTotal { get; }
public int ConflictCount { get; }
public void Initialize(params ConfigSchemeGroup[] schemeGroups);
/* Data model accessors. */
public IReadOnlyList<ConfigActionEntry> GetEntriesForMap(string mapID);
public IReadOnlyList<ConfigActionEntry> GetAllEntries();
public IReadOnlyList<ConfigActionEntry> GetEntriesForAction(string actionID);
/* Rebind lifecycle. */
public void StartRebind(string actionID, int bindingIndex, string schemeGroup);
public void CancelRebind();
/* Batch operations. */
public void ResetAll();
public void ResetAction(string actionID);
public void ClearAll();
public void RefreshAll();
public void RefreshAction(string actionID);
public void SaveBindings(string profileID = null);
public void LoadBindings(string profileID = null);
/* Conflict detection. */
public IReadOnlyList<ConfigConflict> GetConflictsForAction(string actionID);
public IReadOnlyList<ConfigConflict> GetAllConflicts();
public bool HasConflicts(string actionID);
public AutoResolveResult AutoResolveConflicts();
public void Dispose();
}
/* Single rebind session, owned by IInputBindingManager. */
public class InputRebindOperation : IDisposable
{
public bool IsActive { get; }
public bool WasSuccessful { get; }
public string NewPath { get; }
public string OriginalPath { get; }
public string ActionID { get; }
public int BindingIndex { get; }
/* Fluent configuration (call before or after Start; takes effect immediately). */
public InputRebindOperation WithTimeout(float seconds); /* default 5 s */
public InputRebindOperation WithCancelingThrough(string path); /* default <Keyboard>/escape */
public InputRebindOperation WithControlFilter(string prefix);
public InputRebindOperation WithExpectedControlType(string type);
public InputRebindOperation WithExcludedPath(string path);
public InputRebindOperation WithModifierSupport(bool enabled);
public InputRebindOperation WithDeltaPolling(bool enabled);
public InputRebindOperation Start();
public void Cancel();
public event Action<bool, string> OnComplete; /* (success, newPath) */
public event Action<IReadOnlyList<BindingConflict>> OnConflictDetected;
public event Action OnCancelled;
}
/* Immutable record returned by IInputBindingManager.GetConflicts. */
public class BindingConflict
{
public string ConflictingActionID { get; }
public string ConflictingMapID { get; }
public int ConflictingBindingIndex { get; }
public string ConflictingPath { get; }
public string DisplayString { get; }
}
/* Conflict record returned by InputRebindManager's conflict detection. */
public class ConfigConflict
{
public string ActionID { get; }
public string ConflictingActionID { get; }
public string ConflictingMapID { get; }
public string SchemeGroup { get; }
public string SharedPath { get; }
public string DisplayText { get; }
}
/* Storage backend interface and the two shipped implementations. */
public interface IBindingStorage
{
string StorageID { get; }
void SaveBindings(string profileID, string jsonData);
string LoadBindings(string profileID);
bool DeleteBindings(string profileID);
bool HasBindings(string profileID);
IReadOnlyList<string> GetAllProfiles();
}
public class PlayerPrefsBindingStorage : IBindingStorage { /* default; zero config */ }
public class JSONFileBindingStorage : IBindingStorage
{
public string BasePath { get; } /* defaults to {persistentDataPath}/Scylla/Input */
public JSONFileBindingStorage();
public JSONFileBindingStorage(string basePath);
}
/* Bus event payloads. */
public class InputRebindStartedEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public int BindingIndex { get; set; }
}
public class InputRebindCompletedEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public int BindingIndex { get; set; }
public string NewPath { get; set; }
}
public class InputRebindCancelledEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public int BindingIndex { get; set; }
}
public class InputBindingsSavedEvent : Scylla.Core.Events.ScyllaEvent { public string ProfileID { get; set; } }
public class InputBindingsLoadedEvent : Scylla.Core.Events.ScyllaEvent { public string ProfileID { get; set; } }
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Storage backends
The two shipped backends with their trade-offs.
| Backend | Where data lives | Player can inspect / edit | Cross-platform | Best fit |
|---|---|---|---|---|
PlayerPrefsBindingStorage | Unity PlayerPrefs registry (platform-defined location, e.g. Windows registry, plist on macOS) | No (opaque registry entries) | Yes | Default. Console, mobile, and any project where players are not expected to hand-edit the binding file. |
JSONFileBindingStorage | Application.persistentDataPath/Scylla/Input/{profileID}.json (configurable base path) | Yes (plain JSON file) | Yes (desktop) | Desktop. Modding support. Cloud-saved profiles where the JSON gets shared between machines. |
Custom IBindingStorage | Wherever the implementation puts it | Depends | Depends | Cloud sync, save-game integration, platform-specific storage APIs. |
The default storage on Initialize is PlayerPrefsBindingStorage when SetStorage has not been called explicitly. Swapping later does not migrate; existing saved data stays in the previous backend until manually exported and re-imported.
Best Practices
- Always call
SaveBindingsafter a successful rebind. The override is applied to the Unity action immediately, but it does not survive a relaunch unless persisted. The save call is cheap (one storage write per call); inside anOnCompletehandler is the right place for it. - Check
GetConflicts(candidatePath, excludeActionID: rebindActionID)before committing a programmaticSetBindingOverride. The interactive rebind operation does this automatically; programmatic code has to do it explicitly or risk double-binding. - Pass the context manager to
BindingManager.Initializeif your project uses contexts with exclusion groups. Without it, conflict detection cannot skip bindings from mutually exclusive contexts, and you will see phantom conflicts when rebinding shared action names (e.g.Cancel,Confirm). - Default to
JSONFileBindingStoragefor desktop,PlayerPrefsBindingStorageelsewhere. The platform#ifblock in the storage-switching recipe is the typical pattern. Mobile and console platforms restrict direct file system writes and benefit from the PlayerPrefs path; desktop benefits from the inspect-and-edit JSON path. - Construct one
InputRebindManagerper config screen; dispose on close. It is per-screen state; instantiating it once at startup ties the data model to a single set of scheme columns and leaks subscriptions when alternate screens want different columns. - Check
IsInCooldownbefore opening a cell’s mid-rebind UI state. The 200 ms cooldown after a rebind completion is what prevents the mouse-button press that finished the rebind from immediately re-triggering another via Unity UI’s pointer-upOnClick. UI code that skips the check leaves cells stuck in the “press a key” state until the player clicks them again. - Subscribe to
InputBindingsLoadedEventandInputBindingsSavedEventfor HUD prompt refreshes. Button-prompt resolvers need to re-cache labels after a load (the bindings just changed); a small handler that bumps a cache version is the cleanest hook. - Use
ExportBindingsAsJSON/ImportBindingsFromJSONfor cross-device profile transfer. The same JSON payload survives a copy-paste between machines, a cloud-sync round trip, or a community-shared “tournament keymap”. Storage backends are the persistence path; the JSON pair is the portable path. - Reset to defaults with
ResetAllToDefaultsand thenSaveBindings. The reset only clears in-memory overrides; without the save, the next launch loads the player’s old saved overrides and the reset appears to not stick. - Treat
InputActionAssetdefaults as the authoritative bindings. Override-only persistence means a patch can change a default keybind without invalidating saved profiles. Updating a default in the asset is the right shape for “we shipped Crouch on C and playtesters complained; ship Crouch on LeftCtrl in the patch” – players who never rebound C keep getting Crouch on the new default, players who rebound get to keep their custom. - Use
ConfigSchemeGroup.DeviceFilterto restrict which device a column accepts during rebind. Passing"<Gamepad>"as the device filter for the Gamepad column ensures a keyboard key press cannot accidentally land in a gamepad binding cell. Without the filter, any input device is eligible during the capture window.
Pitfalls
Related
- Scylla Input
- Actions and Bindings
- Contexts
- API reference:
Scylla.Input.IInputBindingManager - API reference:
Scylla.Input.InputRebindOperation - API reference:
Scylla.Input.BindingConflict - API reference:
Scylla.Input.IBindingStorage - API reference:
Scylla.Input.JSONFileBindingStorage - API reference:
Scylla.Input.PlayerPrefsBindingStorage - API reference:
Scylla.Input.InputRebindManager - API reference:
Scylla.Input.InputRebindStartedEvent - API reference:
Scylla.Input.InputRebindCompletedEvent - API reference:
Scylla.Input.InputRebindCancelledEvent - API reference:
Scylla.Input.InputBindingsSavedEvent - API reference:
Scylla.Input.InputBindingsLoadedEvent