Part of these game systems:
2D Player Controller Pro
Advanced 2D character controller with wall jump, wall slide, dash, double jump, ledge grab, and moving platform support.
How to Use
1
Attach to your player GameObject with Rigidbody2D + Collider2D
2
Create child empty objects for GroundCheck and WallCheck positions
3
Set Ground Layer and Wall Layer masks
4
Configure wall jump angle and force
5
Adjust dash speed, duration, and cooldown
6
Enable/disable ledge grab and set climb offset
7
Tag moving platforms as 'MovingPlatform'
8
Use one-way PlatformEffector2D — press Down+Jump to drop through
9
Hook UnityEvents for jump/dash/wall-jump SFX and particles
Source Code
C#
using UnityEngine;
using UnityEngine.Events;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController2DPro : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float moveSpeed = 8f;
[SerializeField] private float acceleration = 60f;
[SerializeField] private float deceleration = 50f;
[SerializeField] private float airAcceleration = 40f;
[Header("Jumping")]
[SerializeField] private float jumpForce = 16f;
[SerializeField] private float coyoteTime = 0.15f;
[SerializeField] private float jumpBufferTime = 0.1f;
[SerializeField] private float jumpCutMultiplier = 0.4f;
[SerializeField] private int maxAirJumps = 1;
[Header("Wall Mechanics")]
[SerializeField] private float wallSlideSpeed = 2f;
[SerializeField] private float wallJumpForce = 14f;
[SerializeField] private float wallJumpAngle = 60f;
[SerializeField] private float wallStickTime = 0.2f;
[SerializeField] private LayerMask wallLayer;
[Header("Dash")]
[SerializeField] private float dashSpeed = 24f;
[SerializeField] private float dashDuration = 0.15f;
[SerializeField] private float dashCooldown = 0.8f;
[SerializeField] private bool dashInAir = true;
[Header("Ledge Grab")]
[SerializeField] private bool enableLedgeGrab = true;
[SerializeField] private Vector2 ledgeClimbOffset = new Vector2(0.5f, 1.2f);
[SerializeField] private float ledgeGrabDistance = 0.5f;
[Header("Ground Check")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Wall Check")]
[SerializeField] private Transform wallCheck;
[SerializeField] private float wallCheckDistance = 0.4f;
[Header("Events")]
public UnityEvent OnJumped;
public UnityEvent OnDashed;
public UnityEvent OnWallJumped;
public UnityEvent OnLedgeGrabbed;
private Rigidbody2D rb;
private float moveInput;
private bool isGrounded;
private bool isTouchingWall;
private bool isWallSliding;
private bool isDashing;
private bool isLedgeGrabbing;
private int facingDirection = 1;
private int airJumpsRemaining;
private float coyoteTimer;
private float jumpBufferTimer;
private float wallStickTimer;
private float dashTimer;
private float dashCooldownTimer;
private float originalGravityScale;
private Transform currentPlatform;
private Vector3 lastPlatformPosition;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
rb.freezeRotation = true;
originalGravityScale = rb.gravityScale;
}
private void Update()
{
if (isDashing || isLedgeGrabbing) return;
moveInput = Input.GetAxisRaw("Horizontal");
UpdateFacing();
CheckGround();
CheckWall();
CheckLedge();
HandleJumpInput();
HandleDashInput();
HandlePlatformDrop();
}
private void FixedUpdate()
{
if (isDashing || isLedgeGrabbing) return;
ApplyMovement();
ApplyWallSlide();
ApplyPlatformMovement();
}
private void UpdateFacing()
{
if (moveInput > 0.1f) facingDirection = 1;
else if (moveInput < -0.1f) facingDirection = -1;
}
private void CheckGround()
{
bool wasGrounded = isGrounded;
isGrounded = groundCheck != null &&
Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteTimer = coyoteTime;
airJumpsRemaining = maxAirJumps;
}
else
{
coyoteTimer -= Time.deltaTime;
}
// Platform parenting
if (isGrounded && !wasGrounded)
{
RaycastHit2D hit = Physics2D.Raycast(groundCheck.position, Vector2.down, 0.5f, groundLayer);
if (hit.collider != null && hit.collider.CompareTag("MovingPlatform"))
{
currentPlatform = hit.collider.transform;
lastPlatformPosition = currentPlatform.position;
}
}
else if (!isGrounded)
{
currentPlatform = null;
}
}
private void CheckWall()
{
isTouchingWall = wallCheck != null &&
Physics2D.Raycast(wallCheck.position, Vector2.right * facingDirection,
wallCheckDistance, wallLayer);
isWallSliding = isTouchingWall && !isGrounded && moveInput * facingDirection > 0;
}
private void CheckLedge()
{
if (!enableLedgeGrab || isGrounded || !isTouchingWall || isWallSliding) return;
Vector2 wallPos = wallCheck != null ? (Vector2)wallCheck.position : (Vector2)transform.position;
Vector2 upperCheck = wallPos + Vector2.up * ledgeGrabDistance;
bool upperHit = Physics2D.Raycast(upperCheck, Vector2.right * facingDirection,
wallCheckDistance, wallLayer);
if (!upperHit && isTouchingWall && rb.linearVelocity.y <= 0)
{
StartLedgeGrab();
}
}
private void StartLedgeGrab()
{
isLedgeGrabbing = true;
rb.linearVelocity = Vector2.zero;
rb.gravityScale = 0f;
OnLedgeGrabbed?.Invoke();
// Climb after short delay
Invoke(nameof(CompleteLedgeClimb), 0.3f);
}
private void CompleteLedgeClimb()
{
Vector2 climbTarget = (Vector2)transform.position +
new Vector2(facingDirection * ledgeClimbOffset.x, ledgeClimbOffset.y);
transform.position = climbTarget;
rb.gravityScale = originalGravityScale;
isLedgeGrabbing = false;
}
private void HandleJumpInput()
{
if (Input.GetButtonDown("Jump"))
jumpBufferTimer = jumpBufferTime;
else
jumpBufferTimer -= Time.deltaTime;
// Variable jump height
if (Input.GetButtonUp("Jump") && rb.linearVelocity.y > 0)
rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
if (jumpBufferTimer <= 0f) return;
// Wall jump with stick time
if (isTouchingWall && !isGrounded)
{
wallStickTimer = wallStickTime;
}
if (wallStickTimer > 0f)
{
wallStickTimer -= Time.deltaTime;
if (jumpBufferTimer > 0f)
{
PerformWallJump();
wallStickTimer = 0f;
return;
}
}
// Ground jump
if (coyoteTimer > 0f)
{
PerformJump();
return;
}
// Air jump
if (airJumpsRemaining > 0)
{
airJumpsRemaining--;
PerformJump();
}
}
private void PerformJump()
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
jumpBufferTimer = 0f;
coyoteTimer = 0f;
OnJumped?.Invoke();
}
private void PerformWallJump()
{
float rad = wallJumpAngle * Mathf.Deg2Rad;
Vector2 jumpDir = new Vector2(-facingDirection * Mathf.Sin(rad), Mathf.Cos(rad));
rb.linearVelocity = jumpDir * wallJumpForce;
facingDirection = -facingDirection;
jumpBufferTimer = 0f;
coyoteTimer = 0f;
airJumpsRemaining = maxAirJumps;
OnWallJumped?.Invoke();
}
private void HandleDashInput()
{
dashCooldownTimer -= Time.deltaTime;
if (Input.GetKeyDown(KeyCode.LeftShift) && dashCooldownTimer <= 0f)
{
if (isGrounded || dashInAir)
StartDash();
}
}
private void StartDash()
{
isDashing = true;
dashTimer = dashDuration;
dashCooldownTimer = dashCooldown;
rb.gravityScale = 0f;
rb.linearVelocity = new Vector2(facingDirection * dashSpeed, 0f);
OnDashed?.Invoke();
Invoke(nameof(EndDash), dashDuration);
}
private void OnDisable()
{
rb.gravityScale = originalGravityScale;
isDashing = false;
isLedgeGrabbing = false;
CancelInvoke();
}
private void EndDash()
{
isDashing = false;
rb.gravityScale = originalGravityScale;
rb.linearVelocity = new Vector2(rb.linearVelocity.x * 0.3f, 0f);
}
private void HandlePlatformDrop()
{
if (Input.GetAxisRaw("Vertical") < -0.5f && Input.GetButtonDown("Jump"))
{
Collider2D col = GetComponent<Collider2D>();
if (col != null)
{
col.enabled = false;
Invoke(nameof(ReenableCollider), 0.3f);
}
}
}
private void ReenableCollider()
{
Collider2D col = GetComponent<Collider2D>();
if (col != null) col.enabled = true;
}
private void ApplyMovement()
{
float targetSpeed = moveInput * moveSpeed;
float accel = isGrounded
? (Mathf.Abs(moveInput) > 0.01f ? acceleration : deceleration)
: airAcceleration;
float speedDiff = targetSpeed - rb.linearVelocity.x;
rb.AddForce(Vector2.right * (speedDiff * accel), ForceMode2D.Force);
}
private void ApplyWallSlide()
{
if (isWallSliding && rb.linearVelocity.y < -wallSlideSpeed)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, -wallSlideSpeed);
}
}
private void ApplyPlatformMovement()
{
if (currentPlatform == null) return;
Vector3 delta = currentPlatform.position - lastPlatformPosition;
transform.position += delta;
lastPlatformPosition = currentPlatform.position;
}
/// <summary>True if the player is currently on the ground.</summary>
public bool IsGrounded => isGrounded;
/// <summary>True if the player is wall sliding.</summary>
public bool IsWallSliding => isWallSliding;
/// <summary>True if the player is currently dashing.</summary>
public bool IsDashing => isDashing;
/// <summary>Current facing direction: 1 = right, -1 = left.</summary>
public int FacingDirection => facingDirection;
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
if (wallCheck != null)
{
Gizmos.color = Color.blue;
Gizmos.DrawRay(wallCheck.position, Vector2.right * wallCheckDistance);
Gizmos.DrawRay(wallCheck.position, Vector2.left * wallCheckDistance);
}
}
#endif
}