top of page
Search
  • cedarcantab

Climbing Ladders in Phaser 3, Part 2

Updated: Apr 26, 2022

In the previous post I delved into how you can make characters climb ladders and run across platforms made out of sprites. In this post I look at how to do the same with tilemaps.



Climbing Ladders in TileMaps



Reminding ourselves of the characteristics of sprite vs tiles


Before delving into the actual logic it is helpful to remind ourselves of the respective characteristics, in particular with respect to collision detection.


Very broadly speaking:


Arcade Physics Sprites:

  1. With sprites, you can set up colliders to emit events based on which, relevant piece of your code will be executed.

  2. Fundamentally events are emitted only when there is actual collision or overlap. It is not so easy to "foresee" collisions/overlaps before they happen

In my examples, the fundamental assumption behind all the climbing action, or entering into climbing state is that the character is overlapping with a ladder. Hence in the second example of the previous post, I had to make the top of the ladder "poke" a fraction of a pixel above the platform so that we can easily detect that there is a ladder "below" the character when he is about to climb down onto a ladder from a platform.


Tiles:

  1. You can set up arcade physics colliders between arcade physics sprites vs tiles and pretty much handle collisions/overlaps just like sprites vs sprites

  2. Unlike sprites, Phaser provides you with various methods that allows you to check what tile exists at a certain position in the world.


As with building scenes with sprites, the tricky bits revolve around what to do with intersections between ladders and platforms, and dealing with arrangements where ladders do not "poke out" above the platform.


Creating the tilemap in easy to manipulate way


Take a look at the above game screen, which consists of platforms and ladders that do not appear to intersect with the platforms and importantly, poke out above the platforms. There are a variety of ways of creating this game screen with tiles.


  1. One tilemap layer containing platforms and ladders

  2. One tilemap layer for the platforms, and another tilemap layer for ladders, where the platforms and ladders do not overlap

  3. One tilemap layer for the platforms, and another tilemap layer with ladders, where the platforms and ladders do overlap (of course, where a ladder and platform intersect, only the tiles of the layer created later will be displayed)

I suspect that you could achieve the effects you want with any of the above, but for the purpose of this particular example, I have chosen option 3.


The platforms layer is created like so.



and the ladders layer is created like so.



The platform tiles and the ladder tiles do "overlap" where they intersect. However, the ladder sprites "background" is black and not transparent, hence it looks like there is no platform behind the ladder when the layers are displayed on top of one another.


I created the above tilemaps using TILED and exported the data as JSON file.


From preload, the data is loaded like so


  this.load.tilemapTiledJSON('map', 'chuckie.json');
  this.load.image('tileset', 'tileset.png');

Then the platforms layer and the ladders layer are created like so. Tile with index #1 is the platform tile.


   this.map = this.make.tilemap({ key: 'map', tileWidth: 16, tileHeight: 8 });
    const tileset = this.map.addTilesetImage('tileset');
    this.platforms = this.map.createLayer(0, tileset, 0, 0); // layer index, tileset, x, y
    this.ladders = this.map.createLayer(1, tileset, 0, 0);
    this.map.setCollision(1, true, true, this.platforms);

Arcade Physics Collider to automatically check platforms


Then arcade physics collider is set up as follows.


this.physics.add.collider(this.harry, this.platforms, null, this.checkXSection, this);

Note, I have not set up such a collider for collisions between Mr Dude and the ladders, preferring instead to check for such collisions within the Mr Dude class using getTileAtWorldXY (as I explain later), only when the up or down arrow keys are pressed. You could of course set up a arcade physics collider to check for ladders overlap too and then the structure of the code would be closer to that of the previous post. However, I think this structure is intuitively easier to understand for tilemaps.


Firing the collider event only when needed

As explained in the previous post, we need a way for the collision detection between player and the platforms to STOP firing when the player is climbing or when the player wants to climb down from a platform onto the ladder. This is done by creating the collider callback process as follows.



 checkXSection(harry, tile) {
   
    return !(
     ((harry.status === Harry.Status.Climbing) || (harry.status === Harry.Status.Walking && this.cursors.down.isDown && harry.canClimb()))
   )
  } 


The red highlighted code is checking 3 conditions:

  1. player is currently in walking state

  2. down arrow is being pressed

  3. player is in a position to climb down onto a ladder, ie Harry is properly lined up against a ladder


canClimb method is used to check condition (3).


canClimb() {
    const tile = this.scene.map.getTileAtWorldXY(this.x,this.y, true);
    const isLadder =  tile.index === 2;
    return (
      isLadder && this.body.left>=tile.pixelX && this.body.right<=tile.right
    )
  }

The logic is pretty much the same as the examples in the previous post, except it is checking what tile the player is overlapping with using getTileAtWorldXY method. Tile #2 is the ladder.


The crucial code to make the player transition from walking state to climbing state

The basic logic is the same as in the previous post. I have created a simple state pattern logic to control the character, from the update method.


