Two games can have identical mechanics and still feel completely different. One feels flat and floaty; the other feels punchy and satisfying. The difference is game feel, often called “juice”: the small layers of feedback that make every action read clearly and feel good. This guide covers 12 practical ways to add juice in Unity, with working C# for hit stop, screen shake, squash and stretch, easing and more, plus advice on timing so your game feels responsive rather than noisy.
The Rules Before the Tricks
Juice works when it supports the action, not when it buries it. Three rules keep it under control:
- Respond on the same frame. The first frame of feedback (a flash, a sound, a tiny pause) must happen the instant the player presses the button or the hit lands. Delayed feedback feels like lag.
- Scale feedback with importance. A light jab gets a small effect; a killing blow gets the full stack. If everything shakes the screen, nothing feels special.
- Make it adjustable. Expose strengths in the Inspector and add a “reduce screen shake” option for accessibility.
It also helps to centralise feedback. Instead of calling ten systems from your weapon script, raise one event like OnHit(HitInfo) and let a feedback component react. Our C# events guide shows the pattern.
1. Hit Stop
Hit stop (or hit pause) freezes the game for a few frames when a strong hit connects. It sells weight better than any other single effect, and fighting games have used it for decades. Freeze for 40 to 120 ms depending on hit strength:
using System.Collections;
using UnityEngine;
public class HitStop : MonoBehaviour
{
public static HitStop Instance { get; private set; }
bool active;
void Awake() => Instance = this;
public void Stop(float duration, float timeScale = 0f)
{
if (active) return;
StartCoroutine(DoStop(duration, timeScale));
}
IEnumerator DoStop(float duration, float timeScale)
{
active = true;
float original = Time.timeScale;
Time.timeScale = timeScale;
yield return new WaitForSecondsRealtime(duration);
Time.timeScale = original;
active = false;
}
}
Use WaitForSecondsRealtime, since normal waits are scaled and would never finish at time scale 0. If you also have a pause menu that sets timeScale, store and restore carefully, or apply hit stop only to the attacker and target animators by setting animator.speed = 0 for the duration.
2. Screen Shake
With Cinemachine 3, the cleanest shake uses impulses. Add a CinemachineImpulseListener extension to your CinemachineCamera, and a CinemachineImpulseSource to whatever causes the shake:
using Unity.Cinemachine;
using UnityEngine;
public class ShakeOnHit : MonoBehaviour
{
[SerializeField] CinemachineImpulseSource impulse;
[SerializeField] float strength = 0.4f;
public void Shake(float scale = 1f)
{
impulse.GenerateImpulse(strength * scale);
}
}
Keep shakes short (0.1 to 0.3 seconds) and use a random or noise-based direction rather than a fixed sine wave. For directional hits, call GenerateImpulseWithVelocity with the hit direction so the camera jolts away from the impact. Read our Cinemachine 3 camera guide if you are setting up the camera for the first time.
Without Cinemachine, offset a child of the camera with Perlin noise that decays over time (“trauma” squared works nicely), and never move the camera’s actual gameplay position.
3. Squash and Stretch
Stretch an object along its movement when it jumps, squash it when it lands. Preserve volume by scaling the other axes inversely, and animate the visual child, never the object with the collider:
using System.Collections;
using UnityEngine;
public class SquashStretch : MonoBehaviour
{
[SerializeField] Transform visual;
[SerializeField] float duration = 0.18f;
Coroutine running;
public void Play(float amount) // positive = stretch tall, negative = squash flat
{
if (running != null) StopCoroutine(running);
running = StartCoroutine(Animate(amount));
}
IEnumerator Animate(float amount)
{
float t = 0f;
while (t < duration)
{
t += Time.deltaTime;
float k = 1f - (t / duration); // 1 to 0
float y = 1f + amount * k;
float xz = 1f / Mathf.Sqrt(y); // keep volume roughly constant
visual.localScale = new Vector3(xz, y, xz);
yield return null;
}
visual.localScale = Vector3.one;
}
}
Call Play(0.25f) on jump and Play(-0.3f) on landing. Values of 0.15 to 0.35 look good for cartoon styles; go subtler for realistic games.
4. Hit Flash
Flash the enemy white for a frame or two when it is hit. With URP Lit materials, briefly enable emission using a MaterialPropertyBlock so you don’t create material copies:
using System.Collections;
using UnityEngine;
public class HitFlash : MonoBehaviour
{
[SerializeField] Renderer[] renderers;
[SerializeField] Color flashColor = Color.white;
[SerializeField] float duration = 0.08f;
static readonly int EmissionId = Shader.PropertyToID("_EmissionColor");
MaterialPropertyBlock block;
void Awake() => block = new MaterialPropertyBlock();
public void Flash() => StartCoroutine(DoFlash());
IEnumerator DoFlash()
{
Set(flashColor * 2f);
yield return new WaitForSeconds(duration);
Set(Color.black);
}
void Set(Color c)
{
foreach (var r in renderers)
{
r.GetPropertyBlock(block);
block.SetColor(EmissionId, c);
r.SetPropertyBlock(block);
}
}
}
The material must have Emission enabled (tick it once in the material inspector, with a black emission colour) so the shader keyword is on. Property blocks do break SRP Batcher batching for that renderer, which is fine for a handful of enemies. For sprites, a custom Shader Graph with a “Flash Amount” property is cleaner.
5. Particles on Every Impact
Sparks on a sword hit, dust on landing, debris when something breaks. Small, fast bursts (5 to 20 particles, under half a second) read better than big clouds. Spawn them at the contact point and orient them along the surface normal. Pool your effect objects rather than instantiating each one, as explained in our object pooling guide.
6. Layered, Varied Sound
A satisfying hit is usually several sounds at once: a sharp transient (the click or crack), a body (the thud), and sometimes a tail (debris, a ring). Layer two or three short clips and you get punch that a single sample can’t match.
- Randomise pitch by about plus or minus 5 to 10 percent and pick from 3 to 5 variations to avoid the “machine gun” effect.
- Duck music briefly on big hits with an Audio Mixer snapshot so the impact cuts through.
- Play the sound on the same frame as the visual, never after a delay.
7. Easing Instead of Linear Motion
Linear movement looks robotic. Easing curves make UI panels, pickups and doors feel physical. You don’t need a tween library for the basics:
public static class Ease
{
public static float OutCubic(float t) => 1f - Mathf.Pow(1f - t, 3f);
public static float OutBack(float t)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1f;
return 1f + c3 * Mathf.Pow(t - 1f, 3f) + c1 * Mathf.Pow(t - 1f, 2f);
}
public static float OutElastic(float t)
{
if (t <= 0f) return 0f;
if (t >= 1f) return 1f;
const float c4 = 2f * Mathf.PI / 3f;
return Mathf.Pow(2f, -10f * t) * Mathf.Sin((t * 10f - 0.75f) * c4) + 1f;
}
}
// Usage inside a coroutine:
// transform.localScale = Vector3.LerpUnclamped(Vector3.zero, Vector3.one, Ease.OutBack(t));
Use LerpUnclamped with OutBack and OutElastic, since they overshoot past 1. A coin that pops in with OutBack, or a button that scales to 1.1 and back on click, instantly feels more polished. An AnimationCurve field in the Inspector is another great option because designers can tweak it without code.
✨ Want juice without writing it all yourself?
Feedback and tweening tools on the Asset Store let you stack shake, flash, sound and hit stop in the Inspector and tune them live. We picked the ones worth the money.
8. Camera Kick and FOV Punch
For shooters, a tiny upward camera rotation on each shot (recoil kick) that recovers over 0.1 seconds makes weapons feel powerful. Apply it to a child pivot of the camera, not the aiming transform, so it doesn’t affect accuracy unless you want it to.
An FOV punch briefly widens the field of view by 3 to 8 degrees on a dash, boost or big explosion, then eases back. In Cinemachine 3, change cinemachineCamera.Lens.FieldOfView over a few frames. Combined with a speed-lines particle effect, it sells speed very well.
9 to 12. Knockback, Buffering, Numbers and Anticipation
9. Knockback
Push enemies away from the hit direction with rb.AddForce(dir * force, ForceMode.Impulse), or a short scripted slide for kinematic characters. Even a small push confirms the hit landed. A tiny knockback on the player when they fire a heavy weapon adds weight too.
10. Input Buffering and Coyote Time
Not all juice is visual. If the player presses jump 80 ms before landing, buffer it and jump on landing. If they press jump 80 ms after walking off a ledge, let them (coyote time). Players never notice these directly; they just say the controls feel tight.
float lastGroundedTime, lastJumpPressedTime;
const float coyote = 0.1f, buffer = 0.1f;
void Update()
{
if (isGrounded) lastGroundedTime = Time.time;
if (jumpPressedThisFrame) lastJumpPressedTime = Time.time;
bool canJump = Time.time - lastGroundedTime <= coyote;
bool wantsJump = Time.time - lastJumpPressedTime <= buffer;
if (canJump && wantsJump)
{
Jump();
lastJumpPressedTime = -1f;
lastGroundedTime = -1f;
}
}
11. Damage Numbers and UI Pops
Floating numbers that pop up with OutBack easing, drift upward and fade out tell the player exactly what happened. Make critical hits bigger and a different colour. Pop the score counter (scale to 1.2 and back) whenever it increases.
12. Anticipation and Follow-Through
A brief wind-up before a big attack and a short recovery after it make actions readable. Keep player anticipation very short (under 100 ms) so controls stay responsive, but give enemies longer wind-ups so players can react. Our Animator troubleshooting guide helps if your attack animations aren’t triggering as expected.
Troubleshooting
Hit stop freezes the game forever
You used WaitForSeconds instead of WaitForSecondsRealtime, or the coroutine’s object was disabled mid-stop. Keep HitStop on a persistent object.
Screen shake does nothing
The CinemachineCamera has no CinemachineImpulseListener extension, or the listener’s channel mask doesn’t match the source’s.
Squash and stretch breaks collisions
You scaled the object with the collider. Scale a visual child only.
Hit flash changes every enemy at once
You edited sharedMaterial. Use a MaterialPropertyBlock or per-renderer material instance.
The game feels noisy and tiring
Too much feedback on minor actions. Scale effects by importance and cut shake on anything the player does repeatedly.
FAQ
What is the single most effective juice technique?
For combat, hit stop combined with a good impact sound. For platformers, coyote time, input buffering and squash on landing.
How long should hit stop last?
Around 40 to 60 ms for normal hits and up to 150 ms for big finishers. Longer starts to feel like lag.
Should screen shake be optional?
Yes. Shake can cause motion sickness, so offer a slider or toggle in settings.
Do I need a tween library?
No, but one saves time once you have dozens of animated UI elements. Simple easing functions and coroutines cover the basics.
When should I add juice during development?
Add a basic layer early so playtests feel representative, then tune it near the end once mechanics are stable.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply