Game Feel · Timing · common
Hit Stop
freeze both fighters for a few frames when a blow lands
Hit stop pauses the attacker and target for a few frames on impact. The brief freeze sells weight and gives the eye time to read the hit.
Both rows play the same attack on the same clock. The top row (green marker) freezes cyan attacker and target for the hit-stop duration, with the target flashing orange and jittering; the bottom row has no hit stop and the blow slides past. The thin bar under each row is game time, so the top one visibly lags. Lower the playback speed to count the frozen frames.
What it is
When a hit connects, many action and fighting games stop the attacker and the target for a short moment, typically 3–10 frames, before the knockback plays. The pause reads as resistance, like a blade biting into something solid, and makes heavy attacks feel heavier than light ones even when their animations are similar.
Implement it as a timer that zeroes the game delta time for the frozen objects. Freeze only the participants and let effects (sparks, screen flash, screen shake) keep running so the moment stays alive. A small jitter on the target during the freeze adds to the effect.
Scale the duration with the hit: light jab ~50 ms, heavy strike ~120–200 ms, finishing blow longer. Because inputs keep arriving during the freeze, pair it with an input buffer so combo presses made during the stop are not lost.
Key formulas
- Game time during hit stop
- Duration from damage
Rules of thumb
- Start at 60–100 ms for normal hits, up to ~200 ms for heavy or critical hits. Past ~300 ms it reads as lag.
- Count the timer down with real (unscaled)
dt, not the gamedtit is freezing. - Do not stack durations from several simultaneous hits. Take the max of the remaining time and the new one.
- Keep UI, audio and particles on unscaled time so the world does not feel frozen.
Implementation
class Fighter {
hitStop = 0 // seconds of freeze left
onHit(duration: number) {
this.hitStop = Math.max(this.hitStop, duration)
}
update(realDt: number) {
let dt = realDt
if (this.hitStop > 0) {
const used = Math.min(this.hitStop, realDt)
this.hitStop -= used
dt = realDt - used // 0 while frozen, leftover on the last frame
}
this.animation.update(dt)
this.body.integrate(dt)
}
}Common mistakes
- Freezing the whole game loop, including input and audio, so the pause feels like a frame drop.
- Counting the freeze in frames, which halves its length on a 120 Hz display.
- Using the same duration for every attack, so light and heavy hits feel the same.
- Letting hit stop and knockback both run, so the target slides during the freeze.
Prompt
Add hit stop to my TypeScript Three.js brawler. Each fighter has hitStop seconds; on a hit, both attacker and target get hitStop = max(current, 0.06 + 0.004 * damage), capped at 0.2 s. In update(realDt), consume hitStop from real time and pass the remaining game dt (0 while frozen, the leftover on the last frozen frame) to animation and physics. While frozen, jitter the target's mesh ±0.05 units on x without moving its physics body, and keep particles and UI on unscaled time. Write a Vitest test that at 30 and 144 fps a 0.12 s hit stop removes exactly 0.12 s of game time.
Related
- Screen Shakeadd trauma on impact, shake by trauma squared, let it decay
- Invincibility Framesafter a hit, ignore damage for a short, clearly signalled window
- Hitboxes & Hurtboxesattacks carry hitboxes, bodies carry hurtboxes, and they only meet on active frames
- Input Bufferingremember a press for a moment and fire it when it becomes legal
Further reading
Updated 2026-11-01