Wall jumps turn a flat platformer into a vertical playground. Players slide down walls, kick off them, and climb narrow shafts. This tutorial adds a Unity 2D wall jump and wall slide to a Rigidbody2D character: detecting walls, capping slide speed, jumping away with the right force, and the short input lockout that makes wall jumps feel controlled instead of slippery.
How a Good Wall Jump Feels
Before code, decide the behaviour. Most modern platformers use:
- Wall slide: when airborne, touching a wall and pushing toward it, the player slides down slowly instead of falling.
- Wall jump: pressing jump while sliding launches the player up and away from the wall.
- Short lockout: for a fraction of a second after the wall jump, horizontal input is reduced or ignored, so holding toward the wall doesn’t immediately cancel the push away.
- Coyote time on walls: a short grace period after leaving a wall where a wall jump still works.
This builds on normal movement and jumping. If you need those first, see the 2D platformer movement guide and the double jump and coyote time guide.
Detecting Walls
Use two small overlap boxes, one on each side of the player, against a Ground or Wall layer:
int WallSide()
{
Vector2 p = rb.position;
bool right = Physics2D.OverlapBox(p + new Vector2(wallCheckOffset, 0f), wallCheckSize, 0f, wallLayer);
bool left = Physics2D.OverlapBox(p - new Vector2(wallCheckOffset, 0f), wallCheckSize, 0f, wallLayer);
if (right) return 1;
if (left) return -1;
return 0;
}
Make the boxes tall but thin, and shorter than the player’s collider vertically so ledges and floors don’t count as walls. If some walls shouldn’t be climbable, put them on a different layer that isn’t in wallLayer.
Wall Slide
The player is sliding when: not grounded, touching a wall, and either moving down or pushing toward the wall. While sliding, cap downward speed:
bool sliding = !grounded && wallSide != 0 && rb.linearVelocity.y < 0f &&
Mathf.Sign(moveInput.x) == wallSide && Mathf.Abs(moveInput.x) > 0.1f;
if (sliding)
rb.linearVelocity = new Vector2(rb.linearVelocity.x, Mathf.Max(rb.linearVelocity.y, -wallSlideSpeed));
Requiring input toward the wall is a design choice. It lets players drop off walls by letting go. Some games make sliding automatic on contact; remove the input condition for that.
One physics gotcha: friction. If the player’s collider has friction, pushing into a wall makes the player stick in place. Give the player a Physics Material 2D with Friction 0. The slide speed cap then fully controls sliding.
Wall Jump
When jump is pressed while touching a wall (or within the wall coyote window), set velocity up and away:
void WallJump(int side)
{
rb.linearVelocity = new Vector2(-side * wallJumpForce.x, wallJumpForce.y);
lockoutUntil = Time.time + wallJumpLockout;
facing = -side;
}
A wallJumpForce of around (10, 14) with normal jump velocity 14 works well. Setting velocity rather than adding force gives the same arc every time. Flip the sprite to face away from the wall.
There are two common styles:
- Kick away: strong horizontal push. Players zig-zag between two walls. Classic Mega Man X style.
- Climb: weaker horizontal push and a long lockout, so holding toward the wall carries the player back to it after a moment, letting them climb a single wall. Celeste and Hollow Knight lean toward this.
Input Lockout
Without a lockout, a player holding toward the wall immediately cancels the horizontal push, and the wall jump looks like a normal jump straight up. During the lockout, don’t apply horizontal input, or blend it in gradually:
float control = Time.time < lockoutUntil ? 0f : 1f;
float targetX = moveInput.x * moveSpeed;
float newX = Mathf.Lerp(rb.linearVelocity.x, targetX, control);
rb.linearVelocity = new Vector2(newX, rb.linearVelocity.y);
0.1 to 0.2 seconds of lockout is typical. For a softer feel, ramp control from 0 to 1 over the lockout instead of switching it on suddenly.
The Full Script
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class WallJump2D : MonoBehaviour
{
[SerializeField] InputActionReference moveAction;
[SerializeField] InputActionReference jumpAction;
[SerializeField] float moveSpeed = 8f;
[SerializeField] float jumpVelocity = 14f;
[SerializeField] float wallSlideSpeed = 2.5f;
[SerializeField] Vector2 wallJumpForce = new Vector2(10f, 14f);
[SerializeField] float wallJumpLockout = 0.15f;
[SerializeField] float wallCoyoteTime = 0.1f;
[Header("Checks")]
[SerializeField] LayerMask groundLayer;
[SerializeField] LayerMask wallLayer;
[SerializeField] Vector2 groundCheckSize = new Vector2(0.45f, 0.1f);
[SerializeField] float groundCheckOffset = 0.5f;
[SerializeField] Vector2 wallCheckSize = new Vector2(0.1f, 0.6f);
[SerializeField] float wallCheckOffset = 0.3f;
Rigidbody2D rb;
Vector2 moveInput;
bool grounded;
int wallSide, lastWallSide;
float lastWallTime = -1f, lockoutUntil;
bool jumpQueued;
int facing = 1;
void Awake() => rb = GetComponent<Rigidbody2D>();
void OnEnable() { moveAction.action.Enable(); jumpAction.action.Enable(); }
void OnDisable() { moveAction.action.Disable(); jumpAction.action.Disable(); }
void Update()
{
moveInput = moveAction.action.ReadValue<Vector2>();
if (jumpAction.action.WasPressedThisFrame()) jumpQueued = true;
}
void FixedUpdate()
{
Vector2 p = rb.position;
grounded = Physics2D.OverlapBox(p + Vector2.down * groundCheckOffset, groundCheckSize, 0f, groundLayer);
wallSide = WallSide();
if (!grounded && wallSide != 0) { lastWallSide = wallSide; lastWallTime = Time.time; }
float control = Time.time < lockoutUntil ? 0f : 1f;
float newX = Mathf.Lerp(rb.linearVelocity.x, moveInput.x * moveSpeed, control);
rb.linearVelocity = new Vector2(newX, rb.linearVelocity.y);
bool sliding = !grounded && wallSide != 0 && rb.linearVelocity.y < 0f &&
Mathf.Abs(moveInput.x) > 0.1f && Mathf.Sign(moveInput.x) == wallSide;
if (sliding)
rb.linearVelocity = new Vector2(rb.linearVelocity.x, Mathf.Max(rb.linearVelocity.y, -wallSlideSpeed));
if (jumpQueued)
{
if (grounded)
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpVelocity);
else if (Time.time - lastWallTime <= wallCoyoteTime)
{
rb.linearVelocity = new Vector2(-lastWallSide * wallJumpForce.x, wallJumpForce.y);
lockoutUntil = Time.time + wallJumpLockout;
facing = -lastWallSide;
lastWallTime = -1f;
}
jumpQueued = false;
}
}
int WallSide()
{
Vector2 p = rb.position;
if (Physics2D.OverlapBox(p + Vector2.right * wallCheckOffset, wallCheckSize, 0f, wallLayer)) return 1;
if (Physics2D.OverlapBox(p + Vector2.left * wallCheckOffset, wallCheckSize, 0f, wallLayer)) return -1;
return 0;
}
void OnDrawGizmosSelected()
{
Vector3 p = transform.position;
Gizmos.color = Color.green;
Gizmos.DrawWireCube(p + Vector3.down * groundCheckOffset, groundCheckSize);
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(p + Vector3.right * wallCheckOffset, wallCheckSize);
Gizmos.DrawWireCube(p + Vector3.left * wallCheckOffset, wallCheckSize);
}
}
This is a self-contained version. If you already have a movement script, merge the wall checks, slide cap, wall jump and lockout into it rather than running both, since two scripts writing velocity will fight. Input is read in Update and the jump is queued for FixedUpdate so presses aren’t missed. facing is there for flipping your sprite.
Variations
- Wall grab. Hold a grab button to stop sliding completely. Set vertical velocity to 0 and gravity scale to 0 while grabbing, with a stamina limit.
- Same-wall restriction. Stop players climbing one wall forever by refusing a wall jump from the same side twice until grounded.
- Effects. Dust particles at the contact point while sliding and a puff on the jump sell the move.
- Animation. Add a WallSlide state driven by a bool. See the 2D sprite animation guide.
📚 Study great platformers
The best wall jumps come from careful tuning. Some of the books on our game design reading list break down platformer feel in detail.
Troubleshooting
Player sticks to walls and doesn’t fall
Friction on the collider. Add a Physics Material 2D with Friction 0 to the player.
Wall jump goes straight up
Horizontal input cancels the push. Add or lengthen the input lockout.
Floor counts as a wall
Wall check boxes are too tall or too low. Make them shorter and keep them above the feet.
Can wall jump while standing next to a wall on the ground
That’s expected unless you require not grounded. The script checks grounded first, so a normal jump happens instead.
Wall jump sometimes doesn’t fire
The press happened in a frame without a FixedUpdate. Queue the jump in Update as shown, and add a small wall coyote time.
FAQ
What wall slide speed feels good?
Around 20 to 30 percent of your max fall speed. 2 to 3 units per second for typical character sizes.
Should walls use a separate layer?
Using the Ground layer for both is simplest. A separate layer lets you make some walls unclimbable.
Does this work with a Tilemap?
Yes, as long as the tilemap has a TilemapCollider2D on a layer included in the wall mask.
How do I wall jump with a CharacterController?
CharacterController is 3D. In 3D, use the same logic with a sphere or capsule check and your own velocity variable.
Can I add a wall jump to the double jump script?
Yes. Refill air jumps on wall contact if you want wall jumps to reset the double jump.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply