Chapter 8

Input System

Set up Unity's New Input System to handle keyboard, touch, and swipe controls for your infinite runner.

Old Input Manager vs New Input System

Unity has two input systems. The old Input Manager is simpler but limited. The New Input System (a separate package) is more powerful and is the recommended approach for modern Unity projects.

FeatureOld Input ManagerNew Input System
Multi-device supportManual, painfulBuilt-in, automatic
Rebinding controlsYou build it yourselfBuilt-in UI support
Touch/gamepadSeparate code pathsUnified API
Action MapsNot availableOrganized by context
Setup complexityVery simpleMore initial setup

For our infinite runner, the New Input System is perfect because we need to support both keyboard (desktop) and touch/swipe (mobile) with the same code. Let us set it up.

Installing the New Input System

  1. Open Window > Package Manager.
  2. In the top-left dropdown, select "Unity Registry" (to see official packages).
  3. Search for "Input System".
  4. Click the Input System package, then click Install.
  5. Unity will prompt you: "This package requires a restart to enable the new backend." Click Yes.
  6. Unity will restart. After restarting, the New Input System is active.
Active Input Handling Setting

After installing, go to Edit > Project Settings > Player and scroll to Active Input Handling. Set it to "Both" (this lets you use both old and new input during development). For the final build, you can switch to "Input System Package (New)" only.

Creating an Input Actions Asset

The Input Actions asset is a file that defines what actions the player can perform and what buttons/keys trigger them. Think of it as a configuration file for all your controls.

  1. In the Project window, create a folder: Assets/Input/
  2. Right-click inside and select Create > Input Actions.
  3. Name it PlayerInputActions.
  4. Double-click it to open the Input Actions Editor.

Setting Up Action Maps

An Action Map is a group of related actions. We need one for gameplay:

  1. In the Input Actions Editor, click the + next to "Action Maps" and name it Gameplay.
  2. Now we will add three actions inside this map: Move, Jump, and Slide.

Defining the Move Action

  1. Click the + next to "Actions" to add an action. Name it Move.
  2. Set Action Type to Value.
  3. Set Control Type to Vector2.
  4. Click the + next to the Move action to add a binding. Choose Add Up/Down/Left/Right Composite.
  5. Name the composite WASD.
  6. Set the bindings:
    • Up: W key
    • Down: S key
    • Left: A key
    • Right: D key
  7. Add another composite called Arrows with Arrow keys.
Why Vector2 for a 3-Lane Game?

Even though our runner only moves left and right (and technically only switches lanes), using Vector2 gives us flexibility. The X component tells us left (-1) or right (+1). We ignore Y for lane switching but could use it for jump/slide on gamepads later. Our InputReader script will convert this into simple lane-change commands.

Defining the Jump Action

  1. Add a new action called Jump.
  2. Set Action Type to Button.
  3. Add a binding: Space key.
  4. Add another binding: W key (alternative).
  5. Add another binding: Up Arrow key.

Defining the Slide Action

  1. Add a new action called Slide.
  2. Set Action Type to Button.
  3. Add a binding: S key.
  4. Add another binding: Down Arrow key.
  5. Add another binding: Left Ctrl key (alternative).

Generate the C# Class

  1. In the Inspector for the PlayerInputActions asset, check "Generate C# Class".
  2. Set the file path to Assets/Scripts/Input/PlayerInputActions.cs.
  3. Set the class name to PlayerInputActions.
  4. Click Apply. Unity generates a C# class that wraps all your actions.
Why Generate a C# Class?

The generated class gives you strongly-typed access to all your actions. Instead of looking up actions by string name (error-prone), you get properties like actions.Gameplay.Jump that are checked at compile time. If you rename an action and forget to update your code, the compiler catches it immediately.

Creating the InputReader Script

We do not want our PlayerController to deal with raw input details. Instead, we create an InputReader — a clean wrapper that translates raw input into game-meaningful actions. This follows the same principle from Chapter 7: keep systems decoupled.

Our InputReader will be a ScriptableObject so that any script can reference the same input data without needing a scene reference.

InputReader.csC#
using System;
using UnityEngine;
using UnityEngine.InputSystem;

