BlogJogos

Criando estalactites que caem com Phaser JS - Game Devlog #20

Escrito em 28 de junho de 2021 - 🕒 4 min. de leitura

Fala galera! Estou finalmente de volta com outro devlog para o jogo Super Ollie, e hoje implementarei um elemento clássico de jogos de plataforma 2D, uma daquelas paradas que caem em cima do personagem quando ele passa por baixo, tipo uma estalactite.

Estalactites que caem

Primeiro vou criar a classe stalactite, bem simples:

class Spike extends GameObjects.Sprite {
    constructor({
        scene,
        x = 0,
        y = 0,
        asset = 'spike',
        enablePhysics = true,
        addToScene = true,
        frame,
        name,
    }) {
        super(scene, x, y, asset, frame);
        this.setOrigin(0, 1);
        this.setName(name || asset);

        if (addToScene) {
            scene.add.existing(this);
        }

        if (enablePhysics) {
            scene.physics.add.existing(this);
            this.body.setAllowGravity(false);
            this.body.setImmovable(true);
        }
    }
}

Agora no meu loop do dataLayer (que expliquei no devlog 8) vou adicionar um caso para as estalactites (spike) a serem adicionadas.

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

    if (name === 'spike') {
        // TODO add spike logic here
    }
});

Antes de entrar na lógica, meu objetivo é ser capaz de adicionar uma estalactite onde eu quiser pelo Tiled, e o código irá automaticamente fazer a estalactite funcionar.

Como funciona uma estalactite dessas que caem em jogos de plataforma? Bem, quando o jogador passa por debaixo dela, ela cai nele, imediatamente ou alguns milissegundos depois, e para isso precisamos de um custom collider no chão, que quando o jogador interagir com ele, fará com que a estalactite caia.

Essa é a mecânica que eu quero automatizar, e para isso vou criar uma lógica para que a estalactite decida automaticamente onde o custom collider estará no mapa a partir da sua posição inicial.

if (name === 'spike') {
    const spike = new Spike({
        scene: this,
        x,
        y,
    });

    const customCollider = createColliderFromGameObjectToGround(
        this,
        spike,
        mapSize,
        this.mapData.dynamicLayers.ground
    );

    const { y: spikeY } = spike;
    const collider = this.physics.add.overlap(
        customCollider,
        this.player,
        () => {
            // TODO handle spike falling
        }
    );

    this.physics.add.overlap(
        spike,
        this.player,
        () => {
            // TODO handle spike hitting the player
        }
    );
}

A função createColliderFromGameObjectToGround terá toda a lógica para obter a posição da estalactite e encontrar o tile do tipo ground mais próximo de sua posição e colocar um custom collider lá.

Dentro desta nova função, vou precisar de 2 outras funções, createInteractiveGameObject, que mostrei no devlog 15, e uma nova função chamada isBelowTileCollidable, que receberá um Game Object e verificará se o tile abaixo desse objeto é colidível ou não.

const isBelowTileCollidable = (
    gameObject,
    dynamicLayer // single layer or GameObject Group
) => {
    const { x, y, height } = gameObject;
    const layers = dynamicLayer?.getChildren?.() || [dynamicLayer];

    let tile = null;
    let result = false;
    layers.forEach((layer) => {
        if (result) {
            return;
        }

        tile = layer.getTileAtWorldXY(
            x - 0.5,
            y + height + 0.5
        );

        if (tile) {
            result = tile?.properties?.collideUp;
        }
    });

    return result;
};

Para a função createColliderFromGameObjectToGround, eu irei criar um loop do tamanho da altura do mapa e, a partir da posição da estalactite, começar a buscar pelo tile do tipo ground mais próximo, e também usarei a função some para poder sair do loop quando um tile for encontrado.

const createColliderFromGameObjectToGround = (
    scene,
    gameobject,
    mapSize,
    mapLayer
) => {
    let customCollider;
    let result = false;

    // TODO improve the size of this array
    new Array(mapSize.height / gameobject.height).fill(null).some(
        (val, index) => {
            const posY = gameobject.y + (TILE_HEIGHT * (index + 1));
            result = isBelowTileCollidable({
                    ...gameobject,
                    y: posY,
                },
                mapLayer
            );

            if (result) {
                const spikeHeight = gameobject.height + (posY - gameobject.y);
                customCollider = createInteractiveGameObject(
                    scene,
                    gameobject.x,
                    // posY,
                    posY - spikeHeight + gameobject.height,
                    gameobject.width,
                    // spike.height,
                    spikeHeight,
                    gameobject.name
                );
            }

            return result;
        }
    );

    return customCollider;
};

Com essas 2 funções criadas, eu posso voltar ao meu loop do dataLayer e trabalhar na lógica da estalactite, por enquanto quero que a estalactite caia quando o jogador interagir com o custom collider, e em seguida, fazer a estalactite reaparecer no teto após 2 segundos.

if (name === 'spike') {
    const spike = new Spike({
        scene: this,
        x,
        y,
    });

    const customCollider = createColliderFromGameObjectToGround(
        this,
        spike,
        mapSize,
        this.mapData.dynamicLayers.ground
    );

    // if for some reason the collider was not created, do nothing.
    if (customCollider) {
        const { y: spikeY } = spike;
        const collider = this.physics.add.overlap(
            customCollider,
            this.player,
            () => {
                if (!spike.body.allowGravity) {
                    spike.body.setAllowGravity(true);
                    this.time.delayedCall(2000, () => {
                        spike.body.setAllowGravity(false);

                        spike.body.setAcceleration(0, 0);
                        spike.body.setVelocity(0, 0);
                        spike.setY(spikeY);
                        spike.setActive(true);
                        spike.setVisible(true);
                    });
                }
            }
        );

        this.physics.add.overlap(
            spike,
            this.player,
            () => {
                if (spike?.visible) {
                    this.gameHud.decreasePlayerLife(5);
                    spike.setActive(false);
                    spike.setVisible(false);
                }
            }
        );
    } else {
        spike.destroy();
    }
}

Com o modo de debug ativado, é assim que o custom collider aparece no jogo:

Custom colliders das estalactites
Custom colliders das estalactites

Isso é tudo por hoje, até o próximo devlog!

Tags:


Publicar um comentário

Comentários

Nenhum comentário.