I denne artikkelen skal vi utforske hvordan man skaper et klassisk Snake-spill ved hjelp av HTML, CSS og JavaScript.
Vi vil ikke benytte oss av eksterne biblioteker; spillet vil kjøre direkte i nettleseren. Denne prosessen er en fin måte å utfordre og forbedre dine problemløsningsferdigheter.
Prosjektoversikt
Snake er et enkelt, men engasjerende spill der spilleren styrer en slange og leder den mot mat, samtidig som den unngår å kollidere med vegger eller seg selv. Når slangen spiser mat, vokser den i lengde. Etter hvert som spillet utvikler seg, blir slangen lengre, og spillet gradvis mer utfordrende.
Slangen skal ikke berøre kantene eller sin egen kropp. Jo lenger slangen blir, jo vanskeligere blir spillet.
Målet med denne JavaScript Snake-guiden er å utvikle det samme spillet som vist nedenfor:
Koden for spillet er tilgjengelig på min GitHub. En live-versjon er publisert på GitHub Pages.
Forutsetninger
Vi bygger dette prosjektet ved å bruke HTML, CSS og JavaScript. Vi vil holde oss til grunnleggende HTML og CSS, med hovedfokus på JavaScript. Det er fordelaktig å ha grunnleggende kjennskap til JavaScript for å kunne følge denne guiden. Om ikke, anbefaler vi å sjekke ut ressurser for å lære JavaScript.
Du trenger også et tekstredigeringsprogram for å skrive koden din. I tillegg trenger du en nettleser, noe du sannsynligvis allerede har hvis du leser dette.
Prosjektoppsett
La oss starte med å konfigurere prosjektfilene. Opprett en tom mappe, lag en fil som heter index.html, og legg til følgende kode:
<!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>
Koden ovenfor oppretter en enkel «Game Over»-skjerm. Vi vil styre synligheten til denne skjermen ved hjelp av JavaScript. Den definerer også et lerretselement der vi skal tegne banen, slangen og maten. Koden kobler også til stilarket og JavaScript-koden.
Deretter lager du en fil som heter styles.css for stilene. Legg til følgende CSS-kode:
* { 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; }
Med *
-selektoren tilbakestiller vi marginer og padding for alle elementer. Vi setter også fontfamilien for alle elementer og bruker en mer forutsigbar størrelsesmetode kalt border-box. For body
-elementet setter vi høyden til hele viewporten og sentrerer innholdet, med en cyan bakgrunnsfarge.
Til slutt styler vi «Game Over»-skjermen med en bredde på 500 piksler og en høyde på 200 piksler. Vi gir den en magenta bakgrunn og en svart kant. Vi setter dens posisjon til absolutt, slik at den er utenfor den normale dokumentflyten og sentrert i forhold til skjermen. Vi sentrerer også innholdet. Standardvisningen av skjermen er display: none
, slik at den er skjult.
Lag deretter en fil som heter snake.js. Denne skal vi fylle med kode i de kommende avsnittene.
Opprette globale variabler
Neste steg i denne JavaScript Snake-guiden er å definere noen globale variabler som vi skal bruke. I snake.js-filen legger du til følgende variabeldeklarasjoner øverst:
// Opprette referanser til HTML-elementer let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // Oppretter kontekst som skal brukes til å tegne på lerretet let ctx = canvas.getContext("2d");
Disse variablene lagrer referanser til «Game Over»-skjermen og lerretselementet. Deretter oppretter vi en kontekst som vil bli brukt til å tegne på lerretet.
Legg til disse variabeldeklarasjonene under det første settet.
// Labyrintdefinisjoner let gridSize = 400; let unitLength = 10;
Den første definerer størrelsen på spillområdet i piksler. Den andre definerer en enhetslengde i spillet. Denne enhetslengden vil bli brukt flere steder. For eksempel vil vi bruke den til å definere tykkelsen på kantene, slangen, maten, og avstanden slangen beveger seg i.
Legg deretter til følgende spillvariabler. Disse variablene brukes til å holde oversikt over spillets tilstand.
// Spillvariabler let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
Variabelen snake
holder orden på posisjonene som slangen for øyeblikket befinner seg i. Slangen er satt sammen av enheter, og hver enhet har en posisjon på lerretet. Posisjonen til hver enhet lagres i snake
-arrayet, med x- og y-koordinater. Det første elementet i arrayet representerer halen, mens det siste representerer hodet.
Når slangen beveger seg, vil vi legge til elementer på slutten av arrayet for å flytte hodet fremover. Vi vil også fjerne det første elementet (halen) fra arrayet for å holde lengden konstant.
Variabelen foodPosition
lagrer den nåværende posisjonen til maten ved hjelp av x- og y-koordinater. Variabelen direction
lagrer hvilken retning slangen beveger seg i, mens variabelen collided
er en boolsk variabel som settes til true
når en kollisjon har inntruffet.
Deklarere funksjoner
Hele spillet er delt opp i funksjoner for å gjøre det lettere å skrive og vedlikeholde koden. I denne seksjonen vil vi deklarere funksjonene og beskrive hva de skal gjøre. De følgende avsnittene vil definere funksjonene 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 sagt, setUp
-funksjonen setter opp spillet. Funksjonen checkForCollision
sjekker om slangen har krasjet med en vegg eller seg selv. Funksjonen doesSnakeOccupyPosition
sjekker om en posisjon (gitt ved x- og y-koordinater) er opptatt av slangens kropp. Dette vil være nyttig når vi leter etter en ledig posisjon for mat.
Funksjonen move
beveger slangen i den retningen den peker, mens funksjonen turn
endrer denne retningen. Funksjonen onKeyDown
lytter etter tastetrykk som brukes for å endre retningen. Funksjonen gameLoop
flytter slangen og sjekker for kollisjoner.
Definere funksjonene
I denne seksjonen skal vi definere funksjonene som vi erklærte tidligere. Vi vil også diskutere hvordan hver funksjon fungerer, med en kort beskrivelse av funksjonen før koden og kommentarer for å forklare linje for linje der det er nødvendig.
setUp
-funksjonen
setUp
-funksjonen vil gjøre 3 ting:
- Tegne kantene av labyrinten på lerretet.
- Sette opp slangen ved å legge til dens posisjoner i
snake
-variabelen og tegne den på lerretet. - Generere den første matposisjonen.
Koden for dette vil se slik ut:
// Tegner grenser på lerretet // Lerretet vil ha størrelsen til rutenettet pluss tykkelsen av de to sidekantene canvasSideLength = gridSize + unitLength * 2; // Vi tegner en svart firkant som dekker hele lerretet ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // Vi fjerner midten av det svarte for å lage spillområdet // Dette gir en svart kontur som representerer kanten ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // Deretter lagrer vi de første posisjonene til slangens hode og hale // Startlengden på slangen er 60 px, eller 6 enheter // Slangens hode vil være 30 px eller 3 enheter foran midtpunktet const headPosition = Math.floor(gridSize / 2) + 30; // Slangens hale vil være 30 px eller 3 enheter bak midtpunktet const tailPosition = Math.floor(gridSize / 2) - 30; // Løkke fra hale til hode med enhetslengde som trinn for (let i = tailPosition; i <= headPosition; i += unitLength) { // Lagre posisjonen til slangens kropp og tegn den på lerretet snake.push({ x: i, y: Math.floor(gridSize / 2) }); // Tegn en firkant på den posisjonen med enhetslengde * enhetslengde ctx.fillRect(x, y, unitLength, unitLength); } // Generer mat generateFood();
doesSnakeOccupyPosition
Denne funksjonen tar inn x- og y-koordinater som en posisjon. Den sjekker deretter om en slik posisjon finnes i slangens kropp. Den bruker JavaScript-array-metoden find
for å finne en posisjon med samsvarende koordinater.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
checkForCollision
Denne funksjonen sjekker om slangen har krasjet med noe og setter collided
-variabelen til true
. Vi starter med å se etter kollisjoner med venstre og høyre vegg, deretter topp- og bunnveggene, og til slutt selve slangen.
For å sjekke kollisjoner med venstre og høyre vegg, sjekker vi om x-koordinaten til slangens hode er større enn gridSize
eller mindre enn 0. For å sjekke kollisjoner med topp- og bunnveggene, utfører vi den samme sjekken, men med y-koordinater.
Deretter skal vi se etter kollisjoner med selve slangen. Vi sjekker om noen annen del av kroppen befinner seg på samme posisjon som hodet. Kombinert ser checkForCllision
-funksjonen slik ut:
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // Sjekk for kollisjoner med venstre og høyre vegg if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // Sjekk for kollisjoner med topp- og bunnvegg if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // Sjekk for kollisjoner med slangen selv const body = snake.slice(0, -2); if ( body.find( (position) => position.x == headPosition.x && position.y == headPosition.y ) ) { collided = true; } }
generateFood
Funksjonen generateFood
bruker en do-while-løkke for å finne en posisjon for mat som ikke er opptatt av slangen. Når den er funnet, blir matposisjonen registrert og tegnet på lerretet. Koden for generateFood
ser slik ut:
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
Funksjonen move
starter med å lage en kopi av posisjonen til slangens hode. Deretter, basert på gjeldende retning, øker eller reduserer den verdien av x- eller y-koordinaten. For eksempel tilsvarer en økning av x-koordinaten bevegelse mot høyre.
Etter det skyver vi den nye headPosition
-en til snake
-arrayet, og tegner den nye headPosition
-en på lerretet.
Deretter sjekker vi om slangen har spist mat i det trekket, ved å sammenligne headPosition
med foodPosition
. Hvis slangen har spist mat, kaller vi generateFood
-funksjonen.
Hvis slangen ikke har spist mat, sletter vi det første elementet i snake
-arrayet. Dette elementet representerer halen, og ved å fjerne det holder vi slangens lengde konstant, samtidig som det gir en illusjon av bevegelse.
function move() { // Opprett en kopi av objektet som representerer posisjonen til hodet 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; } // Legg til den nye headPosition til arrayet snake.push(headPosition); ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength); // Sjekk om slangen spiser const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y; if (isEating) { // Generer ny matposisjon generateFood(); } else { // Fjern halen hvis slangen ikke spiser tailPosition = snake.shift(); // Fjern halen fra rutenettet ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
turn
Den siste hovedfunksjonen vi skal se på er funksjonen turn
. Denne funksjonen tar inn en ny retning og endrer direction
-variabelen til den nye retningen. Slangen kan bare snu i en retning som er vinkelrett på den retningen den beveger seg i.
Dermed kan slangen bare snu til venstre eller høyre hvis den beveger seg opp eller ned. Omvendt kan den bare snu opp eller ned hvis den beveger seg til venstre eller høyre. Med disse begrensningene ser turn
-funksjonen slik ut:
function turn(newDirection) { switch (newDirection) { case "left": case "right": // Tillat bare å snu venstre eller høyre hvis den opprinnelig beveget seg opp eller ned if (direction == "up" || direction == "down") { direction = newDirection; } break; case "up": case "down": // Tillat bare å snu opp eller ned hvis den opprinnelig beveget seg til venstre eller høyre if (direction == "left" || direction == "right") { direction = newDirection; } break; } }
onKeyDown
onKeyDown
-funksjonen er en hendelsesbehandler som kaller opp turn
-funksjonen med den retningen som tilsvarer piltasten som trykkes. Funksjonen ser derfor slik ut:
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
Funksjonen gameLoop
kalles opp regelmessig for å holde spillet i gang. Denne funksjonen kaller opp funksjonene move
og checkForCollision
. Den sjekker også om collided
er true
. Hvis det er tilfelle, stopper den en intervalltimer som vi bruker for å kjøre spillet, og viser «game over»-skjermen. Funksjonen ser slik ut:
function gameLoop() { move(); checkForCollision(); if (collided) { clearInterval(timer); gameOverScreen.style.display = "flex"; } }
Starte spillet
For å starte spillet legger du til følgende kodelinjer:
setUp(); document.addEventListener("keydown", onKeyDown); let timer = setInterval(gameLoop, 200);
Først kaller vi setUp
-funksjonen. Deretter legger vi til en event listener for «keydown». Til slutt bruker vi funksjonen setInterval
for å starte timeren.
Konklusjon
Nå skal JavaScript-filen din være den samme som den på min GitHub. Hvis noe ikke fungerer, sjekk med repoen. Du kan også lære hvordan du lager en bildekarusell i JavaScript.