【Unity】Unityでポーズ機能を正しく実装する - Time.timeScaleと「止まらないUI」の作り方

作成: 2026-07-05最終更新: 2026-07-13

Time.timeScale = 0でポーズを作ったら、メニューのアニメまで止まってしまった——ポーズ実装の定番のつまずきを解決します。timeScaleの仕組み、何が止まり何が止まらないか、ポーズ中も動くUIの作り方、timeScale方式の限界までを解説します。

ポーズボタンを押したら Time.timeScale = 0f——ここまでは順調。ところが、ポーズメニューのフェードインまで止まって画面が固まったように見える。慌ててアニメを外すと、今度は「ポーズ中なのにパーティクルだけ動いてる」…… ポーズ実装は、Unityの「時間」の仕組みを知らないと確実にハマる定番ポイントです。

この記事では、Time.timeScaleの仕組みと「何が止まり、何が止まらないのか」の正確な仕分け、ポーズ中も動くUIの作り方、そしてtimeScale方式を使うべきでないケースまでを解説します。

ポーズのイメージ。時が止まった世界で一時停止ボタンだけが光っている

この記事でわかること

  • Time.timeScaleの正体(ゲーム内時間の「蛇口」であること)
  • timeScale = 0 で止まるもの/止まらないものの正確な仕分け
  • イベント通知付きのPauseManagerの実装
  • ポーズ中も動くUI(Unscaled Time / WaitForSecondsRealtime)の作り方
  • timeScale方式の限界と、フラグ方式への切り替え時

Sponsored

Time.timeScaleの正体:ゲーム内時間の蛇口

Unityには「ゲーム内時間」と「現実時間」の2つの時間が流れています。Time.timeScaleは、この2つをつなぐ 蛇口(流量のバルブ) です。

Time.timeScaleの仕組みの図。現実時間が蛇口を通ってゲーム内時間に流れ込み、timeScale=1で等速、0.5でスロー、0で完全に止まる

毎フレームの経過時間Time.deltaTimeは、実際には「現実の経過時間 × timeScale」です。つまり——

  • timeScale = 1: 通常速度(現実と同じ流量)
  • timeScale = 0.5: スローモーション(半分の流量)
  • timeScale = 0: ゲーム内時間が完全に停止(蛇口を閉める)

ポーズが1行で書ける理由はここにあります。Time.deltaTimeを使って動いているものすべてが、蛇口を閉めた瞬間に一斉に止まるのです。

何が止まり、何が止まらないのか

つまずきの元は、「timeScale = 0 ですべてが止まる」という誤解です。実際に止まるのはゲーム内時間に乗っているものだけで、現実時間で動くものは止まりません。

