Titel: Issues with Player Knockback and Collision Response in Unity

Hello Unity Community,

I’m currently developing a game and I’m facing an issue where my player character is knocked back excessively when colliding with enemies. This problem occurs specifically when the enemy hits the player with their attack while the player simultaneously strikes the enemy. I suspect that the issue lies within the scripts that manage player movement and enemy attack logic.

Issue Description:

When my player character (controlled by the PlayerMoment script) collides with an enemy (handled by the ColliderAttackScript), the player is pushed back too far, making the gameplay feel unnatural.

Relevant Scripts

Here are the two scripts I’m working with:

  1. PlayerMoment Script:
using System.Collections;
using UnityEngine;

public class PlayerMoment : MonoBehaviour
{
    // Movement Variables
    [Header("Horizontal Movement Settings")]
    public float moveSpeed; // The speed at which the player moves
    private float moveDirectionX; // Horizontal input value
    private float moveDirectionY; // Vertical input value

    [Header("Knockback-System")]
    [SerializeField] private float knockbackPower; // Multiplier for knockback force
    private float knockbackPowerX = 7f; // Knockback force in X direction
    private float knockbackPowerY = 0.8f; // Knockback force in Y direction
    [HideInInspector] public bool isKnockedBack = false; // Check if the player is currently knocked back
    [HideInInspector] public bool allowKnockback = true; // Allow knockback?

    [Header("Jump Settings Player")]
    [SerializeField] public float JumpForce; // Jump force value
    [SerializeField] private Transform groundCheck; // Position for ground check
    [SerializeField] private float groundCheckY = 0.2f; // Distance to check for ground
    [SerializeField] private LayerMask WhatIsGround; // Layer mask for ground detection
    [SerializeField] private float groundCheckX; // Horizontal offset for ground check

    [Header("Important-System")]
    public Rigidbody2D rb; // Rigidbody component for physics
    Animator anim; // Animator component for animations
    private TrailRenderer _trailRenderer; // Trail renderer for visual effect

    [Header("GameObject-Referenzes")]
    public GameObject RedKnight; // Reference to the red knight enemy
    public GameObject BarsPosition; // Reference to the Bars enemy position

    void Start()
    {
        rb = GetComponent<Rigidbody2D>(); // Get the Rigidbody2D component
        anim = GetComponent<Animator>(); // Get the Animator component
        _trailRenderer = GetComponent<TrailRenderer>(); // Get the TrailRenderer component
    }

    void Update()
    {
        if (!isKnockedBack) // Check if the player is not currently knocked back
        {
            GetInput(); // Get player input
            Move(); // Move the player
            Jump(); // Handle jumping
            Flip(); // Flip the player's sprite based on direction
        }
    }

    public void KnockbackSystem(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("BarsEnemy")) // Check if colliding with Bars enemy
        {
            float direction = Mathf.Sign(transform.position.x - BarsPosition.transform.position.x);
            Vector2 knockbackForce = new Vector2(direction * knockbackPowerX, knockbackPowerY * knockbackPower);
            rb.AddForce(knockbackForce, ForceMode2D.Impulse); // Apply knockback force
            
            isKnockedBack = true; // Set knockback status
            StartCoroutine(ResetKnockback()); // Start coroutine to reset knockback
        }
        if (collision.gameObject.CompareTag("KnightRed")) // Check if colliding with red knight
        {
            if (allowKnockback == true)
            {
                float direction = Mathf.Sign(transform.position.x - RedKnight.transform.position.x);
                Vector2 knockbackForce = new Vector2(direction * knockbackPowerX, knockbackPowerY * knockbackPower);
                rb.AddForce(knockbackForce, ForceMode2D.Impulse); // Apply knockback force

                isKnockedBack = true; // Set knockback status
                StartCoroutine(ResetKnockback()); // Start coroutine to reset knockback
                allowKnockback = false; // Prevent further knockback until reset
            }
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        KnockbackSystem(collision); // Call the knockback system on collision
    }

