Starting with a small Unity example, we’ll work through three problems: separating input from movement, supporting richer controls, and keeping menus from fighting over those controls. The main examples use discrete button actions; continuous values get their own question at the end.

In this article: Input and gameplay · Request flags · Explicit state · Input requirements · Actions and bindings · Context ownership · Putting it together. Optional: Unity’s Input System and design questions.

If you’ve ever followed a Unity tutorial, you’ve probably written something like this:

C#
void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        transform.position += Vector3.up * 5f;
    }
}

Press space, the cube goes up. The first time it works, it feels a little bit like magic — you’ve made a thing react to a button.

But once you start treating this as the foundation of a game, the reaction becomes “Sir, this is disgusting.”

I’m only half-joking. It’s a perfectly useful tutorial example. The problem I want to start with is that the same script reads the key and decides how the character moves.

One script, three jobs

Look at the last line: transform.position += Vector3.up * 5f. The input handler is directly changing the object’s position. For a character meant to move through a Rigidbody, that bypasses the intended physics-driven movement path. Whether the character may move, and how, are decisions this keypress check shouldn’t own.

A frozen status effect makes the problem concrete. If several input handlers move the character directly, adding “can’t move while frozen” means finding and patching every one. The movement rule is scattered across the code that reads the controls.

Separation of concerns gives us a useful boundary here: “the player wants to jump.” Input translates hardware events into that request; gameplay decides whether jumping is allowed and what it does. Gameplay can then delegate the physical motion to the physics engine. Those are the three responsibilities in the diagram.

Player.cs reads Space, interprets it as jump, and changes the Transform directly, spanning input, gameplay and movement responsibilities.
The original script combines input interpretation and movement, bypassing the physics-driven path.

Separating the request from movement

Let the input handler report what the player did, and put the decision elsewhere. This first example uses a request flag to pass a keypress from Update() to FixedUpdate(). It is an illustration, not a complete character controller.

C#
public class Player : MonoBehaviour
{
    private Rigidbody body;
    private bool isGrounded = true;
    private bool jumpRequested;

    void Awake() => body = GetComponent<Rigidbody>();

    // Input side: just reports what was pressed.
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            jumpRequested = true;
    }

    // Gameplay side: decides what to do about it.
    void FixedUpdate()
    {
        if (jumpRequested)
        {
            if (isGrounded)
            {
                body.AddForce(Vector3.up * 5f, ForceMode.VelocityChange);
                isGrounded = false;
            }
            jumpRequested = false;
        }
    }

    void OnCollisionEnter(Collision _) => isGrounded = true;
}

Update() reads the keyboard and sets jumpRequested. FixedUpdate() checks that request against the character’s state, applies force through the Rigidbody if allowed, then clears the request. The flag carries the press across the two update loops; this example discards it after one attempt. 1

The grounding code is deliberately crude: any collision sets isGrounded, and walking off an edge doesn’t clear it. A real controller needs proper ground detection. That limitation is separate from how the jump request reaches it.

An input reader sets jumpRequested; a movement controller checks whether jumping is allowed and calls AddForce; a Rigidbody handles motion and collisions.
The request separates hardware reading from gameplay decisions. Physics handles the resulting motion.

Now suppose the character also needs to crouch and attack. Here’s the relevant input and gameplay code, with request and status fields assumed:

C#
// Input side
void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))        jumpRequested = true;
    if (Input.GetKeyDown(KeyCode.LeftControl))  crouchRequested = true;
    if (Input.GetKeyDown(KeyCode.Mouse0))       attackRequested = true;
}

// Gameplay side
void FixedUpdate()
{
    if (jumpRequested)
    {
        if (isGrounded && !isCrouching && !isAttacking)
        {
            body.AddForce(Vector3.up * 5f, ForceMode.VelocityChange);
            isGrounded = false;
        }
        jumpRequested = false;
    }

    if (crouchRequested)
    {
        if (isGrounded && !isAttacking)
            isCrouching = true;
        crouchRequested = false;
    }

    if (attackRequested)
    {
        if (!isAttacking && !isStunned)
        {
            // play attack animation, queue damage, etc.
            isAttacking = true;
        }
        attackRequested = false;
    }
}

