U ovom tekstu, objasniću proces kreiranja igre Zmija uz korišćenje HTML, CSS i JavaScript tehnologija.
Nećemo se oslanjati na spoljne biblioteke; cela igra će se odvijati direktno u vašem web pregledaču. Razvoj ove igre predstavlja zanimljiv i koristan način za unapređenje vaših veština rešavanja problema.
Pregled projekta
Zmija je jednostavna igra gde kontrolišete kretanje zmije ka hrani, izbegavajući prepreke. Kada zmija dođe do hrane, ona je pojede i raste. Kako igra napreduje, zmija postaje sve duža.
Zmija ne sme udariti u zidove ili samu sebe. Stoga, kako igra teče, zmija postaje duža, a igra sve zahtevnija.
Cilj ovog tutorijala za JavaScript Zmiju je da kreirate igru poput ove prikazane ispod:
Kod igre je dostupan na mom GitHub profilu. Živa verzija je hostovana na GitHub stranicama.
Preduslovi
Ovaj projekat ćemo razvijati pomoću HTML, CSS i JavaScript jezika. Koristićemo osnovne HTML i CSS elemente. Primarni fokus će biti na JavaScriptu. Zbog toga, potrebno je da imate osnovno razumevanje JavaScripta da biste pratili ovaj vodič. Ukoliko ga nemate, preporučujem da pogledate naš članak o najboljim resursima za učenje JavaScripta.
Takođe, biće vam potreban uređivač koda za pisanje koda. Naravno, potreban vam je i web pregledač, koji verovatno već koristite.
Postavljanje projekta
Prvo, pripremimo fajlove projekta. U praznom direktorijumu, napravite datoteku `index.html` i ubacite sledeći HTML kod.
<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>
Navedeni HTML kod kreira osnovni ekran „Game Over“. Vidljivost ovog ekrana ćemo kontrolisati pomoću JavaScripta. Takođe, definiše `canvas` element na kojem ćemo crtati lavirint, zmiju i hranu. HTML kod povezuje i stilove i JavaScript kod.
Zatim kreirajte datoteku `styles.css` za stilizaciju. U nju dodajte sledeće stilove.
* { 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; }
U selektoru ‘*’, ciljamo sve elemente i poništavamo margine i padding. Takođe, postavljamo font za svaki element i koristimo `border-box` za predvidljivije određivanje veličina. Za `body` element, postavljamo njegovu visinu na punu visinu ekrana i poravnavamo sve elemente u centar. Dodajemo i tirkiznu pozadinsku boju.
Na kraju, stilizovali smo ekran ‘Game Over’ tako da mu dodamo visinu i širinu od 200, odnosno 500 piksela. Dodali smo mu magenta pozadinsku boju i crni okvir. Njegova pozicija je postavljena na apsolutnu kako bi bio izvan normalnog toka dokumenta i poravnat sa centrom ekrana. Zatim smo centrirali njegov sadržaj i postavili njegov `display` na `none`, tako da je podrazumevano sakriven.
Zatim kreirajte fajl `snake.js`, koji ćemo popunjavati u narednim sekcijama.
Kreiranje globalnih varijabli
Sledeći korak u ovom vodiču za JavaScript Zmiju je definisanje globalnih varijabli koje ćemo koristiti. U datoteci `snake.js`, dodajte sledeće definicije varijabli na početak:
// Creating references to HTML elements let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // Creating context which will be used to draw on canvas let ctx = canvas.getContext("2d");
Ove varijable čuvaju reference na ‘Game Over’ ekran i `canvas` element. Zatim, kreirali smo kontekst koji će se koristiti za crtanje na platnu.
Dodajte sledeće definicije varijabli ispod prvog seta.
// Maze definitions let gridSize = 400; let unitLength = 10;
Prva varijabla definiše veličinu mreže u pikselima. Druga definiše dužinu jedne jedinice u igri. Ovu dužinu jedinice ćemo koristiti na nekoliko mesta. Na primer, koristićemo je da definišemo debljinu zidova lavirinta, debljinu zmije, visinu i širinu hrane, i korake u kojima se zmija kreće.
Zatim dodajte sledeće varijable za igru. One se koriste za praćenje stanja igre.
// Game play variables let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
Varijabla `snake` prati pozicije koje zmija trenutno zauzima. Zmija se sastoji od jedinica, a svaka jedinica zauzima poziciju na platnu. Pozicija koju svaka jedinica zauzima čuva se u nizu `snake`. Pozicija će imati vrednosti `x` i `y` kao koordinate. Prvi element u nizu predstavlja rep, dok poslednji predstavlja glavu.
Kako se zmija kreće, dodavaćemo elemente na kraj niza. Ovo će pomeriti glavu napred. Takođe ćemo uklanjati prvi element, odnosno rep, iz niza, tako da dužina ostane ista.
Varijabla `foodPosition` čuva trenutnu lokaciju hrane pomoću `x` i `y` koordinata. Varijabla `direction` čuva pravac u kojem se zmija kreće, dok je `collided` logička varijabla koja se postavlja na `true` kada se detektuje sudar.
Deklarisanje funkcija
Cela igra je podeljena na funkcije, što olakšava pisanje i održavanje koda. U ovom odeljku ćemo deklarisati te funkcije i njihovu svrhu. Naredni odeljci će definisati funkcije i razmotriti njihove algoritme.
function setUp() {} function doesSnakeOccupyPosition(x, y) {} function checkForCollision() {} function generateFood() {} function move() {} function turn(newDirection) {} function onKeyDown(e) {} function gameLoop() {}
Ukratko, funkcija `setUp` postavlja igru. Funkcija `checkForCollision` proverava da li se zmija sudarila sa zidom ili sama sa sobom. Funkcija `doesSnakeOccupyPosition` uzima poziciju, definisanu koordinatama `x` i `y`, i proverava da li je bilo koji deo tela zmije u toj poziciji. Ovo će biti korisno kada tražimo slobodnu poziciju za dodavanje hrane.
Funkcija `move` pomera zmiju u trenutnom pravcu, dok funkcija `turn` menja taj pravac. Funkcija `onKeyDown` će osluškivati pritiske tastera za promenu pravca. Funkcija `gameLoop` će pomerati zmiju i proveravati ima li sudara.
Definisanje funkcija
U ovom odeljku ćemo definisati funkcije koje smo ranije deklarisali. Takođe ćemo objasniti kako svaka funkcija radi. Pre koda će biti kratak opis funkcije i komentari koji će objasniti red po red gde je to potrebno.
`setUp` funkcija
Funkcija `setUp` će uraditi 3 stvari:
- Nacrtati granice lavirinta na platnu.
- Postaviti zmiju dodajući njene pozicije u varijablu `snake` i nacrtati je na platnu.
- Generisati početnu poziciju hrane.
Dakle, kod za to će izgledati ovako:
// Drawing borders on canvas // The canvas will be the size of the grid plus thickness of the two side border canvasSideLength = gridSize + unitLength * 2; // We draw a black square that covers the entire canvas ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // We erase the center of the black to create the game space // This leaves a black outline for the that represents the border ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // Next, we will store the initial positions of the snake's head and tail // The initial length of the snake will be 60px or 6 units // The head of the snake will be 30 px or 3 units ahead of the midpoint const headPosition = Math.floor(gridSize / 2) + 30; // The tail of the snake will be 30 px or 3 units behind the midpoint const tailPosition = Math.floor(gridSize / 2) - 30; // Loop from tail to head in unitLength increments for (let i = tailPosition; i <= headPosition; i += unitLength) { // Store the position of the snake's body and drawing on the canvas snake.push({ x: i, y: Math.floor(gridSize / 2) }); // Draw a rectangle at that position of unitLength * unitLength ctx.fillRect(x, y, unitLength, unitLength); } // Generate food generateFood();
`doesSnakeOccupyPosition`
Ova funkcija uzima koordinate `x` i `y` kao poziciju. Zatim proverava da li postoji takva pozicija u telu zmije. Koristi JavaScript-ov metod za pretragu niza da pronađe poziciju sa odgovarajućim koordinatama.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
`checkForCollision`
Ova funkcija proverava da li se zmija sudarila sa nečim i postavlja varijablu `collided` na `true`. Počećemo proverom da li se sudara sa levim i desnim zidom, gornjim i donjim zidom, a zatim i sa samom zmijom.
Da bismo proverili sudar sa levim i desnim zidom, proveravamo da li je `x` koordinata glave zmije veća od veličine mreže ili manja od 0. Da bismo proverili sudare sa gornjim i donjim zidom, radimo istu proveru, ali sa `y` koordinatom.
Zatim ćemo proveriti ima li sudara sa samom zmijom; proverićemo da li neki drugi deo njenog tela zauzima poziciju koju trenutno zauzima glava. Uzimajući sve to u obzir, telo funkcije `checkForCollision` treba da izgleda ovako:
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // Check for collisions against left and right walls if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // Check for collisions against top and bottom walls if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // Check for collisions against the snake itself const body = snake.slice(0, -2); if ( body.find( (position) => position.x == headPosition.x && position.y == headPosition.y ) ) { collided = true; } }
`generateFood`
Funkcija `generateFood` koristi `do-while` petlju da traži poziciju za postavljanje hrane koju zmija ne zauzima. Kada se pronađe, pozicija hrane se beleži i crta na platnu. Kod za funkciju `generateFood` treba da izgleda ovako:
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`
Funkcija `move` počinje kreiranjem kopije pozicije glave zmije. Zatim, na osnovu trenutnog pravca, povećava ili smanjuje `x` ili `y` koordinatu zmije. Na primer, povećavanje `x` koordinate je ekvivalentno pomeranju udesno.
Kada je to urađeno, dodajemo novu `headPosition` u niz zmija. Takođe, crtamo novu poziciju glave na platnu.
Zatim proveravamo da li je zmija pojela hranu u ovom potezu. To radimo tako što proveravamo da li je `headPosition` jednaka `foodPosition`. Ako je zmija pojela hranu, pozivamo funkciju `generateFood`.
Ako zmija nije pojela hranu, brišemo prvi element iz niza zmija. Taj element predstavlja rep, i njegovo uklanjanje će održati dužinu zmije istom, dajući iluziju kretanja.
function move() { // Create a copy of the object representing the position of the head 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; } // Add the new headPosition to the array snake.push(headPosition); ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength); // Check if snake is eating const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y; if (isEating) { // Generate new food position generateFood(); } else { // Remove the tail if the snake is not eating tailPosition = snake.shift(); // Remove tail from grid ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
`turn`
Poslednja glavna funkcija koju ćemo obraditi je funkcija `turn`. Ova funkcija uzima novi pravac i menja varijablu `direction` u taj novi pravac. Međutim, zmija može skretati samo u pravcu koji je okomit na onaj u kojem se trenutno kreće.
Dakle, zmija može skretati samo levo ili desno ako se kreće gore ili dole. Obrnuto, može skretati samo gore ili dole ako se kreće levo ili desno. Imajući na umu ova ograničenja, funkcija `turn` izgleda ovako:
function turn(newDirection) { switch (newDirection) { case "left": case "right": // Only allow turning left or right if they were originally moving up or down if (direction == "up" || direction == "down") { direction = newDirection; } break; case "up": case "down": // Only allow turning up or down if they were originally moving left or right if (direction == "left" || direction == "right") { direction = newDirection; } break; } }
`onKeyDown`
Funkcija `onKeyDown` je rukovalac događaja koji će pozvati funkciju `turn` sa pravcem koji odgovara pritisnutom tasteru sa strelicom. Funkcija, dakle, izgleda ovako:
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`
Funkcija `gameLoop` će se redovno pozivati kako bi igra nastavila da se odvija. Ova funkcija će pozvati funkciju `move` i funkciju `checkForCollision`. Takođe proverava da li je `collided` jednako `true`. Ako jeste, zaustavlja intervalni tajmer koji koristimo za pokretanje igre i prikazuje ekran ‘game over’. Funkcija će izgledati ovako:
function gameLoop() { move(); checkForCollision(); if (collided) { clearInterval(timer); gameOverScreen.style.display = "flex"; } }
Pokretanje igre
Da biste pokrenuli igru, dodajte sledeće linije koda:
setUp(); document.addEventListener("keydown", onKeyDown); let timer = setInterval(gameLoop, 200);
Prvo, pozivamo funkciju `setUp`. Zatim dodajemo osluškivač događaja `keydown`. Na kraju, koristimo funkciju `setInterval` da pokrenemo tajmer.
Zaključak
U ovom trenutku, vaša JavaScript datoteka bi trebalo da izgleda kao ona na mom GitHub profilu. Ako nešto ne radi, proverite ponovo sa repozitorijumom. Takođe, možda ćete želeti da naučite kako da napravite slajder slika u JavaScriptu.