Hitstop

A brief freeze when an attack connects, giving visual weight to the impact. The world pauses for a split second to emphasize the hit.

Practice - what you do

The Feel

Hitstop creates weight and impact. Without it, attacks feel like they pass through enemies. With it, you feel the collision. The pause says: "This mattered."

It's counterintuitive - you're making the videogame slower to make it feel more powerful. But the pause creates contrast that makes the surrounding motion feel faster.

Exposed Variables

Variable Type Default What it does
hitstopDuration float 0.05 How long to freeze (seconds)
timeScale float 0.0 What to set Time.timeScale to during hitstop
affectBoth bool true Freeze both attacker and target (or just target)

Tuning Guide

For light/fast attacks:

  • hitstopDuration = 0.02 - 0.04
  • Brief stutter, maintains speed

Reference: Devil May Cry (light attacks)

For medium attacks:

  • hitstopDuration = 0.05 - 0.08
  • Noticeable pause, solid impact

Reference: Most action games' standard attacks

For heavy/finishing attacks:

  • hitstopDuration = 0.1 - 0.2
  • Dramatic pause, emphasizes power

Reference: God of War finishers, Smash Bros. killing blows

Pseudocode

Pseudocode
On Hit:
  - Store original Time.timeScale
  - Set Time.timeScale to 0 (or near-zero)
  - Start hitstopTimer

Every Frame (using unscaledDeltaTime):
  - If in hitstop:
    - Decrease hitstopTimer
    - If hitstopTimer <= 0:
      - Restore original Time.timeScale

Critical: Use Time.unscaledDeltaTime for the hitstop timer, so it counts down even when time is frozen.

Unity Setup

  1. Add the HitstopManager:
    • Create empty GameObject named "HitstopManager"
    • Add the HitstopManager.cs script below
    • This is a singleton - only need one in the scene
  2. Call from Attack Scripts:
    • When an attack connects, call HitstopManager.Instance.TriggerHitstop(duration)
    • Pass different durations for light (0.03), medium (0.06), heavy (0.12) attacks
  3. Optional - Fixed Timestep:
    • Edit → Project Settings → Time
    • Consider adjusting Fixed Timestep if physics behaves oddly during slow-mo
  4. Audio Note:
    • Audio pitch may scale with timeScale
    • Set minTimeScale to 0.01 instead of 0 if audio sounds wrong
    • Or use AudioSource.ignoreListenerPause = true

The Script

HitstopManager.cs
// ============================================================
// HitstopManager.cs
// A global hitstop system using Time.timeScale.
// Singleton pattern - add to one empty GameObject in scene.
// Unity 6.x
// ============================================================

using UnityEngine;

public class HitstopManager : MonoBehaviour
{
    // ========================================================
    // SINGLETON - Access from anywhere via HitstopManager.Instance
    // ========================================================

    public static HitstopManager Instance { get; private set; }

    // ========================================================
    // HITSTOP PARAMETERS - Tune these in the Inspector
    // ========================================================

    [Header("Hitstop Settings")]
    [Tooltip("Time scale during hitstop (0 = full freeze, 0.01 = near-freeze)")]
    [SerializeField] [Range(0f, 0.1f)] private float minTimeScale = 0f;

    [Tooltip("Default hitstop duration if not specified")]
    [SerializeField] [Range(0.01f, 0.5f)] private float defaultDuration = 0.05f;

    [Header("Preset Durations (for convenience)")]
    [Tooltip("Duration for light/fast attacks")]
    [SerializeField] [Range(0.01f, 0.1f)] private float lightHitDuration = 0.03f;

    [Tooltip("Duration for medium attacks")]
    [SerializeField] [Range(0.03f, 0.15f)] private float mediumHitDuration = 0.06f;

    [Tooltip("Duration for heavy/finishing attacks")]
    [SerializeField] [Range(0.05f, 0.3f)] private float heavyHitDuration = 0.12f;

    [Header("Debug")]
    [Tooltip("Show debug messages in console")]
    [SerializeField] private bool debugMode = false;

    // ========================================================
    // INTERNAL STATE
    // ========================================================

    private float hitstopTimer = 0f;
    private float originalTimeScale = 1f;
    private bool inHitstop = false;

    // ========================================================
    // PUBLIC PROPERTIES
    // ========================================================

    /// <summary>
    /// True while hitstop is active
    /// </summary>
    public bool InHitstop => inHitstop;

    /// <summary>
    /// Remaining hitstop time in seconds
    /// </summary>
    public float RemainingTime => hitstopTimer;

    // ========================================================
    // UNITY LIFECYCLE
    // ========================================================

    void Awake()
    {
        // Singleton setup - destroy duplicates
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;

        // Optional: persist across scenes
        // DontDestroyOnLoad(gameObject);
    }

    void Update()
    {
        // Process hitstop timer
        // CRITICAL: Use unscaledDeltaTime so timer works during freeze!
        if (inHitstop)
        {
            hitstopTimer -= Time.unscaledDeltaTime;

            if (hitstopTimer <= 0f)
            {
                EndHitstop();
            }
        }
    }

