Unityを触り始めた頃は、とりあえず Update() に全部書いてしまいがちですよね。
プレイヤー移動も、入力処理も、カメラも、ヒットストップも、全部ひとつのスクリプトに押し込んでしまうと…

  • どこで何が起きているか分かりづらい
  • 少し仕様を変えるだけで大量のコードを触ることになる
  • プレハブごとに似たような処理をコピペしてカオスになる

といった「Godクラス」状態になりがちです。

そこでこの記事では、「攻撃がヒットしたときにだけ一瞬ゲームをスローにして打撃感を出す」機能を、
ImpactShake コンポーネントとして切り出してみます。
ヒットストップ専用の小さなコンポーネントにしておけば、プレイヤーにも敵にもボスにも、
必要なオブジェクトにポン付けするだけで同じ効果を共有できます。

【Unity】一瞬だけ世界をスローにするヒットストップ!「ImpactShake」コンポーネント

ここでは、攻撃がヒットしたタイミングで

  • Engine.timeScale を一時的に下げる
  • 指定時間が経ったら自動で元の timeScale に戻す
  • 連続ヒット時は「より強い方」「より長い方」を優先して更新する

といった動きをする ImpactShake を実装します。

フルコード:ImpactShake.cs


using System.Collections;
using UnityEngine;

/// <summary>
/// 攻撃ヒット時などに一瞬だけゲーム全体をスローにして
/// 打撃感(ヒットストップ)を出すコンポーネント。
/// 
/// ・Engine.timeScale を直接いじる
/// ・複数回呼ばれても「より強い/長い」リクエストを優先
/// ・シーン内に1つあればどこからでも呼び出し可能(シングルトン風)
///
/// ※注意:timeScale を変更するため、他のスクリプトと競合しないように
///         「ヒットストップはこのコンポーネントに一本化」するのがおすすめです。
/// </summary>
public class ImpactShake : MonoBehaviour
{
    // シーン内で1つだけ存在させるための簡易シングルトン
    public static ImpactShake Instance { get; private set; }

    [Header("ヒットストップ基本設定")]
    [SerializeField]
    [Tooltip("ヒットストップ中の timeScale 値(0~1)。0 に近いほど完全停止に近くなる。")]
    private float slowTimeScale = 0.1f;

    [SerializeField]
    [Tooltip("ヒットストップのデフォルト時間(秒, ゲーム時間ではなく実時間ベース)。")]
    private float defaultDuration = 0.08f;

    [SerializeField]
    [Tooltip("ヒットストップ中に Time.fixedDeltaTime を補正するかどうか。物理挙動の安定に有効。")]
    private bool adjustFixedDeltaTime = true;

    [Header("デバッグ")]
    [SerializeField]
    [Tooltip("シーン再生中にテストキー(Space)でヒットストップを発火させる。")]
    private bool enableDebugKey = false;

    [SerializeField]
    [Tooltip("デバッグ用のトリガーキー。")]
    private KeyCode debugKey = KeyCode.Space;

    // 内部状態
    private Coroutine currentRoutine;
    private float originalTimeScale = 1f;
    private float originalFixedDeltaTime;

    // 現在進行中のヒットストップ情報
    private float currentRequestedDuration = 0f;
    private float currentRequestedSlowScale = 1f;

    private void Awake()
    {
        // シンプルなシングルトンパターン
        if (Instance != null && Instance != this)
        {
            Debug.LogWarning(
                $"[ImpactShake] シーン内に複数の ImpactShake が存在します。" +
                $" 古い方を破棄します。 ({Instance.gameObject.name} -> {gameObject.name})");
            Destroy(Instance);
        }

        Instance = this;

        // 元の値を保存しておく
        originalTimeScale = Engine.timeScale;
        originalFixedDeltaTime = Time.fixedDeltaTime;
    }

    private void OnDestroy()
    {
        // 破棄時に timeScale を元に戻しておく保険
        if (Engine.timeScale != originalTimeScale)
        {
            Engine.timeScale = originalTimeScale;
        }

        if (adjustFixedDeltaTime && Time.fixedDeltaTime != originalFixedDeltaTime)
        {
            Time.fixedDeltaTime = originalFixedDeltaTime;
        }

        if (Instance == this)
        {
            Instance = null;
        }
    }

