top of page
Search
  • cedarcantab

Climbing Ladders in Phaser 3, Part 1

Updated: Apr 24, 2022

Having delved into the mechanics of moving platforms, I take a break from platforms and look at ladders.



Making Mr Dude climb ladders using Phaser 3


Making characters climb ladders may look simple but can be, depending on the set-up, deceptively complicated.


The key areas that I ended up spending some time looking at are as follows:

  1. how to detect a ladder and how to grab hold of a ladder

  2. how to handle intersection between platforms and ladders

  3. what to do at top and bottom of the ladder

  4. ladders that start in mid air

In this first post, I explore each of the above issues using sprites for ladders and ground/platforms, and then move onto apply the principles to tilemaps in the next post.


I have created 2 examples for this post; the first of which is accessible below.



If you are following along, it might make sense to open up the Codepen and read the following in conjunction.


My starting point was to create the ground, platforms and ladders as sprites with physics bodies. For the purpose of this example I have created them as static sprites.


this.platforms = this.physics.add.staticGroup();
    this.platforms.create(0, 600-64, 'ground').setOrigin(0,0).setScale(2).refreshBody(); // this is the ground
    this.platforms.create(200,450,'ground').setOrigin(0).setScale(0.5,1).refreshBody(); 
    this.platforms.create(600+32,600-64-232,'ground').setOrigin(0,0).refreshBody();
    this.platforms.create(600,600-64-232,'ground').setOrigin(1,0).setScale(0.4,1).refreshBody();
  
    this.ladders = this.physics.add.staticGroup();  
    this.ladders.create(600,600-64,'ladder-thin-mid').setOrigin(0,1).refreshBody();
    this.ladders.create(600,100,'ladder-thin-short').setOrigin(0,0).refreshBody();


Crude form of state pattern logic for Mr Dude


I created Mr Dude as a extended sprite with a dynamic physics body.


Mr Dude can move left or right, as well as jump, as in the moving platforms examples, but we also want him to be able to climb. The logic for each "state" (ie walking, jumping or climbing) is quite different so I have adopted a very simple form of state patterns by:

  1. creating a static variable Status: Walking, Jumping and Climbing

  2. in the main update method of Mr Dude, using switch statement to distinguish between the different states

  3. setStatus method to execute "one-off" code upon entering a new state

Everything could have been done with if-else statements, but using states and switch statements makes the code a tiny bit easier to read (although it still ended up as a spaghetti of if-else statements).


How to detect ladder and grab hold of it!


In order to "detect" whether Mr Dude can climb a ladder, we need to confirm whether Mr Dude is standing in the right place to start climbing. This can done using the standard Phaser 3 collision detection routines. Since Mr Dude need to "overlap" with the ladder in order to climb, we use overlap class as opposed to collider class.


 this.physics.add.overlap(this.dude, this.ladders, this.climbLadder, this.canClimb, this);

I'll explain how I actually use this collider to trigger Mr Dude to turn into Climbing state.


In principle, the steps are:

  1. Confirm that Mr Dude is standing in the right position to start climbing via overlap collider

  2. Check that up or down arrow keys are being pressed

  3. Check that Mr Dude has "head-room" to climb

  4. Identify the ladder to climb and set Mr Dude's status to Climbing

Steps 1 to 3 are actually carried out all by this.canClimb callback process


 canClimb(dude, ladder) {

    return   (
      (dude.body.left>=ladder.body.left && dude.body.right<= ladder.body.right) &&
      ((this.cursors.up.isDown && (dude.body.bottom-dude.body.deltaY()) > ladder.body.top) || this.cursors.down.isDown)  
    )
 
  }


1. Check that up or down arrow keys are being pressed


this.cursors.up.isDown

and


this.cursors.down.isDown

2. Confirm that Mr Dude is standing in the right place to be able to climb a ladder


You don't want Mr Dude to be able to climb, if Mr Dude is say, only overlapping by 1 pixel against the ladder. On the other hand, you don't want to make it too "tight", by say, checking that Mr Dude's centre axis is perfectly aligned with the centre axis of the ladder (easy to code) since that would make it very difficult to initiate a climb (like in some of the very old platform games of the 1980's!)


In my example, Mr Dude's sprite texture is 32 pixels wide, but I have shrunk the physics body to 20 pixels wide (offset by 6 pixels from the left). The ladder is 32 pixels wide (and the physics body I have left the same size as the parent sprite). Hence in my example, the "is Mr Dude is in the right place to start climbing check" is as follows;


dude.body.left>=ladder.body.left && dude.body.right<= ladder.body.right

There are of course lots of other ways of checking "alignment", such as calculating of Mr Dude's centre x coodinate vs the centre of the ladder. But in this particular case, the above is a simple way to ensure that Mr Dude does not start climbing the ladder with half his body "hanging out" of the ladder.


