Criando um jogo de pacman usando javascript e pixi.js (parte 1)

Desenvolvimento dos conceitos mais básicos do clássico pacman, como: mapa, animação, deslocamento e detector de colisões.

Postado em 03 janeiro 2023

Atualizado em 03 janeiro 2023



Introdução

Pacman é um jogo clássico criado no início dos anos 80. A lógica do jogo é bastante simples, o usuário controla o pacman e precisa comer todas as frutas dentro do labirinto. Enquanto isso, inimigos irão percorrer o labirinto atrás do pacman, caso o usuário seja capturado pelo inimigo o jogo acaba.

Esse tutorial irá usar javascript e pixi.js para o desenvolvimento do jogo e pode ser um pouco difícil para iniciantes de programação. Caso esteja apenas começando, os tutoriais abaixo são recomendados:

Os jogos acima utilizam a biblioteca p5.js, que é extremamente flexível e de fácil compreensão, permitindo que o programador consiga desenvolver com agilidade.

Esse tutorial será dividido em algumas partes devido ao grande número de informação. Nesse primeiro tutorial, os conceitos básicos serão planejados e desenvolvidos. Esses conceitos são:

  • Planejamento
  • Desenvolvimento do mapa
  • Implementação do pacman
  • Movimentação do pacman
  • Detector de colisões

Ferramentas utilizadas

Como explicado na introdução, esse tutorial utiliza a biblioteca pixi.js. A linguagem de programação será javascript. Os recursos de imagem serão todos obtidos a partir de um único arquivo idêntico a imagem abaixo:

material de criação do jogo pacman
O arquivo acima está comprimido.

Outras ferramentas serão:

Ferramenta Papel
Node.js Ambiente para javascript
npm gerenciador de pacotes

Planejamento

O planejamento será divido em duas partes:

  • Mapa
  • Pacman

Mapa

O mapa será representado por um array contendo os valores zero e um. A representação dos valores é a seguinte:

Valor Representação
0 Espaço vazio
1 Parede

O espaço vazio é o local onde o pacman pode se movimentar. Porém, caso haja a presença de uma parede um espaço, o pacman não conseguirá se movimentar para esta direção.

Cada espaço terá 46px de altura e tamanho, assim como o pacman também terá os mesmos valores. Assim, podemos concluir que o tamanho da tela pode ser configurado usando as seguintes lógicas como base:
width=map.x.length46height=map.y.length46 width = map.x.length * 46 \\ height = map.y.length * 46 \\
Por exemplo, observe o array abaixo:

// O tamanho do array abaixo é 5x5
let map = [
    [1,1,1,1,1],
    [1,0,0,0,1],
    [1,0,1,0,1],
    [1,0,0,0,1],
    [1,1,1,1,1],
];

O array acima possui 5 valores na horizontal (x) e 5 valores na vertical (y). Logo, o tamanho desse array é de 5x5. Podemos concluir que ao utilizar o array acima como base, o tamanho da tela será de 230x230.

Pacman

O pacman terá um tamanho de 46x46 com velocidade de 3px por frame. A movimentação do pacman é efetuada pelas setas do teclado. Antes de se locomover de fato para a direção pressionada pelo usuário, será checado se o pacman pode ou não se mover para o espaço desejado, ou seja, o programa irá verificar o futuro movimento e concluirá se esse movimento é possível ou não.

detector de colisão jogo pacman

Como mostra a imagem acima, pacman pode ir para as direções direita e esquerda, porém não pode se mover para as direções cima e baixo.

Desenvolvimento do mapa

Como planejado, o mapa será desenhado com um array contendo zeros e uns.

const stage = [
    [1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1],
];

O mapa será bastante simples no primeiro tutorial, uma vez que queremos desenvolver e testar do modo mais simples possível.

A altura e a largura da tela irá se basear no array acima, como mostra nas seguintes linhas:

const spriteWidth = 46;
const spriteHeight = 46;
const gameWidth = stage[0].length * spriteWidth;
const gameHeight = stage.length * spriteHeight;

A largura da tela é o tamanho do array na primeira linha e a altura é a quantidade de linhas dentro do primeiro array. Esses valores serão multiplicados pelo tamanho de cada bloco que é 46.

Exibição das paredes

Pelo fato da variável “stage” ser um array multidimensional, duas repetições (loops) serão necessárias.

for (let row = 0; row < stage.length; row++) {
    for (let column = 0; column < stage[row].length; column++) {
        if (stage[row][column] == 0) {
            continue;
        }

        graphics.beginFill(0xDE3249);
        graphics.drawRect(column * spriteWidth, row * spriteHeight, spriteWidth, spriteHeight);
        graphics.endFill();
    }
}

A repetição (loop) interna vai verificar todos os valores da esquerda para a direita em cada linha. Cada bloco irá ter um tamanho de 46x46. Caso o valor seja 1, a parede é desenhada na tela, caso o valor seja 0, a repetição simplesmente continua e não desenha nada na tela.

Desenvolvimento do pacman

