📝 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
TransforminUpdate - 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
Transformmovement 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. InUpdateuseTime.deltaTime; inFixedUpdatethe step is constant (andTime.deltaTimeconveniently 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.
| Call | Returns | Fires |
|---|---|---|
Input.GetKey(KeyCode.W) | bool | Every frame the key is held |
Input.GetKeyDown(KeyCode.Space) | bool | The single frame the key is pressed |
Input.GetKeyUp(KeyCode.Space) | bool | The single frame the key is released |
Input.GetAxis("Horizontal") | float −1..1 | Smoothed A/D & arrow input |
Input.GetAxisRaw("Horizontal") | float −1, 0, or 1 | Instant, 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.
(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.WorldvsSpace.Self:Translatedefaults toSpace.Self(relative to the object's own rotation). For a top-down mover that shouldn't drift when the object turns, passSpace.Worldso "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.
(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.
| Approach | Good for | Runs in |
|---|---|---|
Move the Transform | UI, simple movers, objects without physics, precise scripted motion | Update |
Move a Rigidbody (velocity / MovePosition) | Characters that must collide, get pushed, or interact with physics | FixedUpdate |
💡 Preview: Lesson 2.2 coversRigidbodymovement and collisions properly. For now, know the rule: if it needs physics, don't move its Transform — move its Rigidbody inFixedUpdate. 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:
- Create a Plane (GameObject → 3D Object → Plane) and a Cube named
Playersitting on it. - Write
PlayerMover.cswith a[SerializeField] private float speed, read"Horizontal"and"Vertical", and move on the X/Z plane inUpdate. - Normalize the direction so diagonal movement isn't faster.
- Multiply by
Time.deltaTimeand tunespeedin the Inspector until it feels right. - 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
TransforminUpdate, usingVector3positions and directions. - Multiply every time-based value by
Time.deltaTimeso movement is per second, not per frame — frame-rate independent. - Legacy input:
Input.GetAxis/GetAxisRawfor −1..1 axes,GetKey/GetKeyDownfor 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;
PlayerInputcallsOn<Action>methods. - Move the Transform for non-physics objects; move a
RigidbodyinFixedUpdatewhen physics matters (Lesson 2.2).
📚 Additional Resources
- Scripting Reference — Input.GetAxis
- Scripting Reference — Transform.Translate
- Manual — Input System package
- Scripting Reference — Vector3.Normalize
🚀 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.