În acest material, îți voi prezenta etapele creării unui joc Snake, utilizând HTML, CSS și JavaScript.
Nu vom apela la biblioteci externe; jocul va fi executat direct în browser. Dezvoltarea acestui joc reprezintă o activitate antrenantă, perfectă pentru a-ți exersa abilitățile de rezolvare a problemelor.
Structura proiectului
Snake este un joc simplu, în care conduci un șarpe spre hrană, evitând în același timp obstacole. Când șarpele ajunge la mâncare, o consumă și se mărește. Pe măsură ce jocul progresează, șarpele devine din ce în ce mai lung.
Șarpele nu trebuie să se lovească de marginile ecranului sau de propriul corp. Astfel, pe parcursul jocului, cu cât șarpele devine mai lung, cu atât va fi mai dificil de controlat.
Acest ghid pas cu pas te va ajuta să construiești jocul prezentat mai jos:
Codul complet al jocului este disponibil pe pagina mea GitHub. O versiune demonstrativă poate fi accesată pe GitHub Pages.
Condiții prealabile
Pentru acest proiect vom utiliza HTML, CSS și JavaScript. Vom folosi un cod HTML și CSS simplu, accentul fiind pe JavaScript. Prin urmare, este necesar să ai cunoștințe de bază de JavaScript pentru a putea urma acest tutorial. Dacă nu ești familiarizat cu JavaScript, îți recomand să citești articolul nostru despre cele mai bune surse pentru a învăța JavaScript.
De asemenea, ai nevoie de un editor de cod pentru a scrie codul și de un browser web, pe care probabil îl ai deja dacă citești acest articol.
Inițializarea Proiectului
Pentru început, vom crea fișierele proiectului. Într-un director nou, creează un fișier index.html și adaugă următorul cod HTML:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="https://wilku.top/javascript-snake-tutorial-explained/./styles.css" /> <title>Snake</title> </head> <body> <div id="game-over-screen"> <h1>Game Over</h1> </div> <canvas id="canvas" width="420" height="420"> </canvas> <script src="./snake.js"></script> </body> </html>
Acest cod HTML creează un ecran simplu „Game Over”. Vom controla vizibilitatea acestui ecran prin JavaScript. De asemenea, definește un element canvas, pe care vom desena labirintul, șarpele și hrana. Codul HTML include, de asemenea, referințele către fișierele CSS și JavaScript.
Apoi, creează un fișier styles.css pentru a defini stilurile. Adaugă următoarele stiluri în acest fișier:
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', Courier, monospace; } body { height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #00FFFF; } #game-over-screen { background-color: #FF00FF; width: 500px; height: 200px; border: 5px solid black; position: absolute; align-items: center; justify-content: center; display: none; }
În setul de reguli „*”, selectăm toate elementele și resetăm spațierea. De asemenea, setăm familia de fonturi și ajustăm dimensiunile elementelor folosind modelul border-box. Pentru elementul body, am setat înălțimea la 100% din înălțimea ferestrei, am aliniat toate elementele la centru și am stabilit o culoare de fundal albastră.
Am stilizat ecranul „Game Over” pentru a avea o înălțime de 200px și o lățime de 500px. I-am dat o culoare de fundal magenta și o bordură neagră. Poziția acestui ecran este setată la absolut, astfel încât să fie afișat deasupra conținutului normal și aliniat central. Conținutul este centrat, iar implicit ecranul este ascuns.
În final, creează un fișier snake.js, în care vom scrie codul JavaScript în următoarele secțiuni.
Declararea variabilelor globale
Următoarea etapă în dezvoltarea jocului Snake este definirea variabilelor globale pe care le vom folosi. În fișierul snake.js, adaugă următoarele declarații în partea de sus:
// Crearea referințelor către elementele HTML let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // Crearea contextului pentru desenarea pe canvas let ctx = canvas.getContext("2d");
Aceste variabile stochează referințe către ecranul „Game Over” și elementul canvas. Apoi, creăm un context, care va fi utilizat pentru a desena pe canvas.
Adaugă următoarele declarații de variabile sub primul set:
// Definiții pentru labirint let gridSize = 400; let unitLength = 10;
Prima variabilă definește dimensiunea grilei, în pixeli. A doua variabilă definește o unitate de lungime în joc. Această unitate va fi utilizată în mai multe locuri, cum ar fi grosimea pereților labirintului, grosimea șarpelui, dimensiunea hranei și pașii de mișcare ai șarpelui.
Apoi, adaugă următoarele variabile pentru joc. Aceste variabile vor fi folosite pentru a urmări starea jocului:
// Variabile de joc let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
Variabila șarpe urmărește pozițiile pe care șarpele le ocupă în prezent. Șarpele este compus din unități, fiecare ocupând o poziție pe canvas. Poziția fiecărei unități este stocată în matricea șarpelui. Fiecare poziție are coordonatele x și y. Primul element din matrice reprezintă coada, iar ultimul, capul.
Pe măsură ce șarpele se mișcă, vom adăuga elemente la sfârșitul matricei, mutând capul înainte. De asemenea, vom elimina primul element, coada, astfel încât lungimea șarpelui să rămână constantă.
Variabila foodPosition stochează coordonatele x și y ale hranei. Variabila direction indică direcția în care se mișcă șarpele, iar variabila collided este un boolean care devine adevărat când detectăm o coliziune.
Declararea funcțiilor
Întregul joc este structurat în funcții, pentru o dezvoltare și gestionare mai ușoară. În această secțiune, vom declara funcțiile și vom explica scopul fiecăreia. Următoarele secțiuni vor defini aceste funcții și vor detalia algoritmii.
function setUp() {} function doesSnakeOccupyPosition(x, y) {} function checkForCollision() {} function generateFood() {} function move() {} function turn(newDirection) {} function onKeyDown(e) {} function gameLoop() {}
Pe scurt, funcția setUp inițializează jocul. Funcția checkForCollision verifică dacă șarpele s-a ciocnit de margini sau de propriul corp. Funcția doesSnakeOccupyPosition verifică dacă o anumită poziție, definită prin coordonatele x și y, este ocupată de vreo parte a corpului șarpelui. Acest lucru va fi util pentru a determina o poziție liberă pentru hrană.
Funcția move mută șarpele în direcția curentă, iar funcția turn schimbă direcția. Funcția onKeyDown ascultă apăsările de taste pentru a schimba direcția șarpelui. Funcția gameLoop va muta șarpele și va verifica dacă au avut loc coliziuni.
Definirea funcțiilor
În această secțiune, vom defini funcțiile declarate anterior, explicând modul lor de funcționare. Pentru fiecare funcție, va exista o scurtă descriere înainte de cod, precum și comentarii pentru a clarifica fiecare linie.
Funcția setUp
Funcția setUp îndeplinește 3 sarcini:
Codul pentru această funcție este:
// Desenarea marginilor pe canvas // Dimensiunea canvas-ului va fi egală cu dimensiunea grilei plus grosimea a două margini laterale canvasSideLength = gridSize + unitLength * 2; // Desenăm un pătrat negru care acoperă întregul canvas ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // Ștergem centrul pătratului negru pentru a crea spațiul de joc // Acest lucru va crea o bordură neagră care reprezintă marginea labirintului ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // Următorul pas este stocarea pozițiilor inițiale ale capului și cozii șarpelui // Lungimea inițială a șarpelui va fi de 60px sau 6 unități // Capul șarpelui va fi la 30px sau 3 unități în fața punctului de mijloc const headPosition = Math.floor(gridSize / 2) + 30; // Coada șarpelui va fi la 30px sau 3 unități în spatele punctului de mijloc const tailPosition = Math.floor(gridSize / 2) - 30; // Parcurge pozițiile de la coadă la cap, în incrementări de unitLength for (let i = tailPosition; i <= headPosition; i += unitLength) { // Stocăm poziția corpului șarpelui și desenăm pe canvas snake.push({ x: i, y: Math.floor(gridSize / 2) }); // Desenăm un dreptunghi de dimensiune unitLength * unitLength ctx.fillRect(x, y, unitLength, unitLength); } // Generăm hrana generateFood();
doesSnakeOccupyPosition
Această funcție primește coordonatele x și y ca poziție și verifică dacă o astfel de poziție se află în corpul șarpelui. Folosim metoda find a array-ului JavaScript pentru a căuta o poziție cu coordonatele date.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
checkForCollision
Această funcție verifică dacă șarpele s-a ciocnit cu ceva și setează variabila collided la true. Vom începe prin a verifica dacă există coliziuni cu marginile din stânga și dreapta, cu marginile de sus și jos, și apoi cu șarpele însuși.
Pentru coliziuni cu marginile din stânga și dreapta, verificăm dacă coordonata x a capului șarpelui este mai mare decât gridSize sau mai mică decât 0. Pentru coliziunile cu marginile de sus și de jos, facem aceeași verificare, dar folosind coordonata y.
Apoi, verificăm coliziunile cu șarpele însuși. Verificăm dacă vreo altă parte a corpului său ocupă poziția ocupată în prezent de capul șarpelui. Codul funcției checkForCollision este:
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // Verificăm coliziunile cu marginile din stânga și dreapta if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // Verificăm coliziunile cu marginile de sus și jos if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // Verificăm coliziunile cu corpul șarpelui const body = snake.slice(0, -2); if ( body.find( (position) => position.x == headPosition.x && position.y == headPosition.y ) ) { collided = true; } }
generateFood
Funcția generateFood folosește o buclă do-while pentru a căuta o poziție pentru hrană, care să nu fie ocupată de șarpe. Odată găsită, poziția este înregistrată și desenată pe canvas. Codul funcției generateFood este:
function generateFood() { let x = 0, y = 0; do { x = Math.floor((Math.random() * gridSize) / 10) * 10; y = Math.floor((Math.random() * gridSize) / 10) * 10; } while (doesSnakeOccupyPosition(x, y)); foodPosition = { x, y }; ctx.fillRect(x, y, unitLength, unitLength); }
move
Funcția move începe prin a crea o copie a poziției capului șarpelui. Apoi, în funcție de direcția curentă, crește sau scade coordonatele x sau y ale șarpelui. De exemplu, creșterea coordonatei x este echivalentă cu deplasarea spre dreapta.
Odată ce mișcarea a fost efectuată, adăugăm noua poziție în matricea șarpelui. De asemenea, desenăm noua poziție pe canvas.
Apoi, verificăm dacă șarpele a mâncat hrana în această mișcare. Pentru a face acest lucru, verificăm dacă noua poziție este egală cu poziția hranei. Dacă șarpele a mâncat hrana, apelăm funcția generateFood.
Dacă șarpele nu a mâncat hrana, eliminăm primul element din matricea șarpelui. Acest element reprezintă coada, iar eliminarea acesteia va menține lungimea șarpelui constantă, dând iluzia de mișcare.
function move() { // Creăm o copie a obiectului care reprezintă poziția capului const headPosition = Object.assign({}, snake.slice(-1)[0]); switch (direction) { case "left": headPosition.x -= unitLength; break; case "right": headPosition.x += unitLength; break; case "up": headPosition.y -= unitLength; break; case "down": headPosition.y += unitLength; } // Adăugăm noua poziție a capului în matrice snake.push(headPosition); ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength); // Verificăm dacă șarpele a mâncat hrana const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y; if (isEating) { // Generăm o nouă poziție pentru hrană generateFood(); } else { // Eliminăm coada dacă șarpele nu a mâncat tailPosition = snake.shift(); // Ștergem coada din grilă ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
turn
Ultima funcție majoră pe care o vom analiza este funcția turn. Această funcție primește o nouă direcție și modifică variabila direction în acea direcție. Totuși, șarpele se poate întoarce doar într-o direcție perpendiculară pe direcția sa actuală.
Astfel, șarpele se poate întoarce la stânga sau la dreapta doar dacă se mișcă în sus sau în jos. Similar, se poate întoarce în sus sau în jos dacă se deplasează la stânga sau la dreapta. Codul pentru funcția turn este:
function turn(newDirection) { switch (newDirection) { case "left": case "right": // Permitem întoarcerea la stânga sau dreapta doar dacă șarpele se deplasa inițial în sus sau în jos if (direction == "up" || direction == "down") { direction = newDirection; } break; case "up": case "down": // Permitem întoarcerea în sus sau în jos doar dacă șarpele se deplasa inițial la stânga sau dreapta if (direction == "left" || direction == "right") { direction = newDirection; } break; } }
onKeyDown
Funcția onKeyDown este un handler de eveniment care apelează funcția turn cu direcția corespunzătoare tastei săgeată apăsate. Codul funcției este:
function onKeyDown(e) { switch (e.key) { case "ArrowDown": turn("down"); break; case "ArrowUp": turn("up"); break; case "ArrowLeft": turn("left"); break; case "ArrowRight": turn("right"); break; } }
gameLoop
Funcția gameLoop este apelată în mod regulat pentru a menține jocul în funcțiune. Această funcție apelează funcția move și funcția checkForCollision. De asemenea, verificăm dacă variabila collided este true. Dacă este, oprim timer-ul interval pe care îl folosim pentru joc și afișăm ecranul „game over”. Codul funcției este:
function gameLoop() { move(); checkForCollision(); if (collided) { clearInterval(timer); gameOverScreen.style.display = "flex"; } }
Pornirea jocului
Pentru a porni jocul, adaugă următoarele linii de cod:
setUp(); document.addEventListener("keydown", onKeyDown); let timer = setInterval(gameLoop, 200);
În primul rând, apelăm funcția setUp. Apoi, adăugăm event listener-ul „keydown”. În final, folosim funcția setInterval pentru a porni cronometrul.
Concluzie
În acest moment, fișierul tău JavaScript ar trebui să fie similar cu cel de pe GitHub. Dacă ceva nu funcționează, verifică încă o dată cu codul din repo. Poate dorești să înveți cum se creează un slider de imagini în JavaScript.