Squash & Stretch
Deform the character sprite or model to emphasize motion. Stretch when moving fast, squash on impact. The oldest animation principle, applied to videogames.
The Feel
Squash and stretch adds life and weight. A rigid character feels robotic. A squashing/stretching character feels organic, even when it's a geometric shape.
It communicates physics through shape: "I'm compressing because I hit something" or "I'm elongating because I'm moving fast."
Exposed Variables
| Variable | Type | Default | What it does |
|---|---|---|---|
stretchAmount |
float | 0.2 | Maximum stretch (1.2 = 20% taller) |
squashAmount |
float | 0.2 | Maximum squash (0.8 = 20% shorter) |
returnSpeed |
float | 10.0 | How fast to return to normal |
velocityThreshold |
float | 5.0 | Velocity needed to trigger stretch |
Tuning Guide
For subtle realism:
stretchAmount= 0.05 - 0.1squashAmount= 0.05 - 0.1- Barely noticeable but adds life
Reference: Celeste (subtle character deformation)
For cartoony feel:
stretchAmount= 0.2 - 0.4squashAmount= 0.2 - 0.4- Obvious, playful, bouncy
Reference: Hollow Knight, Ori
For extreme stylization:
stretchAmount= 0.5+squashAmount= 0.4+- Very exaggerated, full cartoon
Reference: Cuphead, classic Disney
Pseudocode
Pseudocode
Every Frame:
- Get vertical velocity
- If falling fast: stretch vertically, compress horizontally
- If rising fast: stretch vertically, compress horizontally
- If grounded: return to normal scale
On Land:
- Apply squash (compress vertically, expand horizontally)
- Lerp back to normal over time
Key: Preserve volume - if you stretch Y, compress X proportionally
Unity Setup (2D)
- Setup Visual Hierarchy:
- Your player object has Rigidbody2D and Collider2D
- Create a child object called "Visual" or "Sprite"
- Move your SpriteRenderer to this child
- The script scales the child, not the physics object
- Add the Script:
- Add
SquashStretch2D.csto the player (parent object) - Assign the visual child to
visualTransform - The physics collider stays unaffected
- Add
- Create Ground Check (optional for landing squash):
- Create empty child "GroundCheck" at player's feet
- Assign to script's
groundCheckfield - Configure ground layer mask
- Tune the Feel:
- Subtle: stretchAmount/squashAmount = 0.05-0.1
- Cartoony: stretchAmount/squashAmount = 0.2-0.4
- Extreme: stretchAmount/squashAmount = 0.5+
The Script (2D)
SquashStretch2D.cs
// ============================================================
// SquashStretch2D.cs
// Deforms a visual sprite based on velocity and landing.
// Attach to player, assign child visual transform.
// Physics collider is NOT affected - only visuals.
// Unity 6.x
// ============================================================
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class SquashStretch2D : MonoBehaviour
{
// ========================================================
// REFERENCES - Assign in Inspector
// ========================================================
[Header("Visual Transform")]
[Tooltip("The child transform to scale (NOT the physics object!)")]
[SerializeField] private Transform visualTransform;
[Header("Ground Detection (for landing squash)")]
[Tooltip("Empty transform at player's feet")]
[SerializeField] private Transform groundCheck;
[Tooltip("Radius of ground check")]
[SerializeField] [Range(0.05f, 0.5f)] private float groundCheckRadius = 0.1f;
[Tooltip("Which layers count as ground")]
[SerializeField] private LayerMask groundLayer;
// ========================================================
// SQUASH & STRETCH PARAMETERS
// ========================================================
[Header("Stretch (during fast vertical movement)")]
[Tooltip("Maximum vertical stretch (0.2 = 20% taller at max velocity)")]
[SerializeField] [Range(0f, 1f)] private float stretchAmount = 0.2f;
[Tooltip("Velocity needed to reach full stretch")]
[SerializeField] [Range(1f, 30f)] private float stretchVelocityMax = 15f;
[Header("Squash (on landing)")]
[Tooltip("Maximum squash on landing (0.3 = 30% shorter)")]
[SerializeField] [Range(0f, 0.5f)] private float squashAmount = 0.25f;
[Tooltip("Fall velocity needed for full squash")]
[SerializeField] [Range(1f, 30f)] private float squashVelocityMax = 12f;
[Header("Recovery")]
[Tooltip("How fast to return to normal scale")]
[SerializeField] [Range(1f, 30f)] private float returnSpeed = 12f;
[Header("Horizontal Stretch (for dashing)")]
[Tooltip("Enable horizontal stretch based on horizontal velocity")]
[SerializeField] private bool enableHorizontalStretch = true;
[Tooltip("Maximum horizontal stretch")]
[SerializeField] [Range(0f, 0.5f)] private float horizontalStretchAmount = 0.15f;
[Tooltip("Horizontal velocity for full stretch")]
[SerializeField] [Range(1f, 50f)] private float horizontalStretchVelocityMax = 20f;
[Header("Debug")]
[Tooltip("Show debug info")]
[SerializeField] private bool debugMode = false;
// ========================================================
// INTERNAL STATE
// ========================================================
private Rigidbody2D rb;
private Vector3 targetScale = Vector3.one;
private Vector3 baseScale = Vector3.one;
private bool isGrounded = false;
private bool wasGrounded = false;
private float lastFallVelocity = 0f;
// ========================================================
// UNITY LIFECYCLE
// ========================================================
void Awake()
{
rb = GetComponent<Rigidbody2D>();
// Store the original scale of the visual
if (visualTransform != null)
{
baseScale = visualTransform.localScale;
}
}
void Update()
{
if (visualTransform == null) return;
// Check ground state
CheckGroundState();
// Calculate target scale based on current state
CalculateTargetScale();
// Smoothly interpolate to target scale
visualTransform.localScale = Vector3.Lerp(
visualTransform.localScale,
targetScale,
returnSpeed * Time.deltaTime
);
}
// ========================================================
// SCALE CALCULATION
// ========================================================
/// <summary>
/// Calculate what the scale should be based on velocity
/// </summary>
private void CalculateTargetScale()
{
float scaleX = baseScale.x;
float scaleY = baseScale.y;
float velocityY = rb.linearVelocity.y;
float velocityX = Mathf.Abs(rb.linearVelocity.x);
// ----------------------------------------
// VERTICAL STRETCH (rising or falling fast)
// ----------------------------------------
if (!isGrounded && Mathf.Abs(velocityY) > 0.5f)
{
// Normalize velocity to 0-1 range
float stretchFactor = Mathf.Clamp01(
Mathf.Abs(velocityY) / stretchVelocityMax
);
// Apply stretch: taller vertically
float stretchY = 1f + (stretchAmount * stretchFactor);
// Preserve volume: compress horizontally
// If Y grows by 20%, X shrinks to 1/1.2 = 0.833
float stretchX = 1f / stretchY;
scaleY = baseScale.y * stretchY;
scaleX = baseScale.x * stretchX;
if (debugMode && stretchFactor > 0.1f)
{
Debug.Log($"Vertical stretch: {stretchFactor:F2} (vel: {velocityY:F1})");
}
}
// ----------------------------------------
// HORIZONTAL STRETCH (dashing/running fast)
// ----------------------------------------
if (enableHorizontalStretch && velocityX > 1f)
{
float hStretchFactor = Mathf.Clamp01(
velocityX / horizontalStretchVelocityMax
);
// Apply horizontal stretch: wider horizontally
float hStretchX = 1f + (horizontalStretchAmount * hStretchFactor);
// Preserve volume: compress vertically
float hStretchY = 1f / hStretchX;
// Blend with vertical stretch (take the more extreme)
scaleX = Mathf.Max(scaleX, baseScale.x * hStretchX);
scaleY = Mathf.Min(scaleY, baseScale.y * hStretchY);
}
// Track fall velocity for landing squash
if (velocityY < 0)
{
lastFallVelocity = Mathf.Min(lastFallVelocity, velocityY);
}
targetScale = new Vector3(scaleX, scaleY, baseScale.z);
}
// ========================================================
// GROUND DETECTION + LANDING SQUASH
// ========================================================
/// <summary>
/// Check ground and trigger squash on landing
/// </summary>
private void CheckGroundState()
{
wasGrounded = isGrounded;
if (groundCheck != null)
{
isGrounded = Physics2D.OverlapCircle(
groundCheck.position,
groundCheckRadius,
groundLayer
);
}
// Just landed - trigger squash!
if (isGrounded && !wasGrounded)
{
ApplyLandingSquash();
}
// Reset fall velocity tracking when grounded
if (isGrounded)
{
lastFallVelocity = 0f;
}
}
/// <summary>
/// Apply instant squash when landing
/// </summary>
private void ApplyLandingSquash()
{
// Calculate squash based on fall velocity
float squashFactor = Mathf.Clamp01(
Mathf.Abs(lastFallVelocity) / squashVelocityMax
);
// Apply squash: shorter vertically, wider horizontally
float squashY = 1f - (squashAmount * squashFactor);
float squashX = 1f / squashY; // Preserve volume
// Apply immediately (then returnSpeed lerps back to normal)
visualTransform.localScale = new Vector3(
baseScale.x * squashX,
baseScale.y * squashY,
baseScale.z
);
if (debugMode)
{
Debug.Log($"Landing squash: {squashFactor:F2} (fall vel: {lastFallVelocity:F1})");
}
}
// ========================================================
// PUBLIC METHODS - Call from other scripts
// ========================================================
/// <summary>
/// Manually trigger a squash (e.g., on hit, on jump anticipation)
/// </summary>
/// <param name="intensity">0-1, how much to squash</param>
public void TriggerSquash(float intensity = 1f)
{
float squashY = 1f - (squashAmount * intensity);
float squashX = 1f / squashY;
visualTransform.localScale = new Vector3(
baseScale.x * squashX,
baseScale.y * squashY,
baseScale.z
);
}
/// <summary>
/// Manually trigger a vertical stretch (e.g., on jump launch)
/// </summary>
/// <param name="intensity">0-1, how much to stretch</param>
public void TriggerStretch(float intensity = 1f)
{
float stretchY = 1f + (stretchAmount * intensity);
float stretchX = 1f / stretchY;
visualTransform.localScale = new Vector3(
baseScale.x * stretchX,
baseScale.y * stretchY,
baseScale.z
);
}
/// <summary>
/// Immediately reset to normal scale
/// </summary>
public void ResetScale()
{
if (visualTransform != null)
{
visualTransform.localScale = baseScale;
targetScale = baseScale;
}
}
// ========================================================
// DEBUG VISUALIZATION
// ========================================================
void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
// ============================================================
// USAGE NOTES:
//
// 1. IMPORTANT: Only scale the VISUAL, not the physics object!
// Structure your player like this:
// - Player (has Rigidbody2D, Collider2D, SquashStretch2D)
// - Visual (child with SpriteRenderer - this gets scaled)
//
// 2. Volume preservation formula:
// If scaleY = 1.2 (20% taller), then scaleX = 1/1.2 = 0.833
// This keeps the visual "area" roughly constant
//
// 3. Call TriggerSquash() manually for:
// - Jump anticipation (brief squash before launch)
// - Taking damage
// - Charging up an attack
//
// 4. Call TriggerStretch() manually for:
// - Jump launch moment
// - Dash start
// - Attack follow-through
//
// 5. Combine with other juice:
// void OnJump()
// {
// squashStretch.TriggerSquash(0.5f); // Anticipation
// // Small delay, then...
// squashStretch.TriggerStretch(0.8f); // Launch
// }
//
// 6. For 3D: Same principles apply, but scale on local Y axis
// and compensate on X and Z to preserve volume
// ============================================================
Common Issues
"It looks wrong/distorted"
You're probably not preserving volume. When you stretch on Y, compress on X by the inverse amount. Total area should stay roughly constant.
"It happens too suddenly"
Use lerp/smoothing for the return to normal. The squash can be instant, but recovery should ease out.
"It doesn't match the movement"
Make sure stretch is based on velocity, not just "jumping" state. A slow rise shouldn't stretch as much as a fast dash.
"Colliders are getting messed up"
Only deform the visual, not the physics collider. Keep your hitbox at constant size.
When to Apply
| Moment | Deformation |
|---|---|
| Fast falling | Stretch vertically |
| Landing | Squash on impact |
| Jump anticipation | Brief squash before launch |
| At jump apex | Return to normal briefly |
| Dash/fast horizontal | Stretch horizontally |
| Hit/damage | Squash in hit direction |
Combine With
- Basic Jump / Variable Jump - squash/stretch the jump arc
- Dash - stretch during dash
- Hitstop - squash at moment of impact
Why Teach This
Squash and stretch is the first of Disney's 12 Principles of Animation. It's been making things look alive since the 1930s. Teaching it connects videogame development to a century of animation craft.
The Volume Principle
The key insight: preserve volume. A ball that squashes down must expand outward. This mimics how real soft objects deform and makes the deformation feel physically plausible even when exaggerated.
Mathematically: if you scale Y by 0.8, scale X by 1/0.8 = 1.25
Teaching Sequence
- Show animation references (bouncing ball tutorial)
- Implement basic squash on landing
- Add stretch based on velocity
- Tune for different aesthetic goals (realistic vs. cartoony)
The Aesthetic Choice
Amount of squash/stretch is a style choice, not a technical one. A realistic military shooter might use almost none. A cartoon platformer might use extreme amounts. Students should calibrate to their intended aesthetic.
Assessment Questions
- Why does preserving volume matter?
- How would you adjust squash/stretch for a realistic vs. cartoony videogame?
- What other animation principles apply to videogame characters?
Heritage Notes
Squash and stretch was codified by Disney animators in the 1930s. The bouncing ball exercise - still taught to animators today - is pure squash and stretch training. The principle predates videogames by decades.
Videogames inherited this from animation, but with a difference: in videogames, the deformation responds to player input, not predetermined keyframes. This makes it feel reactive and alive in a new way.
Why It Works
Our eyes track motion through shape change. A rigid object moving looks mechanical. A deforming object looks organic because real living things (and soft materials) deform under force.
Even geometric shapes like circles seem "alive" when they squash and stretch. This is why the bouncing ball is such an effective demonstration - a circle becomes a character just through deformation.
The 4 A's Application
Squash and stretch is pure Art - it doesn't change the Action or the Arc, only how things look. But by changing the Art, it transforms the Atmosphere. The same jump feels playful or weighty depending on deformation style.
The 12 Principles in Videogames
Disney's 12 Principles of Animation all apply to videogames:
- Squash and Stretch (this page)
- Anticipation (wind-up before jump)
- Staging (camera, composition)
- Straight Ahead / Pose to Pose (animation approach)
- Follow Through / Overlapping Action (secondary motion)
- Slow In / Slow Out (easing)
- Arcs (movement paths)
- Secondary Action (hair, clothes)
- Timing (speed of actions)
- Exaggeration (stylization)
- Solid Drawing (3D form)
- Appeal (character design)
Videogame animators learn these principles and apply them to interactive contexts.
Related
- Basic Jump - where squash/stretch is most visible
- Hitstop - squash at impact moment
- The 4 A's - Art component