Skip to main content

📝 Lesson 2.1: Input and Movement

Time to make something move. This lesson connects two ideas from Module 1 — the Update loop and the Transform — with the player's keyboard. You'll learn frame-rate-independent movement with Time.deltaTime, then read input two ways: the quick legacy Input Manager and the modern Input System.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Move a GameObject by writing to its Transform in Update
  • Explain why every movement is multiplied by Time.deltaTime
  • Read directional input with the legacy Input.GetAxis / Input.GetKey
  • Build a movement vector and normalize it so diagonals aren't faster
  • Read the same input with the modern Input System (PlayerInput + actions)
  • Choose between Transform movement and (a preview of) physics movement

Estimated Time: 60 minutes

Project: A top-down PlayerMover that walks a cube around the plane with WASD / arrow keys, smoothly and at a constant speed.

In This Lesson

Moving a Transform

Every GameObject has a Transform (Lesson 1.1) holding its position, rotation, and scale. To move an object, you change that position over time — inside Update, which runs every frame (Lesson 1.2).

Positions and directions in Unity are Vector3 values (x, y, z). There are two common ways to move:

// 1) Translate: move relative to current position by a delta.
transform.Translate(0f, 0f, 1f);        // slide 1 unit along local Z (forward)

// 2) Assign position directly: set an absolute world position.
transform.position = new Vector3(0f, 0f, 5f);

📖 Definition

Vector3: a struct of three floats (x, y, z) used for positions, directions, and offsets. Unity gives you handy constants: Vector3.forward = (0,0,1), Vector3.up = (0,1,0), Vector3.right = (1,0,0), and Vector3.zero.

For this lesson we'll build a top-down mover on the X/Z plane (Y is up), so left/right is X and forward/back is Z. But first, the single most important rule of movement.

Why Time.deltaTime?

Update runs once per frame, but frames don't come at a fixed rate — a fast PC might render 200 fps, a slow one 40 fps. If you move a fixed amount per frame, your object moves faster on faster hardware. That's a bug.

void Update()
{
    transform.Translate(0f, 0f, 0.1f);   // ❌ 0.1 units PER FRAME — speed depends on fps
}

The fix: multiply by Time.deltaTime, the number of seconds since the last frame. Now your value means "units per second," identical on every machine.

public float speed = 5f;   // units per SECOND

void Update()
{
    transform.Translate(0f, 0f, speed * Time.deltaTime);   // ✅ frame-rate independent
}

💡 The mental model

At 50 fps, Time.deltaTime ≈ 0.02, so each frame moves 5 × 0.02 = 0.1 units — and 50 frames later you've moved 5 units, i.e. one second's worth. At 200 fps each step is smaller, but there are more of them, so the total per second is the same. Any value that changes over time — movement, rotation, timers — gets multiplied by deltaTime.

⚠️ Important: This is the counterpart to Lesson 1.2's physics rule. In Update use Time.deltaTime; in FixedUpdate the step is constant (and Time.deltaTime conveniently returns that fixed step there too).

Reading Input (Legacy Manager)

Unity has two input systems. The older Input Manager (the static Input class) needs no setup and is perfect for learning the concepts. We'll start here, then show the modern system.

CallReturnsFires
Input.GetKey(KeyCode.W)boolEvery frame the key is held
Input.GetKeyDown(KeyCode.Space)boolThe single frame the key is pressed
Input.GetKeyUp(KeyCode.Space)boolThe single frame the key is released
Input.GetAxis("Horizontal")float −1..1Smoothed A/D & arrow input
Input.GetAxisRaw("Horizontal")float −1, 0, or 1Instant, unsmoothed axis

The two default axes — "Horizontal" (A/D + Left/Right) and "Vertical" (W/S + Up/Down) — return −1 to 1, which is exactly what movement wants:

public class PlayerMover : MonoBehaviour
{
    public float speed = 5f;

    void Update()
    {
        float x = Input.GetAxis("Horizontal");   // A/D or Left/Right → -1..1
        float z = Input.GetAxis("Vertical");     // W/S or Up/Down    → -1..1

        Vector3 move = new Vector3(x, 0f, z);    // top-down: Y stays 0
        transform.Translate(move * speed * Time.deltaTime, Space.World);
    }
}