    private void Update()
    {
        // デバッグ用:ゲーム中に Space キーでヒットストップを確認できる
        if (enableDebugKey && Input.GetKeyDown(debugKey))
        {
            // デフォルト設定で発火
            RequestHitStop(defaultDuration, slowTimeScale);
        }
    }

    /// <summary>
    /// 外部からヒットストップを要求するための公開メソッド。
    /// 
    /// 例:
    ///   ImpactShake.Instance.RequestHitStop(0.08f, 0.1f);
    /// </summary>
    /// <param name="duration">ヒットストップ時間(秒, 実時間ベース)</param>
    /// <param name="slowScale">
    /// ヒットストップ中の timeScale 値(0~1)。
    /// 0 に近いほど完全停止に近くなる。
    /// </param>
    public void RequestHitStop(float duration, float slowScale)
    {
        if (duration <= 0f)
        {
            return;
        }

        // slowScale は 0~1 の範囲にクランプ
        slowScale = Mathf.Clamp01(slowScale);

        // すでにヒットストップ中の場合:
        // ・より「長い」duration
        // ・より「遅い」(小さい)slowScale
        // を優先して上書きする。
        bool shouldOverride = duration > currentRequestedDuration
                              || slowScale < currentRequestedSlowScale;

        if (!shouldOverride && currentRoutine != null)
        {
            // 既存のヒットストップの方が強いので何もしない
            return;
        }

        currentRequestedDuration = duration;
        currentRequestedSlowScale = slowScale;

        if (currentRoutine != null)
        {
            StopCoroutine(currentRoutine);
        }

        currentRoutine = StartCoroutine(HitStopRoutine(duration, slowScale));
    }

    /// <summary>
    /// 外部から「とりあえずデフォルト設定でヒットストップしてほしい」場合に使うショートカット。
    /// </summary>
    public void RequestDefaultHitStop()
    {
        RequestHitStop(defaultDuration, slowTimeScale);
    }

    /// <summary>
    /// 実際に Engine.timeScale を下げて、指定時間後に元に戻すコルーチン。
    /// </summary>
    private IEnumerator HitStopRoutine(float duration, float slowScale)
    {
        // 元の値を保存
        float prevTimeScale = Engine.timeScale;
        float prevFixedDeltaTime = Time.fixedDeltaTime;

        // ヒットストップ開始
        Engine.timeScale = slowScale;

        if (adjustFixedDeltaTime)
        {
            // timeScale に合わせて fixedDeltaTime も縮めることで、
            // 物理挙動の極端な遅延を防ぐ。
            Time.fixedDeltaTime = originalFixedDeltaTime * Engine.timeScale;
        }

        // 実時間ベースで待つ(timeScale の影響を受けない)
        float elapsed = 0f;
        while (elapsed < duration)
        {
            elapsed += Time.unscaledDeltaTime;
            yield return null;
        }

        // ヒットストップ終了。元の値に戻す。
        Engine.timeScale = prevTimeScale;

        if (adjustFixedDeltaTime)
        {
            Time.fixedDeltaTime = prevFixedDeltaTime;
        }

        // 状態をリセット
        currentRequestedDuration = 0f;
        currentRequestedSlowScale = 1f;
        currentRoutine = null;
    }
}

使い方の手順

