Nesse post vou mostrar a vocês como codificar um “jogo de navinha” do zero usando o P5JS. Pra quem não é um Gamer raiz e não viveu a “escola clássica” de games, mais conhecido como anos 90… o “jogo de navinha” é um termo genérico usado quando queríamos nos referir a games no qual você é uma nave espacial e fica atirando loucamente para derrotar seus inimigos. Então, agora que você já sabe do quê estamos falando, vamos lá!
Os jogos de naves originais
Esse nome incrível criado pelos brazucas fica um pouco mais “refinado” quando colocamos em inglês, fica algo como “Spacewars” [1] ou então “Space wars” [2].
Essa modalidade de “jogo de navinha” ficou muito famosa e até hoje as pessoas ainda usam esse termo (ao menos os nerds velhos). Antigamente as pessoas frequentavam os famosos fliperamas ou então “Arcades” para jogar games como esse ai ao lado. A simplicidade desse tipo de game é compatível com os recursos computacionais da época, ou seja, pouquíssima memória, capacidade de processamento e recursos gráficos.
Então você pensa, então pra quê vou reproduzir um game de antigamente hoje? Realmente, concordo com você… você não vai ganhar milhões com isso, mas ninguém começa a desenvolver games já fazendo um triple A né?
Sendo assim, o objetivo desse post é mostrar uma forma bem simples de desenvolver um game usando javascript e esse framework incrível que é o P5JS.
Mãos no código!
Bom, o primeiro passo para esse projeto é dividir seu desenvolvimento em 3 partes: 1) desenvolver o player, 2) desenvolver os inimigos e 3) desenvolver os controles do jogo.
Desenvolver o player
O player, não necessariamente precisa ser uma “navinha”, ele pode ser um tanque, carro, homem… ou seja, qualquer coisa. O segredo aqui é o estilo do game que torna ele inconfundível. Para desenvolver o seu player, você pode simplesmente usar o P5.js para definir formas simples como quadrados e círculos que representem algo.
Usando o P5 eu usei esse código para gerar meu player:
draw() {
fill("#2A701D");
rect(this.posX, this.posY, this.size, this.size);
fill("#424242");
rect(this.posX + 10, this.posY - 10, 10, 30);
fill("#424242");
rect(this.posX + 30, this.posY + 5, 5, 20);
rect(this.posX - 5, this.posY + 5, 5, 20);
rect(this.posX + 12, this.posY - 20, 5, 20);
}
O player pode ser desenvolvido usando uma classe para agrupar suas funcionalidades:
class Player {
constructor(posX, posY, size) {
this.posX = posX;
this.posY = posY;
this.size = size;
}
draw() {
fill("#2A701D");
rect(this.posX, this.posY, this.size, this.size);
fill("#424242");
rect(this.posX + 10, this.posY - 10, 10, 30);
fill("#424242");
rect(this.posX + 30, this.posY + 5, 5, 20);
rect(this.posX - 5, this.posY + 5, 5, 20);
rect(this.posX + 12, this.posY - 20, 5, 20);
}
move() {
if (keyIsDown(LEFT_ARROW) && this.posX >= 5) {
this.posX -= 5;
}
if (keyIsDown(RIGHT_ARROW) && this.posX <= width - player.size) {
console.log(this.posX);
this.posX += 5;
}
if (keyIsDown(UP_ARROW) && this.posY >= 5 + player.size) {
this.posY -= 5;
}
if (keyIsDown(DOWN_ARROW) && this.posY <= height - player.size) {
this.posY += 5;
}
}
}
O player possui uma “bala” que é associada a ele… então podemos também codificar essa bala usando uma classe:
class Bullet {
constructor(posX, posY, sizeX, sizeY) {
this.posX = posX;
this.posY = posY;
this.sizeX = sizeX;
this.sizeY = sizeY;
}
draw() {
rect(this.posX, this.posY, this.sizeX, this.sizeY);
// as balas se movem "para cima" no canva... então subtraímos 10pxls a cada drawing
this.posY = this.posY - 10;
}
}
Desenvolvendo os inimigos
Os inimigos são simplesmente quadrados que se movem pela tela, porém é essencial é que esses quadrados sejam gerados em posições aleatórias (geralmente na parte superior). Nosso inimigo é bastante simples, veja:
class Enemy {
constructor(posX, posY, size) {
this.posX = posX;
this.posY = posY;
this.size = size;
}
draw() {
rect(this.posX, this.posY, this.size, this.size);
this.posY++;
}
}
Esse this.posY++; faz com que os inimigos se movam para baixo no canva todas as vezes que ele é desenhado na tela… assim eles se movem sempre nessa direção.
Desenvolver os controles do jogo
Os principais controles do jogo são: 1) atirar, 2) gerar os inimigos iniciais, 3) gerar inimigos aleatoriamente, 3) verificar colisão, 4) verificar pontuação.
O primeiro comando é de atirar e isso deve acontecer todas as vezes que apertamos a tecla de espaço no teclado. Para isso, devemos usar a função do P5 KeyReleased:
function keyReleased(event) {
console.log();
if (event.code === "Space") {
shoot();
}
return false;
}
Veja que essa função está atrelada a função “shoot”:
function shoot() {
let bullet = new Bullet(player.posX + 12, player.posY - 20, 5, 20);
this.bullets.push(bullet);
for (let i = 0; i < bullets.length; i++) {
if (this.bullets[i].posY < 0) {
bullets.splice(i, 1);
}
}
console.log(bullets);
}
Essa função instancia uma nova “bala” e coloca dentro do array de balas atiradas. Porém, para economizarmos memória podemos destruir as balas que ultrapassam o canva.
Agora que nosso player já consegue atirar, vamos gerar inimigos:
function generateRandomEnemies() {
for (let i = 0; i < 5; i++) {
enemies.push(
new Enemy(
floor(random(20, width - 50)),
floor(random(20, height / 3 - 50)),
20
)
);
}
}
Essa função gera inimigos no nosso canva e coloca eles dentro do array de enemies. Uma função similar é a generateEnemies():
function generateEnemy() {
enemies.push(
new Enemy(
floor(random(20, width - 50)),
floor(random(20, height / 3 - 50)),
20
)
);
}
A única diferença é que essa função gera apenas 1 inimigo na tela, enquanto a primeira gera uma quantidade maior de inimigos.
Verificando colisões
Talvez uma das funções mais importantes desse código seja a criação de colisão entre os players, bullets e enemies. Sabendo que tudo deve colidir e uma ação é executada quando isso acontece devemos configurar esses comportamentos. Pense o seguinte, se uma bala atinge o inimigo, você deve eliminar o inimigo e deixar a bala seguir seu caminho. Agora, uma outra possibilidade é o tanque colidir com um inimigo, nesse caso, o jogo recomeça com a pontuação = 0.
function checkHit() {
// verifica colisão das balas
for (let i = 0; i < bullets.length; i++) {
for (let j = 0; j < enemies.length; j++) {
let d = dist(
bullets[i].posX,
bullets[i].posY,
enemies[j].posX,
enemies[j].posY
);
if (d < enemies[j].size) {
enemies.splice(j, 1);
points++;
}
}
}
for (let i = 0; i < enemies.length; i++) {
// verifica colisão dos inimigos e o player.
let d = dist(enemies[i].posX, enemies[i].posY, player.posX, player.posY);
if (d < player.size) {
points = 0;
player.posX = 300;
player.posY = 700;
enemies = [];
generateRandomEnemies();
}
}
}
Por fim, precisamos colocar tudo isso dentro do nosso sketch.js:
let enemies = [];
let timer = 1;
let points = 0;
function setup() {
createCanvas(800, 800);
bullets = [];
player = new Player(300, 700, 30);
generateRandomEnemies();
}
function draw() {
background("rgb(204,242,204)");
player.draw();
player.move();
for (let i = 0; i < bullets.length; i++) {
bullets[i].draw();
}
for (let i = 0; i < enemies.length; i++) {
enemies[i].draw();
}
checkHit();
// esse timer gera 1 inimigo a cada segundo
if (frameCount % 60 == 0 && timer > 0) {
timer--;
if (timer == 0) {
generateEnemy();
timer = 1;
}
}
text("Score: " + points, 10, 30);
textSize(30);
}
Veja o resultado final:
Acesso ao projeto completo
- Acesse o projeto completo no P5.js editor: clique aqui
- Acesse o projeto no nosso Github: clique aqui