BlogJogos

Criando plataformas para o meu jogo em Phaser JS - Game Devlog #22

Escrito em 28 de agosto de 2021 - 🕒 10 min. de leitura

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.

// 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.

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.

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.

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.

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.

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.

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:

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.

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!

Tags:


Publicar um comentário

Comentários

Nenhum comentário.