BlogGames

Creating moving platforms for my Phaser JS game - Game Devlog #22

Written in August 28, 2021 - 🕒 10 min. read

Hello everyone, welcome to another game devlog for my game Super Ollie, and after almost a whole year of development I’m finally tackling something that every platformer must have: Moving platforms.

A platformer needs platforms.

— Sherlock Holmes, 600 BC

Defining platforms types

There are many types of moving platforms, but for my game, I decided to stick to 6 different types for now. I hope you’re ready because this is going to be a big devlog 😬.

Platforms that follow a path

Follow path platform

Platforms that will follow a certain path drawn on Tiled.

Platforms that move in a circular path

Circular movement platforms

Platforms that follow a circular movement based on a circle drawn on Tiled.

Platforms that vanish

Vanishing platform

Platforms that will fall/vanish after a couple of seconds of the hero getting on top of them.

Platforms that fall

Falling platform

Platforms that will start to slowly fall when the hero is on top of them.

Elevator platforms

Elevator platform

Platforms that will move up and down like an elevator, and will only trigger the movement once the hero is on top of them.

Looping platforms

Looping platforms

Platforms that will constantly move vertically until it reaches the end of the stage and then loops back to the top.

The code

I will start by adding a new if condition to my data layer objects loop, and an attribute called platformAI on the platform object on Tiled, so I can create different AIs/Types of platforms.

// in the scene create function
const dataLayer = map.getObjectLayer('data');
dataLayer.objects.forEach((data) => {
    const { x, y, name, height, width } = data;

    if (name === 'platform') {
        const platformAI = properties?.find(
            (property) => property?.name === 'platformAI'
        )?.value;

        const velocity = properties?.find(
            (property) => property?.name === 'velocity'
        )?.value || DEFAULT_PLATFORM_VELOCITY;

        const platform = new Platform({
            scene: this,
            velocity,
            x,
            y,
        });

        this.platforms.add(platform);

        switch (platformAI) {
            case LOOP_PLATFORM: {
                // TODO
            }
            case FALLING_PLATFORM: {
                // TODO
            }
            case ELEVATOR_PLATFORM: {
                // TODO
            }
            case VANISHING_PLATFORM: {
                // TODO
            }
            case CIRCULAR_PATH_PLATFORM: {
                // TODO
            }
            case PATH_PLATFORM:
            default: {
                // TODO
            }
        }
    }
});

With this base code in place, I can now start coding each of the different behaviors for the platforms.

Path follower

For this platform I will need to draw a path on Tiled and then make the platform follow it, and when I do that it comes as an array of positions like [{ x: 1, y: 2 }, { x: 3, y: 6 }], and the idea is to get each of these positions and create a custom collider that when in contact with the platform will set its speed and direction.

For that I’m going to need the createInteractiveGameObject function, that I showed on the devlog 15.

switch (platformAI) {
    case PATH_PLATFORM:
    default: {
        // Get path from Tiled
        const platformPath = dataLayer.find(
            (object) => {
                const pathType = object.properties?.find(
                    (property) => property?.name === 'type'
                )?.value;

                return object.polyline?.length
                    && (object.name === (pathName || objectId))
                    && pathType === PATH_TYPE_PLATFORM;
            }
        );

        platformPath.polyline.forEach((path, index, polylines) => {
            // create a custom interactive object for each of the polyline points
            const platformCollider = createInteractiveGameObject(
                this,
                platformPath.x + path.x,
                platformPath.y + path.y,
                4,
                4,
                'platform_collider'
            );
        });

        // set custom properties for the collider
        platformCollider.setOrigin(0.5);
        platformCollider.nextPoint = polylines[index + 1];
        platformCollider.currentPoint = polylines[index];
        platformCollider.previousPoint = polylines[index - 1];
        platform.direction = 'forward';
        
        // now create a collider for it
        this.physics.add.collider(platform, platformCollider, () => {
            if (!platformCollider.nextPoint) {
                platform.direction = 'backward';
            } else if (!platformCollider.previousPoint) {
                platform.direction = 'forward';
            }

            const {
                nextPoint,
                currentPoint,
                previousPoint,
            } = platformCollider;
            const { direction } = platform;
            const nextPos = direction === 'forward' ? nextPoint : previousPoint;

            const newVelocity = {
                x: nextPos.x !== currentPoint.x ? (velocity * (nextPos.x > currentPoint.x ? 1 : -1)) : 0,
                y: nextPos.y !== currentPoint.y ? (velocity * (nextPos.y > currentPoint.y ? 1 : -1)) : 0,
            };

            platform.body.setVelocity(
                newVelocity.x,
                newVelocity.y
            );
        });
        
        // Create collider for player and platform
        this.physics.add.collider(platform, this.player);
        break;
    }
}

Circular path

This one was the toughest to create, because I needed some knowledge on basic circular motion physics, in which I had none 😅 and I wanted to be able to choose the speed and direction of the spin on Tiled.

So I asked my girlfriend for help since she has a degree in engineer, but she knew so much, and I knew so little, that it was a hard conversation but in the end, I managed to do it with her help.

The base rotation formulas are the following:

