Easy Learn C#

Jump Mechanics

Introduction to Jump Mechanics

Jump mechanics are one of the most important elements in many games, especially platformers. Creating a satisfying jump that feels good to players requires understanding both the physics and the game design principles behind jump mechanics.

In this guide, you'll learn:

  • How to implement basic jumps in Unity
  • Different types of jump mechanics
  • Advanced jump techniques (double jump, wall jump)
  • How to fine-tune your jumps for better gameplay feel
  • Common jump-related problems and solutions

Basic Jump Implementation

The most common approach to jumping in Unity is to apply an upward force or velocity to a Rigidbody when the player presses the jump button.

Basic Jump Implementation (2D):


using UnityEngine;

public class BasicJump2D : MonoBehaviour
{
    // Jump settings
    public float jumpForce = 10f;          // How high the player jumps
    public Transform groundCheck;           // GameObject checking if we're grounded
    public float groundCheckRadius = 0.2f;  // Size of the ground check circle
    public LayerMask groundLayer;           // Which layers count as ground
    
    // References
    private Rigidbody2D rb;                 // Physics component
    private bool isGrounded;                // Tracks grounded state
    
    void Start()
    {
        // Get the Rigidbody2D component
        rb = GetComponent();
        // This component handles the physics simulation
    }
    
    void Update()
    {
        // Check if the player is standing on ground
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        // This creates a circle at groundCheck position and sees if it overlaps with ground
        
        // Jump when the player presses the jump button while grounded
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            Jump();
            // Call our jump method when conditions are met
        }
    }
    
    void Jump()
    {
        // Apply an impulse force upward
        rb.velocity = new Vector2(rb.velocity.x, 0f);  // Zero out current Y velocity
        rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        // This makes the character jump by adding an instant upward force
    }
    
    // Visual debugging of the ground check radius
    void OnDrawGizmosSelected()
    {
        if (groundCheck == null)
            return;
            
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
        // This draws a yellow circle showing the ground detection area in the editor
    }
}
                            

Key components of the basic jump:

  • Ground check: Detects if the player is standing on ground
  • Jump force: Controls the height of the jump
  • AddForce: Applies immediate upward force (using ForceMode.Impulse)
  • Input check: Only jump when the jump button is pressed

Basic Jump Implementation (3D):


using UnityEngine;

public class BasicJump3D : MonoBehaviour
{
    public float jumpForce = 7f;           // How high the player jumps
    public Transform groundCheck;           // GameObject checking if we're grounded
    public float groundCheckDistance = 0.4f; // How far to check for ground
    public LayerMask groundLayer;           // Which layers count as ground
    
    private Rigidbody rb;                   // Physics component
    private bool isGrounded;                // Tracks grounded state
    
    void Start()
    {
        // Get the Rigidbody component
        rb = GetComponent();
        // This component handles 3D physics simulation
    }
    
    void Update()
    {
        // Check if the player is standing on ground using a raycast
        isGrounded = Physics.Raycast(
            groundCheck.position,      // Start position of ray
            Vector3.down,              // Direction of ray (downward)
            groundCheckDistance,       // Length of ray
            groundLayer                // Which layers to check against
        );
        // This casts a ray downward and sees if it hits ground
        
        // Jump when the player presses the jump button while grounded
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            Jump();
            // Call our jump method when conditions are met
        }
    }
    
    void Jump()
    {
        // Apply an impulse force upward
        rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        // This makes the character jump by adding an instant upward force
    }
    
    // Visual debugging of the ground check
    void OnDrawGizmosSelected()
    {
        if (groundCheck == null)
            return;
            
        Gizmos.color = Color.red;
        Gizmos.DrawLine(groundCheck.position, groundCheck.position + Vector3.down * groundCheckDistance);
        // This draws a red line showing the ground detection in the editor
    }
}
                            

Ground Check Methods

Different ways to check if the player is on the ground:

  1. Raycast: Cast a ray downward from the player
  2. OverlapCircle/OverlapBox: Check if a shape overlaps with ground
  3. OnCollisionStay/OnCollisionExit: Track collision events
  4. Character Controller's isGrounded: Use built-in property if using Character Controller

Each method has pros and cons regarding precision and performance.

Jump Physics and Feel

Creating a jump that feels satisfying requires more than just applying force. Let's explore techniques to make jumps feel better.

Improved Jump Physics:


using UnityEngine;