namespace InfiniteRunner.Input
{
    /// <summary>
    /// Reads player input and exposes it as C# events.
    /// This is a ScriptableObject so any system can reference it
    /// without a scene dependency.
    ///
    /// Create via: Right-click > Infinite Runner > Input > Input Reader
    /// </summary>
    [CreateAssetMenu(
        fileName = "InputReader",
        menuName = "Infinite Runner/Input/Input Reader",
        order = 0)]
    public class InputReader : ScriptableObject,
        PlayerInputActions.IGameplayActions
    {
        // ─── Events ─────────────────────────────────────────────
        // Other scripts subscribe to these to react to input.
        // The PlayerController will listen for these.

        /// <summary>Fired when the player presses left or right.
        /// Parameter: -1 for left, +1 for right.</summary>
        public event Action<int> OnMoveInput;

        /// <summary>Fired when the player presses the jump button.</summary>
        public event Action OnJumpInput;

        /// <summary>Fired when the player presses the slide button.</summary>
        public event Action OnSlideInput;

        // ─── Internal State ─────────────────────────────────────
        private PlayerInputActions _inputActions;

        // ─── Lifecycle ──────────────────────────────────────────

        private void OnEnable()
        {
            // Create the generated input actions class
            if (_inputActions == null)
            {
                _inputActions = new PlayerInputActions();

                // Tell the input system: "Send Gameplay callbacks to ME"
                _inputActions.Gameplay.SetCallbacks(this);
            }

            EnableGameplayInput();
        }

        private void OnDisable()
        {
            DisableAllInput();
        }

        // ─── Public Methods ─────────────────────────────────────

        /// <summary>
        /// Enable the Gameplay action map (normal play).
        /// </summary>
        public void EnableGameplayInput()
        {
            _inputActions.Gameplay.Enable();
        }

        /// <summary>
        /// Disable all input (during menus, cutscenes, etc.).
        /// </summary>
        public void DisableAllInput()
        {
            _inputActions.Gameplay.Disable();
        }

        // ─── IGameplayActions Implementation ────────────────────
        // These methods are called automatically by the Input System
        // when the corresponding action is performed.

        /// <summary>
        /// Called by the Input System when the Move action fires.
        /// We extract the X direction and convert to a lane change command.
        /// </summary>
        public void OnMove(InputAction.CallbackContext context)
        {
            // We only care about the "performed" phase (key pressed)
            // not "started" or "canceled"
            if (context.phase == InputActionPhase.Performed)
            {
                // Read the Vector2 value from the input
                Vector2 moveInput = context.ReadValue<Vector2>();

                // Convert to a lane change direction:
                //  -1 = move left one lane
                //   0 = no horizontal input (ignore)
                //  +1 = move right one lane
                int direction = 0;
                if (moveInput.x < -0.5f) direction = -1;
                else if (moveInput.x > 0.5f) direction = 1;

                // Only fire the event if there is actual horizontal input
                if (direction != 0)
                {
                    OnMoveInput?.Invoke(direction);
                }
            }
        }

        /// <summary>
        /// Called by the Input System when the Jump action fires.
        /// </summary>
        public void OnJump(InputAction.CallbackContext context)
        {
            // Only on the initial press, not on hold or release
            if (context.phase == InputActionPhase.Performed)
            {
                OnJumpInput?.Invoke();
            }
        }

        /// <summary>
        /// Called by the Input System when the Slide action fires.
        /// </summary>
        public void OnSlide(InputAction.CallbackContext context)
        {
            if (context.phase == InputActionPhase.Performed)
            {
                OnSlideInput?.Invoke();
            }
        }
    }
}

Save this at Assets/Scripts/Input/InputReader.cs.

Key Concepts Explained

  • PlayerInputActions.IGameplayActions — This interface was auto-generated when we created the Input Actions asset. By implementing it, our InputReader receives callbacks for all Gameplay actions automatically.
  • InputAction.CallbackContext — Contains information about the input event: what phase it is in (Started, Performed, Canceled) and the value.
  • context.phase == InputActionPhase.Performed — We only react when the button is actually pressed. Without this check, the method fires three times: on press, on hold, and on release.
  • context.ReadValue<Vector2>() — Reads the input value. For our WASD/Arrow composite, this returns a Vector2 where X is the horizontal direction.

Touch and Swipe Detection for Mobile

On mobile devices, players use swipes instead of keys. We need to detect swipe direction and translate it into the same events our keyboard input produces. We will create a separate component for this so the InputReader stays clean.

SwipeDetector.csC#
using UnityEngine;
using UnityEngine.InputSystem;

namespace InfiniteRunner.Input
{
    /// <summary>
    /// Detects swipe gestures on touchscreens and translates them
    /// into InputReader events. Attach to a GameObject in the scene.
    ///
    /// How swipe detection works:
    /// 1. When a finger touches the screen, we record the start position.
    /// 2. When the finger lifts off, we record the end position.
    /// 3. We calculate the direction and distance of the swipe.
    /// 4. If the distance exceeds a threshold, we fire the appropriate event.
    /// </summary>
    public class SwipeDetector : MonoBehaviour
    {
        [Header("References")]
        [SerializeField] private InputReader inputReader;

        [Header("Swipe Settings")]
        [Tooltip("Minimum swipe distance in pixels to register as a swipe.")]
        [SerializeField] private float minSwipeDistance = 50f;