The code relating to the Walking state (ie starting state) is shown below.


    switch(this.status) {
      case Harry.Status.Walking:
        
        if (this.scene.cursors.right.isDown) {
          this.setVelocityX(60);
          this.setFlipX(false)
          this.anims.play('walking', true)
        } else if (this.scene.cursors.left.isDown) {   
          this.setVelocityX(-60)
          this.setFlipX(true)
          this.anims.play('walking', true)
        } else {
          this.setVelocityX(0)
          this.anims.play('idle', true);
        }
     
        if (this.scene.cursors.up.isDown && this.canClimb(this)) {
          this.setStatus(Harry.Status.Climbing);
        }       
        if (this.scene.cursors.down.isDown) {
            if (this.canClimb(this) && !this.body.onFloor()) this.setStatus(Harry.Status.Climbing);
        }

        if (Phaser.Input.Keyboard.JustDown(this.scene.spacebar) && this.body.onFloor()) {
          this.setVelocityY(-150);
          this.setStatus(Harry.Status.Jumping);
        }
    
        break;
      

The blue highlighted code is when the up arrow key is pressed. Simultaneously, canClimb method is called to check whether he is in a position to climb up. If so, his state is set to Climbing.


The red code is when the down arrow key is pressed. In addition to checking whether the character is overlapping a ladder, a check is carried out to see if the character is grounded (ie cannot go down).



Allowing the player to jump off the ladder

Once the player's state has been changed to Climbing, the code to move the player up and down the ladder is as follows.


   case Harry.Status.Climbing:
       
        if (this.scene.cursors.right.isDown) {
          this.setVelocityX(60);
          this.setFlipX(false)
          this.anims.play('walking', true);
          this.setStatus(Harry.Status.Walking);
        }
        else if (this.scene.cursors.left.isDown) {   
          this.setVelocityX(-60)
          this.setFlipX(true)
          this.anims.play('walking', true);
          this.setStatus(Harry.Status.Walking);
        } 
        else if (this.scene.cursors.up.isDown && this.isLadder(this.getTopCenter())) {
          this.anims.play('climb', true);
          this.body.setVelocityY(-60)
        }
        else if (this.scene.cursors.down.isDown && this.isLadder(this.getBottomCenter())) {
          this.anims.play('climb', true);
          this.body.setVelocityY(60)
        }
        else {
          this.anims.play('idle-climb',true)
          this.body.setVelocityY(0)
        }
        break;
     
    }

Specifically, the blue code allows the player to fall off the ladder and turn to walking state, by pressing left or right arrows keys while on ladder. If this piece of code is deleted, the player will not be able to jump off the ladder half way up (and indeed, in the original version of the game, Harry could not jump off ladders I believe).


Note, when the left or arrow key is pressed, there should really be an additional check to make sure that the player is not moving into a platform. I have not implemented this but this would be easy enough to do - you just need to check what tile exists to the left or right of the character. Or you could simply not allow the character to walk off a ladder.


Climbing up and down the ladder

The red code checks the up (or down) arrow key, and whether the player is still overlapping with the ladder before giving the player a bit of velocity in the up or down direction. The check with the overlapping tile is necessary to stop the player going up above the ladder, into the sky, and going down, below into the ground.


The overlappingLadder method is very simple, as follows. All it does is to check to see if the index of the tile that the player is overlapping with is 2 (which is the index for the ladder tile).


  overlappingLadder(position) {
    const tile = this.scene.map.getTileAtWorldXY(position.x,position.y, true);
    return (tile.index === 2);  
  }

Items to collect

Whilst not the main purpose of this post, I have added some simple code so that Harry can collect the eggs and grains.


First I create the eggs and grains as sprites with arcade physics bodies from TILED object layer.


 this.eggs = this.physics.add.group({allowGravity: false,});
    this.map.filterObjects('Eggs', (obj) => {this.eggs.create(obj.x, obj.y, 'egg').setOrigin(0,1);});
    this.grains = this.physics.add.group({allowGravity: false,})
    this.map.filterObjects('Grains', (obj) => {this.eggs.create(obj.x, obj.y, 'grain').setOrigin(0,1);});

Then I set up colliders between Harry and the eggs and grains respectively.


  this.physics.add.overlap(this.harry, this.eggs, this.getEgg, null, this);
    this.physics.add.overlap(this.harry, this.grains, this.getGrain, null, this);

There is a method called setTileIndexCallback which is described as follows:


Sets a global collision callback for the given tile index within the layer. This will affect all tiles on this layer that have the same index. If a callback is already set for the tile index it will be replaced. Set the callback to null to remove it.


With a simple tilemap such as this one, the code would be more compact had I created the eggs and grain all in the same layer as the platforms layer and created the call back on the layer for the egg and grain tiles.


The code

And that's pretty much the logic relevant to the climbing tilemap ladders that poke out above the platform.



32 views0 comments
記事: Blog2_Post
bottom of page