⚠️ GetKeyDown belongs in Update, not FixedUpdate

As noted in Lesson 1.2, "was it pressed this frame?" checks (GetKeyDown/GetKeyUp) must be read in Update. FixedUpdate can skip or double frames, so it may miss the one frame the press happened.

Building a Movement Vector

The code above has a subtle classic bug: press up and right together and the vector is (1, 0, 1), whose length is about 1.41 — so diagonal movement is ~41% faster than straight. The fix is to normalize the vector (scale it to length 1) before applying speed.

graph LR A["Raw input
(1, 0, 1) length ≈ 1.41"] -->|"Normalize"| B["(0.71, 0, 0.71)
length = 1"] B -->|"× speed × deltaTime"| C["Correct, even
movement step"] style A fill:#fdecea,stroke:#c0392b,stroke-width:2px style B fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style C fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
public class PlayerMover : MonoBehaviour
{
    [SerializeField] private float speed = 5f;   // Inspector-tunable (Lesson 1.3)

    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float z = Input.GetAxisRaw("Vertical");

        Vector3 direction = new Vector3(x, 0f, z);

        // Only normalize when there's actual input (avoids dividing a zero vector).
        if (direction.sqrMagnitude > 1f)
            direction.Normalize();

        transform.Translate(direction * speed * Time.deltaTime, Space.World);
    }
}

✅ Why sqrMagnitude > 1f?

A single-axis press gives length 1 (fine as-is); only diagonals exceed 1 and need scaling down. Comparing sqrMagnitude (the squared length) avoids a costly square root, and skipping normalize on the zero vector prevents a divide-by-zero producing NaN. Using GetAxisRaw here gives crisp, immediate response ideal for this kind of movement.

💡 Space.World vs Space.Self: Translate defaults to Space.Self (relative to the object's own rotation). For a top-down mover that shouldn't drift when the object turns, pass Space.World so "right" always means world-right.

The Modern Input System

The legacy manager is simple but limited: rebinding keys, supporting gamepads, and handling multiple players are all awkward. Unity's newer Input System package solves this by separating abstract actions ("Move", "Jump") from the physical controls bound to them — so the same code works for keyboard, gamepad, or touch.

📖 Definition

An Input Action is a named intent (like "Move") defined in an Input Actions asset. You bind it to controls (WASD, a stick, arrows) in the editor; your script reacts to the action, never to a specific key. This is the same "program against an abstraction, not a concrete detail" principle you met with interfaces in the intro course.

The most beginner-friendly path is the PlayerInput component. Add it to your object, assign an Input Actions asset with a "Move" action (type Value, control type Vector2), set its Behavior to Invoke Unity Events or Send Messages, and Unity calls a matching method on your script:

using UnityEngine;
using UnityEngine.InputSystem;   // the new Input System namespace

public class PlayerMoverNew : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    private Vector2 moveInput;    // stored input, applied every frame

    // Called by the PlayerInput component for the "Move" action (Send Messages mode).
    // The method name is On + the action name.
    public void OnMove(InputValue value)
    {
        moveInput = value.Get<Vector2>();   // e.g. (1, 0) for right, (0, 1) for up
    }

    void Update()
    {
        // Map the 2D input onto the X/Z ground plane.
        Vector3 direction = new Vector3(moveInput.x, 0f, moveInput.y);
        transform.Translate(direction * speed * Time.deltaTime, Space.World);
    }
}

Notice the shape is the same as before — read input, build a direction, move with deltaTime — but now the reading is event-driven and device-agnostic. The Input System already returns a clamped 2D vector, so a diagonal is length 1 without manual normalizing.

graph LR K["Keyboard WASD"] --> ACT["'Move' Action
(Vector2)"] G["Gamepad stick"] --> ACT T["Touch / on-screen"] --> ACT ACT --> S["OnMove(InputValue)
your script"] style ACT fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style S fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px

💡 Which should you use?

For learning and quick prototypes, the legacy Input class is fine and everywhere in tutorials. For anything you'll ship — rebindable controls, gamepad support, multiple players — prefer the Input System. Many projects (including this one's open scene) already have the package installed. The concepts you learned first (read → vector → move × deltaTime) carry over unchanged.