Three actions are enough to produce a wall of conditional checks. Each behaviour consults a different combination of isGrounded, isCrouching, isAttacking and isStunned. Imagine adding sprinting, blocking and climbing to those conditions.

Making the gameplay state explicit

The character has modes. A state machine names those modes and defines the transitions between them. In the flag-based version, those rules are implicit: you have to reconstruct them by reading every condition and assignment.

I’ve written about implicit state machines separately. Here, we’ll deliberately model standing, crouching, jumping, attacking and being stunned as mutually exclusive modes. That is a simplification, not a rule for all characters: many games allow attacking while airborne or crouched, and independent facts can reasonably remain separate flags.

For this simplified model, a single CharacterState field is easier to inspect than combinations of booleans. It holds one mode at a time. The enum does not enforce legal transitions or control who assigns the field; the gameplay code must still do that.

The following fragment shows only entry into crouching, jumping and attacking. It omits the return paths to standing and all transitions involving stunned, so it is not a drop-in controller. It also chooses stricter rules than the preceding flag example, rather than reproducing every combination that code permits.

C#
public enum CharacterState
{
    Standing,
    Crouching,
    Jumping,
    Attacking,
    Stunned,
}

private CharacterState state = CharacterState.Standing;

// Gameplay side, now state-driven instead of flag-driven:
void FixedUpdate()
{
    if (jumpRequested)
    {
        if (state == CharacterState.Standing)
        {
            body.AddForce(Vector3.up * 5f, ForceMode.VelocityChange);
            state = CharacterState.Jumping;
        }
        jumpRequested = false;
    }

    if (crouchRequested)
    {
        if (state == CharacterState.Standing)
            state = CharacterState.Crouching;
        crouchRequested = false;
    }

    if (attackRequested)
    {
        if (state == CharacterState.Standing || state == CharacterState.Crouching)
            state = CharacterState.Attacking;
        attackRequested = false;
    }
}
Standing can transition to Jumping, Crouching or Attacking; Crouching can transition to Attacking. No return transitions or Stunned transitions are shown.
Only the transitions implemented in the fragment are drawn; Stunned is declared but unused here.

The checks now name the modes in which each request is allowed, and the field cannot simultaneously hold two modes. We still need a clear owner for transitions as the controller grows. 2

What the input side still needs

That gives the gameplay side a more explicit model. The input side is still doing what it did on day one:

C#
void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))        jumpRequested = true;
    if (Input.GetKeyDown(KeyCode.LeftControl))  crouchRequested = true;
    if (Input.GetKeyDown(KeyCode.Mouse0))       attackRequested = true;
}

These checks are enough for a fixed control scheme. Three common requirements take us beyond them:

Rebinding. The mapping “Space means jump” needs to live somewhere the settings menu can change at runtime. Store the bindings as data rather than embedding each mapping in Update().

Multiple devices. A gamepad button may produce the same intent as a keyboard key. Multiple hardware sources should be able to feed Jump without the movement controller needing a new input check for each one.

Recognition. A hold, double-tap or chord needs more than a check for a key going down. Recognizing a hold of half a second, two taps within 200ms, or a press while Shift is held requires timing or state somewhere in the input system.

Actions, bindings, and recognition

From here on, we’re discussing architecture rather than implementing an input package. The examples describe the contract gameplay consumes; the device abstraction and recognition machinery sit behind it.

Action. A named intent such as Jump, Crouch or OpenInventory. Gameplay reads player input through these actions rather than querying hardware directly. The movement controller can handle Jump without knowing whether it came from Space, a gamepad or a touchscreen.

Binding. A mapping from a hardware control to an action. A binding table stores mappings such as “Space → Jump” and “Gamepad South → Jump.” Changing a mapping changes the control without changing the gameplay subscriber. Multiple bindings can target the same action; held actions and continuous values also need a policy for combining or choosing between simultaneous sources.

Recognition. The rules for deciding when input satisfies an action: a press, a completed tap, a sufficiently long hold, or a sequence. Timing rules can be attached to bindings, actions, or a separate recognition layer. 3

For this walkthrough, “the action fires” means its trigger has been satisfied. An input system may also report that recognition has started or ended; the Unity lifecycle section covers that richer contract.

Keyboard, gamepad and mouse input enter a binding and recognition system. Jump, ChargedJump and Crouch reach the movement controller; Attack reaches the combat controller.
Bindings connect controls to actions. The diagram’s Space mappings do not specify how tap and hold compete; the walkthrough below adds that policy.