    void GetInput()
    {
        moveDirectionX = Input.GetAxisRaw("Horizontal"); // Get horizontal input
        moveDirectionY = Input.GetAxisRaw("Vertical"); // Get vertical input
    }

    void Move()
    {
        rb.velocity = new Vector2(moveSpeed * moveDirectionX, rb.velocity.y); // Move the player
        anim.SetBool("isWalking", rb.velocity.x != 0 && Grounded()); // Set walking animation
    }

    void Flip()
    {
        if (moveDirectionX < 0) // If moving left
        {
            transform.localScale = new Vector2(-4, transform.localScale.y); // Flip the sprite
        }
        else if (moveDirectionX > 0) // If moving right
        {
            transform.localScale = new Vector2(4, transform.localScale.y); // Flip the sprite
        }
    }

    public bool Grounded()
    {
        // Check if the player is grounded
        return Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckY, WhatIsGround)
            || Physics2D.Raycast(groundCheck.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, WhatIsGround)
            || Physics2D.Raycast(groundCheck.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, WhatIsGround);
    }

    private void Jump()
    {
        if (Input.GetButtonUp("Jump") && (rb.velocity.y > 0))
        {
            rb.velocity = new Vector2(rb.velocity.x, 0); // Stop jump if button released
        }
        if (Input.GetButtonDown("Jump") && Grounded())
        {
            rb.velocity = new Vector2(rb.velocity.x, JumpForce); // Apply jump force
        }
        anim.SetBool("isJumping", !Grounded() || Input.GetButtonDown("Jump")); // Set jump animation
    }

    private IEnumerator ResetKnockback()
    {
        yield return new WaitForSeconds(0.5f); // Wait 0.5 seconds
        isKnockedBack = false; // Reset knockback status
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red; // Set color to red
        Gizmos.DrawWireSphere(groundCheck.position, groundCheckY); // Draw ground check sphere
    }
}

ColliderAttackScript:

using System.Collections;
using UnityEngine;

public class ColliderAttackScript : MonoBehaviour
{
    [Header("Important-Referenzes")]
    private Rigidbody2D PlayerRb; // Reference to player's Rigidbody2D
    private Animator RedKnightAnimation; // Reference to the enemy's Animator
    public LayerMask PlayerLayer; // Layer mask for player detection

    [Header("GameObject-Referenzes-And-Scripts")]
    public GameObject AttackPoint; // Point of attack for collision detection
    public GameObject player; // Reference to the player
    public GameObject PlayerPosition; // Reference to player's position

    private PlayerMoment playerMomentScript; // Reference to PlayerMoment script
    private PlayerHealthSystem PlayerHealthScripts; // Reference to PlayerHealthSystem script

    [Header("Attack-System")]
    [SerializeField] private float attackRadius; // Radius for attack detection
    [SerializeField] private int AttackDamage; // Damage inflicted on the player
    [SerializeField] private float KnockbackMultiplier; // Multiplier for knockback force

    private float knockbackPowerX = 7f; // Knockback force in X direction
    private float KnockbackPowerY = 0.5f; // Knockback force in Y direction

    void Start()
    {
        if (player != null)
        {
            RedKnightAnimation = player.GetComponent<Animator>(); // Get Animator component
        }
        PlayerHealthScripts = FindObjectOfType<PlayerHealthSystem>(); // Get PlayerHealthSystem
        playerMomentScript = FindObjectOfType<PlayerMoment>(); // Get PlayerMoment script
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (player != null && collision.gameObject.CompareTag("Player")) // Check if colliding with player
        {
            RedKnightAnimation.SetTrigger("isAttacking"); // Trigger attack animation
            AttacktSystem(); // Call the attack system
        }
    }

