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 15 outubro 2022
Atualizado em 15 outubro 2022
Jogo desenvolvido em p5.js, utilizando a linguagem de javascript. O p5.js é uma biblioteca de fácil utilização e permite também o desenvolvimento de jogos em três dimensões (3D).
O nome dado ao projeto chama-se “guerra nas estrelas”. Esse projeto é bastante simples, porém tem um certo ambiente devido aos efeitos sonoros adicionados. A programação desse projeto usa cálculos de física para realizar determinados processos, como movimentação da nave. Esses efeitos dão um clima mais realístico, como a inércia.
Além da movimentação de nave, esse jogo possui muitos outras funcionalidades, como colisões de objetos, controle de cenas e ondas de inimigos.
O jogo “projeto nas estrelas” será executada em javascript. Basta importar a biblioteca p5.js para seguirmos adiante.
Ferramenta | Papel |
---|---|
Javascript | Linguagem de programação |
HTML | Usado para exibir o jogo |
p5.js | Biblioteca de desenvolvimento de jogos |
Node.js | Requerimento para NPM |
npm | Gerenciamento de pacotes Javascript |
Obs: O OS que será utilizado será o macOS Monterey, portanto poderá haver diferença nos comandos para usuários que utilizam outro tipo de OS, como Windows.
Primeiramente, criamos o diretório raiz do projeto.
mkdir GuerraNasEstrelas
No diretório “GuerraNasEstrelas”, armazenaremos todos os arquivos do jogo.
Em seguida, iniciamos o npm para gerenciar as bibliotecas e instalamos o p5.js.
cd GuerraNasEstrelas
npm init
npm install p5
Após instalar a biblioteca, criaremos um diretório para armazenar recursos como imagens e sons, e outro diretório para armazenar os arquivos de javascript, responsáveis pela lógica do jogo.
mkdir resources
mkdir core
Uma vez que a estrutura dos diretórios foi definida, criamos o arquivo principal do jogo e damos o nome de “index.html”, responsável por invocar a biblioteca e exibir o jogo.
touch index.html
Após criar o arquivo acima, editaremos o seu interior com o código abaixo:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="node_modules/p5/lib/p5.min.js"></script>
<script src="node_modules/p5/lib/addons/p5.sound.min.js"></script>
</head>
<body>
<main>
<h2>Guerra nas estrelas</h2>
<div id="sketch-holder">
</div>
</main>
<script src="core/main.js"></script>
</body>
</html>
Finalmente, instalamos a biblioteca para montar o servidor e vermos o resultado.
npm install http-server
http-server
O servidor deve ser estabelecido com o comando acima, porém ainda não criamos o arquivo “main.js” que será o arquivo onde toda a lógica do jogo será escrita.
Antes de começar criando a lógica do jogo é preciso entender os conceitos básicos da biblioteca p5.js. O p5.js tem duas funções essenciais para o seu funcionamento:
A função “setup” só será chamada uma vez antes do jogo ser iniciado. A função “draw” irá ser chamada constantemente durante o jogo, sendo responsável por atualizar a interface do usuário constantemente.
Em seguida, abrimos o “main.js”, localizado no diretório “core”.
A biblioteca p5.js disponibiliza a função “preload”, responsável por importar arquivos externos. Enquanto os arquivos não forem totalmente carregados, o jogo permanecerá na situação de espera.
let shotSoundEffect;
let explosionSoundEffect;
let backMusic;
let gameFont;
let startScreenAsset;
function preload() {
backMusic = loadSound('resources/lost_in_space.mp3');
shotSoundEffect = loadSound('resources/shot.wav');
explosionSoundEffect = loadSound('resources/explosion.wav');
gameFont = loadFont('resources/font.ttf');
startScreenAsset = loadImage('resources/start_screen.png');
}
Arquivos de som, imagem e fonte serão armazenados em variáveis globais, assim é possível acessar essas variáveis de qualquer local quando necessário.
Após a adição dos recursos utilizados no jogo, na função “setup” adicionamos as configurações como tamanho da tela, instância da câmera, tamanho da fonte e posição dos textos:
...
let cam;
function setup() {
let canvas = createCanvas(700, 600, WEBGL);
canvas.parent('sketch-holder');
cam = createCamera();
textFont(gameFont);
textSize(width / 4);
textAlign(CENTER, CENTER);
}
O jogo terá quatro estados:
Cada estado terá um número atribuído, que será mantido por uma única variável. Em seguida, fazemos a implementação do da função “draw”:
...
let gameState = 1; // 1: start, 2: gameplay, 3: gameover, 4: gameclear
function draw() {
background(40, 40, 40);
if (gameState == 1) {
gameStartScreen();
return;
}
if (gameState == 2) {
gamePlayScreen();
return;
}
if (gameState == 3) {
gameOverScreen();
return;
}
if (gameState >= 4) {
gameClearScreen();
return;
}
}
O algoritmo acima é bastante simples. Caso o número do estado atual seja o dentro da condição, a interface do usuário será atualizada e retornará nulo, escapando da função.
As funções dentro das condições ainda serão implementadas.
A tela inicial será a primeira tela a ser exibida após o carregamento dos recursos.
..
const spaceKey = 32;
function gameStartScreen() {
image(startScreenAsset, width / 2 * -1, height / 2 * -1, width, height);
textSize(25);
text('Aperte espaço para iniciar o jogo', 0, height / 4);
if (keyIsDown(spaceKey)) {
gameState = 2;
}
}
A função acima é repetida constantemente até que o usuário aperte a tecla “espaço” no teclado. Uma vez que o usuário aperta tecla, o estado do jogo é trocado para o número dois, iniciando o jogo.
As três primeiras linhas são respectivamente exibição de imagem, tamanho do texto e texto a ser exibido. O cálculo realizado na exibição da imagem tem como objetivo centralizar a imagem e deixar na mesma medida da tela do jogo.
A tela principal é a tela de ação, onde vários comandos são executados.
function gamePlayScreen() {
if (!backMusic.isPlaying()) {
backMusic.play();
}
}
A guerra nas estrelas é um jogo que o usuário terá que avançar por ondas. Conforme o usuário vai avançando, mais difícil torna-se o jogo.
Antes de implementar as ondas, implementaremos a nave do usuário.
O usuário terá os seguintes atributos:
Outros atributos também serão atribuídos, porém os atributos acima serão os mais importantes.
Criaremos uma classe que representará o usuário. Nessa classe serão escritas todas as lógicas envolvendo o usuário, como tempo entre tiros, movimentação da nave, freio e exibição.
class Spaceship {
constructor(nitro) {
this.hp = new Hp(100);
this.bHeigth = -50;
this.bWidth = 20;
this.position = createVector(0, height / 2 + this.bHeigth);
this.velocity = createVector(0, 0);
this.acceleration = createVector(0, 0);
this.accelerationPower = 0.2;
// Nitro
this.nitro = nitro;
// Bullets
this.bullets = [];
this.currentTime = 0;
this.timeBetweenShots = 12;
}
}
Acima, definimos todas as variáveis do usuário. Porém, ainda não implementamos a classe “Hp” que está sendo atribuída a variável que representa a vida do usuário. A classe “Hp” será implementado depois.
A movimentação da nave será feita através das teclas de direção do teclado. Além da movimentação, aceleração e freio serão adicionados.
A função abaixo será implementada dentro da classe “spacheship”.
..
const sKey = 83;
const aKey = 65;
keyboardCommands() {
// Nitro
let accelerationPower = this.accelerationPower;
if (keyIsDown(aKey)) {
accelerationPower += this.nitro.active();
} else {
this.nitro.isActived = false;
}
// Keys Pressed
if (keyIsDown(UP_ARROW)) {
this.acceleration.y -= accelerationPower;
}
if (keyIsDown(DOWN_ARROW)) {
this.acceleration.y += accelerationPower;
}
if (keyIsDown(RIGHT_ARROW)) {
this.acceleration.x += accelerationPower;
}
if (keyIsDown(LEFT_ARROW)) {
this.acceleration.x -= accelerationPower;
}
// Brake
if (keyIsDown(sKey)) {
this.brake();
}
}
As direções em letra maiúscula são variáveis globais já definidas na biblioteca p5.js. Após apertarmos as teclas de direção, o usuário não se movimentará imediatamente. O tempo para mudar de direção vai depender da aceleração atual da nave.
Em seguida, implementamos a função de “freio” do usuário:
brake() {
let verticalBrakingVelocity = this.calculateBrakingVelocity(this.velocity.y);
let horizontalBrakingVelocity = this.calculateBrakingVelocity(this.velocity.x);
if (this.velocity.y > 0) {
this.velocity.y -= verticalBrakingVelocity;
} else if (this.velocity.y < 0) {
this.velocity.y += verticalBrakingVelocity;
}
if (this.velocity.x > 0) {
this.velocity.x -= horizontalBrakingVelocity;
} else if (this.velocity.x < 0) {
this.velocity.x += horizontalBrakingVelocity;
}
}
calculateBrakingVelocity(velocity) {
let coefficient = 0.1;
return velocity * velocity / 2 * coefficient * gravity;
}
As condições da função acima só funcionam quando a nave não está em movimento, caso a nave esteja parada, o freio não irá ter nenhum efeito.
Logo depois, implementamos a função “drive”, responsável por movimentar a nave de fato baseando-se na aceleração e adicionamos a condição necessária para parar a nave quando esta prestes a sair da tela.
drive() {
this.keyboardCommands();
// Add Acceleration
this.velocity.add(this.acceleration);
// Affect position with velocity
this.position.add(this.velocity);
this.acceleration.mult(0);
// Reload Nitro
if (!this.nitro.isActived) {
this.nitro.reload();
}
// Edges
if (this.position.x < (width / 2 - this.bWidth) * -1) {
this.position.x = (width / 2 - this.bWidth) * -1;
this.velocity.mult(0);
}
if (this.position.x > width / 2 - this.bWidth) {
this.position.x = width / 2 - this.bWidth;
this.velocity.mult(0);
}
if (this.position.y < (height / 2 + this.bHeigth) * -1) {
this.position.y = (height / 2 + this.bHeigth) * -1;
this.velocity.mult(0);
}
if (this.position.y > height / 2 + this.bHeigth) {
this.position.y = height / 2 + this.bHeigth;
this.velocity.mult(0);
}
}
A função acima também invoca a função “keyboardCommands”.
Finalmente, adicionamos a função de exibição da nave do usuário:
display(enableControls = true) {
if (enableControls) {
this.drive();
this.fire();
this.hp.display();
}
push();
translate(this.position.x, this.position.y);
cone(this.bWidth, this.bHeigth);
pop();
}
Assim, concluímos o usuário por enquanto.
A classe HP é responsável por exibir e gerenciar a quantidade restante que o usuário tem de vida.
class Hp {
constructor(max) {
this.bWidth = 20;
this.bHeight = 100;
this.left = max;
this.max = max;
}
decrease(damage) {
if (this.left > 0) {
this.left -= damage;
}
if (this.left < 1) {
gameState = 3;
}
}
display() {
push();
normalMaterial();
fill(255, 0, 0, 60);
stroke(255, 0, 0);
let yOrigin = convertRange(this.max, 0, height / 2 - this.bHeight - 10, (height / 2 - this.bHeight - 10) + this.bHeight, this.left);
let currentHeight = convertRange(0, this.max, 0, this.bHeight, this.left);
rect(width / 2 - this.bWidth - 10, yOrigin, this.bWidth, currentHeight);
pop();
}
}
A função “decrease” acima é responsável por diminuir a vida do usuário em situações de dano e por trocar a situação atual do jogo para três (Game over) caso o usuário não tenha mais HP disponível.
Assim como a barra de HP, o nitro também terá uma barra para representar a sua quantidade disponível dentro da nave do usuário.
class Nitro {
constructor(power) {
this.bWidth = 20;
this.bHeight = 100;
this.power = power;
this.left = 3;
this.isActived = false;
}
active() {
if (this.left > 0) {
this.isActived = true;
this.left -= 0.1;
return this.power;
}
return 0;
}
reload() {
if (this.left < 3 && !this.isActived) {
this.left += 0.1;
}
}
display() {
push();
normalMaterial();
fill(0, 191, 255, 60);
stroke(0, 191, 255);
let yOrigin = convertRange(3, 0, height / 2 - this.bHeight - 10, (height / 2 - this.bHeight - 10) + this.bHeight, this.left);
let currentHeight = convertRange(0, 3, 0, this.bHeight, this.left);
rect(width / 2 - (this.bWidth * 2) - 20, yOrigin, this.bWidth, currentHeight);
pop();
}
}
Ao apertar a tecla espaço, tiros são disparados pelo usuário. Cada um desses tiros serão gerenciados por uma uma instância da classe “Bullet”.
class Bullet {
constructor(position, size, speed, isPlayer = true, routeVec = null) {
this.position = position.copy();
this.size = size;
this.speed = speed;
this.isPlayer = isPlayer;
this.damage = 5;
this.routeVec = routeVec;
}
travelThroughSpace() {
if (this.isPlayer) {
this.position.sub(createVector(0, this.speed));
return;
}
if (!this.routeVec) {
this.position.add(createVector(0, this.speed));
return;
}
this.position.add(this.routeVec);
}
...
}
Atributos como direção e autor dos disparos serão definidos ainda no construtor da classe. Assim, podemos usar essa mesma classe para o usuário e os inimigos.
Também criaremos a classe representando a nave inimiga.
class Enemy {
constructor(hp, position, level, bWidth, bHeight, speed, shotSpeed = 2) {
this.hp = hp;
this.position = position;
this.bWidth = bWidth;
this.bHeight = bHeight;
this.speed = speed;
this.shotSpeed = shotSpeed;
this.level = level;
this.bullets = [];
this.currentTime = 0;
this.timeBetweenShots = 40;
this.angleY = 0;
}
}
A estrutura do inimigo e do usuário é bastante parecida. Os tiros disparados pelo inimigo será gerenciado dentro da classe “enemy” pela variável “bullets”.
A implementação dos disparos do usuário são realizados pela função “fire”. Nessa função serão controlados as colisões dos disparos com os inimigos, remoção dos tiros após eles desaparecem da tela e criação de novos disparos caso o usuário aperte a tecla “espaço”.
fire() {
if (keyIsDown(spaceKey)) {
if (this.currentTime <= 0) {
shotSound();
this.bullets.push(new Bullet(this.position, 5, 5));
this.currentTime = this.timeBetweenShots;
}
}
this.currentTime--;
for (let i = 0; i < this.bullets.length; i++) {
this.bullets[i].display();
if (this.bullets[i].position.y < height / 2 * -1) {
this.bullets.splice(i, 1);
continue;
}
// Enemies Collision
for (let j = 0; j < waves[currentWave].enemies.length; j++) {
let enemy = waves[currentWave].enemies[j];
if (this.bullets[i].position.y <= enemy.position.y + enemy.bHeight && this.bullets[i].position.y + (this.bullets[i].size / 2) >= enemy.position.y &&
this.bullets[i].position.x <= enemy.position.x + enemy.bWidth && this.bullets[i].position.x + (this.bullets[i].size / 2) >= enemy.position.x - enemy.bWidth) {
waves[currentWave].enemies[j].setDamage(this.bullets[i].damage);
this.bullets.splice(i, 1);
break;
}
}
}
}
Mesmo se o usuário deixar a barra de espaço pressionada, cada disparo irá demorar um certo período de tempo para ser disparado.
A mesma lógica será utilizada nos disparos do inimigo que irá estabelecer um intervalo a cada disparo e avaliar a detecção de colisões com disparos.
Cada onda será responsável por gerenciar o número de inimigos e os padrões de movimentos do inimigos.
class Wave {
constructor(enemiesCount, level = 1, size = 30, speed = 2, shotSpeed = 2, hp = 100) {
this.enemiesCount = enemiesCount;
this.startVerticalCoordinate = height / 4 * -1;
this.isReady = false;
let enemies = [];
for (let i = 0; i < enemiesCount; i++) {
let enemyPos = createVector(this.getInitialXPos(i, enemiesCount), height / 2 * -1, i * 10);
enemies.push(
new Enemy(hp, enemyPos, level, size, size, speed, shotSpeed)
);
}
this.enemies = enemies;
}
}
No construtor da classe “Wave”, os inimigos serão instanciados e terão suas coordenadas iniciais calculadas.
enemyStrategies() {
for (let i = 0; i < this.enemies.length; i++) {
this.enemies[i].display();
// If Destroyed
if (this.enemies[i].hp <= 0) {
this.enemies[i].loseControl();
if (this.enemies[i].bullets.length <= 0) {
explosionSoundEffect.play();
this.enemies.splice(i, 1);
}
continue;
}
this.enemies[i].position.x += this.enemies[i].speed;
// Edges
if (this.enemies[i].position.x > width / 2 - this.enemies[i].bWidth || this.enemies[i].position.x < (width / 2 - this.enemies[i].bWidth) * -1) {
this.enemies[i].speed *= -1;
}
this.enemies[i].fire();
}
}
A função “enemyStrategies” é bastante simples. Caso um dos inimigos tenha chegado em uma das laterais da tela, eles irão ter sua direção voltada para o lado contrário. Nessa função também serão escritos comandos como disparos e explosão do inimigo após ter sua vida acabada.
As duas telas game over e game clear compartilham a mesma função.
function gameOverScreen(finalText = 'Game Over') {
if (backMusic.isPlaying()) {
backMusic.stop();
}
image(startScreenAsset, width / 2 * -1, height / 2 * -1, width, height);
textSize(25);
text('Aperte espaço para reiniciar o jogo', 0, height / 4);
textSize(30);
text(finalText, 0, height / 4 * -1);
let resetGame = function() {
currentWave = 0;
spaceship = new Spaceship(new Nitro(2));
waves = getWaves();
};
if (keyIsDown(spaceKey)) {
gameState = 2;
resetGame();
}
}
O texto final será passado como parâmetro, assim podendo ser personalizado conforme as nossas necessidades.
Cálculos de física são constantemente utilizados para simular fenômenos que acontecem na vida real. Nesse jogo, simulamos efeitos de atrito e inércia, tornando a movimentação da nave um pouco mais realística.
No desenvolvimento de jogos, outras variáveis que são constantemente definidas são gravidade e a massa. Porém, atribuir um valor realístico pode para essas variáveis pode tornar o game um pouco sem graça. Por isso, atribuímos valores que tornam o jogo mais divertido.
O jogo está disponível para teste no link abaixo (esse jogo não funciona corretamente em smartphones):
Testar jogo
O repositório também está disponível no github:
Ver repositório
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.