I denne artikel vil jeg forklare, hvordan man laver et Snake-spil ved hjælp af HTML, CSS og JavaScript.
Vi vil ikke bruge yderligere biblioteker; spillet kører i en browser. At skabe dette spil er en sjov øvelse, der hjælper dig med at strække og træne dine problemløsende muskler.
Indholdsfortegnelse
Projektoversigt
Snake er et simpelt spil, hvor du guider en slanges bevægelser mod mad, mens du undviger forhindringer. Når slangen når maden, spiser den den og bliver længere. Som spillet skrider frem, bliver slangen mere og mere lang.
Det er ikke meningen, at slangen skal løbe ind i vægge eller sig selv. Derfor, som spillet skrider frem, bliver slangen længere og bliver stadig sværere at spille.
Målet med denne JavaScript Snake Tutorial er at bygge spillet nedenfor:
Koden til spillet er tilgængelig på min GitHub. En live-version er hostet på GitHub-sider.
Forudsætninger
Vi vil bygge dette projekt ved hjælp af HTML, CSS og JavaScript. Vi kommer kun til at skrive grundlæggende HTML og CSS. Vores primære fokus er på JavaScript. Derfor bør du allerede forstå det for at følge med i denne JavaScript Snake Tutorial. Hvis ikke, anbefaler jeg stærkt, at du tjekker vores artikel om de bedste steder at lære JavaScript.
Du skal også bruge en kodeeditor til at skrive din kode i. Ud over det skal du bruge en browser, som du sandsynligvis har, hvis du læser dette.
Opsætning af projektet
Lad os starte med at konfigurere projektfilerne. I en tom mappe skal du oprette en index.html-fil og tilføje følgende markup.
<!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>
Opmærkningen ovenfor skaber en grundlæggende ‘Game Over’-skærm. Vi skifter denne skærms synlighed ved hjælp af JavaScript. Det definerer også et lærredselement, hvorpå vi vil tegne labyrinten, slangen og maden. Markup’et forbinder også stilarket og JavaScript-koden.
Opret derefter en styles.css-fil til stylingen. Tilføj følgende stilarter til det.
* { 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; }
I ‘*’-regelsættet målretter vi mod alle elementer og nulstiller afstanden. Vi indstiller også skrifttypefamilien for hvert element og indstiller størrelsen af elementer til en mere forudsigelig størrelsesmetode kaldet border-box. For kroppen indstillede vi dens højde til den fulde højde af viewporten og justerede alle elementer til midten. Vi gav den også en blå baggrundsfarve.
Til sidst stylede vi ‘Game Over’-skærmen til at give den en højde og bredde på henholdsvis 200 og 500 pixels. Vi gav den også en magenta baggrundsfarve og en sort kant. Vi indstiller dens position til absolut, så den er uden for det normale dokumentflow og justeret til midten af skærmen. Derefter centrerede vi indholdet. Vi indstiller dens visning til ingen, så den er skjult som standard.
Opret derefter en snake.js-fil, som vi vil skrive i løbet af de næste par sektioner.
Oprettelse af globale variabler
Det næste trin i denne JavaScript Snake-tutorial er at definere nogle globale variabler, vi vil bruge. Tilføj følgende variabeldefinitioner øverst i filen snake.js:
// 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");
Disse variabler gemmer referencer til ‘Game Over’-skærmen og lærredselementerne. Dernæst lavede vi en kontekst, som skal bruges til at tegne på lærredet.
Tilføj derefter disse variabeldefinitioner under det første sæt.
// Maze definitions let gridSize = 400; let unitLength = 10;
Den første definerer størrelsen af gitteret i pixels. Den anden definerer en enhedslængde i spillet. Denne enhedslængde kommer til at blive brugt flere steder. For eksempel vil vi bruge det til at definere, hvor tykke labyrintens vægge er, hvor tyk slangen er, højden og bredden af maden og de trin, slangen bevæger sig i.
Tilføj derefter følgende gameplay-variabler. Disse variabler bruges til at holde styr på spillets tilstand.
// Game play variables let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
Slangevariablen holder styr på de positioner, som slangen i øjeblikket besætter. Slangen består af enheder, og hver enhed indtager en position på lærredet. Den position, hver enhed indtager, gemmes i slangearrayet. Positionen vil have x- og y-værdier som sine koordinater. Det første element i arrayet repræsenterer halen, mens det sidste repræsenterer hovedet.
Når slangen bevæger sig, vil vi skubbe elementer til enden af arrayet. Dette vil flytte hovedet fremad. Vi vil også fjerne det første element eller hale fra arrayet, så længden forbliver den samme.
Fødevarepositionsvariablen gemmer den aktuelle placering af mad ved hjælp af x- og y-koordinater. Retningsvariablen gemmer den retning, slangen bevæger sig, mens den kolliderede variabel er en boolsk variabel, der er markeret til sand, når en kollision er blevet opdaget.
Erklære funktioner
Hele spillet er opdelt i funktioner, hvilket gør det nemmere at skrive og administrere. I dette afsnit vil vi erklære disse funktioner og deres formål. De følgende afsnit vil definere funktionerne og diskutere deres algoritmer.
function setUp() {} function doesSnakeOccupyPosition(x, y) {} function checkForCollision() {} function generateFood() {} function move() {} function turn(newDirection) {} function onKeyDown(e) {} function gameLoop() {}
Kort fortalt sætter opsætningsfunktionen spillet op. CheckForCollision-funktionen tjekker, om slangen er kollideret med en væg eller sig selv. Funktionen doesSnakeOccupyPosition tager en position, defineret af x- og y-koordinater, og kontrollerer, om nogen del af slangens krop er i denne position. Dette vil være nyttigt, når du leder efter en ledig stilling at tilføje mad til.
Flyttefunktionen flytter slangen i den retning, den peger, mens svingfunktionen ændrer den retning. Dernæst vil onKeyDown-funktionen lytte efter tastetryk, der bruges til at ændre retning. GameLoop-funktionen flytter slangen og tjekker for kollisioner.
Definition af funktioner
I dette afsnit vil vi definere de funktioner, vi har erklæret tidligere. Vi vil også diskutere, hvordan hver funktion fungerer. Der vil være en kort beskrivelse af funktionen før koden og kommentarer for at forklare linje for linje, hvor det er nødvendigt.
opsætningsfunktion
Opsætningsfunktionen vil gøre 3 ting:
Derfor vil koden til det se sådan ud:
// 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();
gør SnakeOccupyPosition
Denne funktion indtager x- og y-koordinater som en position. Den kontrollerer derefter, at en sådan position findes i slangens krop. Den bruger JavaScript-array-findingsmetoden til at finde en position med matchende koordinater.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
checkFor Collision
Denne funktion kontrollerer, om slangen har kollideret med noget, og indstiller den kolliderede variabel til sand. Vi vil starte med at tjekke for kollisioner mod venstre og højre væg, top- og bundvæggen og derefter mod selve slangen.
For at tjekke for kollisioner mod venstre og højre væg kontrollerer vi, om x-koordinaten på slangens hoved er større end gitteret eller mindre end 0. For at tjekke for kollisioner mod top- og bundvæggen udfører vi samme kontrol, men med y-koordinater.
Dernæst vil vi tjekke for kollisioner mod selve slangen; vi vil kontrollere, om nogen anden del af dens krop indtager den position, som hovedet i øjeblikket indtager. Ved at kombinere alt dette, skal kroppen til checkForCllision-funktionen se sådan ud:
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; } }
generere mad
GenererFood-funktionen bruger en do-while-løkke til at lede efter en position til at placere mad, der ikke er optaget af slangen. Når den er fundet, registreres madpositionen og tegnes på lærredet. Koden til genererFood-funktionen skal se sådan ud:
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); }
bevæge sig
Flyttefunktionen starter med at lave en kopi af slangens hovedposition. Derefter, baseret på den aktuelle retning, øger eller formindsker den værdien af slangens x- eller y-koordinat. For eksempel, at øge x-koordinaten svarer til at flytte til højre.
Når det er gjort, skubber vi den nye headPosition til slange-arrayet. Vi tegner også den nye headPosition til lærredet.
Dernæst tjekker vi, om slangen har spist mad i det træk. Det gør vi ved at tjekke om headPosition er lig med foodPosition. Hvis slangen har spist mad, kalder vi generFood-funktionen.
Hvis slangen ikke har spist mad, sletter vi det første element i slangearrayet. Dette element repræsenterer halen, og fjernelse af den vil holde slangens længde den samme, mens den giver en illusion af bevægelse.
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); } }
tur
Den sidste store funktion, vi vil dække, er turn-funktionen. Denne funktion vil tage en ny retning og ændre retningsvariablen til den nye retning. Slangen kan dog kun dreje i en retning vinkelret på den, de lige nu bevæger sig i.
Derfor kan slangen kun dreje til venstre eller højre, hvis den bevæger sig opad eller nedad. Omvendt kan den kun dreje op eller ned, hvis den bevæger sig til venstre eller højre. Med disse begrænsninger i tankerne ser turfunktionen sådan ud:
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
OnKeyDown-funktionen er en hændelseshandler, der kalder svingfunktionen med den retning, der svarer til den piletast, der er blevet trykket. Funktionen ser derfor sådan ud:
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
GameLoop-funktionen kaldes regelmæssigt for at holde spillet kørende. Denne funktion kalder flyttefunktionen og checkForCollision-funktionen. Den kontrollerer også, om kollisionen er sand. Hvis det er tilfældet, stopper den en intervaltimer, vi bruger til at køre spillet, og viser ‘game over’-skærmen. Funktionen vil se sådan ud:
function gameLoop() { move(); checkForCollision(); if (collided) { clearInterval(timer); gameOverScreen.style.display = "flex"; } }
Start af spillet
For at starte spillet skal du tilføje følgende kodelinjer:
setUp(); document.addEventListener("keydown", onKeyDown); let timer = setInterval(gameLoop, 200);
Først kalder vi opsætningsfunktionen. Dernæst tilføjer vi ‘keydown’-begivenhedslytteren. Til sidst bruger vi setInterval-funktionen til at starte timeren.
Konklusion
På dette tidspunkt skulle din JavaScript-fil se ud som den på min GitHub. Hvis noget ikke virker, skal du dobbelttjekke med repoen. Dernæst vil du måske lære, hvordan du opretter en billedskyder i JavaScript.