Suppose Space supports either a normal jump or a charged jump. We’ll choose exclusive tap-or-hold recognition for this example: releasing before 0.5 seconds emits Jump; reaching 0.5 seconds while held emits ChargedJump once and suppresses Jump for that press. This delays a normal jump until release. It is a design choice for the walkthrough, not something two independent bindings guarantee.

The other bindings are straightforward: pressing Gamepad South emits Jump, Left Ctrl emits Crouch, and Left Mouse emits Attack. A game that wants equivalent charge controls on a gamepad would give that button the same recognition policy.

A brief tap. The player releases Space before the threshold. Jump is emitted, and the movement controller runs its handler. It knows the requested action, not the key or recognition timer.

A long hold. At the threshold, ChargedJump is emitted instead. If we had kept the original press-triggered Jump alongside an independent hold-triggered ChargedJump, both could fire during one hold. That may suit another design, but it is not the exclusive behaviour chosen here.

A different control. Gamepad South reaches the same Jump subscriber. Rebinding the keyboard gesture to Enter likewise leaves the gameplay handlers unchanged; for this exclusive pair, move the tap-and-hold gesture together rather than accidentally splitting its two outcomes.

The action flow is hardware → input system → actions → consumers. Input doesn’t need to know what jumping does; gameplay doesn’t need to know how a jump request was recognized. We still need to decide which controls are active when a menu opens.

Who owns input context?

An input context, as used here, selects the set of bindings currently active. Space might mean Jump in the world and Confirm in a menu. Opening the menu should not also make the character jump behind it.

Gameplay and UI therefore need a way to request context changes. That coordination doesn’t require the input system to depend on concrete menus or controllers. The trouble starts when each caller independently decides what the active context must be.

When the pause menu opens, it calls inputSystem.setActiveContext("pause"). When it closes, it calls setActiveContext("gameplay"). Works perfectly. Ship it.

Then someone adds an inventory. Its open handler calls setActiveContext("inventory"). Its close handler calls setActiveContext("gameplay"). Still works.

Then someone adds a shop. Players can open it from a vendor in the world, or from inside the inventory. The shop’s open handler calls setActiveContext("shop"). The player closes it. What should the active context be now? If they came from the world, gameplay. If they came from the inventory, inventory. A close handler that hardcodes gameplay gets the second case wrong: the inventory remains on screen, but Space starts firing Jump again.

The components can each set a context, but neither coordinates restoration with the other.

Pause Menu, Inventory and Shop all call setActiveContext directly on the input system. Three competing writers select a single active context without an arbiter.
Independent context setters lose track of what should resume when an overlay closes.

Give a context manager responsibility for choosing the active context. Consumers request changes through it; only the manager changes the input system’s active binding set. Keep that setter behind the manager’s interface rather than relying on every caller to remember the convention.

The manager needs a restoration policy, not just a forwarding function. For this example, choose a stack: start with gameplay, push inventory, then push shop. Closing shop pops it and restores inventory. If shop was opened directly over gameplay, popping it restores gameplay instead. The shop no longer guesses where the player came from.

This assumes overlays close in last-in, first-out order. If an underlying screen can disappear while another stays open, a plain pop is insufficient; use owned tokens or another policy that can remove the correct entry. These are proposed policies for the example, not an implementation supplied by the snippets.

Pause Menu, Inventory and Shop send requests to a Context Manager, which alone calls setActiveContext on the input system.
The manager centralizes context changes. Its stack or other restoration policy determines which request takes effect.

Use one authority per player or independent input domain, not necessarily one global singleton. Local multiplayer may need a context manager for each player. 4

Making the next change easier

A useful test is what a tired developer reaches for at 4pm on a Friday. Is adding a binding or requesting a menu context simpler than putting another hardware check in a gameplay script? The interface should make the intended route easy to find and use.

The state field makes the character’s current mode visible, but transition rules still belong to gameplay. The binding table gives control mappings a place to live. The context manager owns selection and restoration rather than leaving each screen to guess. These are specific responsibilities we can inspect and test, not a guarantee that the whole game is correct.

That is the boundary I’d keep as the game grows: input produces actions, gameplay decides what they can do, and a context manager coordinates which controls are active. New features still bring design decisions, but we have somewhere to put them.


