2D Platformer Mechanics
Wall jump, wall slide, dash, and double jump mechanics for 2D platformers. Drop-in extension for the 2D Player Controller.
How to Use
Attach PlatformerMechanics2D to your player (alongside PlayerController2D)
Create a WallCheck child transform at character's side
Assign ground and wall check references
Enable/disable individual mechanics in inspector
Wall slide: hold into wall while falling
Wall jump: press Jump while wall sliding
Double jump: press Jump in mid-air
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
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
}