O pacman é controlado pelo usuário com as setas de direção do teclado. Assim como cada bloco do jogo, o pacman tem um tamanho de 46x46. O pacman será desenvolvido em 3 etapas:

  1. Animação
  2. Deslocamento
  3. Detecção de colisões

Animação do sprite do pacman

Ao se deslocar o pacman irá abrir e fechar a boca continuamente. Esse efeito dará a impressão de que o pacman está caminhando. Quando o pacman estiver parado, sua boca para de abrir e fechar e permanece do modo que parou.

Felizmente, o pixi.js já possui várias funcionalidades para animação. Primeiro, iremos criar as texturas das quatro direções da imagem.

const baseTexture = PIXI.Texture.from('resources/characters.png');
const pacmanTextures = [];

pacmanTextures['right'] = [];
pacmanTextures['right'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 10, 0, spriteWidth, 46)));
pacmanTextures['right'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 11, 0, spriteWidth, 46)));

pacmanTextures['left'] = [];
pacmanTextures['left'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 10, spriteHeight * 2, spriteWidth, 46)));
pacmanTextures['left'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 11, spriteHeight * 2, spriteWidth, 46)));

pacmanTextures['up'] = [];
pacmanTextures['up'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 10, spriteHeight * 3, spriteWidth, 46)));
pacmanTextures['up'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 11, spriteHeight * 3, spriteWidth, 46)));

pacmanTextures['down'] = [];
pacmanTextures['down'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 10, spriteHeight * 1, spriteWidth, 46)));
pacmanTextures['down'].push(new PIXI.Texture(baseTexture, new PIXI.Rectangle(spriteWidth * 11, spriteHeight * 1, spriteWidth, 46)));

Cada direção possuirá duas texturas, uma com a boca aberta e outra com a boca fechada. São duas texturas para cada direção e quatro direções ao todo, totalizando 8 texturas.

sprites do pacman e divisão

Uma vez que as texturas estão preparadas, criamos o sprite do pacman.

var pacman = new PIXI.CustomAnimatedSprite(pacmanTextures['right']);

pacman.animationSpeed = 0.1;
// O pacman inicia no centro da tela
pacman.x = (gameWidth - spriteWidth) / 2;
pacman.y = (gameHeight - spriteHeight) / 2;
pacman.play();

Em seguida, testamos a animação do pacman.

sprites do pacman e divisão

Deslocamento do pacman

O deslocamento será efetuado com duas funções do javascript:

  • onKeyDown
  • onKeyUp

A função “onKeyDown” é executada quando o usuário pressiona alguma tecla. A função “onKeyUp” é executada quando o usuário solta um tecla. Com essas duas funções, sabemos se o usuário está com a tecla pressionada. Caso a tecla esteja pressionada, o pacman continua avançando na direção desejada.

Definindo as direções

Usaremos um simples objeto contendo 5 valores para gerenciar todas as direções do jogo.

const direction = {
    "stand": 0,
    "left": 1,
    "up": 2,
    "right": 3,
    "down": 4,
};

O valor “stand” representa o pacman em repouso. Os outros valores indicam que o pacman está em movimento.

Além das direções, precisamos saber a direção atual do usuário. Isso pode ser feito com uma variável que armazena as coordenadas do pacman.

var currentDirection = {
    horizontal: direction.stand,
    vertical: direction.stand,
};

Como podemos observar, o pacman irá ser inicializado em repouso.

Deslocando o personagem

Toda vez que o usuário pressionar uma tecla, a função abaixo será executada:

function onKeyDown(key) {

    if (key.code == "ArrowDown") {
        currentDirection.vertical = direction.down;
    }

    if (key.code == "ArrowUp") {
        currentDirection.vertical = direction.up;
    }

    if (key.code == "ArrowRight") {
        currentDirection.horizontal = direction.right;
    }

    if (key.code == "ArrowLeft") {
        currentDirection.horizontal = direction.left;
    }
}

O código acima é bastante simples. Simplesmente renovamos a variável de direção atual. Ao largarmos a tecla, queremos que o pacman pare de avançar. Isso é feito com a função a seguir:

function onKeyUp(key) {
    if (key.code == "ArrowLeft" || key.code == "ArrowRight") {
        currentDirection.horizontal = direction.stand;
    }

    if (key.code == "ArrowUp" || key.code == "ArrowDown") {
        currentDirection.vertical = direction.stand;
    }
    pacman.stop();
}

Ao largarmos a tecla, a animação do pacman também é pausada com o seguinte comando: pacman.stop(). Uma vez que já sabemos que direção o usuário quer se deslocar, a única coisa que falta é saber se o pacman pode ou não se deslocar na direção pressionada.

Detecção de colisões

Na maioria dos jogos de pacman, o personagem principal é menor do que o tamanho de cada bloco. Nesse tutorial, pacman tem o mesmo tamanho que os blocos. Com isso, algoritmos de detecção de colisões podem ser diferentes dos algoritmos tradicionais.

O centro do pacman será o ponto principal de referência. Quando o usuário pressiona um tecla de direção, adicionaremos um valor que é a metade do tamanho de pacman para checar se pode ou não se movimentar para tal direção.

