Input

Action-based input with a context stack and interactive rebinding, built on Unity's Input System.

Action maps, a context stack, and interactive rebinding on top of Unity's Input System · Optional event publishing to the Event Bus · No-op replacement: NullInputService

CGS reads keyboard, mouse, and gamepad input through IInputService — a thin layer over Unity's Input System package. Instead of scattering raw keycode checks through your code, you work with action maps: named sets of controls, such as "Gameplay" or "Menu", that you define once in an .inputactions asset. On top of that, the service adds three things Unity does not give you out of the box: a context stack that switches which action map is active, interactive rebinding ("press a key to reassign") with automatic saving, and optional publishing of input events to the Event Bus.

Before anything else: your project must be on the new input backend. Installing the Input System package (the CGS dependency manifest does that for you) does not switch the project setting called Active Input Handling. On a fresh project it stays Input Manager (Old) — and then the demo scene, the runnable samples, and your own action maps silently ignore every key press. The Welcome window (Tools > Common Game System > Welcome) detects this and shows a one-click Enable the new Input System button; the editor restarts once and input works. Manual path: Edit > Project Settings > Player > Other Settings > Active Input Handling → "Input System Package (New)" or "Both". See the FAQ for details.

What it does

  • Action-based reads. Look an action up once (GetAction("Gameplay", "Jump")), then poll it every frame or subscribe to its events. Your gameplay code never mentions specific keys or buttons, so keyboard and gamepad work through the same path.
  • Context stack. Push a context (PushContext("Menu")) to enable only that action map and disable all others; pop it and the previous context takes over. The newest context always wins, which is exactly the behavior you want for nested menus over gameplay.
  • Interactive rebinding. Start a rebind, let the player press the new key, and get a completion callback. Overrides persist to PlayerPrefs with one call.
  • Event publishing (opt-in). Ask the service to publish an action's presses to the Event Bus, so decoupled systems can react to input without holding an InputAction reference.

The service ships in the optional CommonGameSystem.Input assembly. If you remove the Input System package from your project, that assembly excludes itself automatically and the rest of CGS still compiles and boots.

Getting the service

Resolve once and cache the reference in Awake:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MyGameMode : MonoBehaviour
{
    private IInputService _input;

    private void Awake()
    {
        _input = ServiceLocator.Resolve<IInputService>();
    }
}

The [DefaultExecutionOrder(100)] attribute makes sure the CGS bootstrapper has finished registering services before your Awake runs. Never call Resolve inside Update() — it is a dictionary lookup; cache it once.

API reference

Action lookup

InputAction GetAction(string actionMapName, string actionName)

Returns the InputAction for, say, "Gameplay" / "Jump", or null if that map or action is not defined in your .inputactions asset. Cache the result — do not look actions up on hot paths.

Polling (call in Update or FixedUpdate)

T ReadValue<T>(InputAction action)           // Current-frame value (Vector2, float, ...)
bool IsPressed(InputAction action)           // true while held
bool WasPressedThisFrame(InputAction action) // true only on the press frame
bool WasReleasedThisFrame(InputAction action)// true only on the release frame

Publishing to the Event Bus (opt-in)

IDisposable PublishOnStarted(InputAction action)   // publishes InputActionStartedEvent
IDisposable PublishOnPerformed(InputAction action) // publishes InputActionPerformedEvent
IDisposable PublishOnCanceled(InputAction action)  // publishes InputActionCanceledEvent

Each call returns a token; dispose it to stop publishing. See the Event Bus page for subscribing.

The context stack

IDisposable PushContext(string actionMapName) // Enable only this map, disable the rest
string CurrentContext { get; }                // Top of the stack (null when empty)
IReadOnlyList<string> ContextStack { get; }   // Bottom-to-top snapshot (allocates; debug use)

PushContext returns a token; disposing the token pops that context and re-activates whatever is below it. Pushing the same map name twice creates two separate stack entries.

Interactive rebinding

IInputRebindOperation StartInteractiveRebind(
    InputAction action,
    int bindingIndex = -1,            // -1 = the action's first binding
    string controlsExcluding = "Mouse") // comma-separated controls to ignore

Returns an operation token with IsCompleted, IsCanceled, ResultBindingPath, and a Completed event. Only one rebind can run at a time — starting a second one throws InvalidOperationException. If the player does not press anything within the timeout (5 seconds by default; configurable via InputServiceOptions.RebindTimeoutSeconds), the rebind cancels itself.

Saving and restoring bindings