    public void AttacktSystem()
    {
        if (AttackPoint == null) return; // Exit if AttackPoint is null

        int combinedLayerMask = PlayerLayer; // Combine layer masks
        Collider2D[] PlayerHit = Physics2D.OverlapCircleAll(AttackPoint.transform.position, attackRadius, combinedLayerMask); // Detect players in the attack radius

        foreach (Collider2D playerGameObject in PlayerHit) // Iterate through hit players
        {
            PlayerHealthSystem playerHealthSystem = playerGameObject.GetComponent<PlayerHealthSystem>(); // Get PlayerHealthSystem

            if (playerHealthSystem != null)
            {
                playerHealthSystem.TakeDamage(AttackDamage); // Inflict damage on player
                playerMomentScript.allowKnockback = true; // Allow knockback for the player
                float direction = Mathf.Sign(player.transform.position.x - playerGameObject.transform.position.x); // Determine direction for knockback
                Vector2 knockbackForce = new Vector2(direction * knockbackPowerX * KnockbackMultiplier, KnockbackPowerY * KnockbackMultiplier);
                playerGameObject.GetComponent<Rigidbody2D>().AddForce(knockbackForce, ForceMode2D.Impulse); // Apply knockback force
                StartCoroutine(ResetKnockback()); // Start coroutine to reset knockback
            }
        }
    }

    IEnumerator ResetKnockback()
    {
        yield return new WaitForSeconds(0.5f); // Wait 0.5 seconds
        playerMomentScript.allowKnockback = false; // Prevent further knockback until reset
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red; // Set color to red for visibility
        Gizmos.DrawWireSphere(AttackPoint.transform.position, attackRadius); // Draw attack radius
    }
}



Observations:

  • The excessive knockback occurs specifically when the enemy attacks the player and both characters collide at the same time.
  • It appears that the combined knockback forces from both the player’s and enemy’s scripts are too strong.
  • I am considering using Mathf.Clamp() to limit the knockback force but am unsure how to implement this correctly

Questions

  1. What could be the cause of the excessive knockback?
  2. How can I effectively clamp the knockback force to prevent this issue?

I would appreciate any insights or suggestions to resolve this problem. Thank you!

Perhaps multi-colliding? You do get one callback per collider involved.

Perhaps too large an added-up knockback?

You can debug it and find out first-hand! See below.

Obviously you can clamp a single instance of knockback just with good old clamp or clamp magnitude.

But to clamp several different knockbacks that might happen all in the same frame, or in very short successive frames, one approach is to keep a float that is a “knockback bank.”

  • it would start full
  • when a knockback comes in, it is checked against the bank
    • if there is enough in the bank, that amount is withdrawn and applied
    • if there isn’t enough, then the knockback is limited to the amount in the bank and the bank is zeroed
  • the knockback bank would recharge, probably in one of the following ways:
    • after a few frames of no knocks, full recharge?
    • after a few frames of no knocks, slow recharge?
    • charge slowly immediately after use
  • never charges past its “full” amount, which would be chosen to not be too much

That way if you get bonked too much all at once, the bank runs empty and the knockback is zeroed.

To find out what your code is actually doing… that means… time to start debugging!

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

1 Like

Thanks a lot for your detailed answer! The idea of multiple collisions or knockback adding up makes a lot of sense, and I’ll definitely dig into that.

I’ve actually tried using Debug.Log all over the possible problematic code before, but I removed it after checking, so I’ll focus more on the knockback bank and clamping ideas you mentioned.

I really like the knockback bank approach, so I’ll give it a shot and experiment with how it recharges.

Thanks again for your help! Have a great evening!

1 Like

I use it for other stuff like bangs and bumps when I crash my spaceship in Jetpack Kurt.

One helpful trick for debugging is to actually put the knockback bank value onscreen as a slider ramp, something you can visually see how full it is, to help you adjust the refill rate, etc., as you are playtesting. Then when the game is finished just turn that UI part off. :slight_smile:

1 Like