My Climbing Ladder System!

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.

ezgif-4-c9f0a843bf

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.

6 Likes

Yee at it again creating amazing content. Keep up the phenomenal work!

1 Like

This looks overall incredible, great work!

Would you at all be able to make a version that used the Input System as done in the video? Totally fine if not, just would be curious to see how you would implement it within the Input System.

1 Like

Thanks! I might do what you suggested as a challenge.

You can play the final game here, it’s really short, but I liked it a lot.

And here’s the final repo in case you want to download the full project, the code is not particularly good, but it gets the job done.

1 Like

Awesome man, I’ll definitely check it out! And yeah it could be a fun challenge if you wanted to give it a go.

1 Like

Privacy & Terms