3. Check that Mr Dude is able to "climb" in the desired direction


By which I mean, if the up arrow is being pressed but Mr Dude is standing at the top of the ladder (in my particular set up, this is unlikely to happen in practice). Similarly I should probably include a check for the case where down arrow is being pressed but Mr Dude is standing on the ground - but the collision detection will prevent Mr Dude from "burrowing" into the ground anyway, so I have not included this check.


dude.body.bottom-dude.body.deltaY()) > ladder.body.top

Note that I am comparing the ladder top against Mr Dude's physics body bottom adjusted for deltaY. It works ok without this deltaY adjustment but this results in a slightly more precise check against the top fo the ladder.


4. If all of the above conditions, set the status of Mr Dude to climbing and identify which ladder is being climbed


To achieve all of the above, the overlap object is created in the create method of the game scene as follows;


 this.physics.add.overlap(this.dude, this.ladders, this.climbLadder, this.canClimb, this);

And as explained above, the callback process this.canClimb is defined as follows:


  canClimb(dude, ladder) {

    return   (
      (dude.body.left>=ladder.body.left && dude.body.right<= ladder.body.right) &&
      ((this.cursors.up.isDown && (dude.body.bottom-dude.body.deltaY()) > ladder.body.top) || this.cursors.down.isDown)  
    )
 
  }

and the call back method this.climbLadder is defined as follows:


  climbLadder(dude, ladder) {
    dude.lockedTo = ladder;
    dude.setStatus(Dude.Status.Climbing);   // setStatus method kills gravity   
    
  }