        [Tooltip("Maximum time in seconds for a swipe to be valid.")]
        [SerializeField] private float maxSwipeTime = 0.5f;

        // Internal tracking
        private Vector2 _touchStartPosition;
        private float _touchStartTime;
        private bool _isSwiping;

        // Reference to the touchscreen (New Input System)
        private Touchscreen _touchscreen;

        private void Awake()
        {
            _touchscreen = Touchscreen.current;
        }

        private void Update()
        {
            // Only run on devices with a touchscreen
            if (_touchscreen == null)
            {
                // Try to find it again (in case it was connected)
                _touchscreen = Touchscreen.current;
                if (_touchscreen == null) return;
            }

            DetectSwipe();
        }

        private void DetectSwipe()
        {
            var primaryTouch = _touchscreen.primaryTouch;

            // Phase 1: Finger just touched the screen
            if (primaryTouch.press.wasPressedThisFrame)
            {
                _touchStartPosition = primaryTouch.position.ReadValue();
                _touchStartTime = Time.unscaledTime;
                _isSwiping = true;
            }

            // Phase 2: Finger lifted off the screen
            if (primaryTouch.press.wasReleasedThisFrame && _isSwiping)
            {
                _isSwiping = false;

                Vector2 touchEndPosition = primaryTouch.position.ReadValue();
                float swipeDuration = Time.unscaledTime - _touchStartTime;

                // Check if the swipe was fast enough
                if (swipeDuration > maxSwipeTime) return;

                // Calculate the swipe vector
                Vector2 swipeDelta = touchEndPosition - _touchStartPosition;
                float swipeDistance = swipeDelta.magnitude;

                // Check if the swipe was long enough
                if (swipeDistance < minSwipeDistance) return;

                // Determine the swipe direction
                ProcessSwipe(swipeDelta);
            }
        }

        /// <summary>
        /// Determines swipe direction and fires the appropriate event.
        /// </summary>
        private void ProcessSwipe(Vector2 swipeDelta)
        {
            // Normalize to get pure direction
            swipeDelta.Normalize();

            // Is the swipe more horizontal or more vertical?
            if (Mathf.Abs(swipeDelta.x) > Mathf.Abs(swipeDelta.y))
            {
                // Horizontal swipe
                if (swipeDelta.x > 0)
                {
                    // Swipe RIGHT — move to right lane
                    Debug.Log("[SwipeDetector] Swipe Right");
                    // We fire the same events that keyboard input fires.
                    // The InputReader exposes events, but for swipes we need
                    // to go through a different path. See note below.
                    SimulateMoveInput(1);
                }
                else
                {
                    // Swipe LEFT — move to left lane
                    Debug.Log("[SwipeDetector] Swipe Left");
                    SimulateMoveInput(-1);
                }
            }
            else
            {
                // Vertical swipe
                if (swipeDelta.y > 0)
                {
                    // Swipe UP — jump
                    Debug.Log("[SwipeDetector] Swipe Up (Jump)");
                    SimulateJumpInput();
                }
                else
                {
                    // Swipe DOWN — slide
                    Debug.Log("[SwipeDetector] Swipe Down (Slide)");
                    SimulateSlideInput();
                }
            }
        }

        // ─── Simulate Input Events ─────────────────────────────
        // These methods invoke the same events on the InputReader
        // that keyboard input would. This keeps the PlayerController
        // completely unaware of input source.

        private void SimulateMoveInput(int direction)
        {
            // We need the InputReader to fire its OnMoveInput event.
            // Since OnMoveInput is an event (can only be invoked by owner),
            // we add a public method to InputReader for this purpose.
            inputReader.RaiseMoveInput(direction);
        }

        private void SimulateJumpInput()
        {
            inputReader.RaiseJumpInput();
        }

        private void SimulateSlideInput()
        {
            inputReader.RaiseSlideInput();
        }
    }
}

We need to add the public raise methods to InputReader so the SwipeDetector can fire events through it. Add these methods to the InputReader class:

InputReader.cs (add these methods)C#
// Add these inside the InputReader class, after the IGameplayActions methods:

// ─── Public Raise Methods (for SwipeDetector) ──────────────
// These allow external sources (like SwipeDetector) to fire input events.

/// <summary>Manually raise a move input event.</summary>
public void RaiseMoveInput(int direction)
{
    OnMoveInput?.Invoke(direction);
}

/// <summary>Manually raise a jump input event.</summary>
public void RaiseJumpInput()
{
    OnJumpInput?.Invoke();
}

/// <summary>Manually raise a slide input event.</summary>
public void RaiseSlideInput()
{
    OnSlideInput?.Invoke();
}
The Swipe Algorithm Visualized

