Skip to content

Gameplay Input Integration

Lobby input and controller profiles

Lobby Join/Leave/Navigate/Confirm belong to the shared input services and scene UI presenters; configuring a gameplay action asset does not rebind them. Default lobby controls provide two logical keyboard roles plus recognized gamepads. Additional hardware needs a matching profile and usable controls, not merely a device visible in Input Debugger.

For a generic joystick or supported HID exposed through a Joystick-derived layout, create a GenericControllerProfile and assign it to Shared Joystick Input → Profiles. Add Couch Co-op Builder > Joystick Control Discovery temporarily to a scene object, enable Diagnostics Enabled, enter Play Mode and operate the device to inspect reusable control paths in Console, then configure Join, Leave, Confirm, Cancel and navigation. Keep a specific base layout/interface and exact hardware metadata filters. Reported HID VID/PID select the controller type; manufacturer/product provide the configured fallback when required HID IDs are unavailable. A reported HID mismatch is rejected rather than bypassed by text matching. Avoid a broad layout-only profile unless that scope is intentional.

One reusable profile can match identical controllers. Runtime device/display names (including appended numbers) do not identify physical hardware. The most specific matching profile is selected; ties are ambiguous and need corrected configuration or an explicit runtime profile assignment. Each joined device still gets independent actions, InputUser and session. Type/profile matching is not proof of unique physical identity across launches; saved overrides use Input Binding Profiles.

Gameplay input binding

Resolution chooses which object represents the player. Input mode chooses how that object consumes the authoritative session's input. Configure them separately:

Input strategy is independent of representation mode. A resolved object can use Built-in Actions, PlayerInput Adapter or Custom Adapter when configured for that strategy. API Only can consume the context directly without a representation binding.

Configure GameplayPlayerBinding on the prefab or existing object. Its default is Handle Input Myself, so gameplay input evaluation remains opt-in. Runtime owners can call binding.ConfigureInput(mode, settings, existingPlayerInput, adapter) after resolution. Changing configuration explicitly releases the old input cycle. Inspector changes apply on the next binding cycle.

Four input strategies

  1. Handle Input Myself: no gameplay actions, PlayerInput creation or gameplay method calls. Use Context.TryGetInputUser and read PairedDevices, Slot, PlayerIndex and SessionId directly. API-only resolution requires no binding at all. No missing-settings error is emitted.
  2. Built-in Actions: assign GameplayInputSettings, which references a customer-owned InputActionAsset and map names for Keyboard A, Keyboard B, Gamepad and Joystick. The selected map provides Move/Look (Vector2) and PrimaryAction/SecondaryAction/Jump/Pause (Button). Those names/types are the built-in adapter contract; use Custom Adapter for another contract. Each player gets a clone, including runtime binding overrides; only its selected map is enabled. Access binding.Input and its actions or ActionTriggered. No movement, physics or camera behavior is included.
  3. PlayerInput Adapter: a limited, explicit bridge for an already-authored PlayerInput using native Unity Events. See the limitations below. PlayerInput is never required or automatically added.
  4. Custom Adapter: assign a MonoBehaviour implementing IGameplayInputAdapter, or pass a plain interface instance to ConfigureInput. TryBind receives the real Context and existing GameplayPlayerBinding. Unbind is called once when that input cycle ends, including rollback after TryBind fails/throws. Make Unbind safe after partial initialization. Use one adapter instance per representation; sharing one live instance is rejected. Custom code owns its created resources and must clean them up.

The binding's Context is the only authoritative player association. Input setup follows context assignment. Awake/OnEnable may precede binding, especially on a spawned prefab. Subscribe to InputBound for context plus input readiness; inspect IsInputReady/InputFailure. ContextChanged is emitted after the initial input attempt; it does not guarantee a successful optional adapter. Scene systems can use GameplayPlayerSetup.PlayerResolved, then inspect input readiness. Late consumers replay the current binding state. Input failure leaves the valid representation/context in place, logs player/session details and raises InputBindingFailed; it does not silently change input strategy or prevent other players from binding.

Session ownership and keyboard roles

The built-in evaluator does not create an InputUser, pair devices or call AssociateActionsWithUser. The installed Input System supports one associated action collection per user: replacing it would reset the persistent lobby map's device/mask filtering. Instead, gameplay clones maintain explicit device lists from the existing user's paired devices and subscribe to InputUser/device changes. An empty list is kept explicit; it never becomes the null/all-devices fallback. Session availability and pairing govern map enabling. There are no per-frame searches, action cloning or input allocations in this evaluator.

Keyboard A/B intentionally share a physical keyboard and have distinct session users. Device filtering alone cannot separate them. As with the existing lobby architecture, they use two explicit, disjoint logical action maps. Both maps are validated against the owned keyboard: resolved controls must be individual keys and must not overlap across roles. Full-keyboard/wildcard/mouse bindings and missing/equal role maps are rejected. Align customer gameplay role maps with the customer's lobby roles; editing a lobby template does not automatically remap the separate gameplay asset. The supplied defaults use the established WASD/F and Arrow/Enter role controls, plus distinct gameplay-only buttons.

