For my game I wanted the ladders to work similarly to Megaman X, after studying how they work I was able to implement a climbing system that works almost identically to that of Megaman x.
Things the player can do:
- Climb the ladders up and down.
- Jump from the ladders.
- When going sideways you cancel the climb (I might change this later).
- Walk at the top of the ladders.
- When at the top of the ladders the player can climb down.
- Get off the ladders by going all the way down.
- The player gets moved to the center of the ladder to avoid weird collisions.
I’m using placeholder art so it might not accurately represent all the features described above.
All of this requires a little bit of setup outside the code, for instance, at the top of the ladder there’s an Edge Collider using a Platform Effector 2D, this means you can climb up but you can’t climb down. When climbing down from the top of the ladders the Edge Collider gets deactivated. It gets activated once the player stops climbing.
My code is also quite different from Rick’s, I’m not using Unity’s new input system and it is separated into 3 different scripts;
- Controller: Handles Input and conditions for the inputs.
- Climber: Handles the Climbing state and ladders.
- Mover: Handles movement and everything related to the Rigidbody.
Controller Script - Click to Read
using UnityEngine;
using AK.Movements;
using AK.MovementStates;
namespace AK.Controls
{
[RequireComponent(typeof(Mover))]
[RequireComponent(typeof(Collider2D))]
public class Controller : MonoBehaviour
{
[SerializeField] LayerMask jumpableMask = 0;
[SerializeField] LayerMask climbableMask = 0;
Mover mover;
Climber climber;
Collider2D col;
private void Awake()
{
col = GetComponent<Collider2D>();
mover = GetComponent<Mover>();
climber = GetComponent<Climber>();
climber.SetMove(mover);
}
private void Update()
{
ReadWalkInput();
ReadJumpInput();
ControlClimbState();
}
private void ReadWalkInput()
{
float xAxis = Input.GetAxisRaw("Horizontal");
if (climber.GetIsClimbing && Mathf.Abs(xAxis) > Mathf.Epsilon) { climber.StopClimbing(); }
mover.Move(xAxis, Input.GetAxisRaw("Vertical"), climber.GetIsClimbing);
}
private void ReadJumpInput()
{
if (Input.GetButtonDown("Jump") && col.IsTouchingLayers(jumpableMask))
{
climber.StopClimbing();
mover.Jump();
}
if (Input.GetButtonUp("Jump")) { mover.HaltJump(); }
}
private void ControlClimbState()
{
StartClimb();
StopClimb();
}
private void StartClimb()
{
if (Input.GetButton("Vertical"))
{
climber.CheckIfStartClimb(col.IsTouchingLayers(climbableMask), Input.GetAxisRaw("Vertical"), climbableMask);
}
}
private void StopClimb()
{
if (climber.GetIsClimbing)
{
bool touchingGround = col.IsTouchingLayers(LayerMask.GetMask("Ground"));
climber.CheckIfStopClimbing(touchingGround, Input.GetAxisRaw("Vertical") < 0, col.bounds.min.y);
}
}
}
}
Climber Script - Click to Read
using UnityEngine;
using AK.Movements;
namespace AK.MovementStates
{
public class Climber : MonoBehaviour
{
[SerializeField] float ladderTopOffset = 0.2f;
bool isClimbing;
Mover mover;
Collider2D ladder;
EdgeCollider2D topOfLadder;
public bool GetIsClimbing { get => isClimbing; }
public void SetMove(Mover mover) { this.mover = mover; }
public void CheckIfStartClimb(bool touchingLadder, float yAxis, LayerMask climbableMask)
{
if (touchingLadder && Mathf.Abs(yAxis) > 0 && !isClimbing) { StartClimbing(yAxis, climbableMask); }
}
public void StartClimbing(float yAxis, LayerMask climbableMask)
{
ladder = Physics2D.OverlapCircle(transform.position, 1, climbableMask);
topOfLadder = ladder.transform.GetComponentInChildren<EdgeCollider2D>();
if (topOfLadder.IsTouchingLayers(LayerMask.GetMask("Player")) && yAxis > 0) { return; }
isClimbing = true;
topOfLadder.enabled = false;
transform.position = new Vector2(ladder.transform.position.x, transform.position.y);
mover.SetGravity(false, 0);
mover.StopRigidbody();
}
public void CheckIfStopClimbing(bool touchingGround, bool goingDown, float bottomPlayerCollider)
{
if ((touchingGround && goingDown) || (ladder.bounds.max.y + ladderTopOffset < bottomPlayerCollider))
{
StopClimbing();
}
}
public void StopClimbing()
{
isClimbing = false;
mover.SetGravity(true);
if (topOfLadder != null) { topOfLadder.enabled = true; }
}
}
}
Mover Script - Click to Read
using UnityEngine;
namespace AK.Movements
{
public class Mover : MonoBehaviour
{
[SerializeField] float moveSpeed = 5f;
[SerializeField] float jumpForce = 15f;
[SerializeField] float climbingSpeed = 2;
float initialGravity;
Rigidbody2D rb;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
initialGravity = rb.gravityScale;
}
public void SetGravity(bool setToInital, float gravityScale = 0) { rb.gravityScale = setToInital ? initialGravity : gravityScale; }
public void StopRigidbody() { rb.velocity = Vector2.zero; }
public void Move(float xAxis, float yAxis, bool isClimbing) { rb.velocity = CalculateDirectionalSpeed(xAxis, yAxis, isClimbing); }
public void Jump() { rb.velocity = new Vector2(rb.velocity.x, jumpForce); }
public void HaltJump()
{
if (rb.velocity.y < 0) { return; }
Vector2 haltSpeed = rb.velocity;
haltSpeed.y *= 0.5f;
rb.velocity = haltSpeed;
}
private Vector2 CalculateDirectionalSpeed(float xAxis, float yAxis, bool isClimbing)
{
Vector2 directionalSpeed = Vector2.zero;
directionalSpeed.x = xAxis * moveSpeed;
directionalSpeed.y = isClimbing ? yAxis * climbingSpeed : rb.velocity.y;
return directionalSpeed;
}
}
}
Hope this helps anyone trying to make a slightly fancier climbing ladders system.
[Edit - WARNING]
This system might not work properly with animations but it does not require a lot of effort to adjust it.