Knockback

Push an entity away when hit. The physical consequence of damage. Creates space, shows impact, and can create environmental hazards.

Practice - what you do

The Feel

Knockback creates physical consequence. Damage isn't just a number going down - it's being pushed back, losing ground, potentially falling into danger. It makes combat spatial.

Good knockback also creates opportunities: knock an enemy off a ledge, into spikes, or into another enemy.

Exposed Variables

Variable Type Default What it does
knockbackForce float 8.0 Horizontal push force
knockbackUp float 4.0 Vertical lift during knockback
hitstunDuration float 0.2 Time target can't act after hit
knockbackDecay float 10.0 How fast knockback velocity decays

Tuning Guide

For light hit reactions:

  • knockbackForce = 3 - 5
  • knockbackUp = 0 - 2
  • hitstunDuration = 0.1
  • Quick recovery, fast combat

Reference: Hack-and-slash, character action games

For heavy, consequential hits:

  • knockbackForce = 10 - 15
  • knockbackUp = 5 - 8
  • hitstunDuration = 0.3 - 0.5
  • Major displacement, time to reposition

Reference: Smash Bros., platform fighters

For environmental kill potential:

  • High knockbackForce
  • Significant knockbackUp (launches into air)
  • Design levels with hazards at edges

Pseudocode

Pseudocode
On Hit:
  - Calculate direction from attacker to target
  - Apply velocity: (direction * knockbackForce) + (up * knockbackUp)
  - Start hitstun timer
  - Disable target's movement/actions

Every Frame during hitstun:
  - Decay knockback velocity
  - Prevent target actions

On hitstun end:
  - Restore target control

Unity Setup (2D)

  1. Setup the Target (entity that receives knockback):
    • Add Rigidbody2D (Gravity Scale: 3, Freeze Rotation Z: checked)
    • Add Collider2D (BoxCollider2D or CapsuleCollider2D)
    • Add the KnockbackReceiver2D.cs script below
  2. From Attack Scripts:
    • Get target's KnockbackReceiver2D component
    • Call ApplyKnockback(attackerPosition, force, upForce)
    • Or use the preset methods for light/medium/heavy hits
  3. Optional - Movement Integration:
    • Your movement script should check IsInHitstun
    • Disable player input while in hitstun
  4. Optional - Weight System:
    • Set weight in Inspector (1 = normal, 2 = heavy, 0.5 = light)
    • Heavier entities receive less knockback

The Script (2D)

KnockbackReceiver2D.cs
// ============================================================
// KnockbackReceiver2D.cs
// Attach to any entity that can be knocked back when hit.
// Includes hitstun (action disable) and weight system.
// Unity 6.x
// ============================================================

using UnityEngine;
using System;

[RequireComponent(typeof(Rigidbody2D))]
public class KnockbackReceiver2D : MonoBehaviour
{
    // ========================================================
    // KNOCKBACK PARAMETERS - Tune these in the Inspector
    // ========================================================

    [Header("Weight")]
    [Tooltip("How resistant to knockback (1 = normal, 2 = half knockback, 0.5 = double)")]
    [SerializeField] [Range(0.1f, 5f)] private float weight = 1f;

    [Header("Knockback Decay")]
    [Tooltip("How fast horizontal knockback velocity decays")]
    [SerializeField] [Range(0f, 20f)] private float horizontalDecay = 8f;

    [Tooltip("Should we apply decay or let physics handle it?")]
    [SerializeField] private bool useManualDecay = true;

    [Header("Preset Forces (for ApplyLightHit, etc.)")]
    [Tooltip("Light hit: horizontal force")]
    [SerializeField] private float lightForce = 4f;
    [Tooltip("Light hit: upward force")]
    [SerializeField] private float lightUpForce = 2f;
    [Tooltip("Light hit: hitstun duration")]
    [SerializeField] private float lightHitstun = 0.1f;

    [Tooltip("Medium hit: horizontal force")]
    [SerializeField] private float mediumForce = 8f;
    [Tooltip("Medium hit: upward force")]
    [SerializeField] private float mediumUpForce = 4f;
    [Tooltip("Medium hit: hitstun duration")]
    [SerializeField] private float mediumHitstun = 0.2f;