Optional: mapping this to Unity

Unity’s Input System package replaces the legacy Input.GetKey... API used in the opening snippets. Its vocabulary maps well to the architecture above, with a few distinctions worth keeping. The documentation linked here is for package version 1.11.

Actions and bindings. Actions such as Jump, Move and OpenInventory can have multiple control bindings. Code can subscribe to action callbacks or read values as appropriate. See Unity’s Actions guide.

Action Maps. Named collections of actions that can be enabled or disabled together. Gameplay and UI maps can implement our contexts, but Unity allows multiple maps to be enabled at once. Exclusively switching between them is the policy chosen in this article, not a requirement of Action Maps.

Processors. Value transformations such as deadzones, inversion, normalization and sensitivity scaling. They reshape values rather than define temporal recognition rules; the resulting values can still affect downstream action behaviour.

Interactions. Recognition rules such as Hold, Tap, MultiTap and SlowTap. They can be configured on an action or on an individual binding. Interaction ordering and cancellation matter when several rules share a control; don’t assume adding a tap and a hold automatically implements the exclusive policy above. 5

Lifecycles: started, performed, canceled

The main walkthrough treats an action as a single event. Unity exposes phases so consumers can also respond while recognition is underway. The sequence depends on the action type and its interactions; it is not a universal three-step lifecycle.

Take a button with a Hold interaction. Pressing it starts recognition (Started). Holding it long enough satisfies the rule (Performed). Releasing too early cancels before performance (Canceled). In this example, “the action fires” refers to Performed.

Canceled does not always mean a failed attempt. A default Button action can perform on press and then cancel on release. PassThrough actions have a different phase pattern. Subscribers should interpret callbacks according to the configured action and interaction, rather than treating every cancellation as failure.

These phases can support charge feedback and cleanup without inventing a separate action name for every change. Gameplay may still own the charge itself, especially if stamina, upgrades or interruptions affect it.

A note on hierarchical state machines

The character model can grow independently of the input architecture. A hierarchical state machine nests states: an OnFoot parent might contain Standing, Moving and Attacking, with shared parent behaviour or transitions. Swimming can have its own children. A hierarchy does not automatically deduplicate similar sibling submachines; code reuse is a separate implementation choice.

Use it when the shared behaviour is genuinely structural, not just a few coincidentally similar transitions. Entry and exit actions, history and parallel regions are further design choices, not a checklist every state machine needs. The separate state-machine article is a better starting point for that branch of the design.


Optional: design questions to explore

The architecture leaves room for several choices. These are worth working through for your own game; each changes the contract between input, gameplay and feedback.

Charge feedback

A bar fills while a button is held. Who owns its progress? If the duration is purely recognition, the input layer may expose progress; if charge depends on stamina or weapon rules, gameplay may own the timer. Decide which information the visual needs and which component can supply it without duplicating the rules.

Haptics

The controller rumbles when you fire, take damage or hit a wall. These are game events, not all direct consequences of player input. What routes feedback to the right player’s device: gameplay, the input layer, or a dedicated feedback service? Pick a boundary that keeps device handling out of each individual effect.

Continuous values

A mouse delta, stick position or gyroscope reading carries a value, not just a discrete request. Should gameplay subscribe to changes, poll a current value, or use both? Decide how the action handles simultaneous sources and updates at different rates. Unity’s action types are a useful comparison.

Input buffering

Pressing attack 100ms before a recovery animation ends might still count. Who owns that allowance: the action, a buffer between input and gameplay, or the character controller? The choice determines who knows whether a request remains valid, when it expires, and how forgiving the game feels.

Consumption and priority

A pause menu and the world both listen for Cancel. When the player presses Escape, which should respond? Define whether the event can be consumed and how priority is chosen. Subscription order alone is a fragile way to encode that policy, and the answer needs to agree with context selection.

Recording and replay

For a replay, ghost run or bug reproduction, what must the recording preserve? Raw hardware events can include the binding configuration used to interpret them. Action records can include timestamps, phases and values. Choose a recording point based on whether you need to reproduce recognition, gameplay requests, or both. Neither approach alone guarantees deterministic gameplay replay; simulation state and timing also matter.

Leave a Reply

Your email address will not be published. Required fields are marked *