Transform vs Physics Movement

Everything above moves the object by writing its Transform directly. That's simple and precise — but it teleports the object, ignoring physics. If your object has a Rigidbody and should collide with walls or be pushed, moving the Transform can shove it straight through colliders.

ApproachGood forRuns in
Move the TransformUI, simple movers, objects without physics, precise scripted motionUpdate
Move a Rigidbody (velocity / MovePosition)Characters that must collide, get pushed, or interact with physicsFixedUpdate
💡 Preview: Lesson 2.2 covers Rigidbody movement and collisions properly. For now, know the rule: if it needs physics, don't move its Transform — move its Rigidbody in FixedUpdate. Our top-down cube has no Rigidbody, so Transform movement is exactly right.

⚠️ Watch Out

Mixing the two — a Rigidbody object whose Transform you also set every frame — causes jitter and tunneling as the two systems fight. Pick one movement method per object.

Exercise & Quiz

🏋️ Exercise: Top-Down Player Mover

Objective: Drive a cube around a plane with the keyboard, at a constant speed in every direction.

Instructions:

  1. Create a Plane (GameObject → 3D Object → Plane) and a Cube named Player sitting on it.
  2. Write PlayerMover.cs with a [SerializeField] private float speed, read "Horizontal" and "Vertical", and move on the X/Z plane in Update.
  3. Normalize the direction so diagonal movement isn't faster.
  4. Multiply by Time.deltaTime and tune speed in the Inspector until it feels right.
  5. Bonus: add a "run" modifier — hold Left Shift (Input.GetKey(KeyCode.LeftShift)) to multiply speed by 2.

Starter Code:

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    [SerializeField] private float speed = 5f;

    void Update()
    {
        // TODO: read Horizontal & Vertical, build a direction on X/Z,
        // normalize it, then Translate by speed * Time.deltaTime in world space.
    }
}
💡 Hint

Build new Vector3(x, 0f, z). Guard the normalize with if (direction.sqrMagnitude > 1f) direction.Normalize();. For the bonus, compute the effective speed before moving: float s = Input.GetKey(KeyCode.LeftShift) ? speed * 2f : speed;.

✅ Solution
using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    [SerializeField] private float runMultiplier = 2f;

    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float z = Input.GetAxisRaw("Vertical");

        Vector3 direction = new Vector3(x, 0f, z);
        if (direction.sqrMagnitude > 1f)
            direction.Normalize();

        float currentSpeed = Input.GetKey(KeyCode.LeftShift)
            ? speed * runMultiplier
            : speed;

        transform.Translate(direction * currentSpeed * Time.deltaTime, Space.World);
    }
}

Every changing quantity — the move step and the run boost — flows through Time.deltaTime, so behavior is identical at any frame rate. Normalizing keeps diagonals honest.

🎯 Quick Quiz

Question 1: Why do you multiply movement by Time.deltaTime?

Question 2: Pressing "up" and "right" together makes the player move faster than pressing one alone. What fixes it?

Question 3: What is the main advantage of the modern Input System over reading KeyCodes directly?

Summary

🎉 Key Takeaways

  • Move an object by changing its Transform in Update, using Vector3 positions and directions.
  • Multiply every time-based value by Time.deltaTime so movement is per second, not per frame — frame-rate independent.
  • Legacy input: Input.GetAxis/GetAxisRaw for −1..1 axes, GetKey/GetKeyDown for buttons.
  • Normalize a multi-axis direction vector so diagonal movement isn't faster than straight.
  • The modern Input System maps physical controls to abstract actions (e.g. "Move"), so one script serves many devices; PlayerInput calls On<Action> methods.
  • Move the Transform for non-physics objects; move a Rigidbody in FixedUpdate when physics matters (Lesson 2.2).

📚 Additional Resources

🚀 What's Next?

Our cube slides around, but it walks through walls. In Lesson 2.2, Physics and Collisions, we add a Rigidbody and colliders so objects bump, stack, and trigger events — and you'll see exactly why physics movement lives in FixedUpdate.

🎉 It moves!

You've turned key presses into motion — the first truly interactive thing you've built. From here, gameplay only gets richer.