攻撃ヒット時にヒットストップを入れるまでの流れを、プレイヤー近接攻撃を例にして説明します。

  1. ImpactShake をシーンに配置する
    • 空の GameObject を作成し、名前を ImpactShake などにする
    • 上記の ImpactShake.cs をアタッチする
    • インスペクタで
      • Slow Time Scale:0.05 ~ 0.2 くらい(好み)
      • Default Duration:0.05 ~ 0.12 秒くらい

      を設定する(迷ったらデフォルトのままでもOK)

    • テスト用に Enable Debug Key にチェックを入れると、再生中に Space キーでヒットストップを確認できます
  2. 攻撃のヒット判定側から呼び出す
    例えば、プレイヤーの近接攻撃が敵に当たったときにヒットストップをかけたい場合、
    攻撃判定スクリプト(例:PlayerAttackHitbox)の OnTriggerEnter などで呼び出します。
    
    using UnityEngine;
    
    /// <summary>
    /// プレイヤーの近接攻撃の当たり判定サンプル。
    /// 敵に当たったらダメージを与えつつヒットストップを発生させる。
    /// </summary>
    public class PlayerAttackHitbox : MonoBehaviour
    {
        [SerializeField]
        private int damage = 10;
    
        [SerializeField]
        private float hitStopDuration = 0.08f;
    
        [SerializeField]
        private float hitStopScale = 0.1f;
    
        private void OnTriggerEnter(Collider other)
        {
            // ここでは単純に "Enemy" タグで判定する例
            if (!other.CompareTag("Enemy"))
            {
                return;
            }
    
            // ダメージ処理(実際には敵側の HP コンポーネントなどを呼び出す想定)
            // other.GetComponent<EnemyHealth>()?.TakeDamage(damage);
    
            // ヒットストップを要求
            if (ImpactShake.Instance != null)
            {
                ImpactShake.Instance.RequestHitStop(hitStopDuration, hitStopScale);
            }
            else
            {
                Debug.LogWarning("[PlayerAttackHitbox] ImpactShake インスタンスがシーンに存在しません。");
            }
        }
    }
    
  3. 敵側にも同じ仕組みを使い回す
    敵同士がぶつかったときや、ボスの大技が着弾したときにも同じように
    ImpactShake.Instance.RequestHitStop(...) を呼び出すだけでOKです。
    • 雑魚敵の攻撃:duration = 0.04f, slowScale = 0.3f
    • ボスの必殺技:duration = 0.12f, slowScale = 0.05f

    のように威力に応じてパラメータを変えると、ゲームのメリハリが一気に増します。

  4. 動く床やギミックにも適用してみる
    動く床やトラップがプレイヤーを吹き飛ばした瞬間にもヒットストップを入れると、
    「当たってしまった!」という感覚が強調されてゲームが気持ちよくなります。
    ギミック側のスクリプトで、ダメージを与えるタイミングに合わせて
    ImpactShake.Instance.RequestDefaultHitStop() を呼び出しましょう。

メリットと応用

ImpactShake をコンポーネントとして切り出しておくと、次のようなメリットがあります。

  • プレハブがスリムになる
    プレイヤーや敵のスクリプトから「Engine.timeScale を直接いじるコード」を追放できます。
    各キャラは「ヒットしたら ImpactShake にお願いする」だけなので、責務がはっきり分かれます。
  • レベルデザインが楽になる
    「この攻撃はもっと重く感じさせたいな」と思ったら、攻撃側の
    hitStopDurationhitStopScale をちょっと変えるだけ。
    シーン全体のヒットストップのバランスは ImpactShake 側のパラメータで一括調整できます。
  • 競合バグを防ぎやすい
    timeScale をいじるスクリプトが1か所にまとまるので、
    「タイトル演出でスローにしているのに、どこかのヒットストップが上書きしてしまった」
    みたいな事故を減らせます。

さらに少しだけ改造して、「ヒットストップ終了時にカメラを軽く揺らす」などの演出も追加できます。
例えば、以下のような void 関数を ImpactShake に足してみるのもアリですね。


/// <summary>
/// ヒットストップ終了直後に、簡易的なカメラシェイクを行う例。
/// メインカメラの位置を一瞬だけランダムに揺らしてから元に戻す。
/// 実際には Cinemachine などと組み合わせるとより自然な揺れになります。
/// </summary>
private void DoSimpleCameraShake()
{
    Camera mainCam = Camera.main;
    if (mainCam == null)
    {
        return;
    }

    // ここでは簡易的にコルーチンを使わず、1フレームだけ揺らす例
    // 実用するならコルーチン化して数フレームかけて減衰させるのがおすすめです。
    Vector3 originalPos = mainCam.transform.position;

    // ランダム方向にわずかにオフセット
    Vector3 offset = Random.insideUnitSphere * 0.1f;
    mainCam.transform.position = originalPos + offset;

    // 次のフレームで元に戻す
    // ※実用ではコルーチンや LateUpdate などで制御しましょう。
    StartCoroutine(ResetCameraPositionNextFrame(mainCam, originalPos));
}

private IEnumerator ResetCameraPositionNextFrame(Camera cam, Vector3 originalPos)
{
    yield return null; // 1フレーム待つ
    if (cam != null)
    {
        cam.transform.position = originalPos;
    }
}

このように、「ヒットストップ専用コンポーネント」を用意しておけば、
あとからカメラシェイクや画面フラッシュなどの演出を足したくなっても、
影響範囲を最小限に抑えつつ、気持ちいい打撃感をどんどん盛っていけます。
小さな責務のコンポーネントに分けていく習慣をつけていきましょう。