Criando um RPG. Detectando colisões com phaserjs

Colisões PhaserJS – Como Criar Jogos com PhaserJS

O problema

Se você já jogou um bom RPG você já deve ter se deparado com algum inimigo. Talvez um Goblin, Orc, etc. O resumo da ópera é, você manda ataques à este inimigo até que ele tenha seus pontos de vida zerados e ele seja vencido. Acontece que neste processo todo a gente não para pra pensar no que vai por trás de tudo isso, simplesmente jogamos sem pensar no amanhã 😛 Hoje vou mostrar pra você como detectar colisões dentro de nosso projeto de RPG utilizando o PhaserJS.

Habilitando Physics no jogo

Precisamos em nosso caso habilitar o nosso framework de Physics que vai nos habilitar a detectar colisões. O PhaserJS, no momento em que estou escrevendo este artigo possui 3 deles, que se chamam: Arcade, Impact e MatterJS. Nós vamos utilizar o Arcade.

No seu objeto de configurações, adicione as configurações de Arcade.

 physics: {
        default: 'arcade',
        arcade: {
            gravity: {
                y: 0
            },
            debug: true
        }
    }

Criando o inimigo

Mas em que vamos bater Jonatan? ;s Vamos bater no Sprite de um morceguinho que eu peguei emprestado do BrowserQuest. Para isso, eu precisei importar o SpriteSheet do projeto deles, é uma imagem com vários frames do morcego que segue o mesmo padrão do nosso jogador. Também é preciso criar uma Classe do Tipo Enemy.

export function Enemy(armor, x, y, vida)
{
    let scale = 2;
    let frameRate = 5;
    let sprite;
    let player_armor = armor;
    let hp = vida;



    function create(context)
    {
        fetch(`public/assets/sprites/${player_armor}.json`).then(response =>
        {
            response.json().then(spritesheet =>
            {
                context.load.spritesheet(player_armor, `public/assets/img/${player_armor}.png`, { frameWidth: spritesheet.width, frameHeight: spritesheet.height });
            })

        }).catch(err =>
        {
            alert("Erro ao carregar armadura do jogador.");
        })

    }

    function setHp(newHp)
    {
        hp = newHp;
    }

    function getHp()
    {
        return hp;
    }

    function setFrameRate(newFrame)
    {
        frameRate = newFrame;
    }

    function setPositionX(positionX)
    {
        sprite.x = Math.floor(positionX);
    }

    function setPositionY(positionY)
    {
        sprite.y = Math.floor(positionY);
    }

    function getPositionX()
    {
        return sprite.x;
    }

    function getPositionY()
    {
        return sprite.y;
    }

    function getSprite()
    {
        return sprite;
    }


    function configPlayer(context)
    {
        createArmadura(context);
        sprite = context.add.container(x, y,
            [
                context.add.sprite(0, 0, player_armor),
            ]);
        playAnimation('idle_down');
        // console.log(sprite);


    }

    function createArmadura(context)
    {
        fetch(`public/assets/sprites/${player_armor}.json`).then(response =>
        {
            response.json().then(ar =>
            {
                for (let armadura in ar.animations) {

                    let length = ar.animations[armadura].length - 1;
                    let start = ar.animations[armadura].row * 5;
                    context.anims.create({
                        key: armadura + player_armor,
                        frameWidth: ar.width,
                        frameHeight: ar.height,
                        frames: context.anims.generateFrameNames(player_armor, { start: start, end: start + length }),
                        repeat: -1,
                        frameRate: frameRate
                    });
                }



            });

        }).catch(err =>
        {
            alert("Erro ao carregar armadura do jogador.");
        })
    }


    function playAnimation(animation)
    {
        sprite.list[0].play(animation + player_armor);
    }

    function stopAnimation(animation)
    {
        sprite.list[0].anims.stop(null, true);
    }

    return {
        create,
        setFrameRate,
        configPlayer,
        getSprite,
        playAnimation,
        stopAnimation,

        getPositionX,
        getPositionY,
        setPositionX,
        setPositionY,
        setHp,
        getHp
    }
}

Não se preocupe com o script grande, no final do artigo você pode copiar o projeto funcional.

Detectando colisões

Agora vamos configurar o nosso arquivo principal para detectar as colisões. Lembre-se que é preciso ter o objeto de configuração do Jogo com as simulações físicas do Arcade Ligadas, somente se esta configuração estiver presente o jogo estará detectando colisões.

    // Adiciona o sprite da arma do jogador aos elementos físicos do jogo.
    this.physics.add.existing(player.getSprite().list[1]);
    // Adiciona o sprite do inimigo aos elementos físicos do jogo.
    this.physics.add.existing(enemy.getSprite().list[0]);

    // Adiciona um Listener de colisões entre a espada e o inimigo.
    this.physics.add.collider(player.getSprite().list[1], enemy.getSprite().list[0],
        function (p, e)
        {
            // Muda o status de colisão para falso para colidir apenas 1 vez
            player.setColliderStatus(false);
            // Verifica se o HP do inimigo chegou a 0
            if (enemy.getHp() - player.getStrength() > 0) {
                enemy.setHp(enemy.getHp() - player.getStrength());
            } else {
                // Se o HP do Inimigo Zerar ele destroi o inimigo.
                e.destroy();
            }

        },
        function ()
        {
            // Se retornar falso não realiza colisão, isso previne que a função acima seja chamada multiplas vezes seguidas.
            return player.getColliderStatus();
        }, this);

