My solution for a Phaser.Physics.Arcade.Sprite to use stairs

Hi! I would to propose a solution for a problem which take me maybe 12 hours to resolve.
How to make an Arcade Sprite use stairs. It is a pretty dirty solution in my opinion but it works, so…
Maybe you would know a better way to do it, and maybe some people would be happy to find my snippet. So I am open to suggestion and, if I can help someone else, it is a win win :slight_smile:

Here is my level. As you can see, My player will have 3 stairs to go through.

I tried moveToObject, things like that, but nothing to do, it seemed to be impossible to use any built-in easy Phaser stuff to resolve this kind of usecase.

So I’ve found inspiration by reading the source code of the Rexrainbow’s pathfollower plugin.

Above each of my stairs, I draw a line with a direction property. It can be LEFT or RIGHT, it is the direction of the stairs relative to the start of the level (my player goes from left to right).


For each stairs line, I will create two things:
An Object that I will insert into a Group
A curve which represent this stairs line
Each time my player overlaps a line, I will split the related curve in many points and I will update my player’s position according to each points
This will create the illusion of climbing up/down the stairs.
When the player goes to a specific direction and collides a stair, according to my current player direction, I check the stair direction and decide how to make my player cross the stair.

So here is my code:

First in my scene

// into create()
// below I get all the stairs line
const stairs = this.tilemap.filterObjects("stairs_detection", obj => obj.name === "stairs_line");
// I create my player sprite, passing the stairs objects
this.player = new Player(this, this.start.x, this.start.y, stairs);
// no matter how I get the x, y coordinates tho

Then in my Player class

export default class Player extends Phaser.Physics.Arcade.Sprite {
    constructor(
    	scene: Phaser.Scene,
    	x: number,
    	y: number,
    	stairs: Phaser.Types.Tilemaps.TiledObject[],
	) {
        super(scene, x, y, 'player', 'idle-1');
        
        // I will check overlaping between the player and the stairs
        this.stairsGroup = this.scene.physics.add.staticGroup();

        // By default, my player is not using the stairs
        this.isClimbing = false;
        
        // I will iterate on each stairs line in order to fill my stairsGroup
        this.stairs = stairs;
    }

    create() {
    	// Check the overlap between this player and my stairs
        this.scene.physics.add.overlap(
        	this,
        	this.stairsGroup,
        	(_, stair) => {
        		// if player was not climbing, soooo now he will!
		        if (!this.isClimbing) {
		        	// this curve has been setted few lines below
		            const curve = stair.getData('curve');
		            // the direction of my stairs as setted in Tiled
		            this.climbingDirection = stair.getData('direction');
		            // splitting my curve in many points
		            this.currentStairsClimbing = curve.getSpacedPoints(curve.getLength() / 7);
		            this.isClimbing = true;
		        }
	    	},
	        	undefined,
	        	this,
	    	);

        if (this.stairs.length > 0) {
        	// for each stairs line I got from the scene
            for (const stair of this.stairs!) {
            	// stair object is built in a strange way,
            	// I have to make some stupid calculations in order to get the real line start and end coordinates
                const start = { x: stair!.x, y: stair!.y };
                const poly = stair!.polyline![1];
                const end = { x: stair!.x!+poly.x!, y: stair!.y!+poly.y! };
                // create my curve line
                const curve = new Phaser.Curves.Line(
                    new Phaser.Math.Vector2(start.x, start.y),
                    new Phaser.Math.Vector2(end.x, end.y)
                );

                // adding in my stairsGroup an invisible sprite based on the curve coordinates
                const oneStair = this.stairsGroup.create(curve.getBounds().x, curve.getBounds().y, undefined, undefined, false);
                oneStair.setScale(curve.getBounds().width / 32, curve.getBounds().height / 32);
                oneStair.setOrigin(0);
                oneStair.setDataEnabled();
                // below setting the data I will use in the overlap callback above
                const direction = stair.properties.find((prop) => prop.name === 'direction');
                oneStair.setData('direction', direction.value);
                oneStair.setData('curve', curve);
                oneStair.body.width = curve.getBounds().width;
                oneStair.body.height = curve.getBounds().height;
                // if I do not refresh the group, its content is not usable (weird)
                this.stairsGroup.refresh();
            }
        }
    }

    update() {
    	// this loop helps me to define when I stop to overlap my stairs
    	// if I do not, I can't pass this.isClimbing at false...
        this.stairsGroup.children.entries.forEach(() => {
            if (this.isClimbing) {
                if (this.body.touching.none) {
                    this.isClimbing = false;
                    // details for those two things later
                    this.currentStairsClimbing = [];
                    this.currentstairsClimbingIndex = 0;
                }
            }
        });
        // move player according to left or right down
        this.movePlayer();
    }

    private movePlayer() {
        if (this.isMovable) {
            if (this.left.isDown) {
                this.goLeft = true;
            } else {
                this.goLeft = false;
            }
            if (this.right.isDown) {
                this.goRight = true;
            } else {
                this.goRight = false;
            }

            if (this.goLeft) {
            	// flip my player to the left and change his internal direction
                this.flipX = true;
        		this.direction = 'left';
        		// if he is climbing, I will process the climbing algorythm
                if (this.isClimbing) {
                    this.climbOnLeftDirection();
                } else {
                	// if he is not climbing, so he just go ahead
                    this.setVelocityX(-this.speedX);
                }

            } else if (this.goRight) {
                this.flipX = false;
        		this.direction = 'right';
                if (this.isClimbing) {
                    this.climbOnRightDirection();
                } else {
                    this.setVelocityX(this.speedX);
                }
            } else {
                this.setVelocityX(0);
            }
        }
    }