    // ========================================================
    // PUBLIC METHODS - Call these to trigger hitstop
    // ========================================================

    /// <summary>
    /// Trigger hitstop with a custom duration
    /// </summary>
    /// <param name="duration">How long to freeze (seconds)</param>
    public void TriggerHitstop(float duration)
    {
        // Don't interrupt a longer hitstop with a shorter one
        if (inHitstop && hitstopTimer > duration)
        {
            if (debugMode) Debug.Log($"Hitstop ignored: existing ({hitstopTimer:F3}s) > new ({duration:F3}s)");
            return;
        }

        // Store original time scale (only if not already in hitstop)
        if (!inHitstop)
        {
            originalTimeScale = Time.timeScale;
        }

        // Start hitstop
        inHitstop = true;
        hitstopTimer = duration;
        Time.timeScale = minTimeScale;

        if (debugMode)
        {
            Debug.Log($"Hitstop triggered: {duration:F3}s (timeScale: {minTimeScale})");
        }
    }

    /// <summary>
    /// Trigger hitstop with default duration
    /// </summary>
    public void TriggerHitstop()
    {
        TriggerHitstop(defaultDuration);
    }

    /// <summary>
    /// Trigger light attack hitstop
    /// </summary>
    public void TriggerLightHit()
    {
        TriggerHitstop(lightHitDuration);
    }

    /// <summary>
    /// Trigger medium attack hitstop
    /// </summary>
    public void TriggerMediumHit()
    {
        TriggerHitstop(mediumHitDuration);
    }

    /// <summary>
    /// Trigger heavy attack hitstop
    /// </summary>
    public void TriggerHeavyHit()
    {
        TriggerHitstop(heavyHitDuration);
    }

    /// <summary>
    /// Immediately end hitstop (e.g., on scene change)
    /// </summary>
    public void ForceEndHitstop()
    {
        if (inHitstop)
        {
            EndHitstop();
        }
    }

    // ========================================================
    // INTERNAL METHODS
    // ========================================================

    /// <summary>
    /// End hitstop and restore normal time
    /// </summary>
    private void EndHitstop()
    {
        inHitstop = false;
        hitstopTimer = 0f;
        Time.timeScale = originalTimeScale;

        if (debugMode)
        {
            Debug.Log($"Hitstop ended. Time restored to {originalTimeScale}");
        }
    }

    // ========================================================
    // CLEANUP
    // ========================================================

    void OnDestroy()
    {
        // Always restore time scale when destroyed
        if (inHitstop)
        {
            Time.timeScale = originalTimeScale;
        }
    }
}

// ============================================================
// USAGE EXAMPLES:
//
// 1. From an attack script when hit connects:
//    void OnHitEnemy(Enemy enemy)
//    {
//        // Trigger hitstop based on attack type
//        HitstopManager.Instance.TriggerMediumHit();
//
//        // Do damage, knockback, etc.
//        enemy.TakeDamage(damage);
//    }
//
// 2. Custom duration for special attacks:
//    void OnFinalBlow()
//    {
//        // Extra long hitstop for dramatic effect
//        HitstopManager.Instance.TriggerHitstop(0.2f);
//    }
//
// 3. Combine with other effects:
//    void OnCriticalHit()
//    {
//        HitstopManager.Instance.TriggerHeavyHit();
//        ScreenShake.Instance.TriggerShake(0.3f, 0.5f);
//        SpawnHitEffect(hitPosition);
//        PlaySound(criticalHitSound);
//    }
//
// 4. Check if in hitstop (for UI, input buffering):
//    if (HitstopManager.Instance.InHitstop)
//    {
//        // Buffer this input for after hitstop ends
//    }
//
// 5. For objects that should move during hitstop (like UI),
//    use Time.unscaledDeltaTime for their movement:
//    transform.position += direction * speed * Time.unscaledDeltaTime;
// ============================================================

Common Issues

"Nothing happens during hitstop"
Make sure your timer uses unscaledDeltaTime. Regular deltaTime will be 0 during the freeze.

"Hitstop affects things it shouldn't"
Time.timeScale is global. For selective freezing, use per-object pause states instead of timeScale.

"It feels choppy, not impactful"
Hitstop alone isn't enough. Combine with screen shake, hit effects, and sound. The pause is one part of a larger impact moment.

"Audio sounds wrong during hitstop"
Audio pitch may scale with timeScale. Either use unscaledTime for audio or keep timeScale at a low value (0.01) instead of 0.

Combine With

The Impact Stack

Great combat feel comes from layering techniques. A satisfying hit often includes:

Layer What it does
Hitstop Temporal emphasis - time acknowledges the hit
Screen Shake Visual emphasis - camera reacts to hit
Hit VFX Sparks, flashes, particles
Sound Impact audio, voice
Knockback Physical consequence

Related

Glossary Terms