Now Mr Dude is ready to climb, Mr Dude's status is set to Climbing using the setStatus method, which is as below. Essentially, all it does it to switch off gravity (when entering Climbing state) and switch it back on when re-entering Walking state.


  setStatus(newStatus) {
    this.status = newStatus;
    switch (this.status) {
      case Dude.Status.Walking:
        this.body.setAllowGravity(true);
        break;
      case Dude.Status.Jumping:
        break;
      case Dude.Status.Climbing:
        this.body.stop();
        this.body.setAllowGravity(false);
        break;
    }


Once Status has been set to Climbing...

Once Mr Dude status is set to climbing, the actual logic to allow Mr Dude to move up and down the ladder is relatively straight forward. The relevant code to handle the key presses when Mr Dude is in Climbing state is as follows. Specifically, the red highlighted code handles the moving up and down the ladder action.


   case Dude.Status.Jumping:
        if (this.body.onFloor()) this.setStatus(Dude.Status.Walking);      
        break;
        
      case Dude.Status.Climbing:
        if (this.scene.cursors.right.isDown) {
          this.setStatus(Dude.Status.Walking);
        }
        else if (this.scene.cursors.left.isDown) {   
          this.setStatus(Dude.Status.Walking);
        }
        else if (this.scene.cursors.up.isDown) {
          if (this.body.bottom > this.lockedTo.body.top) {        
            this.anims.play('climb', true);
            this.body.setVelocityY(-160);
          } 
          else {
            this.body.stop(); // get rid of this if you want Mr Dude to do a little hop as he completes his climb
            this.setStatus(Dude.Status.Walking);
          }          
        }
        else if (this.scene.cursors.down.isDown) {      
          if (this.body.onFloor() || this.body.top >= this.lockedTo.body.bottom) {
            this.setStatus(Dude.Status.Walking);
          } 
          else {
            this.anims.play('climb', true);
            this.body.setVelocityY(160)
          }          
        }
        else {
          this.anims.play('idle-climb',true)
          this.body.setVelocityY(0)
        }
        break;

When the up arrow is pressed, you need to give Mr Dude a bit of upward velocity. A check to see that he has not reached the top of the ladder is also included, otherwise he will be able to climb to the sky. Similar, when the down arrow key is pressed, as long as Mr Dude is not already on the ground, give him a bit of downward velocity.


The tricky part is what to do with Mr Dude at the ends of the ladder, particularly the top of the ladder.


Intersection between Ground/Platform


The logic to make Mr Dude behave correctly at the top of the ladder can differ depending on how the ladders and the platforms intersect. First, I look at the case where they do not (ie the platform sprite and the ladder platform do not actually overlap) and aladder does not "stick-out" from the top of the platform.



The way the ladders and platforms are arranged matters because, say if the platform and the ladder "overlap", we would need to figure out how Mr Dude climbs up the ladder through the platform (there is a collider set up between the Mr Dude and the platform, so that Mr Dude can walk on top of the platform) and arrive at the top of the platform. In this first example, we do not face this problem since the platform does not get in the way. Which then raises the question; when Mr Dude wants to walk along the platform, across the ladder top, we need to figure out how to stop Mr Dude from falling through the "gap"


Let Mr Dude stand "on top" of the ladder

The trick is to be able to "stand" on top of the ladder. This can be achieved by creating a Arcade Physics Collider object for the ladder, in addition to the overlap object. The Collider will then automatically call the Arcade Physics built-in separate function to make sure that Mr Dude will remain "on top" the ladder that it has collided with.


...but only when you want him to stand on the ladder

However, you do not want the collider to function when Mr Dude is actually climbing - in fact, any time other than when Mr Dude is "standing" or "walking" on top of the platform, we don't want the collider to work. Put another way, we want to disable the collision detection if the down arrow key is being pressed and Mr Dude is in the correct position to be able to climb down.


Through experimentation and looking at examples, I have found that you can actually achieve this kind of "selective" collision/separation with the standard Arcade Physics Collider. The official documentation is a bit light on detail but I think below is how things work (at least it seems to achieve the desired effect!)


The trick is to utilise the processCallback parameter of the Arcade Physics Collider. The processCallback parameter is an additional check that you can perform on the 2 objects that have collided. This should be a function that returns a boolean. If it returns true then collision carries on (separating the two objects). If it returns false the collision is assumed to have failed and the collision handling aborts, no further checks or separation happens.


  this.physics.add.overlap(this.dude, this.ladders, this.climbLadder, this.canClimb, this);
    this.physics.add.collider(this.dude, this.ladders, null, this.onLadderTop, this);

and the processCallback is coded as follows:


  onLadderTop(dude, ladder) {
    // if Mr Dude is on top of the ladder and cursor down is not being pressed then let collision separation happen
    if ( ( (dude.body.bottom-dude.body.deltaY()) <= ladder.body.top)  && !this.cursors.down.isDown) return true
    else return false
 
  }

In the code above, there is no additional callback defined (null), hence if the boolean return is false, nothing happens, but if true, Phaser's built-in collision separation will be executed, allowing Mr Dude to "stand" on top of the ladder.


To check whether Mr Dude is "on top" of the ladder, I am comparing "dude.body.bottom - dude.body.deltaY()" against the top of the ladder. I have included deltaY since that seems to give a more precise check.


Could I not simply call World.collide only when needed?

By adding a Collider as above, the Arcade Physics engine automatically calls the collision detection and separation code every refresh (physics engine step). You could also call the collide function "once". So rather than creating processCallbaks and collideCallbak, I tried also calling the collide method when needed (ie Walking). But I found the results unstable, perhaps because there are elements of the code that relies on the physics engine firing continuously (eg standing on ground status).


When reached the top or bottom of the ladder


In my example, if down arrow key is pressed when Mr Dude has reached the ground, his status is turned to walking. You do not necessarily need to do this. You can simply stop him from moving.


Similarly, if up arrow key is pressed when Mr Dude reaches the top of the ladder, his status will be turned to Walking. Again, you might want to simply do nothing, if you don't want the below type of behaviour happening.














And that's pretty much it for the case of ladders that do not overlap with the platform and do not "stick-out" from the top of the platform.


Ladders that overlap with platforms


Now we turn our attention to arrangements where ladders do overlap with platforms and "sticks-out" through the platforms.


















How to make Mr Dude "pass-through" the intersecting platforms


The logic for detecting when Mr Dude is in the right position to start climbing, and setting his status to Climbing is pretty much the same as the first example. The major difference comes in making Mr Dude behave himself at the intersections between ladder and platform.


This time, rather than give Mr Dude the ability to stand on top of the ladder, we need him to be able to "pass through" the platform (i) when he is climbing up a ladder and through the platform, and (ii) when he climbs onto the ladder to go down from a platform.


The first condition is easily achieved by putting in place a callback process in the collider class between Mr Dude and the platforms, as follows:


this.physics.add.collider(this.dude, this.platforms, null, this.passThru, this);

and the passThru is defined as follows:


 passThru(dude, ladder) {
    return (dude.status !== Dude.Status.Climbing)  
  }

With the above, Mr Dude will also be able climb down a ladder through a platform, EXCEPT where the top of the platform is flush against the platform, like the ladder to the right, in the picture above. The Mr Dude is in walking state, then naturally collider between Mr Dude and the platform remains alive, so he will not be able to get onto the ladder (since he is not overlapping with the ladder).


Making the ladder "poke-out" from the platform

The easiest (albeit a little bit hackey) solution is to make the ladder "stick-out" from the ladder by a fraction of a pixel.


this.ladders.create(250,249.99,'ladder-thin-mid').setOrigin(0,0).refreshBody(); 

Visually, the top of the platform will be flat against the platform, but this is enough to trigger the collider so that if you press the down arrow key, Mr Dude's state will be set to Climbing.


And that's pretty much it for two patterns of ladders and platforms, using sprites.


The second example code can be accessed below.




Key references




85 views0 comments
記事: Blog2_Post
bottom of page