public class ImprovedJump : MonoBehaviour
{
    [Header("Jump Parameters")]
    public float jumpForce = 12f;               // Initial jump force
    public float fallMultiplier = 2.5f;         // Increased gravity when falling
    public float lowJumpMultiplier = 2f;        // Increased gravity when jump button released
    
    [Header("Ground Check")]
    public Transform groundCheck;
    public float groundCheckRadius = 0.2f;
    public LayerMask groundLayer;
    
    // References
    private Rigidbody2D rb;
    private bool isGrounded;
    private bool isJumping;
    
    void Start()
    {
        rb = GetComponent();
    }
    
    void Update()
    {
        // Ground check
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        // This creates a circle at groundCheck position and checks for ground
        
        // If we're on the ground, we're not jumping
        if (isGrounded && rb.velocity.y <= 0)
        {
            isJumping = false;
        }
        
        // Jump input
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            Jump();
            // Initiate jump when jump button is pressed and we're grounded
        }
        
        // Apply better jump physics
        ApplyJumpPhysics();
        // This method applies variable gravity for better jump feel
    }
    
    void Jump()
    {
        // Reset Y velocity then apply jump force
        rb.velocity = new Vector2(rb.velocity.x, 0f);
        rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        
        isJumping = true;
        // Track that we're now in a jump
    }
    
    void ApplyJumpPhysics()
    {
        // If we're falling, apply the fall multiplier
        if (rb.velocity.y < 0)
        {
            // Apply extra gravity when falling
            rb.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
            // This makes the character fall faster than it rises, creating a more satisfying arc
        }
        // If we're jumping but the jump button isn't held, apply the low jump multiplier
        else if (rb.velocity.y > 0 && !Input.GetButton("Jump"))
        {
            // Apply extra gravity when ascending but jump button released
            rb.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
            // This allows for variable jump height based on how long the button is pressed
        }
    }
    
    void OnDrawGizmosSelected()
    {
        if (groundCheck == null)
            return;
            
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
    }
}
                            

What makes this jump feel better:

  • Variable gravity: Increased gravity when falling creates a more satisfying arc
  • Button sensitivity: Quick-release of jump button creates a shorter jump
  • Better grounding: Proper ground detection ensures consistent jumping
  • Reset Y velocity: Ensures consistent jump height regardless of previous motion

Jump Feel Tips

  • Jump Arc: The path the character follows during a jump should have a nice arc
  • Gravity Multipliers: Different gravity for rising/falling creates better feel
  • Variable Height: Allow players to control jump height by button press duration
  • Coyote Time: Allow jumps slightly after leaving a platform
  • Jump Buffering: Allow jumps slightly before landing
  • Visual Feedback: Add animations, particles, or sound for feedback

Advanced Jump Techniques

Let's explore more advanced jump mechanics like double jump, wall jump, and variable height jumping.

Double Jump Implementation:


using UnityEngine;

public class DoubleJumpController : MonoBehaviour
{
    [Header("Jump Settings")]
    public float jumpForce = 10f;           // Initial jump force
    public float doubleJumpForce = 8f;      // Force of the second jump
    public int maxJumps = 2;                // Maximum number of jumps allowed
    
    [Header("Ground Check")]
    public Transform groundCheck;
    public float groundCheckRadius = 0.2f;
    public LayerMask groundLayer;
    
    // References and state tracking
    private Rigidbody2D rb;
    private int jumpsRemaining;             // How many jumps we have left
    private bool isGrounded;
    
    void Start()
    {
        rb = GetComponent();
        jumpsRemaining = maxJumps;  // Start with full jumps
    }
    
    void Update()
    {
        // Check if grounded
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        
        // Reset jumps when touching ground
        if (isGrounded && rb.velocity.y <= 0)
        {
            jumpsRemaining = maxJumps;
            // When we land, restore all available jumps
        }
        
        // Jump input
        if (Input.GetButtonDown("Jump") && jumpsRemaining > 0)
        {
            // Choose the appropriate jump force
            float force = (jumpsRemaining == maxJumps) ? jumpForce : doubleJumpForce;
            // Use the primary jump force for the first jump, and double jump force for subsequent jumps
            
            // Reset Y velocity and apply jump force
            rb.velocity = new Vector2(rb.velocity.x, 0f);
            rb.AddForce(Vector2.up * force, ForceMode2D.Impulse);
            
            // Decrement jumps remaining
            jumpsRemaining--;
            // Track that we've used up one of our available jumps
        }
    }
}
                            

