Fala galera, bem-vindos a mais um devlog do meu jogo Super Ollie, e depois de quase um ano inteiro de desenvolvimento, estou finalmente tentando resolver algo que todo jogo de plataforma precisa ter: Plataformas.

Um jogo de plataforma precisa de plataformas.

— Sherlock Holmes, 600 BC

Definindo tipos de plataformas#

Existem vários tipos de plataformas, mas para o meu jogo, decidi ficar com 6 tipos diferentes por enquanto. Espero que você esteja pronto porque este será um grande devlog 😬.

Plataformas que seguem um caminho#

Follow path platform

Plataformas que seguirão um determinado caminho desenhado no Tiled.

Plataformas que se movem em um caminho circular#

Circular movement platforms

Plataformas que seguem um movimento circular baseado em um círculo desenhado em Tiled.

Plataformas que desaparecem#

Plataformas que desaparecem

Plataformas que desaparecem alguns segundos após o herói subir nelas.

Plataformas que caem#

Plataformas que caem

Plataformas que começarão a cair lentamente quando o herói estiver em cima delas.

Plataformas que funcionam como um elevador#

Platforma elevador

Plataformas que se movem para cima e para baixo como um elevador e só acionam o movimento quando o herói está em cima delas.

Plataformas em loop#

Plataformas em loop

Plataformas que se movem constantemente na vertical até atingir o final do estágio e então voltam ao topo.

O código#

Primeiro eu vou adicionar uma nova condição no meu loop do dataLayer para lidar com o código de novas plataformas, e no Tiled vou um novo atributo chamado platformAI nas plataformas para que eu possa criar AIs / Tipos de plataformas diferentes.

javascript
// 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
            }
        }
    }
});

Com esse código base pronto, eu posso começar a codificar cada um dos diferentes comportamentos para as plataformas.

Seguidora de caminho#

Para esta plataforma, vou precisar desenhar um caminho no Tiled e, em seguida, fazer a plataforma segui-lo, e quando eu faço, o JSON do Tiled vem como uma array de posições no formato [{x: 1, y: 2}, {x: 3, y: 6}] , e a ideia é pegar cada uma dessas posições e criar um custom collider que, quando em contato com a plataforma, mudará sua velocidade e direção.

Para isso, vou precisar da função createInteractiveGameObject, que mostrei no devlog 15.

javascript
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;
    }
}

Caminho circular#

Essa foi a mais difícil de criar, porque eu precisava de algum conhecimento em física de movimento circular, no qual não tinha nenhum 😅 e, além disso, eu queria poder escolher a velocidade e a direção da rotação no Tiled.

Então eu pedi ajuda à minha namorada porque ela é formada em engenharia, mas ela sabia tanto, e eu sabia tão pouco, que foi uma conversa difícil, mas no final ela conseguiu me ajudar. Yay!

As fórmulas de rotação que vou usar são as seguintes:

raio = diametroDoCirculo / 2

rotacaoPorSegundo = gravidade / velocidade

velocidadeAngular = 360 / rotacoesPorSegundo

velocidade = (raio * (π * 2)) / rotacoesPorSegundo

O truque é criar um objeto que sera usado apenas para referência e definir uma rotação nele com a função setAngularVelocity e, em seguida, copiar a velocidade desse objeto para a plataforma usando a função velocityFromRotation.

javascript
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;
    }
}

Desaparecendo#

Esta é simples, vou apenas criar um custom collider que quando o herói tocar a parte de cima da plataforma, irei definir um timer para que a plataforma desapareça e um timer para que a plataforma apareça novamente.

javascript
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;
    }
}

Caindo#

Para este, vou precisar de uma função update para a minha plataforma para fazê-la voltar ao lugar lentamente depois que o herói não estiver mais em cima dela. Quanto ao resto, é praticamente o mesmo que as plataformas que desaparecem.

javascript
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;
    }
}

Elevador#

A plataforma elevador precisa de muito código para criar todos os recursos que eu queria. Esses são:

  • Quando o herói pisa na plataforma, ele se move para cima / para baixo e depois para.
  • Se o herói pular e voltar para a plataforma, o movimento será acionado novamente.
  • Se a plataforma não estiver no mesmo nível do herói quando o herói se aproximar da plataforma, ela deve se mover até o herói.

Para isso, eu preciso de um custom collider no chão, que fará com que a plataforma desça quando estiver em cima, e um custom collider no nível superior para ativar a plataforma para subir quando estiver abaixada, e uma função update para determinar se a plataforma deve ser acionada ou não quando o herói está em cima dela.

javascript
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#

Outra fácil, irei apenas definir a velocidade y para ela, e quando estiver fora do mapa, mudar sua posição para o topo do mapa novamente.

javascript
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;
    }
}

E é isso… ou será que é…?

O problema#

Se você apenas copiou todo esse código para o seu próprio jogo e foi testá-lo, notará que o herói está meio bugado quando a plataforma está se movendo para baixo.

Jump jump jump ju-ju-jump ♫

Isso acontece porque a aceleração do herói é mais lenta que a da plataforma. Para consertar isso tive que fazer uma gambiarra: Sempre que o herói estiver em cima de uma plataforma, aumentar a gravidade do herói em cerca de 10 vezes 😬.

A solução#

Primeiro, adicionarei uma nova propriedade às plataformas chamada changeHeroPhysics:

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

Então, na função update do herói, adicionarei uma verificação para ver se o herói está tocando um objeto com a propriedade changeHeroPhysics definida como true e, se estiver, aumentarei a gravidade do herói.

javascript
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);
    }
}

Conclusão#

Ai esta, uma enorme lista de plataformas super úteis para o seu platformer no Phaser. Por favor, deixe um comentário se eu perdi algum tipo de plataforma e talvez eu possa adicioná-los à lista no futuro.

Por hoje é só, até o próximo devlog!