Gamepad maps evaluate only their session's paired device(s), independent of Gamepad.current, Gamepad.all ordering or device display names. Built-in Joystick maps optionally use Context.JoystickProfile, the profile actually assigned to that session. Move reuses its directional bindings, processors and effective layout paths. Optional Gameplay controls on GenericControllerProfile customize Look/Primary/Secondary/Jump/Pause. Empty Primary/Secondary/Pause use Confirm/Cancel/Join respectively; empty Look/Jump remain unbound. Leave remains the existing profile Leave control. Identity matching and join policy are unchanged. Turn off Use Joystick Profile in GameplayInputSettings to use a customer-authored joystick map instead. PlayerInput mode uses its authored action bindings rather than rewriting native action IDs.

Lobby and gameplay have separate action instances and callbacks. Disposable lobby presenters/UI modules unsubscribe on scene exit. Persistent session maps continue managing the existing Join/Leave/reservation behavior; they are not forwarded into gameplay and are not replaced with a new associated collection. A physical key/button can have an intentional meaning in each scene. Avoid assigning gameplay actions to existing Leave controls unless Leave is intended.

PlayerInput bridge: support and limitations

The installed PlayerInput exposes a read-only user and automatically creates/pairs its own user in OnEnable. There is no public API to adopt the existing session user or set its native playerIndex. This bridge never enables PlayerInput's native lifecycle and uses no private-field reflection or Input System modifications.

Requirements:

  • Author PlayerInput disabled, on the same gameplay object as GameplayPlayerBinding, before it can be instantiated/activated. Built-in spawning rejects opted-in input prefabs with enabled PlayerInput components before Instantiate. An already-enabled component is incompatible; the adapter cannot undo auto-pairing that customer initialization already performed.
  • Select Invoke Unity Events. Its existing persistent action events are retained and installed on the isolated cloned asset through the public actions getter.
  • Assign a source action asset that is not enabled elsewhere; the bridge rejects an enabled source instead of disabling someone else's actions.
  • Do not assign a UI Input Module. The bridge owns gameplay action evaluation only.
  • Keep the gameplay object active while using the bridge. Deactivation releases its clone; activation begins a fresh input cycle.

The bridge temporarily assigns the cloned asset/current map to the runtime PlayerInput, preserves default map/control scheme/camera/notification/event configuration, and restores the original asset on release. Shared keyboard uses explicit KeyboardA/KeyboardB maps, validated as above. With optional GameplayInputSettings, its map names select the player's map; otherwise controller roles use PlayerInput.defaultActionMap. Only the selected gameplay map is enabled.

Native PlayerInput.user/devices/playerIndex remain invalid/empty/-1. The originating session's InputUser, paired devices and PlayerIndex remain unchanged and are exposed through GameplayPlayerBinding.Context. Native active-player registration, PlayerInputManager, automatic control schemes, SwitchCurrentActionMap and native device-lost/regained notifications are not supported by this bridge. Use Context availability and the owned Input.Map instead. Do not enable PlayerInput or call ActivateInput/SwitchCurrentControlScheme while bound.

Send/Broadcast Messages and native C# onActionTriggered are deliberately rejected: the installed PlayerInput gates those paths on its private input-active state. Native Unity action events do not have that gate and can be safely wired without a second user. C# consumers can instead subscribe to binding.Input.ActionTriggered or use Built-in/Custom adapters. This is a UnityEvent compatibility bridge, not adoption of the canonical user into PlayerInput.

Lifetime, live joins and failures

Input bindings follow representation binding, release, deactivation and scene teardown. Owned action maps are disabled/disposed, all framework callbacks are unsubscribed, native UnityEvent hooks are removed and the original PlayerInput asset is restored. Customer objects/source assets are not destroyed. Custom Unbind exceptions are diagnosed without stopping other players' cleanup. Do not retain an Input instance after release.

A live join binds input once when its representation resolves. The built-in spawner still waits for its first character selection. An intentional Leave releases only that player's gameplay input/object; survivors keep indexes/users/instances. Device Lost disables unavailable owned actions but retains the same session, object and adapter. The existing services restore ownership on reconnect; device/user notifications update filtering without creating another user, callback set or representation. Your game owns reconnection feedback and pause policy.

Missing settings/assets/maps/actions, wrong built-in value types, keyboard overlap, incompatible PlayerInput configuration and custom adapter exceptions are isolated input failures. Unexpected object destruction also releases input through the existing binding lifecycle and resolution diagnostic. Explicit reconfiguration or deactivation/activation starts a new input cycle; failures are not retried every frame.

Minimal custom adapter

The public factory is also useful when custom code wants the standard isolated evaluator. The returned object is owned by the caller, who must Dispose it. Other custom adapters may consume Context directly instead.

using LevelArgento.CouchCoopBuilder;
using UnityEngine;
using UnityEngine.InputSystem;

public sealed class MyGameplayAdapter : MonoBehaviour, IGameplayInputAdapter
{
    [SerializeField] private GameplayInputSettings settings;
    private GameplayActionInput input;

    public bool TryBind(GameplayPlayerContext context,
        GameplayPlayerBinding binding, out string failureMessage)
    {
        input = GameplayActionInput.Create(context, settings);
        input.ActionTriggered += OnAction;
        failureMessage = null;
        return true;
    }

    private void OnAction(InputAction.CallbackContext callback)
    {
        if (callback.performed) { /* Consume this player's action. */ }
    }

    public void Unbind()
    {
        if (input == null) return;
        input.ActionTriggered -= OnAction;
        input.Dispose();
        input = null;
    }
}

See Input Binding Profiles for runtime rebinding and profile-based persistence. Lobby controls, gameplay controls and saved profile identity are separate concepts.