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

Features

  • Wall slide with configurable slide speed when pressing into walls
  • Wall jump that launches away from the wall with adjustable force vector
  • Double jump (or triple+) with configurable max air jumps
  • Dash with speed, duration, cooldown, and gravity override
  • Ground and wall detection via Physics2D with LayerMask filtering
  • Editor gizmos for wall check ray visualization

When to Use This

Designed for 2D platformers like Celeste-style or Metroidvania games that need advanced movement mechanics. Drop this alongside the basic 2D Player Controller to add wall jumping, dashing, and double jumping. Each mechanic can be individually toggled, so use only what your game needs.

Common Mistakes

You must create a wallCheck child Transform positioned at the character's side edge — without it, wall detection won't work. The wallLayer and groundLayer must be set correctly in the Inspector; if they overlap with the player's own layer, you'll get false positives. The dash temporarily sets gravityScale to 0, so don't modify gravity from another script during a dash.

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
}