Key components of double jump:

  • Jump counter: Track how many jumps the player has used
  • Reset on ground: Restore jumps when the player touches the ground
  • Different forces: Often the second jump is slightly weaker
  • Separate input: Each jump requires a new button press

Wall Jump Implementation:


using UnityEngine;

public class WallJumpController : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed = 7f;
    public float jumpForce = 12f;
    
    [Header("Wall Jump")]
    public float wallSlideSpeed = 2f;       // How fast player slides down walls
    public float wallJumpForce = 16f;       // Force of wall jump
    public float wallJumpAngle = 65f;       // Angle of wall jump (degrees)
    public float wallJumpTime = 0.15f;      // Time player can't control movement after wall jump
    
    [Header("Checks")]
    public Transform groundCheck;
    public Transform wallCheck;
    public float groundCheckRadius = 0.2f;
    public float wallCheckDistance = 0.5f;
    public LayerMask groundLayer;
    
    // References and state
    private Rigidbody2D rb;
    private bool isGrounded;
    private bool isWallSliding;
    private bool isTouchingWall;
    private float horizontalInput;
    private int facingDirection = 1;  // 1 for right, -1 for left
    private float wallJumpTimer;      // Timer for wall jump input lockout
    
    void Start()
    {
        rb = GetComponent();
    }
    
    void Update()
    {
        // Process inputs and state
        CheckInput();
        CheckGrounded();
        CheckWallSliding();
        
        // Handle wall jump timer
        if (wallJumpTimer > 0)
        {
            wallJumpTimer -= Time.deltaTime;
            // Count down the timer that prevents movement after a wall jump
        }
        
        // Handle jump input
        if (Input.GetButtonDown("Jump"))
        {
            if (isGrounded)
            {
                NormalJump();
                // If on ground, do a normal jump
            }
            else if (isWallSliding)
            {
                WallJump();
                // If sliding on a wall, do a wall jump
            }
        }
    }
    
    void FixedUpdate()
    {
        // Move horizontally if not in wall jump recovery
        if (wallJumpTimer <= 0)
        {
            rb.velocity = new Vector2(horizontalInput * moveSpeed, rb.velocity.y);
            // Apply horizontal movement based on input
        }
        
        // Apply wall slide if touching wall
        if (isWallSliding)
        {
            // Limit downward velocity while wall sliding
            if (rb.velocity.y < -wallSlideSpeed)
            {
                rb.velocity = new Vector2(rb.velocity.x, -wallSlideSpeed);
                // This creates a slower descent when sliding against a wall
            }
        }
    }
    
    void CheckInput()
    {
        horizontalInput = Input.GetAxisRaw("Horizontal");
        
        // Update facing direction
        if (horizontalInput > 0 && facingDirection == -1)
        {
            Flip();
        }
        else if (horizontalInput < 0 && facingDirection == 1)
        {
            Flip();
        }
    }
    
    void CheckGrounded()
    {
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
    }
    
    void CheckWallSliding()
    {
        // Check if touching wall
        isTouchingWall = Physics2D.Raycast(
            wallCheck.position,
            Vector2.right * facingDirection,
            wallCheckDistance,
            groundLayer
        );
        // This casts a ray in the facing direction to detect walls
        
        // Wall slide if touching wall, not grounded, and moving toward wall
        isWallSliding = isTouchingWall && !isGrounded && horizontalInput * facingDirection > 0;
        // Only wall slide when actively pushing toward the wall
    }
    
    void NormalJump()
    {
        rb.velocity = new Vector2(rb.velocity.x, 0f);
        rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
    }
    
    void WallJump()
    {
        // Stop sliding
        isWallSliding = false;
        
        // Calculate wall jump direction
        float wallJumpDirectionX = -facingDirection;  // Jump away from wall
        Vector2 wallJumpDirection = new Vector2(wallJumpDirectionX, 1f).normalized;
        
        // Convert angle to radians for calculation
        float wallJumpAngleRadians = wallJumpAngle * Mathf.Deg2Rad;
        Vector2 forceToApply = new Vector2(
            -facingDirection * wallJumpForce * Mathf.Cos(wallJumpAngleRadians),
            wallJumpForce * Mathf.Sin(wallJumpAngleRadians)
        );
        
        // Reset velocity and apply force
        rb.velocity = Vector2.zero;
        rb.AddForce(forceToApply, ForceMode2D.Impulse);
        
        // Flip character to face jump direction
        if (facingDirection * wallJumpDirectionX < 0)
        {
            Flip();
        }
        
        // Set timer to prevent immediate movement control
        wallJumpTimer = wallJumpTime;
    }
    
    void Flip()
    {
        // Flip the character's facing direction
        facingDirection *= -1;
        transform.Rotate(0f, 180f, 0f);
        // This rotates the character to face the other way
    }
    
    void OnDrawGizmosSelected()
    {
        // Draw ground check
        if (groundCheck != null)
        {
            Gizmos.color = Color.green;
            Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
        }
        
        // Draw wall check
        if (wallCheck != null)
        {
            Gizmos.color = Color.blue;
            Gizmos.DrawLine(
                wallCheck.position,
                wallCheck.position + Vector3.right * facingDirection * wallCheckDistance
            );
        }
    }
}
                            