timeScale=0で止まるものと止まらないものの仕分け図。止まる側にはdeltaTimeの移動・物理・Animator・パーティクル・WaitForSeconds、止まらない側にはUpdateの呼び出し・入力・unscaledDeltaTime・WaitForSecondsRealtime・オーディオ
止まる(ゲーム内時間)止まらない(現実時間)
Time.deltaTimeを使う移動・回転Updateの呼び出しそのもの(deltaTimeが0になるだけ)
物理演算とFixedUpdate(呼ばれなくなる)プレイヤーの入力(キー・マウス・ボタン)
Animator(Update ModeがNormalの場合)Time.unscaledDeltaTime / unscaledTime
パーティクル、InvokeWaitForSecondsWaitForSecondsRealtime、Animator(Unscaled Time)
オーディオ(止めるならAudioListener.pause

特に重要なのが右上の2つです。Updateは呼ばれ続けるので「ポーズ中だけの処理」を書く場所に困りませんし、入力も生きているのでポーズ解除の操作を受け付けられます。逆に、オーディオは勝手には止まらないため、明示的に止める必要があります。

補足: FixedUpdateだけは「deltaTimeが0になる」のではなく呼び出し自体が止まります。物理の時間が進まない以上、物理更新のタイミングが永遠に来ないためです。UpdateFixedUpdateの関係は UpdateとFixedUpdateの記事 で整理できます。

Sponsored

基本実装:イベント通知付きPauseManager

実装は「蛇口を閉める」だけでは半人前です。「ポーズ状態が変わったこと」をイベントで通知するところまで作ると、UIの表示切り替えやオーディオ停止を各システムが自分で対応でき、PauseManagerが肥大化しません(処理は直接、通知はイベント——の2段構えです)。

using System;
using UnityEngine;

public class PauseManager : MonoBehaviour
{
    // ポーズ状態の変更を通知するイベント(引数: ポーズ中ならtrue)
    public static event Action<bool> OnPauseChanged;

    public static bool IsPaused { get; private set; }

    // ポーズ前の流量を覚えておく(スロー演出中のポーズに対応)
    private static float previousTimeScale = 1f;

    public static void SetPaused(bool paused)
    {
        if (IsPaused == paused) return;
        IsPaused = paused;

        // 1. ゲーム内時間の蛇口を開け閉めする
        //    「1に戻す」ではなく「ポーズ前の流量に戻す」のがポイント
        if (paused)
        {
            previousTimeScale = Time.timeScale;
            Time.timeScale = 0f;
        }
        else
        {
            Time.timeScale = previousTimeScale;
        }

        // 2. オーディオは自動では止まらないので明示的に止める
        AudioListener.pause = paused;

        // 3. 変わったことを全システムへ通知(UIの表示切り替えなどは購読側が行う)
        OnPauseChanged?.Invoke(paused);
    }
}

呼び出す側(例:ポーズボタンやEscapeキー)は PauseManager.SetPaused(!PauseManager.IsPaused) を呼ぶだけ。ポーズメニューのUIはOnPauseChangedを購読して自分で表示・非表示を切り替えます(購読の書き方は イベントとデリゲートの記事OnEnable/OnDisableの鉄則どおりです)。

ミニ実験:スロー演出中にポーズしてみる

previousTimeScaleの意味は、実験すると1分で腑に落ちます。どこかのキーでTime.timeScale = 0.25f(必殺技のスロー演出のつもり)にしてから、ポーズ→再開してみてください。

  • 「1に戻す」実装: 再開した瞬間にスローが解けて、演出が壊れます
  • 「ポーズ前の値に戻す」実装(上のコード): スローのまま、何事もなく再開します

ついでに観察してほしいのが、このスロー中でも ポーズメニューのフェード速度は変わらない ことです。フェードはunscaledDeltaTime(現実時間)で動いているからで、「ゲーム内時間 scaled /現実時間 unscaled」の仕分けがそのまま目に見えます。

なお、SetPaused先頭のif (IsPaused == paused) return;は連打対策も兼ねています。ポーズキーをどれだけ速く連打しても、状態はきれいに交互へ切り替わるだけで、通知が二重に飛んだり速度の保存が壊れたりしません。

この実験で見えた3つ——スローのまま帰ってくる速度、凍った世界で動き続けるフェード、連打してもきれいに反転する状態——が確認できれば、ポーズ機能は完成です。今後ヒットストップやスロー演出などtimeScaleを触る機能を足すときも、この「ポーズ前の値に戻す」土台がそのまま効いてきます。

ポーズ中も動くUIの作り方

さて冒頭の問題です。ポーズメニューのフェードインが止まるのは、そのアニメーションがゲーム内時間に乗っているからでした。世界は凍らせたまま、メニューだけ現実時間で動かします。

凍った世界と動くメニューの実例図。ゲーム世界(敵や弾)は氷漬けで停止しているが、手前のポーズメニューだけはスライドインして動いている

方法は使っている仕組みごとに1つずつです。

  • Animatorのアニメ: Animatorコンポーネントの Update ModeNormalUnscaled Time に変更。これだけでポーズ中も再生されます
  • コルーチンの待機: WaitForSecondsWaitForSecondsRealtime に置き換え
  • スクリプトで動かすUI: Time.deltaTimeTime.unscaledDeltaTime に置き換え
// ポーズ中も動くフェードイン(unscaledDeltaTimeを使う)
using UnityEngine;

public class PauseMenuFade : MonoBehaviour
{
    [SerializeField] private CanvasGroup canvasGroup;

    void Update()
    {
        if (!PauseManager.IsPaused) return;

        // deltaTimeは0だが、unscaledDeltaTimeは現実時間で進み続ける
        canvasGroup.alpha = Mathf.MoveTowards(
            canvasGroup.alpha, 1f, Time.unscaledDeltaTime * 4f);
    }
}

なお、uGUIのボタンのクリック判定そのものは現実時間で動くため、特別な対応なしでポーズメニューを操作できます。

Sponsored

注意点:timeScale方式の引き際

timeScale方式は「世界を丸ごと止める」豪快な仕組みです。だからこそ、丸ごと止めてはいけないゲームでは使えません

  • マルチプレイヤー: 自分の都合で世界の時間は止められません。ポーズ=メニューを開くだけで、ゲームは進み続けます
  • ポーズ中も世界が動く演出: 「メニューの背景で敵がゆっくり動いている」「一部のオブジェクトだけ止めたい」ような要件では、全停止は乱暴すぎます

こうした場合は、IsPausedフラグを各システムが見て、止めたい処理だけ自分で止める方式に切り替えます。先ほどのPauseManagerはすでにIsPausedOnPauseChangedを持っているので、Time.timeScaleの行を消すだけでフラグ方式に転用できます。敵AIはif (PauseManager.IsPaused) return;で動きを止め、背景の演出は無視して動き続ける——という選択的なポーズです。

判断基準は1行です。 「その処理はゲーム内時間で動くべきか、現実時間で動くべきか」 ——この仕分けさえできれば、timeScale方式でもフラグ方式でも迷いません。

おまけ:先に知っておくと良いこと

ポーズが動いたら、次はこのあたりが視野に入ってきます。

  • ゲーム全体の状態管理と統合する: ポーズは「Playing⇄Paused」という状態遷移の一部です。GameManagerパターンの記事GameStateに組み込むと、タイトルやゲームオーバーと一貫した管理になります。
  • 通知をイベントチャネルにする: ポーズ通知の購読者(敵AI・BGM・タイマー…)がシーンをまたいで増えてきたら、イベントチャネルの記事 のケース2がそのままこの話です。
  • スローモーション演出への応用: timeScale = 0.3fのような中間値は、必殺技のスロー演出に使えます。このとき物理の粒度を保つためTime.fixedDeltaTimeも連動させる(0.02f * timeScale)という定番テクニックがあります。

まとめ

ポーズ実装は「時間の仕組み」の理解がすべてです。

  • Time.timeScaleゲーム内時間の蛇口deltaTime = 現実時間 × timeScaleなので、0にすれば時間依存の処理が一斉に止まる。
  • Updateと入力は止まらない(deltaTimeが0になるだけ)。FixedUpdateは呼ばれなくなる。オーディオはAudioListener.pauseで明示的に止める。
  • PauseManagerはtimeScale切り替え+イベント通知のセットで作る。UIやオーディオの対応は購読側に任せる。
  • ポーズ中も動かしたいものは現実時間へ: AnimatorはUnscaled Time、コルーチンはWaitForSecondsRealtime、スクリプトはunscaledDeltaTime
  • マルチプレイヤーや選択的ポーズにはフラグ方式。「ゲーム内時間か、現実時間か」の1行の問いかけで仕分ける。

「世界は止まっているのに、メニューはぬるっと動く」——あの気持ちいいポーズ画面が、これで自分のゲームにも作れます。