Skip to content

Input Binding Profiles and Rebinding

See Quick Start for lobby setup and Gameplay Integration for owned action binding.

This opt-in runtime API belongs to Couch Co-op Builder. A game such as Rocket Rumble can build its settings UI on top of it. The framework does not create a controls menu, change existing Join/Leave listeners, or rebind Smart UI Focus & Navigation. It changes the owned gameplay action clones exposed by GameplayActionInput. Source InputActionAssets and GenericControllerProfile assets are not rewritten.

Identity and shared profiles

PlayerIndex is a reusable session seat displayed as P1–P8; it remains stable until permanent Leave. InputSlot separately identifies the reserved input role/source, and SessionId is stable for that session. None is a persistent binding-profile key. Editing "P3 Controls" means editing playerBinding.Input.BindingProfile, the input source currently assigned to P3. After Leave, the next occupant receives its own source's profile.

Source Default persistent identity Consequence
Keyboard A KeyboardA Follows the logical A role across seat changes.
Keyboard B KeyboardB Follows the logical B role across seat changes.
Standard gamepad Gamepad:Profile: plus a metadata family fingerprint Same reported family shares saved overrides. Xbox and DualSense families do not inherit each other's overrides through seat reuse.
Generic joystick Joystick:Profile: plus the matched GenericControllerProfile identity fingerprint Uses the actual discovered profile; identical devices share that profile.

Gamepad family metadata includes the Unity layout, interface, manufacturer, product and HID VID/PID when reported. Display names, runtime device names, InputDevice.deviceId and serial numbers are not used as unique identity. A serial field alone is not evidence of cross-launch stability or uniqueness. A transport or layout change can select a different family fallback; this is a default/shared profile, not a claim of physical-device identity.

For generic joysticks, assign PersistentBindingProfileId once in the profile Inspector, or ConfigureBindingProfileId during configuration. Reuse that stable customer-owned ID when updating the profile asset. Existing assets with a blank ID use exact base-layout/interface/manufacturer/product/HID match fields. The fallback does not depend on asset/display names or generated binding GUIDs. Changing those match fields changes the fallback key. Use distinct explicit IDs for distinct profiles with otherwise identical match metadata.

Identical controllers cannot be reliably distinguished across launches by this fallback. They have independent actions, sessions and InputUsers, but share binding settings. Editing/resetting that shared profile updates every attached consumer of it. No fuzzy matching or invented physical identity is used.

A platform/customer integration with verified stable, unique physical identity can implement IInputBindingDeviceIdentityProvider and supply it to InputBindingProfileResolver. Returning true asserts both properties. Do not return a transient deviceId, enumerated display name, player seat, or unverified serial. Verified gamepad keys survive metadata/transport changes; verified joystick keys remain scoped to their controller profile. The framework itself provides no cross-platform guarantee of unique physical identity.

Service and storage

Keep one InputBindingProfileService for the game's controls settings lifetime, usually across scene transitions. It owns one complete versioned document in an IInputBindingProfileStore. The core model has no dependency on UI or PlayerPrefs.

JsonFileInputBindingProfileStore is the simple built-in desktop/mobile backend. Its default file is Application.persistentDataPath/CouchCoopBuilder/input-bindings.json. An explicit file path is also supported. It writes a sibling temporary file and replaces the previous document. Customers can implement the two-method store interface using their own save system. Save must replace the whole document atomically, or throw without changing the previous document. Use one writer per store. Platforms without suitable filesystem support, including WebGL/cloud save integrations, should supply their own backend.

Load explicitly and handle a failure before exposing editing/resetting. A failed load retains the last successfully loaded document and live bindings. An empty or missing file means defaults. Unsupported versions, malformed data and duplicate records are rejected. Do not silently overwrite an unknown or unreadable save by continuing with defaults after a failed load.

var profiles = new InputBindingProfileService(new JsonFileInputBindingProfileStore());
if (!profiles.TryLoad(out string loadFailure))
{
    // Show/report loadFailure and stop editing until the save is resolved.
    profiles.Dispose();
    return;
}

// After an existing GameplayPlayerBinding has bound its input:
var playerProfile = profiles.Attach(playerBinding.Input);
bool saved = playerProfile.TryRebind(
    "PrimaryAction", 0, "<Gamepad>/buttonNorth", out string failure);

Attach after gameplay input is available, for example from the existing GameplayPlayerBinding.InputBound callback. Reattach to the new input when the scene representation is recreated. The service automatically detaches when that input is disposed and retains saved overrides. It never creates another InputUser or replaces InputUser.actions. Custom integrations can attach to their own GameplayActionInput.Create result and remain responsible for disposing it.

An input can have one binding profile session. A stable optional domain (default Gameplay) separates different customer action schemas in the same profile. Keep the domain stable across scenes using the same schema. Only the owned gameplay map is enabled/modified, not every map in the source asset.

Settings before joining