Imagine the screen as a grid. When the finger touches down, we mark that point. When it lifts off, we draw an arrow from start to end. If the arrow is long enough (passes the threshold), we look at its angle. Mostly horizontal and pointing right? Swipe right. Mostly vertical and pointing up? Jump. The Mathf.Abs(x) > Mathf.Abs(y) check determines whether the swipe is more horizontal or vertical.

Testing Your Input

Before connecting input to the player, let us verify it works with a simple test script.

InputTester.csC#
using UnityEngine;

namespace InfiniteRunner.Input
{
    /// <summary>
    /// Temporary script to test input events.
    /// Attach to any GameObject and assign the InputReader asset.
    /// Delete this before shipping!
    /// </summary>
    public class InputTester : MonoBehaviour
    {
        [SerializeField] private InputReader inputReader;

        private void OnEnable()
        {
            inputReader.OnMoveInput += HandleMove;
            inputReader.OnJumpInput += HandleJump;
            inputReader.OnSlideInput += HandleSlide;
        }

        private void OnDisable()
        {
            inputReader.OnMoveInput -= HandleMove;
            inputReader.OnJumpInput -= HandleJump;
            inputReader.OnSlideInput -= HandleSlide;
        }

        private void HandleMove(int direction)
        {
            string dir = direction < 0 ? "LEFT" : "RIGHT";
            Debug.Log($"<color=green>[Input]</color> Move {dir}");
        }

        private void HandleJump()
        {
            Debug.Log("<color=yellow>[Input]</color> JUMP!");
        }

        private void HandleSlide()
        {
            Debug.Log("<color=cyan>[Input]</color> SLIDE!");
        }
    }
}
  1. In the Project window, right-click in Assets/ScriptableObjects/ and select Create > Infinite Runner > Input > Input Reader. Name it InputReader.
  2. Create an empty GameObject named [InputTester].
  3. Attach the InputTester script.
  4. Drag the InputReader ScriptableObject asset into the "Input Reader" field.
  5. Press Play.
  6. Press A/D or Left/Right arrows — you should see "Move LEFT" or "Move RIGHT" in the Console.
  7. Press Space — you should see "JUMP!" in the Console.
  8. Press S or Down Arrow — you should see "SLIDE!" in the Console.
Testing Touch on Desktop

You can test touch input without a phone. In the Unity Editor, go to Window > Analysis > Input Debugger. This shows all connected input devices. You can also use Unity's Device Simulator (Window > General > Device Simulator) to simulate a mobile screen and touch gestures.

Input Architecture Overview

Input FlowArchitecture
  PHYSICAL INPUT              INPUT READER              GAME SYSTEMS
  (hardware)              (ScriptableObject)           (subscribers)

  +----------------+      +------------------+      +------------------+
  | Keyboard       |      |                  |      | PlayerController |
  | A/D, Space, S  | -->  |   InputReader    | -->  | "switch lane,    |
  +----------------+      |                  |      |  jump, slide"    |
                           |  OnMoveInput     |      +------------------+
  +----------------+      |  OnJumpInput     |
  | Touchscreen    |      |  OnSlideInput    |      +------------------+
  | Swipe gestures | -->  |                  | -->  | UI System        |
  +----------------+      +------------------+      | "navigate menus" |
       |                         ^                   +------------------+
       v                         |
  +----------------+             |
  | SwipeDetector  | ────────────+
  | (MonoBehaviour)|  calls RaiseMoveInput(), etc.
  +----------------+

  Key: The PlayerController never knows if input came from
  a keyboard or a touchscreen. It just reacts to events.

Project Files After This Chapter

Project StructureFolder
Assets/
  Input/
    PlayerInputActions.inputactions   <-- The Input Actions asset
  Scripts/
    Input/
      PlayerInputActions.cs           <-- Auto-generated C# wrapper
      InputReader.cs                  <-- Our ScriptableObject input wrapper
      SwipeDetector.cs                <-- Touch/swipe handler for mobile
      InputTester.cs                  <-- Temporary test script
  ScriptableObjects/
    InputReader.asset                 <-- The InputReader SO instance

What We Built

  • Installed Unity's New Input System package.
  • Created an Input Actions asset with Move, Jump, and Slide actions.
  • Set up keyboard bindings (WASD, Arrows, Space, Ctrl).
  • Built an InputReader ScriptableObject that translates raw input into clean C# events.
  • Created a SwipeDetector for mobile touch/swipe input that feeds into the same event system.
  • Verified everything with a test script and Debug.Log output.

In the next chapter, we will build the Player Controller that subscribes to these input events and makes the character run, switch lanes, jump, and slide.

Save Your Progress

Input is wired up. Before moving on, save this working state.

git status
git add .
git commit -m "Add input system with keyboard and swipe support"

Before staging, try running git diff to see exactly what lines you changed since your last commit. This is incredibly useful for reviewing your own work before committing.