radius = circleDiameter / 2

secondsPerRotation = gravity / velocity

angularSpeed = 360 / secondsPerRotation

speed = (radius * (π * 2)) / secondsPerRotation

The trick is to create a dummy object and set a spin on it with the setAngularVelocity function, then copy the velocity from the dummy object to the platform using the velocityFromRotation function.

switch (platformAI) {
    case CIRCULAR_PATH_PLATFORM: {
        const direction = properties?.find(
            (property) => property?.name === 'direction'
        )?.value || DIRECTION_RIGHT;

        const platformCircularPath = dataLayers.find(
            (object) => {
                const pathType = object.properties?.find(
                    (property) => property?.name === 'type'
                )?.value;

                return object.ellipse
                    && (object.name === (pathName || objectId))
                    && pathType === PATH_TYPE_PLATFORM;
            }
        );

        const radius = platformCircularPath.width / 2;

        // set platform position to the top of the circle to start the rotation
        platform.setPosition(
            platformCircularPath.x + radius - (platform.width / 2),
            platformCircularPath.y + (platform.height / 2)
        );

        // This is where the magic happens
        // I honestly have no idea why this works, but it works
        const secondsPerRotation = this.physics.world.gravity.y / velocity;
        // increase angular speed, decreases radius
        const angularSpeed = 360 / secondsPerRotation;
        // increase speed, increase radius
        const speed = (radius * (Math.PI * 2)) / secondsPerRotation;

        // create a dummy object to create the base rotation physics
        const dummyRotation = new GameObjects.Rectangle(
            this,
            platformCircularPath.x + radius,
            platformCircularPath.y + radius,
            1,
            1
        );

        this.physics.add.existing(dummyRotation);
        dummyRotation.body.setAllowGravity(false);
        dummyRotation.body.setImmovable(true);
        dummyRotation.body.setMaxVelocity(speed);

        if (direction === DIRECTION_LEFT) {
            platform.setY(platform.y + (radius * 2));
            dummyRotation.body.setAngularVelocity(-angularSpeed);
        } else {
            dummyRotation.body.setAngularVelocity(angularSpeed);
        }

        // set the platform velocity to be equal to the one from the dummy object
        platform.update = (time, delta) => {
            this.physics.velocityFromRotation(
                PhaserMath.DegToRad(dummyRotation.body.rotation),
                speed,
                platform.body.velocity
            );
        };

        // Create collider for player and platform
        this.physics.add.collider(platform, this.player);
        break;
    }
}

Vanishing

This one is simple, I will just create a collider that when the player touches the upper part of the platform, I will set a timer for the platform to vanish and a timer for the platform to show up again.

switch (platformAI) {
    case VANISHING_PLATFORM: {
        platform.isTriggered = false;
        const originalCheckCollision = {
            ...platform.body.checkCollision,
        };

        const platformReapearingCallback = () => {
            platform.isTriggered = false;
            platform.body.setVelocityY(0);
            platform.body.checkCollision = originalCheckCollision;

            platform.setPosition(
                platformX,
                platformY
            );
        };
        
        const platformVanishingCallback = () => {
            platform.body.setVelocityY(velocity);

            this.time.delayedCall(
                2000,
                platformReapearingCallback
            );
        };

        const colliderCallback = () => {
            if (platform.isTriggered) {
                return;
            }

            platform.isTriggered = true;
            const {
                x: platformX,
                y: platformY,
            } = platform;

            this.time.delayedCall(
                1000,
                platformVanishingCallback
            );
        };

        this.physics.add.collider(
            platform,
            this.player,
            colliderCallback
        );
        
        break;
    }
}

Falling

For this one, I will need an update function for my platform to make it slowly come back up after the player is no longer on top of it. As for the rest, it is pretty much the same as the vanishing platforms.

switch (platformAI) {
    case FALLING_PLATFORM: {
        // get initial position of the platform
        // to reset it to
        const {
            y: platformY,
        } = platform;

        platform.update = (time, delta) => {
            if (!platform.touchingUpObject) {
                if (platform.y > platformY) {
                    platform.body.setVelocityY(
                        -platform.velocity
                    );
                } else {
                    // reset platform to original position
                    platform.body.setVelocityY(0);
                    platform.setY(platformY);
                }
            }
        };

        this.physics.add.collider(
            platform,
            this.player,
            () => {
                if (platform.touchingUpObject) {
                    platform.body.setVelocityY(velocity);
                }
            }
        );

        break;
    }
}

Elevator

The elevator platform needs a lot of code to create all the features that I wanted. Those are:

  • When the player steps on the platform, it will move up/down and then stop.
  • If the player jumps and steps back into the platform, it will trigger the move again.
  • If the platform is not at the same level as the player when the player approaches the platform, it should move to the player.

For that, I need one custom collider in the ground, that will trigger the platform to come down when it is up, a custom collider in the upper level to trigger the platform to go up when it is down, and an update function to determine if the platform should be triggered or not when the player is on top of it.

