โธ๏ธ Lesson 4.2: Main Menu & Pause System
Every game needs two screens before it needs anything else: a main menu to start from, and a pause menu to escape to. In this lesson you'll build both. You'll freeze the whole game with a single line, toggle a pause panel on and off, wire Resume and Quit, and route the pause key through the new Input System โ then handle the one gotcha that catches everyone: animations that die when the game freezes.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Lay out a main menu and a pause overlay as UI panels
- Show and hide a panel by toggling a GameObject's active state
- Freeze and unfreeze gameplay with
Time.timeScale - Wire Resume, Restart, and Quit buttons in code
- Trigger pause from a new Input System action callback
- Keep menu animations running while paused using unscaled time
Estimated Time: 45 minutes ยท Prerequisite: Lesson 4.1 (Canvas Render Modes & Scaling) and Module 3 (the new Input System)
In This Lesson
The Pause Menu Hierarchy
The pause menu is different from the main menu: it lives inside your gameplay scene, hidden until needed. The trick is to build it as a single parent Panel that you switch on and off. Here's the Hierarchy layout you're aiming for:
PauseController.The key idea: build the panel once, set it inactive (untick the checkbox at the top-left of its Inspector), and let a script flip it on. Everything inside it โ the dim background, the title, the buttons โ appears and disappears with the parent.
๐ Definition
Overlay panel: a full-screen UI child (usually an Image stretched to all four anchors, tinted black at partial alpha) that sits above gameplay to dim it and hold menu content. Toggling its GameObject active/inactive is the cheapest, most reliable way to show and hide a menu.
Freezing the Game with Time.timeScale
Pausing isn't just hiding the game behind a panel โ the action underneath has to stop. Unity gives you one global dial for this: Time.timeScale.
Time.timeScale = 1fโ normal speed (the default).Time.timeScale = 0fโ frozen. Physics stops,Time.deltaTimebecomes 0, and anything driven by it halts.Time.timeScale = 0.5fโ half speed (handy for slow-motion effects).
Setting it to 0 freezes almost everything automatically: Rigidbody motion, most animations, and any code that multiplies by Time.deltaTime (which, if you followed Fundamentals, is most of your movement code). That's why it's the standard pause switch.
๐ Definition
Time.timeScale: a global multiplier applied to the flow of scaled game time. At 0 the game clock stops, so Time.deltaTime reads 0 and time-based updates freeze. It does not stop Update() from running โ your scripts still tick, they just see no elapsed time.
โ ๏ธ Update still runs at timeScale 0
Freezing time does not pause your scripts' Update() methods โ they keep being called every frame. What changes is that Time.deltaTime becomes 0, so any motion computed as speed * Time.deltaTime produces no movement. Code that ignores deltaTime (like reading a raw input each frame) still runs. This is exactly why your pause key still works while paused.
Toggling the Pause Panel
Now the controller that ties the panel and the time freeze together. It tracks whether we're paused, shows/hides the panel with SetActive, and drives Time.timeScale. The button methods (Resume, Restart, QuitToMenu) are public so the Inspector's On Click events can call them.
using UnityEngine;
using UnityEngine.SceneManagement;
public class PauseController : MonoBehaviour
{
[SerializeField] GameObject pauseMenu; // the PauseMenu panel
public bool IsPaused { get; private set; }
public void TogglePause()
{
if (IsPaused) Resume();
else Pause();
}
public void Pause()
{
IsPaused = true;
pauseMenu.SetActive(true); // show the panel
Time.timeScale = 0f; // freeze gameplay
}
public void Resume()
{
IsPaused = false;
pauseMenu.SetActive(false); // hide the panel
Time.timeScale = 1f; // unfreeze
}
public void Restart()
{
Time.timeScale = 1f; // ALWAYS restore before leaving!
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public void QuitToMenu()
{
Time.timeScale = 1f; // ALWAYS restore before leaving!
SceneManager.LoadScene("MainMenu");
}
}
โ ๏ธ Always restore timeScale before changing scenes
Notice every method that leaves the scene sets Time.timeScale = 1f first. timeScale is global and persists across scene loads โ if you quit to the menu while frozen and forget to restore it, your next game starts completely frozen and looks broken. This is one of the most common "my game won't move after restart" bugs.
Pause from the Input System
In Module 3 you built Input Actions and read them through callbacks. Pausing is a perfect one-shot action. Add an action called Pause to your Action Map (bind it to Escape on keyboard and Start on gamepad), set its Action Type to Button, and subscribe to its performed callback.
Here's the pause controller wired to a PlayerInput-style action reference. Using an InputActionReference keeps the binding editable in the Inspector:
using UnityEngine;
using UnityEngine.InputSystem;
public class PauseInput : MonoBehaviour
{
[SerializeField] PauseController pause;
[SerializeField] InputActionReference pauseAction; // the "Pause" action
void OnEnable()
{
pauseAction.action.performed += OnPause;
pauseAction.action.Enable();
}
void OnDisable()
{
pauseAction.action.performed -= OnPause;
}
void OnPause(InputAction.CallbackContext ctx)
{
pause.TogglePause(); // one press flips between paused and playing
}
}
Because input polling and action callbacks are driven by real time (not scaled game time), the Pause action keeps firing even when Time.timeScale is 0 โ which is exactly what lets the player press Escape again to un-pause. If you had instead read a key inside a deltaTime-based loop, you'd be fine here too, since reading input doesn't depend on deltaTime.
โ Pro Tip
Give your pause action its own tiny Action Map, or make sure it lives in a map that stays enabled during gameplay. A frequent mistake is disabling the whole "Player" map on pause (to stop movement) and accidentally disabling the pause key with it โ leaving the player unable to resume.
Animating While Paused: Unscaled Time
Here's the gotcha that surprises everyone. You freeze the game with Time.timeScale = 0, then your pause menu's fade-in or button-hover animationโฆ doesn't play. Because the Animator and any Time.deltaTime code are running on scaled time โ which you just set to zero.
The fix is unscaled time โ a parallel clock that ignores timeScale and always ticks in real seconds:
- In code: use
Time.unscaledDeltaTimeinstead ofTime.deltaTimefor any animation that must run while paused (a menu fade, a spinner). - On an Animator: set its Update Mode to Unscaled Time (Animator component โธ Update Mode dropdown). It will now animate even at timeScale 0.
- On coroutines: yield
new WaitForSecondsRealtime(0.3f)instead ofWaitForSeconds, which would wait forever at timeScale 0.
A simple unscaled fade for the pause panel's CanvasGroup:
using System.Collections;
using UnityEngine;
public class PanelFade : MonoBehaviour
{
[SerializeField] CanvasGroup group;
[SerializeField] float duration = 0.25f;
public void FadeIn()
{
gameObject.SetActive(true);
StartCoroutine(Fade(0f, 1f));
}
IEnumerator Fade(float from, float to)
{
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime; // real time, ignores the pause
group.alpha = Mathf.Lerp(from, to, t / duration);
yield return null;
}
group.alpha = to;
}
}
โ ๏ธ WaitForSeconds hangs forever when paused
A coroutine that does yield return new WaitForSeconds(1f) will never complete at timeScale = 0, because scaled time never advances. Use WaitForSecondsRealtime for anything that must progress during a pause.
The whole pause flow, end to end:
panel hidden } state Paused { [*] --> Frozen note right of Frozen: timeScale = 0
panel shown
menu anims use unscaled time }
Figure 2: The pause state flow. The Pause action toggles between Playing and Paused; any exit path first restores timeScale to 1.
Hands-on Challenge
๐๏ธ Exercise 1: A working pause overlay
Objective: Pause and resume your game with a key and a button.
- In your gameplay scene, add an Overlay Canvas with a PauseMenu panel: a dim full-screen Image, a "Paused" title, and Resume / Restart / Quit buttons. Set the panel inactive.
- Add a
GameManagerobject withPauseController; drag the PauseMenu panel into its field. - Wire each button's On Click to the matching
PauseControllermethod. - Add a Pause Input Action (Escape / gamepad Start) and a
PauseInputcomponent; hook it to the controller. - Press Play. Tap Escape: the game freezes and the panel appears. Tap again (or click Resume): it unfreezes.
๐ก Hint: pressing Escape does nothing
Confirm the Pause action is enabled (the OnEnable call), that its binding is Escape, and that you subscribed to performed (not started). Also make sure the object with PauseInput is active in the scene.
โ Success check
Movement and physics stop the instant you pause, the panel dims the game, Resume returns you to normal speed, and Restart/Quit both work without the next scene starting frozen.
๐๏ธ Exercise 2: Fade the panel in while frozen
Add a CanvasGroup to the PauseMenu panel and the PanelFade script. Call FadeIn() from Pause() instead of a bare SetActive(true). Confirm the fade animates smoothly even though Time.timeScale is 0 โ proof that Time.unscaledDeltaTime is doing its job. Then break it on purpose: swap in Time.deltaTime and watch the fade freeze, so you understand exactly why unscaled time matters.
๐ฏ Quick Quiz
Question 1: What is the standard way to freeze all gameplay when the player pauses?
Question 2: Your pause-menu fade animation freezes when you pause. What's the fix?
Question 3: Why must you set Time.timeScale = 1f before loading a new scene from the pause menu?
Summary
๐ Key Takeaways
- A main menu is usually its own scene with buttons that call script methods (
PlayGame,QuitGame). - A pause menu is a panel inside the gameplay scene, built once, inactive by default, and toggled with
SetActive. Time.timeScale = 0ffreezes physics and time-based motion;= 1fresumes. It's global and persists across scene loads โ always restore it before leaving.- Route the pause key through a new Input System action's
performedcallback; input runs on real time, so it still fires while frozen. - Menu animations that must run while paused need unscaled time:
Time.unscaledDeltaTime, an Animator set to Unscaled Time, orWaitForSecondsRealtime.
๐ What's Next?
Your menus are done. Now for the UI that changes every frame while you play. In Lesson 4.3: HUD โ Health Bars & Live Data you'll build a health bar from a Filled Image, drive it from a health event, and smooth its motion with Lerp so it glides instead of snapping.
โธ๏ธ Start, pause, resume, quit
You've built the four screens every shippable game needs, learned the one global dial that freezes time, and dodged the unscaled-time trap. That's the skeleton of every game's front end.