Key components of wall jump:

  • Wall detection: Raycasts to check if touching a wall
  • Wall sliding: Slows descent when pressed against a wall
  • Jump direction: Jumps away from the wall at an angle
  • Input lockout: Brief period after wall jump where player can't change direction
  • Character flipping: Changes facing direction based on movement

Coyote Time and Jump Buffering:


using UnityEngine;

public class AdvancedJumpController : MonoBehaviour
{
    [Header("Jump Parameters")]
    public float jumpForce = 12f;
    
    [Header("Coyote Time")]
    public float coyoteTime = 0.15f;         // Time player can jump after leaving ground
    private float coyoteTimeCounter;         // Tracks remaining coyote time
    
    [Header("Jump Buffering")]
    public float jumpBufferTime = 0.2f;      // Time before landing that jump input is remembered
    private float jumpBufferCounter;         // Tracks remaining buffer time
    
    [Header("Ground Check")]
    public Transform groundCheck;
    public float groundCheckRadius = 0.2f;
    public LayerMask groundLayer;
    
    // References
    private Rigidbody2D rb;
    private bool isGrounded;
    
    void Start()
    {
        rb = GetComponent();
    }
    
    void Update()
    {
        // Check if grounded
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        
        // COYOTE TIME LOGIC
        if (isGrounded)
        {
            // Reset coyote time when grounded
            coyoteTimeCounter = coyoteTime;
            // This gives the player the full coyote time window when leaving a platform
        }
        else
        {
            // Count down coyote time when in air
            coyoteTimeCounter -= Time.deltaTime;
            // This decreases the remaining time window for jumping after leaving ground
        }
        
        // JUMP BUFFER LOGIC
        if (Input.GetButtonDown("Jump"))
        {
            // Start the jump buffer when jump is pressed
            jumpBufferCounter = jumpBufferTime;
            // This starts the window of time where a jump input is remembered
        }
        else
        {
            // Count down jump buffer time
            jumpBufferCounter -= Time.deltaTime;
            // This decreases the remaining time window for the buffered jump
        }
        
        // JUMP EXECUTION
        // Jump if we have a buffered jump input AND either grounded or in coyote time
        if (jumpBufferCounter > 0f && coyoteTimeCounter > 0f)
        {
            // Execute jump
            rb.velocity = new Vector2(rb.velocity.x, 0f);
            rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
            
            // Reset jump buffer to prevent double jumps
            jumpBufferCounter = 0f;
            // Clear the buffered jump input so we don't jump again
        }
        
        // Cancel coyote time if player is falling
        if (rb.velocity.y < 0)
        {
            coyoteTimeCounter = 0f;
            // If player is falling (not from a jump), don't allow coyote time
        }
    }
}
                            

Making jumps feel more responsive:

  • Coyote Time: Allows the player to jump shortly after leaving a platform
  • Jump Buffering: Remembers jump input shortly before landing
  • Combined Effect: These techniques make the controls feel more responsive
  • Counter Reset: Carefully resetting counters prevents unintended jumps

Jump Variations and Game Feel

Different games require different jump mechanics. Here are some common variations and how to implement them.

Jump Variations

  • Floaty Jump: Low gravity, high jump force, longer hang time
  • Snappy Jump: High gravity, high jump force, quick ascent and descent
  • Charged Jump: Hold button to increase jump power
  • Air Control: How much the player can influence movement while in air
  • Multidirectional Jump: Can jump in any direction (common in zero-gravity games)