profiles.OpenProfile(kind, gameplaySettings, device, joystickProfile) opens a settings preview using the same identity, storage, validation and reset operations. The returned InputBindingProfileSession.ActionMap is an owned, disabled clone for display and editing. It creates no PlayerSession, InputUser or synthetic device. Dispose the preview when changing profile or leaving settings. Later Attach(playerBinding.Input) consumes the saved overrides for that input source.

Both keyboard roles are validated in the preview even when nobody has joined. With no device, a preview can display defaults/saved data and reset, but cannot capture/rebind (CanRebind is false). A device-less gamepad preview represents the generic default Gamepad family; it is not a saved Xbox/DualSense physical identity. When a controller is connected, open its actual resolved family profile. Joystick previews require a configured GenericControllerProfile and use its identity, even when its device is absent. Connected joysticks must match that profile. SharedJoystickInput.ResolveProfile exposes the same strict discovery scoring and ambiguity result without enabling join listeners.

Capture, paths and conflicts

var capture = playerProfile.BeginInteractiveRebind("PrimaryAction", 0);
capture.Finished += result =>
{
    // Inspect Status: Succeeded, Rejected or Canceled, and Failure on rejection.
    // Refresh UI here. Dispose/unsubscribe UI handlers when the menu closes.
};
// The UI's Cancel button or teardown calls capture.Cancel()/Dispose().

Only one capture can run per service. The target map is disabled during capture; other players' maps keep working. Capture accepts and suppresses only events from the target's paired devices. Runtime device paths restrict capture but are never saved. Other controllers and another player's Escape key cannot choose/cancel a binding. Cancel is explicit, or occurs at the default ten-second timeout, device loss/disable/unpair, input disposal, session disposal or service disposal. The operation releases native resources and restores the target map when its input is still available. The timeout is configurable. A caller can optionally supply cancelControl to BeginInteractiveRebind; it must belong to the selected source and uses Unity's existing native cancellation path. It is reserved for cancel during that capture, not saved as an override. Without it, cancellation remains explicit or by timeout.

Programmatic overrides must be a concrete layout-relative path such as <Keyboard>/h, <Gamepad>/buttonNorth or <Joystick>/button11. An empty string unbinds. Runtime instance paths and wildcard/usage paths are rejected. The path must resolve on the source's paired device and fit the action's expected control type. Rebind individual composite parts by binding index, not a composite header. Keyboard sources accept individual keys.

Keyboard A/B share one physical keyboard. The service validates both logical role maps, including the defaults/saved role that has not joined, and rejects cross-role overlap before saving or mutating live actions. Conflicting capture is reported as Rejected with a reason. Reuse by different actions in the same gameplay map is also rejected; multiple bindings of the same action are valid. Identical logical button paths on separate gamepads/joysticks are valid; they are not cross-device conflicts.

The consumer should close or otherwise gate its lobby controls while editing if captured keys also have existing Join/Leave/UI meaning. This foundation does not replace those listeners or implement a modal controls menu.

Reset and action schema updates

  • playerProfile.ResetAction(actionName, out failure) resets that profile/action in the current domain.
  • playerProfile.ResetProfile(out failure) implements Reset Player: reset the current source's persistent profile, across its domains and attached consumers.
  • profiles.ResetAll(out failure) clears all persisted framework profile overrides, including profiles with no currently attached player.
  • The service also exposes profile-based ResetAction/ResetProfile for settings screens that do not have a live player.

Reset restores the authored defaults captured at attachment, including any customer-authored template overrides, processors and interactions. It does not rewrite source assets. Changes validate using scratch maps and save before live maps change. A failed store save leaves live actions and the document unchanged. Finish/cancel capture before another mutation or reset. ProfilesChanged signals successful changes; PersistedProfileKeys lists profiles with saved overrides. All service operations are intended for Unity's main thread.

Binding addresses use action name and original structural binding information, not Unity-generated GUIDs (joystick actions are reconstructed with fresh GUIDs). If an action/binding schema changes, unmatched stored addresses are retained and listed in playerProfile.UnappliedBindings; they are not applied to a different binding. Plan an explicit customer save migration or reset for schema changes.

Dispose the service at application teardown. Dispose/cancel captures when their UI closes. Detaching a live session restores its owned map's defaults; disposing the underlying GameplayActionInput detaches without touching destroyed assets.

Simultaneous action conflicts

Profile validation rejects one physical control assigned to different actions in the selected gameplay map/domain, including composite parts and parent controls. Two bindings of the same action are valid. Both keyboard role maps are compared because they share a Keyboard. Separate controller devices are not compared. Lobby/UI maps and other binding domains are independent and may reuse controls. InputBindingProfileService.LastConflict exposes control display/path, both semantic action names and profile keys after a rejected transaction. Rejection leaves live bindings and storage unchanged. Authored defaults must also be valid for their simultaneously active map; resets are validated before saving.