Direção Cálculo
Direita x + 23
Esquerda x - 23
Cima y - 23
Baixo y + 23

O gráfico abaixo ilustra como funciona essa lógica:

detecção de colisões pacman

Porém, existe um grande problema nesse método. A altura do pacman não é levada em consideração. Isso faz com que o usuário possa passar em espaços que contém paredes.

Para resolver esse problema, devemos calcular a direção desejada e o valor vertical do sprite de pacman como mostra na imagem abaixo:
detecção de colisões pacman

O método acima quase funciona, porém surge outro problema. Por estar muito perto da parede, os pontos de detecção de colisões (ponto vermelho) detectam colisões. Isso acontece pelo fato de que o pacman tem o mesmo tamanho de um bloco. Se ele fosse um pouco menor, talvez não tivéssemos esse problema.

A resolução do problema é dividir a metade da altura pela sua própria metade, ou melhor dividir por 3.

detecção de colisões pacman

Dessa forma, resolvemos todos os problemas de detecção de colisões.

	// Ignora caso pacman esteja em repouso
    if (currentDirection.horizontal == direction.stand && currentDirection.vertical == direction.stand) {
        return;
    }

	// Calcula o centro do pacman
    let currentVerticalPos = (pacman.y + (spriteHeight / 2)) / spriteHeight;
    let currentHorizontalPos = (pacman.x + (spriteHeight / 2)) / spriteWidth;
	// Calcula em qual bloco o pacman está situado
    let roundedCurrentVerticalPos = Math.abs(Math.trunc(currentVerticalPos));
    let roundedCurrentHorizontalPos = Math.abs(Math.trunc(currentHorizontalPos));
    
    // Ativa a animação de abrir e fechar a boca do pacman
    pacman.play();
    
    // Caso a seta para direita for pressionada
    if (currentDirection.horizontal == direction.right) {
        // Calcula o próximo movimento do pacman usando a metade do tamanho na horizontal (23px)
        let nextMov = currentHorizontalPos + ((spriteWidth / 2) / spriteWidth);
        // Calcula o próximo movimento do pacman no sentido vertical embaixo (y + 7,6px)
        let nextMovUp = currentVerticalPos + ((spriteHeight / 3) / spriteHeight);
        // Calcula o próximo movimento do pacman no sentido vertical encima (y - 7,6px)
        let nextMovDown = currentVerticalPos - ((spriteHeight / 3) / spriteHeight);
        
        // Arredonda os valores para descobrir com bloco a porção do pacman está situada
		let roundedNextMove = Math.abs(Math.trunc(nextMov));
        let roundedNextMoveUp = Math.abs(Math.trunc(nextMovUp));
        let roundedNextMoveDown = Math.abs(Math.trunc(nextMovDown));
        
        // Calcula os limites da tela (direita)
        let isRightBoundLimit = roundedCurrentHorizontalPos >= stage[roundedCurrentVerticalPos].length - 1;
		
		// Detectores de colisão. Caso encotrem uma parede (1) cancela o movimento
        let checkNextMovFromCenter = stage[roundedCurrentVerticalPos][roundedNextMove] == 1;
        let checkNextMovFromCenterUp = stage[roundedNextMoveUp][roundedNextMove] == 1;
        let checkNextMovFromCenterDown = stage[roundedNextMoveDown][roundedNextMove] == 1;
        if (isRightBoundLimit || checkNextMovFromCenter || checkNextMovFromCenterUp || checkNextMovFromCenterDown){
            return;
        }

        pacman.x += speed;
        pacman.textures = pacmanTextures['right'];
    }
    ...

A mesma lógica é aplicada para o resto das direções. Abaixo, temos uma pequena demonstração:

detecção de colisões pacman

Conclusão

Nesse tutorial de pacman foram desenvolvidas as seguintes funcionalidades:

  • Planejamento (esboço do projeto)
  • Mapa
  • Movimentação do pacman
  • Detecção de colisões

Apesar de parecer simples, pacman tem vários desafios. O detector de colisões é um exemplo. Talvez diminuir o tamanho do sprite de pacman pode facilitar o desenvolvimento do jogo devido a imprevistos nos cálculos do bloco atual que o usuário se encontra.

Diferente de outros detectores de colisões, o jogo do pacman calcula colisões com base do posicionamento atual do personagem.

Nos próximos tutoriais de pacman serão implementados:

  • Frutas
  • Inimigos
  • Especial do pacman

O código do tutorial está disponível no Github.

Postagens mais vistas

Os 5 principais componentes do computador

Os 5 principais componentes do computador são a unidade de controle, unidade aritmética e lógica, memória, dispositivo de entrada e dispositivo de saída.

Portas TCP e UDP

A porta é um número de 16 bits que é adicionado no final do endereço IP, insinuando qual aplicativo está vinculado e atuando nessa porta.

LAN

Rede local de computadores (LAN) é um conjunto de computadores ou dispositivos conectados uns aos outros de forma isolada em um pequeno local.

Retornar aos projetos