Esse site utiliza cookies
Nós armazenamos dados temporariamente para melhorar a sua experiência de navegação e recomendar conteúdo do seu interesse.
Ao utilizar os nossos serviços, você concorda com as nossas políticas de privacidade.
Esse site utiliza cookies
Nós armazenamos dados temporariamente para melhorar a sua experiência de navegação e recomendar conteúdo do seu interesse.
Ao utilizar os nossos serviços, você concorda com as nossas políticas de privacidade.
Postado em 08 dezembro 2022
Atualizado em 08 dezembro 2022
O jogo da serpente é um jogo que já existia desde os anos 80. Além de ser um jogo simples, não necessita de muitos requerimentos para sua funcionalidade.
O desenvolvimento do jogo da serpente é um bom candidato para programadores que ainda estão na fase de aprendizado tentando aprender os conceitos básicos de programação.
Esse projeto irá mostrar como desenvolver um jogo da serpente bastante simples, utilizando javascript como linguagem de programação e p5.js como framework.
Estarei utilizando o editor online disponibilizado pelo p5.js que pode ser acessado pelo link abaixo:
https://editor.p5js.org/
Ao clicar no link acima, o editor já estará com duas funções escritas.
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
}
A função “setup” é responsável pela inicialização de variáveis e a invocação de alguns métodos que só precisam ser executados apenas uma vez.
A função “draw” é repetida infinitamente até que o usuário feche a janela do navegador.
Ao executarmos o programa acima no editor do p5.js, veremos uma janela cinza com tamanho de 400x400. A cor e o tamanho da janela estão sendo definidos pela função “background()” e “createCanvas()” respectivamente.
Conceitos básicos serão definidos antes do desenvolvimento entrar em prática. Os conceitos são:
Atributo | Valor |
---|---|
Comprimento da tela | 600 |
Altura da tela | 400 |
FPS | 10 |
Tamanho do corpo da serpente | 25 |
Tamanho da fruta | 25 |
Os atributos acima serão constantes durante o jogo inteiro. O corpo da serpente será composto por vários blocos quadrados com tamanho 25. A fruta será um círculo com o mesmo tamanho do que um bloco do corpo da serpente. A tela será atualizada 10 vezes a cada segundo (10 FPS).
Como já dito anteriormente, o corpo da serpente será constituída de blocos. Quanto mais frutas a serpente come, mais comprido seu corpo se torna.
class Body {
constructor(x, y, currentDirection) {
this.size = 25;
this.direction = currentDirection;
this.x = x;
this.y = y;
}
}
Ao todo, a classe “Body” possui quatro variáveis. Os detalhes estão escritos na tabela abaixo:
Atributo | Valor |
---|---|
size | Tamanho do bloco |
direction | Direção atual que o bloco se desloca |
x | Coordenada horizontal |
y | Coordenada vertical |
O movimento da serpente acontece em cascata, ou seja, os blocos devem seguir o mesmo caminho que a cabeça da serpente traçou. Por isso, cada bloco deve ter uma variável responsável pela sua direção atual.
A classe da serpente será a classe principal do jogo. Ela será responsável pelas seguintes funções:
A classe da serpente irá manter a variável responsável pelo gerenciamento dos blocos.
constructor() {
this.body = [];
let newBody = new Body(screenWidth / 2, screenHeight / 2, null);
// Adiciona bloco ao corpo da serpente
this.body.push(newBody);
}
Ao ser inicializada pela primeira vez, a classe da serpente irá exibir um bloco no centro da tela. Esse bloco será o primeiro bloco no corpo da serpente, consequentemente sendo a cabeça da serpente.
A função “draw()” será responsável por exibir todos os blocos do corpo da serpente.
draw() {
for (let i = 0; i < this.body.length; i++) {
square(this.body[i].x, this.body[i].y, this.body[i].size);
}
}
Os blocos serão exibidos um de cada vez, como mostra na função acima.
Conforme a serpente se alimenta, seu corpo vai se tornando mais comprido. Ao consumir uma fruta, um novo bloco será adicionado na sua cauda.
addBody() {
let lastBody = this.body[this.body.length - 1];
let currentDirection = this.calculateDirection(lastBody.direction);
let positionX = lastBody.x - (lastBody.size * currentDirection.horizontalDirection);
let positionY = lastBody.y - (lastBody.size * currentDirection.verticalDirection);
let newBody = new Body(positionX, positionY, lastBody.direction);
this.body.push(newBody);
}
Dependendo da direção atual do último bloco da serpente, a posição que o novo bloco será adicionado será diferente. A lógica do algoritmo acima pode ser representado na tabela abaixo:
Direção último bloco | Posição novo bloco |
---|---|
Esquerda | Direita |
Direita | Esquerda |
Cima | Baixo |
Baixo | Cima |
Se o último bloco estiver direcionado para a esquerda (avançando para a esquerda), o novo bloco deverá ser adicionado à direita do último bloco.
O deslocamento de cada bloco do corpo da serpente irá seguir a direção atual do bloco. Se um bloco estiver direcionado para a direita, ele irá ser deslocado para a direita e assim por diante.
Uma vez que o bloco se deslocou uma posição, este bloco irá herdar a direção do bloco na sua frente. A herança de direções irá acontecer com todos os blocos até chegar na cabeça da serpente que irá ter a sua direção controlada pelo usuário.
move() {
// Calcula o deslocamento da cobra baseado na direção
let headDirection = this.calculateDirection(this.body[0].direction);
this.body[0].x += this.body[0].size * headDirection.horizontalDirection;
this.body[0].y += this.body[0].size * headDirection.verticalDirection;
for (let i = this.body.length - 1; i > 0; i--) {
let currentBodyDirection = this.calculateDirection(this.body[i].direction);
// Deslocamento
this.body[i].x += this.body[i].size * currentBodyDirection.horizontalDirection;
this.body[i].y += this.body[i].size * currentBodyDirection.verticalDirection;
// Herda direção do bloco seguinte
this.body[i].direction = this.body[i - 1].direction;
}
}
O algoritmo responsável por calcular a direção e está presente em quase todas as funções da serpente é o “calculateDirection()”.
calculateDirection(currentDirection) {
let horizontalDirection = 0;
let verticalDirection = 0;
switch (currentDirection) {
case direction.left:
horizontalDirection = -1;
break;
case direction.right:
horizontalDirection = 1;
break;
case direction.up:
verticalDirection = -1;
break;
case direction.down:
verticalDirection = 1;
break;
}
return {
horizontalDirection: horizontalDirection,
verticalDirection: verticalDirection,
};
}
As fórmulas de deslocamento podem ser representadas pelas resoluções abaixo:
Por exemplo, se a coordenada atual x do bloco é 200 e sua direção for para a direita, o resultado da coordenada x após o deslocamento será 225. Caso a direção seja para a esquerda, a coordenada x será 175.
Uma vez que a classe serpente está pronta, só precisamos invocá-la para ver o resultado.
const screenWidth = 600;
const screenHeight = 400;
function setup() {
createCanvas(screenWidth, screenHeight);
frameRate(10);
snake = new Snake();
}
function draw() {
background(220);
snake.draw();
snake.move();
}
A serpente irá ser controlada através das setas pelo usuário. O p5.js já possui funcionalidades que agilizam o processo. O algoritmo abaixo já é o suficiente para gerenciar os controles.
var isRunning = true;
var direction = {
up: 38,
down: 40,
left: 37,
right: 39
};
...
function keyPressed() {
if (! isRunning) {
return;
}
if (keyCode === UP_ARROW && snake.body[0].direction != direction.down) {
snake.body[0].direction = direction.up;
} else if (keyCode === DOWN_ARROW && snake.body[0].direction != direction.up) {
snake.body[0].direction = direction.down;
} else if (keyCode === LEFT_ARROW && snake.body[0].direction != direction.right) {
snake.body[0].direction = direction.left;
} else if (keyCode === RIGHT_ARROW && snake.body[0].direction != direction.left) {
snake.body[0].direction = direction.right;
}
}
Como é possível observar acima, as teclas mudam a direção da cabeça da serpente. O deslocamento da serpente é realizado em outro algoritmo e não sofre influência das teclas diretamente.
A fruta terá apenas três atributos:
Atributo | Função |
---|---|
size | Tamanho da fruta |
x | Coordenada horizontal |
y | Coordenada vertical |
Com esses atributos podemos definir o local na tela que a fruta será exibida e baseando-se nessas variáveis, implementaremos o detector de colisões mais adiante.
class Food {
constructor(x, y, size) {
this.size = size;
this.x = x;
this.y = y;
}
draw() {
circle(this.x, this.y, this.size);
}
}
Queremos que a fruta seja exibida em lugares aleatórios e que estejam alinhados com o trajeto que a serpente pode percorrer. Para fazer isso, precisamos implementar um algoritmo dependente do tamanho da tela e o tamanho da fruta. Ainda dentro da classe da fruta, adicionaremos o seguinte algoritmo:
static instantiate() {
// Tamanho da fruta
let size = 25;
// Quantidade de espaços que a fruta pode ocupar
let screenPosX = screenWidth / size;
let screenPosY = screenHeight / size;
// Deixa a fruta alinhada com o trajeto que a serpente pode traçar
let foodX = round(random(0, screenPosX - 1)) * size + (size / 2);
let foodY = round(random(0, screenPosY - 1)) * size + (size / 2);
// Inicializa a fruta
let newFood = new Food(foodX, foodY, size);
return newFood;
}
A quantidade de espaços que a fruta pode ocupar pode ser calculada da seguinte maneira:
São 24 espaços que podem ser ocupados pela fruta na horizontal de modo que ela fique alinhada com a serpente. Lembrando que um bloco que compõem o corpo da serpente também tem tamanho 25. Isso significa que um bloco do corpo da serpente só pode ocupar 24 espaços na horizontal.
A quantidade de espaços na vertical segue o mesmo raciocínio, tendo um resultado de 16 espaços.
Em seguida, um cálculo usando valores aleatórios são executados para determinar as coordenadas da fruta.
round(random(0, screenPosX - 1))
A função “random” aceita dois parâmetros: mínimo e máximo. O resultado retornado será um valor entre esses dois parâmetros. No caso da resolução acima, um valor entre 0 e 23 será devolvido. A função “round” tem o papel de arredondar o número retornado, uma vez que a função “random” retorna um valor float.
O valor acima será multiplicado pelo tamanho da fruta e somado com o tamanho da fruta.
... * size + (size / 2);
O motivo da divisão se dá pelo modo de como o círculo é desenhado. Na biblioteca p5.js o círculo é expandido a partir do centro. A imagem abaixo ilustra muito bem a diferença entre o ponto inicial do quadrado e do círculo.
O detector de conflitos irá buscar por conflitos a cada frame. Teremos três tipos de detector de colisões:
Os dois últimos conflitos finalizam o jogo imediatamente, como se fosse um game over. O conflito com a fruta aumenta o corpo da serpente em um bloco.
Dentro da função padrão “draw”, adicionaremos todos os detectores de colisão.
for (let i = 0; i < foods.length; i++) {
// Aproveitando o loop pra exibir a fruta
let food = foods[i];
food.draw();
// Detector de conflitos entre a cabeça da serpente e a fruta
if (snake.body[0].x + snake.body[0].size > food.x &&
snake.body[0].x < food.x + (food.size / 2) &&
snake.body[0].y + snake.body[0].size > food.y &&
snake.body[0].y < food.y + (food.size / 2)) {
foods.splice(i, 1);
foods.push(Food.instantiate());
snake.addBody();
return;
}
}
O primeiro bloco dentro do corpo da serpente será a cabeça. O cálculo acima detecta a colisão quando a cabeça da serpente entra no mesmo espaço que a fruta se encontra. No exato momento que os dois estão no mesmo espaço, uma nova fruta é criada e um novo bloco é adicionado ao corpo da serpente.
Precisamos de uma variável global que gerencie o andamento do jogo.
var isRunning = true;
Caso a variável acima seja trocada para o valor “false” o jogo é pausado, indicando um game over. A função responsável por trocar o valor da variável acima será a função “stopGame()”.
function stopGame() {
isRunning = false;
for (let i = 0; i < snake.body.length; i++) {
snake.body[i].direction = null;
}
}
A função também reinicia a direção atual de todos os blocos da serpente, fazendo com que a serpente trave. E para finalizar, novas linhas de código também serão adicionadas na função “keyPressed()”, responsável pelos controles de entrada do usuário.
function keyPressed() {
if (! isRunning) {
return;
}
...
}
Uma vez que temos todas as ferramentas necessários implementadas, adicionado o detector de colisões entre os limites da tela e a cabeça da serpente.
if (snake.body[0].x < 0 ||
snake.body[0].x + snake.body[0].size > screenWidth ||
snake.body[0].y < 0 ||
snake.body[0].y + snake.body[0].size > screenHeight) {
stopGame();
}
A condição acima é bem simples. Caso a cabeça da serpente esteja saindo por alguns dos limites da tela, a função “stopGame()” é chamada.
A lógica do detector de colisões do corpo e da cabeça da serpente é igual a colisão com a fruta.
for (let i = 1; i < snake.body.length; i++) {
if (snake.body[0].x + snake.body[0].size > snake.body[i].x &&
snake.body[0].x < snake.body[i].x + snake.body[i].size &&
snake.body[0].y + snake.body[0].size > snake.body[i].y &&
snake.body[0].y < snake.body[i].y + snake.body[i].size) {
stopGame();
}
}
Caso a cabeça da serpente e algum bloco do corpo da serpente estejam ocupando o mesmo espaço, a função “stopGame()” é acionada.
O jogo pode ser acessado e testado abaixo:
Para medir o espaço que cada bloco ou fruta pode ocupar, colocamos linhas na vertical e horizontal. Caso estes estejam alinhados com os espaços divididos pelas linhas, os cálculos são considerados corretos.
O jogo da serpente é um jogo extremamente simples, porém exige um pequeno esforço do programador para criar a lógica do jogo. Jogos clássicos como pacman e block breaker podem se tornar um grande aprendizado para programadores iniciantes, o jogo da serpente não é diferente.
O DicionarioTec criou a lógica desse jogo sem nenhuma referência, isso significa que podem haver outras formas muito mais simples de criar o jogo da serpente. O código foi disponibilizado para programadores que desejam aperfeiçoar o algoritmo.
Postagens mais vistas
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.
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.
Rede local de computadores (LAN) é um conjunto de computadores ou dispositivos conectados uns aos outros de forma isolada em um pequeno local.