Part of these game systems:
intermediate Movement

2D Platformer Mechanics

Wall jump, wall slide, dash, and double jump mechanics for 2D platformers. Drop-in extension for the 2D Player Controller.

Unity 2022.3+ · 3.5 KB · PlatformerMechanics2D.cs

How to Use

1

Attach PlatformerMechanics2D to your player (alongside PlayerController2D)

2

Create a WallCheck child transform at character's side

3

Assign ground and wall check references

4

Enable/disable individual mechanics in inspector

5

Wall slide: hold into wall while falling

6

Wall jump: press Jump while wall sliding

7

Double jump: press Jump in mid-air

8

Dash: press Shift (configurable) for burst speed

Source Code

PlatformerMechanics2D.cs
C#
using UnityEngine;

/// <summary>
/// Advanced 2D platformer mechanics: wall slide, wall jump, dash, and double jump.
/// Works alongside PlayerController2D.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlatformerMechanics2D : MonoBehaviour
{
    [Header("Wall Detection")]
    [SerializeField] private Transform wallCheck;
    [SerializeField] private float wallCheckDistance = 0.3f;
    [SerializeField] private LayerMask wallLayer;

    [Header("Wall Slide")]
    [SerializeField] private bool enableWallSlide = true;
    [SerializeField] private float wallSlideSpeed = 2f;

    [Header("Wall Jump")]
    [SerializeField] private bool enableWallJump = true;
    [SerializeField] private Vector2 wallJumpForce = new Vector2(12f, 16f);
    [SerializeField] private float wallJumpDuration = 0.15f;

    [Header("Double Jump")]
    [SerializeField] private bool enableDoubleJump = true;
    [SerializeField] private float doubleJumpForce = 14f;
    [SerializeField] private int maxAirJumps = 1;

    [Header("Dash")]
    [SerializeField] private bool enableDash = true;
    [SerializeField] private float dashSpeed = 25f;
    [SerializeField] private float dashDuration = 0.15f;
    [SerializeField] private float dashCooldown = 0.8f;
    [SerializeField] private KeyCode dashKey = KeyCode.LeftShift;

    [Header("Ground Check")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckRadius = 0.2f;
    [SerializeField] private LayerMask groundLayer;

    private Rigidbody2D rb;
    private int airJumpsRemaining;
    private bool isWallSliding;
    private bool isWallJumping;
    private float wallJumpTimer;
    private int wallDirection;
    private bool isDashing;
    private float dashTimer;
    private float dashCooldownTimer;
    private bool isGrounded;
    private float originalGravityScale;

    /// <summary>Is the character currently wall sliding?</summary>
    public bool IsWallSliding => isWallSliding;

    /// <summary>Is the character currently dashing?</summary>
    public bool IsDashing => isDashing;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        originalGravityScale = rb.gravityScale;
    }

    private void Update()
    {
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);

        if (isGrounded)
            airJumpsRemaining = maxAirJumps;

        CheckWall();
        HandleWallSlide();
        HandleWallJump();
        HandleDoubleJump();
        HandleDash();
    }

    private void CheckWall()
    {
        float moveInput = Input.GetAxisRaw("Horizontal");
        bool isTouchingWall = false;

        if (wallCheck != null && Mathf.Abs(moveInput) > 0.1f)
        {
            isTouchingWall = Physics2D.Raycast(
                wallCheck.position,
                Vector2.right * Mathf.Sign(moveInput),
                wallCheckDistance,
                wallLayer
            );
            wallDirection = (int)Mathf.Sign(moveInput);
        }

        isWallSliding = enableWallSlide && isTouchingWall && !isGrounded && rb.linearVelocity.y < 0f;
    }

    private void HandleWallSlide()
    {
        if (isWallSliding)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, -wallSlideSpeed);
        }
    }

    private void HandleWallJump()
    {
        if (isWallJumping)
        {
            wallJumpTimer -= Time.deltaTime;
            if (wallJumpTimer <= 0f)
                isWallJumping = false;
        }

        if (enableWallJump && isWallSliding && Input.GetButtonDown("Jump"))
        {
            isWallJumping = true;
            wallJumpTimer = wallJumpDuration;

            rb.linearVelocity = new Vector2(
                -wallDirection * wallJumpForce.x,
                wallJumpForce.y
            );

            // Flip character away from wall
            transform.localScale = new Vector3(-wallDirection, 1f, 1f);
        }
    }

    private void HandleDoubleJump()
    {
        if (!enableDoubleJump) return;

        if (Input.GetButtonDown("Jump") && !isGrounded && !isWallSliding && airJumpsRemaining > 0)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, doubleJumpForce);
            airJumpsRemaining--;
        }
    }

    private void HandleDash()
    {
        if (!enableDash) return;

        if (dashCooldownTimer > 0f)
            dashCooldownTimer -= Time.deltaTime;

        if (isDashing)
        {
            dashTimer -= Time.deltaTime;
            if (dashTimer <= 0f)
            {
                isDashing = false;
                rb.gravityScale = originalGravityScale;
            }
            return;
        }

        if (Input.GetKeyDown(dashKey) && dashCooldownTimer <= 0f)
        {
            isDashing = true;
            dashTimer = dashDuration;
            dashCooldownTimer = dashCooldown;

            float direction = transform.localScale.x;
            rb.linearVelocity = new Vector2(direction * dashSpeed, 0f);
            rb.gravityScale = 0f;
        }
    }

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        if (wallCheck != null)
        {
            Gizmos.color = Color.blue;
            Gizmos.DrawLine(wallCheck.position, wallCheck.position + Vector3.right * wallCheckDistance);
            Gizmos.DrawLine(wallCheck.position, wallCheck.position + Vector3.left * wallCheckDistance);
        }
    }
#endif
}