    // so here, my player goes to the right direction
    private climbOnRightDirection() {
    	// if he is overlaping a LEFT stairs
        if (this.climbingDirection === 'LEFT') {
            this.climbFromRightPositionToLeftStairs();
        }
        // if he is overlaping a RIGHT stairs
        else {
            this.climbFromRightPositionToRightStairs();
        }
    }

	// so here, my player goes to the left direction
    private climbOnLeftDirection() {
        if (this.climbingDirection === 'LEFT') {
            this.climbFromLeftPositionToLeftStairs();
        } else {
            this.climbFromLeftPositionToRightStairs();
        }
    }

	// my player goes to the right direction and is overlaping a RIGHT stairs
	// in this direction, I will use stairs from first point to last
    private climbFromRightPositionToRightStairs() {
        this.setVelocityX(this.speedX / 2);
        // I check if I'm not out of bound for the current stairs
        if (this.currentstairsClimbingIndex + 1 <= this.currentStairsClimbing?.length) {
        	// I take the current stairs point at current index (default 0, the first one)
            const currentPosition = this.currentStairsClimbing[this.currentstairsClimbingIndex];
            // increase index
            this.currentstairsClimbingIndex++;
            // set player position
            this.setPosition(currentPosition.x, currentPosition.y);
        }
    }

	// my player goes to the right direction and is overlaping a LEFT stairs
	// in this direction too, I will use stairs from first point to last
    private climbFromRightPositionToLeftStairs() {
        this.setVelocityX(this.speedX / 2);
        if (this.currentstairsClimbingIndex + 1 <= this.currentStairsClimbing?.length) {
            const currentPosition = this.currentStairsClimbing[this.currentstairsClimbingIndex];
            this.currentstairsClimbingIndex++;
            this.setPosition(currentPosition.x, currentPosition.y);
        }
    }

    // In the two directions below, I will use stairs from the other side, from last point to first
    // my player goes to the left direction and is overlaping a RIGHT stairs
    private climbFromLeftPositionToRightStairs() {
        this.setVelocityX(-this.speedX / 2);
        if (this.currentstairsClimbingIndex + 1 <= this.currentStairsClimbing?.length) {
            const currentPosition = this.currentStairsClimbing[this.currentStairsClimbing.length - 1 - this.currentstairsClimbingIndex];
            this.currentstairsClimbingIndex++;
            this.setPosition(currentPosition.x, currentPosition.y);
        }
    }

    // my player goes to the left direction and is overlaping a LEFT stairs
    private climbFromLeftPositionToLeftStairs() {
        this.setVelocityX(-this.speedX / 2);
        if (this.currentstairsClimbingIndex + 1 <= this.currentStairsClimbing?.length) {
            const currentPosition = this.currentStairsClimbing[this.currentStairsClimbing.length - 1 - this.currentstairsClimbingIndex];
            this.currentstairsClimbingIndex++;
            this.setPosition(currentPosition.x, currentPosition.y);
        }
    }
}

For more details on the climbFrom stuff, here one image for each case:

climbFromRightPositionToRightStairs

climbFromRightPositionToLeftStairs

climbFromLeftPositionToLeftStairs

climbFromLeftPositionToRightStairs

Open to any question, I think it is not very clear at all :smiley:

After 2 hours, news look on my solution, I’ve removed useless notions. In fact, no matter the direction of each stair is (LEFT, RIGHT). The thing which is important is the direction you used to draw each stairs_line on Tiled (in fact, which is the FIRST point and which is the SECOND).
Relativly to a reading left to right of your map:
For the line you will draw for the stairs which go up, its first point must be on the floor, and the 2nd one at the top of the stair.
For the line you will draw for the stairs which go down, its first point must be on the top, and the 2nd one at the floor.

So, I eventually fix my code like this:

export default class Player extends Phaser.Physics.Arcade.Sprite {

	//...

    create() {
        this.scene.physics.add.overlap(
        	this,
        	this.stairsGroup,
        	(_, stair) => {
		        if (!this.isClimbing) {
		        	
		        	//...
		            
					// you do not need this line below		            
		            this.climbingDirection = stair.getData('direction');
		        }
	    	},
	        	undefined,
	        	this,
	    	);

        if (this.stairs.length > 0) {
            for (const stair of this.stairs!) {
            	
            	//...

            	// you do not need those two lines below
                const direction = stair.properties.find((prop) => prop.name === 'direction');
                oneStair.setData('direction', direction.value);

                //...
            }
        }
    }

    // you can now remove the methods climbFromRightPositionTo... and climbFromRightPositionTo...

    // and just keep the two below

    // my player goes to the right direction, no matter if he go down or up in the stairs
	private climbOnRightDirection() {
		this.setVelocityX(this.speedX / 2);
		if (this.currentstairsClimbingIndex + 1 <= this.currentStairsClimbing?.length) {
			const currentPosition = this.currentStairsClimbing[this.currentstairsClimbingIndex];
			this.currentstairsClimbingIndex++;
			this.setPosition(currentPosition.x, currentPosition.y);
		}
	}

	// my player goes to left direction, no matter if he go down or up in the stairs
	private climbOnLeftDirection() {
		this.setVelocityX(-this.speedX / 2);
		if (this.currentstairsClimbingIndex + 1 <= this.currentStairsClimbing?.length) {
			const currentPosition = this.currentStairsClimbing[this.currentStairsClimbing.length - 1 - this.currentstairsClimbingIndex];
			this.currentstairsClimbingIndex++;
			this.setPosition(currentPosition.x, currentPosition.y);
		}
	}
}
1 Like

how about simply creating a “stair-step” Gamesprite and if the player collides with it, you walk up otherwise you simply fall down a step

That would work but in my case, my stairs sprite has the size of a tile, so in only one sprite I have many steps.