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.
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
- Add the HitstopManager:
- Create empty GameObject named "HitstopManager"
- Add the
HitstopManager.csscript below - This is a singleton - only need one in the scene
- 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
- When an attack connects, call
- Optional - Fixed Timestep:
- Edit → Project Settings → Time
- Consider adjusting Fixed Timestep if physics behaves oddly during slow-mo
- Audio Note:
- Audio pitch may scale with timeScale
- Set
minTimeScaleto 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
- Screen Shake - visual impact alongside temporal impact
- Knockback - movement after the pause
- Input Buffer - catch inputs during freeze
- Squash & Stretch - deform on impact
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 |
Why Teach This
Hitstop is the perfect example of counterintuitive design. Slowing things down makes them feel faster. Students learn that feel doesn't come from literal representation - it comes from emphasis and contrast.
Teaching Sequence
- Show two versions of the same attack - with and without hitstop
- Have students describe the difference in feel (not mechanics)
- Implement basic hitstop - observe the transformation
- Vary duration per attack type - learn to calibrate impact
The "Juice" Conversation
Hitstop is often grouped under "juice" or "game feel." This is a good entry point for discussing:
- What is juice? (Feedback that makes actions feel satisfying)
- Why does juice matter? (It's the difference between functional and delightful)
- How much is too much? (When it obscures rather than emphasizes)
Assessment Questions
- Why does pausing the game make an attack feel stronger?
- How would you vary hitstop for different attack types?
- What other effects should accompany hitstop for maximum impact?
Heritage Notes
Hitstop originated in fighting games, likely accidentally. Early Street Fighter games had processing delays when calculating hit effects. Players grew to associate the pause with impact. When hardware got faster, developers intentionally preserved the pause because removing it made hits feel weak.
This is Aesthetic Heritage at work: a technical limitation became an aesthetic expectation.
Why Pause = Power
Several psychological mechanisms explain hitstop's effectiveness:
- Contrast: The pause makes surrounding motion feel faster by comparison
- Attention: A sudden stop captures attention at the moment of impact
- Anticipation: The pause creates micro-tension before the consequence (knockback)
- Film language: Slow motion in film emphasizes important moments; hitstop is extreme slow motion
The 4 A's of Hitstop
Hitstop is a micro-Gesture with all four A's:
- Action: The attack input and its connection
- Art: The frozen frame, often accompanied by VFX
- Arc: The brief duration of the pause
- Atmosphere: The feeling of weight, impact, power
Even this tiny moment contains the full framework.
When Not to Use Hitstop
Hitstop isn't always appropriate:
- Continuous damage: Hitstop on every tick of poison would be annoying
- Environmental hazards: Walking into spikes might not need dramatic pause
- Story emphasis: Sometimes you want the violence to feel quick and brutal, not dramatic
Related
- Screen Shake - visual impact feedback
- Knockback - what happens after the pause
- Gesture - hitstop as micro-gesture