switch (platformAI) {
    case ELEVATOR_PLATFORM: {
        // get path from Tiled
        const platformPath = combinedDataLayers.find(
            (object) => {
                const pathType = object.properties?.find(
                    (property) => property?.name === 'type'
                )?.value;

                return object.polyline?.length
                    && (object.name === (pathName || objectId))
                    && pathType === PATH_TYPE_PLATFORM;
            }
        );

        const {
            y: platformY,
        } = platform;

        // get base positions
        const groundFloorPosY = platform.y - (platform.height - platform.body.height);
        const secondFloorPosY = platformY + platformPath.polyline[1].y;
        
        // create ground object custom collider
        const elevatorGroundCollider = createInteractiveGameObject(
            this,
            platformPath.x - platform.width,
            platformPath.y - platform.height,
            platform.width + (TILE_WIDTH * 2),
            platform.height + TILE_HEIGHT,
            'ground-collider'
        );

        const elevatorGroundColliderCallback = () => {
            if (platform.body.velocity.y === 0) {
                if (platform.y <= secondFloorPosY) {
                    platform.body.setVelocityY(velocity);
                }
            }
        };

        this.physics.add.overlap(
            elevatorGroundCollider,
            this.player,
            elevatorGroundColliderCallback
        );

        // create floor object custom collider
        const elevatorFloorCollider = createInteractiveGameObject(
            this,
            platformPath.x - platform.width,
            secondFloorPosY - (platform.height + TILE_HEIGHT),
            platform.width + (TILE_WIDTH * 2),
            platform.height + TILE_HEIGHT,
            'floor-collider'
        );

        const elevatorFloorColliderCallback = () => {
            if (platform.body.velocity.y === 0) {
                if (platform.y >= groundFloorPosY) {
                    platform.body.setVelocityY(-velocity);
                }
            }
        };

        this.physics.add.overlap(
            elevatorFloorCollider,
            this.player,
            elevatorFloorColliderCallback
        );

        // create the platform update function
        platform.update = (time, delta) => {
            if (!platform.touchingUpObject) {
                platform.wasTriggered = false;
            }

            if (
                platform.body.velocity.y === 0
                && platform.y < groundFloorPosY
                && platform.y > secondFloorPosY
            ) {
                platform.body.setVelocityY(velocity);
            }

            if (platform.y >= groundFloorPosY) {
                platform.setY(groundFloorPosY);
                if (platform.body.velocity.y > 0) {
                    platform.body.setVelocityY(0);
                }
            }

            if (platform.y <= secondFloorPosY) {
                if (platform.body.velocity.y < 0) {
                    platform.body.setVelocityY(0);
                }

                if (!platform.touchingUpObject) {
                    platform.setY(secondFloorPosY);
                }
            }
        };

        const colliderCallback = () => {
            if (!platform.body.touching.up) {
                return;
            }

            if (platform.y < (this.player.y + platform.body.height)) {
                return;
            }

            if (platform.wasTriggered) {
                return;
            }

            if (platform.y >= groundFloorPosY) {
                platform.body.setVelocityY(-velocity);
            } else if (platform.y <= secondFloorPosY) {
                platform.body.setVelocityY(velocity);
            }

            platform.wasTriggered = true;
        };

        this.physics.add.collider(
            platform,
            this.player,
            colliderCallback
        );

        break;
    }
}

Looping

Another easy one, I will just set the y velocity for it, and when it’s off the map, I will reset it to the top of the map.

switch (platformAI) {
    case LOOP_PLATFORM: {
        platform.body.setVelocityY(platform.velocity);

        platform.update = (time, delta) => {
            if ((platform.y - platform.height) > mapSize.height) {
                platform.setY(platform.height);
            }
        };

        this.physics.add.collider(platform, this.player);
        break;
    }
}

And that’s it… or is it…?

The problem

If you just copied all this code into your own game and went ahead to test it, you will notice that the player is in a constant falling state when the platform is moving downwards.

Jump jump jump ju-ju-jump ♫

This happens because the acceleration of the player is slower than the platform’s, to fix this I will make a very hacky solution, make the player’s gravity around 10 times stronger when they are on top of a platform 😬.

The solution

First I will add a new property to the platforms called changeHeroPhysics:

const platform = new Platform({
    changeHeroPhysics: true,
    scene: this,
    velocity,
    x,
    y,
});

Then in the player update function, I will add a check to see if the player is touching an object with the property changeHeroPhysics set to true and if so, increase the player’s gravity.

if (this.touchingDownObject?.changeHeroPhysics) {
    const platformVelocityY = this.touchingDownObject?.body?.velocity?.y;
    const { gravity } = this.scene.physics.world;

    if (platformVelocityY > 0) {
        this.body.setGravity(
            0,
            gravity.y * (platformVelocityY / 12)
        );
    } else {
        this.body.setGravity(0, 0);
    }
} else {
    // eslint-disable-next-line no-lonely-if
    if (this.body.gravity.y !== 0) {
        // set back original gravity to the hero
        this.body.setGravity(0, 0);
    }
}

Wrap up

There you go, a huge list of super useful platforms for your Phaser platformer game. Please let me know if I missed any types of platforms and I might add them to the list in the future.

Have a good one and happy coding!

Tags:


Post a comment

Comments

No comments yet.