    [Tooltip("Heavy hit: horizontal force")]
    [SerializeField] private float heavyForce = 15f;
    [Tooltip("Heavy hit: upward force")]
    [SerializeField] private float heavyUpForce = 8f;
    [Tooltip("Heavy hit: hitstun duration")]
    [SerializeField] private float heavyHitstun = 0.4f;

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

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

    private Rigidbody2D rb;
    private float hitstunTimer = 0f;
    private bool inHitstun = false;
    private Vector2 knockbackVelocity = Vector2.zero;

    // ========================================================
    // EVENTS - Subscribe to react to knockback
    // ========================================================

    /// <summary>
    /// Fired when knockback starts (direction, force)
    /// </summary>
    public event Action<Vector2, float> OnKnockbackStart;

    /// <summary>
    /// Fired when hitstun ends
    /// </summary>
    public event Action OnHitstunEnd;

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

    /// <summary>
    /// True while in hitstun (cannot act)
    /// </summary>
    public bool IsInHitstun => inHitstun;

    /// <summary>
    /// Remaining hitstun time
    /// </summary>
    public float HitstunRemaining => hitstunTimer;

    /// <summary>
    /// This entity's weight (affects knockback received)
    /// </summary>
    public float Weight => weight;

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

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // Process hitstun timer
        if (inHitstun)
        {
            hitstunTimer -= Time.deltaTime;

            if (hitstunTimer <= 0f)
            {
                EndHitstun();
            }
        }
    }

    void FixedUpdate()
    {
        // Apply manual knockback decay
        if (useManualDecay && inHitstun)
        {
            // Decay horizontal velocity toward zero
            float decayAmount = horizontalDecay * Time.fixedDeltaTime;
            float currentX = rb.linearVelocity.x;

            if (Mathf.Abs(currentX) > decayAmount)
            {
                float newX = currentX - Mathf.Sign(currentX) * decayAmount;
                rb.linearVelocity = new Vector2(newX, rb.linearVelocity.y);
            }
            else
            {
                rb.linearVelocity = new Vector2(0f, rb.linearVelocity.y);
            }
        }
    }

    // ========================================================
    // PUBLIC METHODS - Call these to apply knockback
    // ========================================================

    /// <summary>
    /// Apply knockback with full control over parameters
    /// </summary>
    /// <param name="attackerPosition">Position of the attacker (knockback away from this)</param>
    /// <param name="horizontalForce">Force pushing away horizontally</param>
    /// <param name="upwardForce">Force pushing upward</param>
    /// <param name="hitstunDuration">How long target can't act</param>
    public void ApplyKnockback(Vector2 attackerPosition, float horizontalForce,
                                float upwardForce, float hitstunDuration)
    {
        // Calculate direction FROM attacker TO this entity
        Vector2 direction = ((Vector2)transform.position - attackerPosition).normalized;

        // If directly above/below, default to facing away from attacker's right
        if (Mathf.Abs(direction.x) < 0.1f)
        {
            direction.x = transform.position.x > attackerPosition.x ? 1f : -1f;
        }

        // Apply weight modifier (heavier = less knockback)
        float weightModifier = 1f / weight;
        float finalHorizontal = horizontalForce * weightModifier;
        float finalUpward = upwardForce * weightModifier;

        // Build knockback velocity
        knockbackVelocity = new Vector2(
            Mathf.Sign(direction.x) * finalHorizontal,
            finalUpward
        );

        // Apply velocity directly
        rb.linearVelocity = knockbackVelocity;

        // Start hitstun
        inHitstun = true;
        hitstunTimer = hitstunDuration;

        // Fire event
        OnKnockbackStart?.Invoke(direction, horizontalForce);

        if (debugMode)
        {
            Debug.Log($"Knockback applied! Direction: {direction}, " +
                      $"Force: ({finalHorizontal}, {finalUpward}), " +
                      $"Hitstun: {hitstunDuration}s");
        }
    }

    /// <summary>
    /// Apply knockback in a specific direction (not away from attacker)
    /// </summary>
    public void ApplyKnockbackDirection(Vector2 direction, float horizontalForce,
                                         float upwardForce, float hitstunDuration)
    {
        float weightModifier = 1f / weight;
        knockbackVelocity = new Vector2(
            direction.x * horizontalForce * weightModifier,
            upwardForce * weightModifier
        );

        rb.linearVelocity = knockbackVelocity;

        inHitstun = true;
        hitstunTimer = hitstunDuration;

        OnKnockbackStart?.Invoke(direction, horizontalForce);
    }

    /// <summary>
    /// Apply a light hit knockback
    /// </summary>
    public void ApplyLightHit(Vector2 attackerPosition)
    {
        ApplyKnockback(attackerPosition, lightForce, lightUpForce, lightHitstun);
    }

    /// <summary>
    /// Apply a medium hit knockback
    /// </summary>
    public void ApplyMediumHit(Vector2 attackerPosition)
    {
        ApplyKnockback(attackerPosition, mediumForce, mediumUpForce, mediumHitstun);
    }

    /// <summary>
    /// Apply a heavy hit knockback
    /// </summary>
    public void ApplyHeavyHit(Vector2 attackerPosition)
    {
        ApplyKnockback(attackerPosition, heavyForce, heavyUpForce, heavyHitstun);
    }

    /// <summary>
    /// Immediately end hitstun (for special recovery moves)
    /// </summary>
    public void CancelHitstun()
    {
        if (inHitstun)
        {
            EndHitstun();
        }
    }

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

    /// <summary>
    /// End hitstun and restore control
    /// </summary>
    private void EndHitstun()
    {
        inHitstun = false;
        hitstunTimer = 0f;

        OnHitstunEnd?.Invoke();

        if (debugMode)
        {
            Debug.Log("Hitstun ended - control restored");
        }
    }
}