void SaveBindingOverrides()                           // Persist all overrides to PlayerPrefs
void LoadBindingOverrides()                           // Re-apply saved overrides
void ResetBindingOverrides(InputAction action = null) // Clear overrides (null = all actions)

Device queries

bool IsDeviceConnected<TDevice>() where TDevice : InputDevice // Any such device present?
TDevice GetDevice<TDevice>() where TDevice : InputDevice      // First matching device, or null

For example, IsDeviceConnected<Gamepad>() tells you whether to show gamepad button prompts.

Full example

A player controller that polls movement, publishes jump presses to the Event Bus, and swaps contexts when the game pauses:

using System;
using CommonGameSystem.Core;
using UnityEngine;
using UnityEngine.InputSystem;

[DefaultExecutionOrder(100)]
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float _moveSpeed = 5f;

    private IInputService _input;
    private InputAction _moveAction;
    private InputAction _jumpAction;
    private IDisposable _jumpEvents;
    private IDisposable _gameplayToken;
    private IDisposable _menuToken;

    private void Awake()
    {
        _input = ServiceLocator.Resolve<IInputService>();
        _moveAction = _input.GetAction("Gameplay", "Move");
        _jumpAction = _input.GetAction("Gameplay", "Jump");
    }

    private void Start()
    {
        // Publish Jump presses to the event bus.
        _jumpEvents = _input.PublishOnPerformed(_jumpAction);

        // Enable the Gameplay action map. Keep the token so we can
        // pop the context when the game pauses.
        _gameplayToken = _input.PushContext("Gameplay");
    }

    private void Update()
    {
        var move = _input.ReadValue<Vector2>(_moveAction);
        transform.Translate(move * _moveSpeed * Time.deltaTime);
    }

    public void OnPause()
    {
        _gameplayToken?.Dispose();               // Pop Gameplay
        _menuToken = _input.PushContext("Menu"); // Push Menu
    }

    public void OnResumeGame()
    {
        _menuToken?.Dispose();                   // Pop Menu
        _gameplayToken = _input.PushContext("Gameplay");
    }

    private void OnDestroy()
    {
        _jumpEvents?.Dispose();
        _menuToken?.Dispose();
        _gameplayToken?.Dispose();
    }
}

The "Gameplay" and "Menu" maps and the "Move" and "Jump" actions come from your own .inputactions asset — the service reads your asset, it never creates maps for you.

Turning it off

ServiceLocator.Replace<IInputService>(new NullInputService());

With the null implementation in place:

CallResult
ReadValue<T>()default(T) (Vector2.zero, 0f, ...)
IsPressed() / WasPressedThisFrame() / WasReleasedThisFrame()false
GetAction()null
PushContext() / PublishOn*()an empty token (does nothing)
StartInteractiveRebind()a stub operation that is already canceled
Save/Load/ResetBindingOverrides()no-op
IsDeviceConnected() / GetDevice()false / null

Use this for a cutscene-only mode, a headless server, or a test harness. Everything else (UI, scene flow) still compiles and runs without input.

Common pitfalls

  • Dead input on a fresh project = wrong backend. If nothing responds to the keyboard or mouse, check Active Input Handling first — see the callout at the top of this page and the FAQ.

  • Context tokens pop newest-first. Dispose the token returned by PushContext() (typically in OnDestroy) to pop that context. If objects are destroyed out of order the service copes gracefully, but popping a context that is not on top is not logged.

  • Action maps come from YOUR asset. The service does not create maps or actions. Define "Gameplay", "Menu", "Dialogue", and so on in your .inputactions asset.

  • One interactive rebind at a time. StartInteractiveRebind() throws InvalidOperationException if a rebind is already running. Disable other rebind buttons in your settings UI while one is active.

  • Your asset controls interaction processing. Deadzones, hold interactions, multi-tap — all configured on your .inputactions asset in the Inspector. The service exposes guidance defaults (such as InputServiceOptions.StickDeadzoneMin = 0.125f) but never overrides your asset's configuration.

  • Event Bus — the input events this service can publish: InputActionStartedEvent, InputActionPerformedEvent, InputActionCanceledEvent, InputContextPushedEvent, InputContextPoppedEvent, InputDeviceConnectedEvent, InputDeviceDisconnectedEvent, InputRebindStartedEvent, InputRebindCompletedEvent
  • UI Framework — pushes and pops input contexts automatically as panels open and close
  • Logger — context pushes/pops, device connects, and rebind timeouts are logged under the Input category
  • FAQ — the Active Input Handling fix, step by step