Construindo Jogos com JavaScript
- #HTML
- #CSS
- #JavaScript
Neste artigo mostro um simples projeto que criei, para por
em prática os conhecimentos que adquiri nos cursos da dio.
Neste projeto recrie o jogo Arkanoid usando apenas javascript e css.
Abaixo segue o link do jogo:
link do Jogo : https://urutaudev.com.br/games/arkanoid/
Segue link do repositório do projeto:
https://urutaudev.com.br/index.php/2024/10/07/como-fazer-arkanoid-game-em-javascript/
Código usado no projeto:
Estrutura em html:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arkanoid Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="scenes">
<div id="menu">
<img src="logo-arkanoid.jpeg" alt="">
<button id="startButton">Start Game</button>
</div>
<div id="levelUp">
<h2>Level Completed!</h2>
<button id="nextLevelButton">Next Level</button>
</div>
<div id="gameOver">
<h1>Game Over</h1>
<button id="restartButton">Restart Game</button>
<div id="gameOverScore">
<h2>Your Score: 100</h2>
<h2>Best Score: 100</h2>
</div>
</div>
<canvas id="gameCanvas" width="480" height="320"></canvas>
</div>
<div class="game-info">
<div id="controls" class="controls">
<div class="sound-control">
<label>Sound:</label>
<button id="toggleSound">TURN OFF</button>
</div>
<div class="volume-control">
<label for="volumeControl" id="volumeControlLabel">Music Volume:</label>
<input type="range" id="volumeControl" min="0" max="1" step="0.1" value="0.3">
</div>
</div>
<div class="score">
<h3>Records</h3>
<ul id="recordsList">
<li>1º - 000 Points</li>
<li>2º - 000 Points</li>
<li>3º - 000 Points</li>
<li>4º - 000 Points</li>
<li>5º - 000 Points</li>
</ul>
</div>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
Código de estilização do Jogo:
@font-face {
font-family: "Poppins";
src: url("https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap");
}
*,
*:after,
*:before {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
vertical-align: baseline;
text-decoration: none;
}
ul {
list-style: none;
}
button:focus {
outline: 0;
}
:root {
--green: rgb(166, 247, 80);
}
html,
body {
height: 100%;
font-family: "Poppins", sans-serif;
color: #6e7888;
}
body {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
background-color: #222738;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
}
.scenes {
display: flex;
flex-direction: column;
}
canvas {
border: 1px solid var(--green);
background-color: #181825;
}
#menu,
#gameOver,
#levelUp {
min-width: 400px;
min-height: 300px;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.95);
background-color: #181825;
padding: 30px;
border: 2px solid #0095dd;
text-align: center;
border-radius: 10px;
gap: 1rem;
box-shadow: rgba(6, 24, 44, 0.4) 0px 0px 0px 2px,
rgba(6, 24, 44, 0.65) 0px 4px 6px -1px,
rgba(255, 255, 255, 0.08) 0px 1px 0px inset;
font-size: 2rem;
font-weight: 900;
color: var(--green);
}
#menu > img{
width: 250px;
max-height: 30%;
}
#gameOver h1 {
color: #f22c3d;
}
.game-info {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 1rem;
gap: 1rem;
font-size: 1rem;
font-weight: 800;
}
.controls {
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
gap: 0.5rem;
}
button {
padding: 0.4rem 0.5rem;
background: #0095dd;
color: white;
cursor: pointer;
border: none;
border-radius: 4px;
font-size: 0.7rem;
line-height: 0.7rem;
font-weight: 800;
}
button:hover {
background: #007bb5;
}
#controls label {
font-size: 1rem;
color: var(--green);
}
.sound-control {
display: flex;
flex-direction: column;
gap: 0.2rem;
border-radius: 4px;
}
.volume-control {
display: flex;
flex-direction: column;
gap: 0.8rem;
border-radius: 4px;
}
.volume-control input {
width: calc(80% -20px);
height: 10px;
appearance: none;
outline: none;
background: #6e7888;
border-radius: 5px;
box-shadow: rgba(6, 24, 44, 0.4) 0px 0px 0px 2px,
rgba(6, 24, 44, 0.65) 0px 4px 6px -1px,
rgba(255, 255, 255, 0.08) 0px 1px 0px inset;
}
.volume-control input::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
background: #0095dd;
border: solid 2px white;
border-radius: 50%;
cursor: grab;
}
.score h3 {
font-weight: 900;
color: var(--green);
}
Código da logica do jogo em Javascript:
// Constantes de configuração
const BALL_SPEED_MULTIPLIER = 1.2;
const INITIAL_LIVES = 1;
const MAX_LEVELS = 5;
const PADDLE_SPEED = 7;
const PARTICLE_COUNT = 15;
// Elementos do DOM
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const menu = document.getElementById("menu");
const startButton = document.getElementById("startButton");
const gameOverMenu = document.getElementById("gameOver");
const gameOverScore = document.getElementById("gameOverScore");
const restartButton = document.getElementById("restartButton");
const levelUpMenu = document.getElementById("levelUp");
const nextLevelButton = document.getElementById("nextLevelButton");
const toggleSoundButton = document.getElementById("toggleSound");
const volumeControl = document.getElementById("volumeControl");
const volumeControlLabel = document.getElementById("volumeControlLabel");
const recordsList = document.getElementById("recordsList");
// Controle de som
let soundOn = true;
// Sons do jogo
const music = new Audio("sounds/background.mp3");
music.loop = true;
music.volume = 0.3;
class Ball {
constructor(x, y, radius, dx, dy, color = "#0095DD") {
this.x = x;
this.y = y;
this.radius = radius;
this.dx = dx;
this.dy = dy;
this.color = color;
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
move() {
this.x += this.dx;
this.y += this.dy;
}
reset(x, y, dx, dy) {
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
}
}
class Paddle {
constructor(width, height, canvasWidth, color = "#0095DD") {
this.width = width;
this.height = height;
this.canvasWidth = canvasWidth;
this.x = (canvasWidth - width) / 2;
this.color = color;
this.speed = PADDLE_SPEED;
}
draw(ctx) {
ctx.beginPath();
ctx.rect(this.x, canvas.height - this.height, this.width, this.height);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
move(direction) {
if (direction === "right" && this.x < this.canvasWidth - this.width) {
this.x += this.speed;
} else if (direction === "left" && this.x > 0) {
this.x -= this.speed;
}
}
reset() {
this.x = (this.canvasWidth - this.width) / 2;
}
}
class Brick {
constructor(x, y, width, height, status = 1, color = "rgb(166, 247, 80)") {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.status = status;
this.color = color;
}
draw(ctx) {
if (this.status === 1) {
ctx.beginPath();
ctx.rect(this.x, this.y, this.width, this.height);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
}
}
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
this.size = Math.random() * 3 + 1;
this.speedX = Math.random() * 2 - 1;
this.speedY = Math.random() * 2 - 1;
this.alpha = 1;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
ctx.restore();
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.alpha -= 0.02;
}
}
class SoundManager {
constructor() {
this.sounds = {
bounce: new Audio("sounds/bounce.wav"),
brick: new Audio("sounds/brick.wav"),
gameOver: new Audio("sounds/gameover.wav"),
levelUp: new Audio("sounds/levelup.wav"),
};
this.soundOn = true;
}
playSound(soundName) {
const sound = this.sounds[soundName];
sound.volume = 0.1;
if (this.soundOn && sound) {
sound.currentTime = 0;
sound
.play()
.catch((error) => console.error("Erro ao reproduzir som:", error));
}
}
toggleSound() {
this.soundOn = !this.soundOn; // Alterna entre ativar/desativar o som
}
}
// Classe principal do jogo
class Game {
constructor() {
this.ball = new Ball(canvas.width / 2, canvas.height - 30, 10, 2, -2);
this.paddle = new Paddle(75, 10, canvas.width);
this.bricks = [];
this.particles = [];
this.score = 0;
this.lives = INITIAL_LIVES;
this.level = 1;
this.maxLevels = MAX_LEVELS;
this.rightPressed = false;
this.leftPressed = false;
this.isRunning = false;
this.bricks = this.createBricks();
this.soundManager = new SoundManager();
this.musicManager = new SoundManager();
this.addEventListeners();
this.setRecords();
this.showStartMenu();
}
createBricks() {
const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;
const bricks = [];
for (let r = 0; r < brickRowCount; r++) {
for (let c = 0; c < brickColumnCount; c++) {
const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
const brick = new Brick(brickX, brickY, brickWidth, brickHeight);
bricks.push(brick);
}
}
console.log(bricks);
return bricks; // Retorna uma lista plana de tijolos
}
addEventListeners() {
document.addEventListener("keydown", this.handleKey.bind(this), false);
document.addEventListener("keyup", this.handleKey.bind(this), false);
}
handleKey(e, isKeyDown) {
const keyActions = {
Right: "rightPressed",
ArrowRight: "rightPressed",
Left: "leftPressed",
ArrowLeft: "leftPressed",
};
const action = keyActions[e.key];
if (action) {
e.preventDefault();
// Define o estado com base no tipo do evento (keydown = true, keyup = false)
this[action] = e.type === "keydown";
}
}
collisionDetection() {
// Filtra apenas os tijolos ativos para colisão
const activeBricks = this.bricks.filter((brick) => brick.status === 1);
// Verifica colisão dos tijolos ativos
activeBricks.forEach((brick) => {
if (this.isCollidingWithBrick(brick)) {
this.handleBrickCollision(brick);
}
});
}
isCollidingWithBrick(brick) {
// Verifica se a bola está colidindo com o tijolo
return (
this.ball.x > brick.x &&
this.ball.x < brick.x + brick.width &&
this.ball.y > brick.y &&
this.ball.y < brick.y + brick.height
);
}
handleBrickCollision(brick) {
// Reage à colisão: atualiza o status, incrementa a pontuação e trata efeitos
this.soundManager.playSound("brick");
this.ball.dy = -this.ball.dy;
brick.status = 0;
this.score++;
this.createParticles(brick.x + brick.width / 2, brick.y + brick.height / 2);
this.checkLevelCompletion();
}
createParticles(x, y) {
for (let i = 0; i < PARTICLE_COUNT; i++) {
this.particles.push(new Particle(x, y, "#0095DD"));
}
}
updateParticles() {
this.particles = this.particles.filter((particle) => {
particle.update();
return particle.alpha > 0;
});
}
drawParticles() {
this.particles.forEach((particle) => particle.draw(ctx));
}
checkLevelCompletion() {
const totalBricks = this.bricks.filter(
(brick) => brick.status === 1
).length;
if (totalBricks < 1) {
this.showLevelUpMenu();
}
}
saveHighScore() {
let scores = JSON.parse(localStorage.getItem("highScores")) || [];
scores.push(this.score);
scores.sort((a, b) => b - a);
if (scores.length > 5) {
scores.pop();
}
localStorage.setItem("highScores", JSON.stringify(scores));
}
displayHighScores() {
const highScoresList = document.createElement("div");
const scores = JSON.parse(localStorage.getItem("highScores")) || [];
const userScore = document.createElement("h2");
userScore.textContent = `Your Score: ${this.score}`;
highScoresList.appendChild(userScore);
const bestScoreValue = scores.length > 0 ? Math.max(...scores) : this.score;
const bestScore = document.createElement("h2");
bestScore.textContent = `Best Score: ${bestScoreValue}`;
highScoresList.appendChild(bestScore);
gameOverScore.innerHTML = "";
gameOverScore.appendChild(highScoresList);
}
showGameOverMenu() {
this.stopGame();
this.saveHighScore();
this.toggleMenu(gameOverMenu, true);
this.displayHighScores();
this.setRecords();
}
showLevelUpMenu() {
this.stopGame();
this.toggleMenu(levelUpMenu, true);
this.soundManager.playSound("levelUp");
}
showStartMenu() {
this.toggleMenu(menu, true);
}
stopGame() {
this.isRunning = false;
music.pause();
}
resetBallAndPaddle() {
this.ball.reset(canvas.width / 2, canvas.height - 30, 2, -2);
this.paddle.reset();
}
resetBricks() {
this.bricks = this.createBricks();
}
resetGame() {
this.toggleMenu(menu, false);
this.toggleMenu(gameOverMenu, false);
this.toggleMenu(levelUpMenu, false);
canvas.style.display = "block";
this.isRunning = true;
music.play();
this.score = 0;
this.lives = INITIAL_LIVES;
this.level = 1;
this.resetBricks();
this.resetBallAndPaddle();
this.draw();
this.setRecords();
}
drawText(text, x, y, font = "16px Arial", color = "#0095DD") {
ctx.font = font;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
}
drawScore() {
this.drawText(`Level: ${this.level} | Score: ${this.score} `, 8, 20);
}
drawLives() {
this.drawText(`Lifes: ${this.lives}`, canvas.width - 65, 20);
}
drawBricks(ctx) {
this.bricks.forEach((brick) => brick.draw(ctx));
}
draw() {
if (!this.isRunning) {
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawBricks(ctx);
this.ball.draw(ctx);
this.paddle.draw(ctx);
this.drawScore();
this.drawLives();
this.drawParticles();
this.collisionDetection();
// Movimentação da bola
this.ball.move();
// Colisão com as paredes
if (
this.ball.x + this.ball.dx > canvas.width - this.ball.radius ||
this.ball.x + this.ball.dx < this.ball.radius
) {
this.ball.dx = -this.ball.dx;
this.soundManager.playSound("bounce");
}
if (this.ball.y + this.ball.dy < this.ball.radius) {
this.ball.dy = -this.ball.dy;
this.soundManager.playSound("bounce");
} else if (this.ball.y + this.ball.dy > canvas.height - this.ball.radius) {
if (
this.ball.x > this.paddle.x &&
this.ball.x < this.paddle.x + this.paddle.width
) {
// Ajustar a direção da bola com base no ponto de impacto na raquete
const relativeHit =
(this.ball.x - (this.paddle.x + this.paddle.width / 2)) /
(this.paddle.width / 2);
this.ball.dx = relativeHit * 5;
this.ball.dy = -this.ball.dy;
this.soundManager.playSound("bounce");
} else {
this.lives--;
this.soundManager.playSound("gameOver");
if (this.lives === 0) {
this.showGameOverMenu();
} else {
this.resetBallAndPaddle();
}
}
}
// Movimentação da raquete
if (this.rightPressed) {
this.paddle.move("right");
} else if (this.leftPressed) {
this.paddle.move("left");
}
this.updateParticles();
requestAnimationFrame(this.draw.bind(this));
}
nextLevel() {
this.toggleMenu(levelUpMenu, false);
this.level++;
if (this.level > this.maxLevels) {
alert("Parabéns! Você completou todos os níveis!");
document.location.reload();
} else {
// Aumentar a velocidade da bola
this.ball.dx *= BALL_SPEED_MULTIPLIER;
this.ball.dy *= BALL_SPEED_MULTIPLIER;
// Resetar os tijolos
this.resetBricks();
// Resetar a posição da bola e raquete
this.resetBallAndPaddle();
this.isRunning = true;
music.play();
this.draw();
}
}
setRecords() {
const scores = JSON.parse(localStorage.getItem("highScores")) || [];
const listItems = scores
.map((score, index) => `<li>${index + 1}º - ${score} Points</li>`)
.join("");
recordsList.innerHTML = listItems;
}
toggleSound() {
this.soundManager.toggleSound();
}
// Funções de interface
toggleMenu(selectedMenu, show) {
selectedMenu.style.display = show ? "flex" : "none";
canvas.style.display = show ? "none" : "block";
}
}
function updateVolumeControlLabel() {
const volumeValue = volumeControl.value * 100;
volumeControlLabel.innerText = `Music Volume: ${volumeValue}%`;
}
// Controle de som
toggleSoundButton.addEventListener("click", () => {
soundOn = !soundOn;
music.muted = !soundOn;
game.toggleSound();
toggleSoundButton.textContent = soundOn ? "Turn OFF" : "Turn ON";
});
// Controle do volume
volumeControl.addEventListener("input", () => {
music.volume = volumeControl.value;
updateVolumeControlLabel();
});
// Eventos dos botões
startButton.addEventListener("click", () => game.resetGame());
restartButton.addEventListener("click", () => game.resetGame());
nextLevelButton.addEventListener("click", () => game.nextLevel());
// Instanciar o jogo
const game = new Game();
updateVolumeControlLabel();