// ============================================================
// USAGE EXAMPLES:
//
// 1. From an attack script when dealing damage:
//    void DealDamage(GameObject target, int damage)
//    {
//        var knockback = target.GetComponent<KnockbackReceiver2D>();
//        if (knockback != null)
//        {
//            knockback.ApplyMediumHit(transform.position);
//        }
//        // Also apply damage to health, play effects, etc.
//    }
//
// 2. Custom knockback for special attacks:
//    void SuperPunch(GameObject target)
//    {
//        var knockback = target.GetComponent<KnockbackReceiver2D>();
//        // Massive knockback with long hitstun
//        knockback.ApplyKnockback(transform.position, 20f, 12f, 0.6f);
//    }
//
// 3. In your movement script, disable input during hitstun:
//    void Update()
//    {
//        if (knockbackReceiver.IsInHitstun)
//        {
//            // Don't process movement input
//            return;
//        }
//        // Normal movement code...
//    }
//
// 4. Subscribe to knockback events:
//    void Start()
//    {
//        GetComponent<KnockbackReceiver2D>().OnKnockbackStart += OnHit;
//        GetComponent<KnockbackReceiver2D>().OnHitstunEnd += OnRecover;
//    }
//    void OnHit(Vector2 dir, float force) { PlayHitAnimation(); }
//    void OnRecover() { PlayRecoverAnimation(); }
//
// 5. Combine with HitstopManager for full impact:
//    void OnAttackHit(GameObject target)
//    {
//        HitstopManager.Instance.TriggerMediumHit();
//        target.GetComponent<KnockbackReceiver2D>().ApplyMediumHit(transform.position);
//        ScreenShake.Instance.TriggerShake(0.2f, 0.3f);
//    }
//
// 6. For 3D: Change Rigidbody2D to Rigidbody, Vector2 to Vector3
// ============================================================

Common Issues

"Knockback goes the wrong direction"
Make sure you're calculating direction FROM attacker TO target, not the reverse. Also check if target's facing direction affects the calculation.

"Target gets stuck in walls"
Add collision checks or reduce knockback when near walls. Some videogames "slide" along walls; others cancel knockback.

"Combat feels floaty"
knockbackUp might be too high relative to gravity. Also check knockbackDecay - slow decay = floaty.

"Player gets knocked into pits unfairly"
Consider invincibility frames or reduced knockback near hazards. Or lean into it - environmental kills can be a feature, not a bug.

Combine With

Knockback Variations

Type Description
Fixed knockback Same force regardless of damage dealt
Damage-scaled More damage = more knockback (Smash Bros.)
Weight-modified Heavier targets fly less far
Directional Different attacks knock in different directions
Wall bounce Hitting a wall bounces you back into play

Related

Glossary Terms