Charged Jump Implementation:


using UnityEngine;
using UnityEngine.UI; // For the UI elements

public class ChargedJumpController : MonoBehaviour
{
    [Header("Jump Settings")]
    public float minJumpForce = 5f;       // Minimum jump force
    public float maxJumpForce = 15f;      // Maximum jump force
    public float chargeTime = 1.5f;       // Time to reach maximum charge
    
    [Header("Charge Indicator")]
    public Image chargeBar;               // UI element showing charge (optional)
    
    [Header("Ground Check")]
    public Transform groundCheck;
    public float groundCheckRadius = 0.2f;
    public LayerMask groundLayer;
    
    // References and state
    private Rigidbody2D rb;
    private bool isGrounded;
    private bool isCharging = false;
    private float chargeAmount = 0f;
    
    void Start()
    {
        rb = GetComponent();
        
        // Initialize charge bar if assigned
        if (chargeBar != null)
        {
            chargeBar.fillAmount = 0f;
        }
    }
    
    void Update()
    {
        // Check if grounded
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        
        // Start charging when the jump button is pressed while grounded
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            isCharging = true;
            chargeAmount = 0f;
            // Start charging the jump
        }
        
        // Charge while jump button is held
        if (Input.GetButton("Jump") && isCharging)
        {
            // Increase charge amount over time
            chargeAmount += Time.deltaTime / chargeTime;
            chargeAmount = Mathf.Clamp01(chargeAmount);
            // Increase charge but cap it at 100%
            
            // Update UI if assigned
            if (chargeBar != null)
            {
                chargeBar.fillAmount = chargeAmount;
                // Update the visual charge indicator
            }
        }
        
        // Release jump when button is released
        if (Input.GetButtonUp("Jump") && isCharging)
        {
            // Calculate jump force based on charge amount
            float jumpForce = Mathf.Lerp(minJumpForce, maxJumpForce, chargeAmount);
            
            // Apply the jump force
            rb.velocity = new Vector2(rb.velocity.x, 0f);
            rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
            
            // Reset charging state
            isCharging = false;
            chargeAmount = 0f;
            
            // Update UI
            if (chargeBar != null)
            {
                chargeBar.fillAmount = chargeAmount;
            }
        }
        
        // Cancel charging if player leaves the ground
        if (!isGrounded && isCharging)
        {
            isCharging = false;
            chargeAmount = 0f;
            
            // Update UI
            if (chargeBar != null)
            {
                chargeBar.fillAmount = chargeAmount;
            }
        }
    }
}
                            

Charged jump components:

  • Charge timer: Tracks how long the button is held
  • Visual feedback: UI or animation showing charge level
  • Variable force: Jump force based on charge amount
  • Cancel conditions: How to handle interrupted charging

Common Jump Problems and Solutions

  1. Inconsistent Ground Detection

    Solution: Use multiple raycasts or a slightly larger collision check area. Consider using a dedicated "feet" collider.

  2. Sticking to Walls

    Solution: Use a layer mask that excludes walls from ground checks, or implement proper wall detection for wall jumps.

  3. Unresponsive Jump Controls

    Solution: Implement coyote time and jump buffering as shown earlier.

  4. Varying Jump Heights

    Solution: Apply extra gravity when the jump button is released to cut the jump short.

  5. Jittery Movement

    Solution: Apply movement in FixedUpdate() and handle input in Update() for smoother physics.

  6. Getting Stuck in Colliders

    Solution: Adjust collider sizes and use Physics2D/Physics settings to prevent tunneling.

  7. Poor Game Feel

    Solution: Add animations, particles, and sound effects. Tweak gravity multipliers for better jump arcs.

Best Practices for Jump Mechanics

  1. Prioritize feel over realism: Games often have exaggerated physics for better gameplay
  2. Test extensively: What feels good to you may not feel good to players
  3. Add visual feedback: Jump animations, dust particles, impact effects
  4. Add audio feedback: Jump sounds, landing sounds
  5. Consider accessibility: Options to adjust timing windows or auto-jump features
  6. Be consistent: Maintain consistent jump behavior throughout your game
  7. Reference existing games: Study successful platformers to understand what works
  8. Use Unity's input system: Consider the new Input System for better cross-platform support
  9. Separate logic: Keep ground detection, input handling, and physics application in separate methods
  10. Document your code: Make it easy for others (or future you) to understand your jump implementation