Up to now you have learned C, C++, C#, and data structures mostly on their own, disconnected from any particular engine. Starting here you open Unity and build an actual, running game with it. Almost everything Unity does — moving a character, playing an animation, running physics, reading a controller, drawing the screen — happens inside one repeating idea: the game loop. Understand the loop and the rest of the engine stops feeling like magic.
Every section below follows the same shape: a small piece of runnable C# code (or a worked trace when there is no console to print to), what it actually does, then a plain explanation of why. Some of these scripts assume a Unity GameObject with the script attached, the way you already know from earlier chapters.
A game is not a program that runs once and stops. It is a program that runs a small chunk of work, shows you the result, and immediately does it again — usually 30 to 240+ times every second. Each pass through that chunk of work is called a frame. The chunk of work itself is the game loop, and its three basic jobs, in order, are:
Read input, update the game world based on it, draw the new world, repeat. That is the entire idea. Every game you have ever played, from a text adventure to a racing game, is built on some version of this loop. Here is the shape of it as plain C#, outside of Unity, just so you can see it with your own eyes before Unity hides it from you:
using System;
class ToyLoop
{
static void Main()
{
bool isRunning = true;
int frame = 0;
while (isRunning)
{
frame++;
Console.WriteLine("frame " + frame + ": input -> update -> render");
if (frame >= 5) isRunning = false; // stop after 5, just for this demo
}
}
}
Output:
frame 1: input -> update -> render
frame 2: input -> update -> render
frame 3: input -> update -> render
frame 4: input -> update -> render
frame 5: input -> update -> render
In a real game this loop never stops on its own — it keeps going as long as the game is running, which is why it needs to be fast: every single frame it has to read input, move everything, and draw the whole screen, and it has to do all of that in a tiny fraction of a second or the game feels slow and jerky.
You never write while (isRunning) in Unity. Unity already contains that loop — internally it is called the player loop — and it is running before your code exists and after your game closes. What you write instead is small pieces of behavior, and Unity's loop calls them for you at the right moment, every frame, automatically. The most common piece is a method named Update inside a MonoBehaviour (the base class every Unity script you attach to a GameObject inherits from):
using UnityEngine;
public class LoopDemo : MonoBehaviour
{
void Update()
{
// Unity calls this once per frame, automatically.
Debug.Log("Update ran at time " + Time.time);
}
}
You did not write a loop anywhere in that script, yet Update runs once every frame for as long as the GameObject is active and enabled. That is the whole trick of an engine: it owns the loop, and it invites your code in at fixed points inside it. The rest of this chapter is about those points — what runs when, what a "frame" costs in real time, and how to read input correctly inside that loop.
MonoBehaviour has more than one loop-callback method (Update, FixedUpdate, LateUpdate, and a few others). Each one is called by Unity's player loop at a different, specific point in the frame. Sections 3 and 4 explain exactly when and why.Not every frame takes the same amount of real time. A frame with three enemies on screen might take 8 milliseconds; a frame where an explosion spawns a thousand particles might take 30 milliseconds. Unity measures how long the previous frame took and hands you that number as Time.deltaTime — a float, in seconds, that changes every frame. "Delta" just means "the change in" — deltaTime is "the change in time" since the last frame.
Why does this matter? Because if you move something by a fixed amount every frame instead of every second, its real-world speed depends on how fast the player's machine happens to run:
using UnityEngine;
public class MoverBad : MonoBehaviour
{
public float speed = 5f;
void Update()
{
// BAD: moves 5 units every FRAME, not every SECOND.
transform.position += Vector3.right * speed;
}
}
On a machine running at 30 frames per second, this object crosses 150 units every second. On a machine running at 60 frames per second, the exact same code crosses 300 units every second — twice as fast, with no change to speed. That is obviously broken: a faster computer should not turn your character into a different, faster character. The fix is to scale movement by Time.deltaTime, turning "units per frame" into "units per second":
using UnityEngine;
public class MoverGood : MonoBehaviour
{
public float speed = 5f; // units per SECOND
void Update()
{
// GOOD: scaled by deltaTime, so it always moves 5 units per second.
transform.position += Vector3.right * speed * Time.deltaTime;
}
}
Trace it by hand for one second at two different frame rates:
This property — the result staying the same no matter how the same total time got split into frames — is called frame-rate independence. Any time you move, rotate, scale, fade, or change a number over time inside Update, multiply the per-second rate by Time.deltaTime.
Time.deltaTime on a value that changes over time — health regeneration, a fade-out, a cooldown timer — is one of the most common beginner bugs in Unity. It usually "works" during testing (because your machine's frame rate stays fairly constant) and then breaks visibly on a slower or faster machine, or the moment the frame rate dips during a busy scene.Time.deltaTime, as you just saw, is different every frame — that is a variable timestep. It is perfectly fine for most gameplay code and for anything the player only sees, like movement or animation. But it causes real problems for physics, and to see why, it helps to recall the calculus chapter.
Remember: velocity is the rate of change of position, and acceleration is the rate of change of velocity — derivatives. A physics engine has to go the other way: given an acceleration (like gravity) and a starting velocity, it has to work out the new position over time — an integral. Since there is no neat formula for arbitrary game forces, the engine approximates the integral numerically, one small step at a time. The simplest version of this is Euler integration, the same idea from the calculus chapter's numerical methods: take the current rate, assume it stays constant for one small step of size dt, and add rate * dt to the total.
using UnityEngine;
public class SimpleFall : MonoBehaviour
{
public float gravity = -9.81f;
private float velocityY = 0f;
void FixedUpdate()
{
// Euler integration -- same idea as the calculus chapter:
// velocity changes by acceleration * dt, position changes by velocity * dt.
velocityY += gravity * Time.fixedDeltaTime;
transform.position += new Vector3(0f, velocityY * Time.fixedDeltaTime, 0f);
}
}
Trace five steps with the default fixed step, 0.02 seconds:
Now the key point: like any numerical approximation, Euler integration is only as accurate and as stable (meaning it does not blow up into nonsense numbers) as its step size lets it be. Small, consistent steps track the real physics closely. Large or irregular steps accumulate error fast, and physics is especially sensitive to this — a fast object can tunnel straight through a thin wall in one oversized step, or a stack of crates can suddenly explode apart because the error made two crates overlap and the engine pushed them apart too hard. Since Update's deltaTime can spike whenever the game hitches (loading a texture, a garbage collection pause, a busy frame), it is a bad clock for physics. Unity's answer is a second, separate loop callback with its own fixed, unchanging step: FixedUpdate, driven by Time.fixedDeltaTime (0.02 seconds, or 50 times a second, by default — you can change it in Project Settings, but it never varies frame to frame the way Update's deltaTime does).
So the rule is simple: anything that uses Unity's physics (Rigidbody, Rigidbody2D, or any force/velocity you set yourself to move a physics object) belongs in FixedUpdate, using Time.fixedDeltaTime. Everything else — reading input, moving a Transform directly, animation, UI — belongs in Update, using Time.deltaTime.
FixedUpdate try to "catch up" forever after a huge hitch — there is a setting called Maximum Allowed Timestep that caps it. Without that cap, a big enough hitch could make the game spend so long running catch-up physics steps that it causes the next frame to hitch too, forever falling further behind. That failure mode has a name: the spiral of death. The cap trades perfect physics accuracy for the game staying responsive, which is almost always the right trade.Now put sections 1 through 3 together. Inside one Unity frame, your code runs at several distinct points, always in the same order:
LateUpdate exists specifically for code that needs to react to something that already happened this frame in Update. The classic example is a camera that follows a player:
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public Transform target;
public Vector3 offset = new Vector3(0f, 5f, -10f);
void LateUpdate()
{
// runs AFTER every Update this frame, so target has already moved
transform.position = target.position + offset;
}
}
If this ran in Update instead, it might run before the player's own Update moves the player this frame (Unity does not guarantee which Update runs first among different scripts), so the camera would be tracking last frame's position — a one-frame lag that shows up as a subtle jitter, especially on fast camera movement. Putting camera-follow, and anything else that depends on "where did everything end up this frame", in LateUpdate removes that ordering problem entirely.
A state is a mode the whole game is in, where the game behaves in a completely different way depending on which mode is active. A main menu waits for a button press and does not run gameplay. Playing runs gameplay and does not show a menu. Paused freezes gameplay and shows a pause panel. Game over stops gameplay and shows a results screen. These four are the classic starting set for almost any game, and a state machine is just code whose whole job is tracking "which one of these is active right now" and switching between them cleanly.
The simplest possible state machine is an enum (a type that can only hold one of a fixed list of named values) plus a switch statement:
using UnityEngine;
public class GameManagerSimple : MonoBehaviour
{
public enum GameState { Menu, Playing, Paused, GameOver }
public GameState currentState = GameState.Menu;
void Update()
{
switch (currentState)
{
case GameState.Menu:
UpdateMenu();
break;
case GameState.Playing:
UpdatePlaying();
break;
case GameState.Paused:
UpdatePaused();
break;
case GameState.GameOver:
UpdateGameOver();
break;
}
}
void UpdateMenu()
{
if (Input.GetKeyDown(KeyCode.Return))
{
Debug.Log("Menu -> Playing");
currentState = GameState.Playing;
}
}
void UpdatePlaying()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
Debug.Log("Playing -> Paused");
currentState = GameState.Paused;
}
}
void UpdatePaused()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
Debug.Log("Paused -> Playing");
currentState = GameState.Playing;
}
}
void UpdateGameOver()
{
if (Input.GetKeyDown(KeyCode.Return))
{
Debug.Log("GameOver -> Menu");
currentState = GameState.Menu;
}
}
}
Trace it by hand for a short sequence of key presses:
player presses Enter: Menu -> Playing
player presses Escape: Playing -> Paused
player presses Escape: Paused -> Playing
player presses Escape: Playing -> Paused
Here is the same set of states and transitions as a picture:
This enum + switch style works and is completely fine for a small prototype. But watch what happens as the game grows: every state's setup code, every state's per-frame logic, and every state's exit/cleanup code all end up crammed into the same one GameManagerSimple class. Add a fifth state — a cutscene, say — and you are editing this one already-long class again, hunting through four other states' logic to find where to add it.
The State pattern fixes the crowding problem by giving each state its own small class instead of its own branch in a shared switch. Every state class agrees to the same contract — an interface — with three methods: Enter (run once, the moment the state becomes active), Tick (run every frame while the state is active), and Exit (run once, the moment the state stops being active, right before switching to the next one).
public interface IGameState
{
void Enter();
void Tick(); // called every frame while this state is active
void Exit();
}
A tiny StateMachine class holds "whichever state is current" and does nothing clever beyond calling the right method at the right time:
public class StateMachine
{
private IGameState currentState;
public void ChangeState(IGameState newState)
{
currentState?.Exit(); // clean up the old state, if any
currentState = newState;
currentState.Enter(); // set up the new state
}
public void Tick()
{
currentState?.Tick();
}
}
Now each state is its own small, independent class:
using UnityEngine;
public class MenuState : IGameState
{
private readonly StateMachine machine;
public MenuState(StateMachine machine) { this.machine = machine; }
public void Enter() { Debug.Log("enter MENU"); }
public void Exit() { Debug.Log("exit MENU"); }
public void Tick()
{
if (Input.GetKeyDown(KeyCode.Return))
machine.ChangeState(new PlayingState(machine));
}
}
public class PlayingState : IGameState
{
private readonly StateMachine machine;
public PlayingState(StateMachine machine) { this.machine = machine; }
public void Enter() { Debug.Log("enter PLAYING"); }
public void Exit() { Debug.Log("exit PLAYING"); }
public void Tick()
{
if (Input.GetKeyDown(KeyCode.Escape))
machine.ChangeState(new PausedState(machine));
}
}
public class PausedState : IGameState
{
private readonly StateMachine machine;
public PausedState(StateMachine machine) { this.machine = machine; }
public void Enter() { Time.timeScale = 0f; Debug.Log("enter PAUSED"); }
public void Exit() { Time.timeScale = 1f; Debug.Log("exit PAUSED"); }
public void Tick()
{
if (Input.GetKeyDown(KeyCode.Escape))
machine.ChangeState(new PlayingState(machine));
}
}
And a small MonoBehaviour just drives the machine — it does not know anything about menus, pausing, or gameplay itself:
using UnityEngine;
public class GameManager : MonoBehaviour
{
private StateMachine machine;
void Start()
{
machine = new StateMachine();
machine.ChangeState(new MenuState(machine));
}
void Update()
{
machine.Tick();
}
}
Trace the same key presses as before through this version:
(game starts) enter MENU
player presses Enter: exit MENU
enter PLAYING
player presses Esc: exit PLAYING
enter PAUSED
player presses Esc: exit PAUSED
enter PLAYING
Notice what the enum version could not do cleanly: PausedState.Enter sets Time.timeScale = 0f (freezing physics and any code that uses deltaTime-based movement), and PausedState.Exit reliably sets it back to 1f the moment you leave pause, no matter how you leave it. There is exactly one place this setup and teardown lives, and it cannot be forgotten in some other branch of a giant switch. Adding a fifth state (a cutscene, a loading screen) means writing one new class — every existing state class stays untouched.
bool CanEnter() or similar, so a state can refuse a transition (for example, refusing to leave GameOver until a results screen animation has finished). You do not need this for a first game, but it is a natural extension once you understand Enter/Tick/Exit.currentState = newState directly instead of going through ChangeState, which skips Exit() entirely. This is exactly how you end up with a game permanently stuck at Time.timeScale = 0f after leaving pause — the freeze was set in Enter, but nothing ever called the matching Exit to undo it. Always go through one single transition method.There are two fundamentally different ways code finds out that a button was pressed. Polling means asking the question yourself, every frame: "is this key down right now?" Events mean the opposite — you register a function once, ahead of time, and the system calls that function for you the moment the thing happens, without you ever having to ask.
using UnityEngine;
public class PollingExample : MonoBehaviour
{
void Update()
{
// POLLING: ask the question every single frame.
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log("jump (polled)");
}
}
}
Here is the same idea using a C# event instead — a built-in language feature for "let other code subscribe a function to be called when something happens":
using System;
using UnityEngine;
public class Button : MonoBehaviour
{
public event Action OnPressed; // other code subscribes to this
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
OnPressed?.Invoke(); // fire the event -- tell everyone listening
}
}
public class Listener : MonoBehaviour
{
public Button button;
void Start()
{
// EVENT: we register once, and get called only when it happens.
button.OnPressed += HandlePress;
}
void HandlePress()
{
Debug.Log("jump (event)");
}
}
Both scripts print a jump message the moment Space is pressed, so the visible behavior looks identical here — because under the hood, Button is still polling every frame; it just hides that polling behind an event so Listener does not have to do it too. That hiding is the actual point. Imagine ten different systems all care about the jump key: the player controller, a sound effect, a tutorial hint, an achievement tracker. With polling, all ten write their own Input.GetKeyDown check, every frame. With an event, one place polls once, and ten places subscribe.
Unity's own newer input package leans heavily on the event style, and takes it a step further: it can raise events straight from the operating system's input hardware messages, so nothing in your game even has to run its own polling loop to make events possible. That package is the subject of the next section.
Reading KeyCode.Space directly, like the examples so far, hardcodes your game to one specific keyboard key. If you want a player on a gamepad to jump too, you now need a second check for the gamepad's button, and a third for a touch-screen tap, scattered through the same method. Unity's Input System package solves this by adding a layer of naming in between: instead of code asking about a specific key or button, it asks about a named action, like "Jump", and the action is separately bound to whichever physical controls should trigger it — space bar, gamepad South button, a touch region, all at once.
using UnityEngine;
using UnityEngine.InputSystem;
public class JumpAction : MonoBehaviour
{
public InputActionReference jumpActionRef; // assigned in the Inspector
void OnEnable()
{
// EVENT: Unity calls HandleJump only when the action "performs".
jumpActionRef.action.performed += HandleJump;
jumpActionRef.action.Enable();
}
void OnDisable()
{
jumpActionRef.action.performed -= HandleJump;
jumpActionRef.action.Disable();
}
void HandleJump(InputAction.CallbackContext context)
{
Debug.Log("jump (Input System event)");
}
}
HandleJump does not care whether the action fired because of a keyboard, a gamepad, or a touch screen — that decision was made once, in the binding, not scattered through your gameplay code.
Actions are grouped into named action maps — related actions that are usually active together. A typical game has a "Gameplay" map (Move, Jump, Pause) and a "UI" map (Navigate, Submit, Cancel), and usually only one map is enabled at a time:
This connects directly back to section 6. A PlayerInput component (a Unity component that manages a whole action asset for you) exposes SwitchCurrentActionMap, and the natural place to call it is inside your state classes' Enter methods:
using UnityEngine.InputSystem;
public class InputMapSwitcher : MonoBehaviour
{
public PlayerInput playerInput; // has "Gameplay" and "UI" action maps
public void EnterPaused()
{
playerInput.SwitchCurrentActionMap("UI"); // jump/move stop firing
}
public void EnterPlaying()
{
playerInput.SwitchCurrentActionMap("Gameplay");
}
}
Once PausedState.Enter switches to the "UI" map, the physical Space key simply stops meaning "Jump" — it might now mean nothing, or "Submit" on a menu button, depending on what the "UI" map binds it to. Your gameplay code does not need a single if (state == Playing) guard anywhere; the input system itself refuses to fire an action from a disabled map. Rebinding — letting a player change which key means "Jump" in the options menu — is also built on this same idea: you are only ever changing what a control is bound to, never touching the action's name or the gameplay code that reads it.
Enable() on an action, or SwitchCurrentActionMap to the wrong map name, so the action silently never fires and nothing seems to happen when the button is pressed. Because there is no error message for "this action was never enabled", this is one of the more confusing first bugs with the Input System — if a button press does nothing, check the map and the Enable() call before you assume your gameplay code is wrong.Input.GetKeyDown (and the Input System's performed event) is only true on the exact single frame the press happened. If your character cannot act on that exact frame — say, a jump button pressed one frame before the character actually lands — the press is simply lost. Mashing the button does not help, because a single physical press only produces one true frame; if that frame was the wrong one, it is gone. To a player this feels like the game "ate" a perfectly-timed press, and it is one of the most common complaints about a platformer that otherwise plays fine.
The fix is to remember that a press happened for a short window of time — a buffer — instead of only ever checking the single instant it occurred, and consume it as soon as the game is ready:
using UnityEngine;
public class JumpBuffer : MonoBehaviour
{
public float bufferTime = 0.15f; // how long a press "stays valid"
private float bufferTimer = -1f; // -1 means "no buffered press"
public bool isGrounded; // set elsewhere by a ground check
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
bufferTimer = bufferTime; // remember: "jump was requested"
}
if (bufferTimer > 0f)
{
bufferTimer -= Time.deltaTime;
if (isGrounded)
{
Debug.Log("jump! (buffered)");
bufferTimer = -1f; // consumed, clear it
}
}
else
{
bufferTimer = -1f;
}
}
}
Trace a player pressing jump slightly before landing, with a 0.15 second buffer:
A buffer window of 100-150 milliseconds is common. It is short enough that a player cannot press jump a full second early and have it "save up", but long enough to cover ordinary human reaction timing and a few frames of animation or physics settling.
Input buffering forgives a press that happens slightly too early. Coyote time forgives the opposite case: a press that happens slightly too late, right after the character has already walked off a ledge. It is named after the cartoon coyote who runs straight off a cliff edge and does not fall until he looks down — for a few frames after leaving solid ground, the character is still allowed to jump as if it were still grounded. Without this, a player who steps off a platform and presses jump a fraction of a second later — which, to a human, feels like perfect timing — gets nothing, because a strict if (isGrounded) check already sees them as airborne.
using UnityEngine;
public class JumpWithCoyoteTime : MonoBehaviour
{
public float bufferTime = 0.15f;
public float coyoteTime = 0.1f;
private float bufferTimer = -1f;
private float coyoteTimer = 0f;
public bool isGrounded;
void Update()
{
// track how recently we were grounded
if (isGrounded)
coyoteTimer = coyoteTime; // reset the grace window
else
coyoteTimer -= Time.deltaTime; // grace window ticking away
if (Input.GetKeyDown(KeyCode.Space))
bufferTimer = bufferTime;
else
bufferTimer -= Time.deltaTime;
bool canJump = coyoteTimer > 0f; // grounded OR just left the ground
bool wantsToJump = bufferTimer > 0f; // pressed recently
if (canJump && wantsToJump)
{
Debug.Log("jump! (coyote time saved it)");
bufferTimer = -1f;
coyoteTimer = 0f; // used up -- no double jump from this
}
}
}
Trace a player who walks off a ledge and presses jump 60 milliseconds later, with a 0.1 second coyote window:
Put both windows on one timeline and the idea becomes visual: a jump succeeds whenever the press and the "close enough to grounded" moment overlap.
Every jump script so far calls Input.GetKeyDown directly inside the same method that decides whether to actually jump. That is fine for a small demo, but it quietly mixes two separate jobs into one piece of code: figuring out what the player wants (reading hardware) and deciding what the character does about it (game logic). Mixing them causes real problems once a project grows: you cannot test jump logic without a real keyboard attached, you cannot replay a recorded run or drive the same character with a simple AI without editing the movement code, and every place that reads Input.* directly is one more place that has to separately respect the game state from sections 5 and 6 (should input even do anything while paused?).
The fix is a small middle layer — an input reader — that exposes only "what does the player currently want" as plain data, and nothing else in the game is allowed to read Input.* or the Input System directly.
public interface IPlayerInput
{
float MoveInput { get; } // -1 to 1
bool JumpPressed { get; } // true only on the press frame
}
using UnityEngine;
public class KeyboardInputReader : MonoBehaviour, IPlayerInput
{
public float MoveInput { get; private set; }
public bool JumpPressed { get; private set; }
void Update()
{
// this is the ONLY place in the whole game that touches Input.*
MoveInput = Input.GetAxis("Horizontal");
JumpPressed = Input.GetKeyDown(KeyCode.Space);
}
}
The character controller now depends only on the interface, never on Input itself:
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public MonoBehaviour inputSource; // must implement IPlayerInput
private IPlayerInput input;
public float speed = 5f;
void Awake()
{
input = inputSource as IPlayerInput;
}
void Update()
{
// PlayerController never reads Input.* directly -- only the interface
transform.position += Vector3.right * input.MoveInput * speed * Time.deltaTime;
if (input.JumpPressed)
Debug.Log("jump!");
}
}
Here is the payoff. A second class implements the exact same interface with no hardware involved at all — a tiny scripted routine that could drive a tutorial, a cutscene, or a placeholder AI:
using UnityEngine;
public class ScriptedInputReader : MonoBehaviour, IPlayerInput
{
public float MoveInput { get; private set; } = 1f; // always walk right
public bool JumpPressed { get; private set; }
private float timer;
void Update()
{
timer += Time.deltaTime;
JumpPressed = (timer > 2f && timer < 2.02f); // jump once, at t=2s
}
}
Swap which script is assigned to inputSource in the Inspector, and PlayerController runs identically either way, with zero changes to its own code. The same class now drives a real player, a scripted cutscene, or — later, once you learn testing — an automated test that checks movement math without ever touching a keyboard. This is also the natural home for the game-state check from section 6: have the input reader report MoveInput = 0 and JumpPressed = false whenever the game is not in the Playing state, and PlayerController never needs to know pausing exists at all.
PlayerController only knows about the small IPlayerInput contract, never about which concrete class is behind it. Keeping engine-specific, hard-to-test code (reading real hardware) in a thin, separate layer, away from your core gameplay logic, is one of the most useful habits you can build early.Update's deltaTime) that changes from frame to frame.FixedUpdate's fixedDeltaTime) that never changes, used for stable physics.MonoBehaviour callbacks run once per frame, once per fixed physics step, and once per frame after every Update, respectively.MonoBehaviour methods at the right point every frame.Enter/Tick/Exit contract.MenuState, PlayingState, PausedState), the game starts and the player presses these keys in order: Enter, Esc, Esc, Esc, Enter. List every line the code would Debug.Log, in order, including the moment (if any) where a key press produces no log line at all, and explain why.enter MENU
exit MENU
enter PLAYING
exit PLAYING
enter PAUSED
exit PAUSED
enter PLAYING
exit PLAYING
enter PAUSED
Walk it state by state: start in MenuState (enter MENU). Enter switches to PlayingState (exit MENU, enter PLAYING). First Esc switches to PausedState (exit PLAYING, enter PAUSED). Second Esc switches back to PlayingState (exit PAUSED, enter PLAYING). Third Esc switches to PausedState again (exit PLAYING, enter PAUSED). The final Enter produces nothing — PausedState.Tick only reacts to KeyCode.Escape, so pressing Return while paused does not call ChangeState and logs no line at all. This is the same trap section 5's diagram shows: there is no drawn arrow from PAUSED back to MENU on Enter, only PAUSED to PLAYING on Esc.
transform.position += Vector3.right * speed * Time.deltaTime; where speed = 12 (units per second). Over three consecutive frames, Time.deltaTime measures 0.02s, then 0.05s (a stutter), then 0.03s. How far did the ship move on each of the three frames, and what is the total distance after all three? Then, in one sentence, explain why the total would come out almost the same if instead you added up 60 steady frames at 60 FPS covering the same amount of real time.frame 1: 12 * 0.02 = 0.24 units
frame 2: 12 * 0.05 = 0.60 units
frame 3: 12 * 0.03 = 0.36 units
total: 0.24 + 0.60 + 0.36 = 1.20 units
Total elapsed real time across the three frames is 0.02 + 0.05 + 0.03 = 0.10 seconds, and 12 units/second * 0.10 seconds = 1.20 units — matching exactly. That is not a coincidence: because each frame's move is speed * deltaTime, the total move over any stretch of frames is always speed * (sum of those deltaTimes), which only depends on how much real time passed, never on how that time got chopped into frames. Sixty steady 60 FPS frames covering the same 0.10 seconds of real time would sum to the same 1.20 units, just split into much smaller, even pieces — this is exactly the frame-rate independence from section 2.
coyoteTime = 0.15 and bufferTime = 0.1: the player walks off a ledge at t=3.00s (isGrounded becomes false), presses Space at t=3.20s, and does not land again until t=3.50s. Does the jump fire? Work out coyoteTimer at the moment of the press and explain your answer.The jump does not fire. coyoteTimer starts counting down from 0.15 the instant the player leaves the ground at t=3.00s, so it reaches exactly 0 at t=3.15s and keeps going negative after that (nothing resets it, since the player has not landed yet). By the time Space is pressed at t=3.20s, 0.20 seconds have passed since leaving the ground — 0.05 seconds past the 0.15 second coyote window — so coyoteTimer is already negative and canJump is false. bufferTimer does get set to 0.1 by the press, so wantsToJump is true, but the code only fires when both canJump and wantsToJump are true, so nothing happens. The buffer window then also expires by t=3.30s, well before the player actually lands at t=3.50s, so landing does not retroactively trigger anything either. The player simply jumped too late for this coyote window — a slightly larger coyoteTime (or the player reacting a little faster) is the only thing that would have saved it.
That covers the heartbeat of a Unity game: the loop that reads input, updates the world, and renders it many times a second; the two different clocks — variable deltaTime for everything you see, fixed fixedDeltaTime for stable physics — and why physics insists on the second one; clean state machines that keep menu, playing, paused, and game-over logic from turning into a tangle; and input read carefully enough that a jump feels fair even when a press lands a few milliseconds early or late. Every later chapter that adds movement, combat, or AI builds directly on top of these four ideas.