O código está bem comentado, mas vale explicar aqui novamente. A primeira coisa que fazemos é adicionar os Sprites da espada do Jogador e o Sprite do Inimigo à simulação física. Após isso adicionamos um Listener para checar se existem colisões entre a espada do Jogador e o Inimigo. A segunda função que não recebe parâmetros serve para verificar se eles pode colidir, esta função não deixa que a espada acione a função de colisão mais de uma vez, sempre que retornar false ele não deixa realizar a colisão, se não houver esta função a colisão será detectada multiplas vezes, fazendo assim com que o inimigo morra rápido demais. Por fim, a primeira função do Listener, verifica se o HP do Inimigo chegou a 0, se sim, ele destroi o inimigo, se não, ele remove um pouco do seu HP baseado na força da arma que o jogador está usando.

Alterações no Jogador

Do lado do Jogador foi preciso fazer algumas alterações também. Foi preciso adicionar algumas variáveis globais para controlar o período de ataque, verificar se é possível colidir com o inimigo e a força da arma.

    let atackInterval = [];
    let colliderActivated = false;
    let strength;

Cada variável teve os seus getters e setters criados conforme a necessidade.

    function getColliderStatus()
    {
        return colliderActivated;
    }

    function setColliderStatus(status)
    {
        colliderActivated = status;
    }

    function setStrength(newStrength)
    {
        strength = newStrength;
    }

    function getStrength()
    {
        return strength;
    }

Na criação do Sprite utilizamos agora o Objeto physics para adicionar sprites ao container. Também precisamos habilitar a colisão com os limites do nosso Mapa (World), do contrário nossos elementos podem desaparecer na “Borda do Mundo”. Você também vai notar que eu reduzi o tamanho do corpo do jogador.

sprite = context.add.container(300, 400,
            [
                context.physics.add.sprite(0, 0, player_armor),
                context.physics.add.sprite(0, 0, player_weapon)
            ]);

sprite.list[0].setSize(32, 42, true);
sprite.list[0].setCollideWorldBounds(true);
sprite.list[1].setCollideWorldBounds(true);

No JSON de configuração do sprite da espada eu coloquei a força da espada: “strength”: 25 e dentro da função createWeapon eu coloquei a atribuição desta força.

strength = ar.strength;

Atacando

Bom, a parte de atacar é um pouco mais complicada. Para realizar um ataque somente no meu inimigo eu preciso utilizar uma HitBox separada apenas para a espada. Eu faço ela aparecer por apenas uma fração de segundos no momento em que a espada está no meio do ataque. Ao parar de atacar eu removo esta iteração de ataque e HitBox.

    function atack()
    {
        atackInterval = [];
        console.log(sprite.list[1].body);
        playAnimation('atk_right');
        if (sprite.list[1].anims.currentAnim.key.includes('atk')) {
            atackInterval[0] = setInterval(() =>
            {

                sprite.list[1].body.setSize(16, 16);
                sprite.list[1].body.setOffset(50, 24);
                setColliderStatus(true);

            }, 500);
            atackInterval[1] = setInterval(() =>
            {
                setColliderStatus(false);
                resetBodySize()

            }, 1000);
        }
    }

    function resetBodySize()
    {
        sprite.list[1].body.setSize(0, 0);
        sprite.list[1].body.setOffset(0, 0);
    }


    function stopAtackInterval()
    {
        clearInterval(atackInterval[0]);
        clearInterval(atackInterval[1]);
        setColliderStatus(false);
        resetBodySize();
    }

    function playAnimation(animation)
    {
        stopAtackInterval();
        sprite.list[0].play(animation + player_armor);
        sprite.list[1].play(animation + player_weapon);

    }

Não se esqueça de adicionar as novas funções ao retorno da nossa Classe.

    return {
        create,
        configPlayer,
        atack,
        playAnimation,
        stopAnimation,

        getSprite,
        setFrameRate,
        getPositionX,
        getPositionY,
        setPositionX,
        setPositionY,
        getColliderStatus,
        setColliderStatus,
        setStrength,
        getStrength
    }

Para atacar o inimigo basta chegar ao seu lado esquerdo e digitar no console do navegador player.atack()

Atacando inimigo

Conclusão

Basicamente é isso, já estamos detectando colisões. Talvez da próxima vez que você estiver fazendo aquela Dungeon com os seus amigos você vai lembrar de todo o processo que atacar um Monstro Chefe tem 😀

O nosso próximo passo será adicionar mais inimigos em nosso mapa e detectar a direção do inimigo e do golpe.

Link para o Projeto GIT

Referências

Leave a comment

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Copyright © – Jonatan Pietroski