Lag Snake-spill med HTML, CSS & JavaScript!

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.