JavaScript Snake Tutorial förklaras

I den här artikeln kommer jag att förklara hur man gör ett Snake-spel med HTML, CSS och JavaScript.

Vi kommer inte att använda ytterligare bibliotek; spelet kommer att köras i en webbläsare. Att skapa det här spelet är en rolig övning som hjälper dig att sträcka ut och träna dina problemlösande muskler.

Projektöversikt

Snake är ett enkelt spel där du styr en orms rörelser mot mat samtidigt som du undviker hinder. När ormen når maten äter den upp den och blir längre. Allt eftersom spelet fortskrider blir ormen allt längre.

Ormen är inte tänkt att springa in i väggar eller sig själv. Allteftersom spelet fortskrider blir därför ormen längre och blir allt svårare att spela.

Målet med denna JavaScript Snake Tutorial är att bygga spelet nedan:

Koden för spelet finns på min GitHub. En liveversion finns på GitHub-sidor.

Förutsättningar

Vi kommer att bygga detta projekt med HTML, CSS och JavaScript. Vi kommer bara att skriva grundläggande HTML och CSS. Vårt primära fokus ligger på JavaScript. Därför bör du redan förstå det för att följa med denna JavaScript Snake Tutorial. Om inte, rekommenderar jag starkt att du kollar in vår artikel om de bästa ställena att lära sig JavaScript.

Du behöver också en kodredigerare att skriva din kod i. Utöver det behöver du en webbläsare, som du förmodligen har om du läser detta.

Att sätta upp projektet

Till att börja, låt oss ställa in projektfilerna. Skapa en index.html-fil i en tom mapp och lägg till följande uppmärkning.

<!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>

Markeringen ovan skapar en grundläggande ”Game Over”-skärm. Vi kommer att växla den här skärmens synlighet med JavaScript. Den definierar också ett dukelement på vilket vi ska rita labyrinten, ormen och maten. Markeringen länkar även stilmallen och JavaScript-koden.

Skapa sedan en styles.css-fil för stylingen. Lägg till följande stilar till den.

* {
    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 regeluppsättningen ’*’ riktar vi in ​​oss på alla element och återställer avståndet. Vi ställer också in teckensnittsfamiljen för varje element och ställer in storleken på element till en mer förutsägbar storleksmetod som kallas border-box. För kroppen ställde vi in ​​dess höjd till visningsportens fulla höjd och riktade in alla objekt till mitten. Vi gav den också en blå bakgrundsfärg.

Slutligen stylade vi ”Game Over”-skärmen för att ge den en höjd och bredd på 200 respektive 500 pixlar. Vi gav den också en magentafärgad bakgrundsfärg och en svart kant. Vi ställer in dess position till absolut så att den är utanför det normala dokumentflödet och justerat mot mitten av skärmen. Sedan centrerade vi dess innehåll. Vi ställer in dess visning till ingen, så den är dold som standard.

Skapa sedan en snake.js-fil, som vi kommer att skriva under de kommande avsnitten.

Skapa globala variabler

Nästa steg i denna JavaScript Snake-handledning är att definiera några globala variabler som vi kommer att använda. Lägg till följande 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");

Dessa variabler lagrar referenser till ”Game Over”-skärmen och canvaselementen. Därefter skapade vi ett sammanhang, som kommer att användas för att rita på duken.

Lägg sedan till dessa variabeldefinitioner under den första uppsättningen.

// Maze definitions
let gridSize = 400;
let unitLength = 10;

Den första definierar storleken på rutnätet i pixlar. Den andra definierar en enhetslängd i spelet. Denna enhetslängd kommer att användas på flera ställen. Till exempel kommer vi att använda den för att definiera hur tjocka labyrintens väggar är, hur tjock ormen är, höjden och bredden på maten och i vilka steg ormen rör sig.

Lägg sedan till följande spelvariabler. Dessa variabler används för att hålla reda på tillståndet i spelet.

// Game play variables
let snake = [];
let foodPosition = { x: 0, y: 0 };
let direction = "right";
let collided = false;

Ormvariabeln håller reda på de positioner som för närvarande upptas av ormen. Ormen består av enheter och varje enhet intar en position på duken. Positionen varje enhet upptar lagras i ormuppsättningen. Positionen kommer att ha x- och y-värden som koordinater. Det första elementet i arrayen representerar svansen, medan det sista representerar huvudet.

När ormen rör sig kommer vi att skjuta element till slutet av arrayen. Detta kommer att flytta huvudet framåt. Vi kommer också att ta bort det första elementet eller svansen från arrayen så att längden förblir densamma.

Matpositionsvariabeln lagrar matens aktuella plats med hjälp av x- och y-koordinater. Riktningsvariabeln lagrar riktningen som ormen rör sig, medan den kolliderade variabeln är en boolesk variabel flaggad till sann när en kollision har upptäckts.

Deklarera funktioner

Hela spelet är uppdelat i funktioner, vilket gör det lättare att skriva och hantera. I det här avsnittet kommer vi att förklara dessa funktioner och deras syften. Följande avsnitt kommer att definiera funktionerna och diskutera deras algoritmer.

function setUp() {}
function doesSnakeOccupyPosition(x, y) {}
function checkForCollision() {}
function generateFood() {}
function move() {}
function turn(newDirection) {}
function onKeyDown(e) {}
function gameLoop() {}

Kortfattat, setUp-funktionen ställer in spelet. CheckForCollision-funktionen kontrollerar om ormen har kolliderat med en vägg eller sig själv. Funktionen doesSnakeOccupyPosition tar en position, definierad av x- och y-koordinater, och kontrollerar om någon del av ormens kropp är i den positionen. Detta kommer att vara användbart när du letar efter en ledig position att lägga till mat till.

Flyttfunktionen flyttar ormen i vilken riktning den än pekar, medan svängfunktionen ändrar den riktningen. Därefter kommer onKeyDown-funktionen att lyssna efter tangenttryckningar som används för att ändra riktning. GameLoop-funktionen kommer att flytta ormen och leta efter kollisioner.

Definiera funktionerna

I det här avsnittet kommer vi att definiera de funktioner vi deklarerade tidigare. Vi kommer också att diskutera hur varje funktion fungerar. Det kommer att finnas en kort beskrivning av funktionen före koden och kommentarer för att förklara rad för rad vid behov.

inställningsfunktion

Inställningsfunktionen kommer att göra 3 saker:

  • Rita labyrintens gränser på duken.
  • Ställ in ormen genom att lägga till dess positioner till ormvariabeln och rita den till duken.
  • Generera den ursprungliga matpositionen.
  • Därför kommer koden för det att se ut så här:

      // 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örSnakeOccupyPosition

    Denna funktion tar in x- och y-koordinater som en position. Den kontrollerar sedan att en sådan position finns i ormens kropp. Den använder JavaScript-matrisen för att hitta en position med matchande koordinater.

    function doesSnakeOccupyPosition(x, y) {
      return !!snake.find((position) => {
        return position.x == x && y == foodPosition.y;
      });
    }

    checkFor Collision

    Den här funktionen kontrollerar om ormen har kolliderat med något och ställer in den kolliderade variabeln till sant. Vi börjar med att kontrollera om det finns kollisioner mot vänster och höger vägg, över- och undervägg, och sedan mot själva ormen.

    För att kontrollera kollisioner mot vänster och höger vägg kontrollerar vi om x-koordinaten för ormens huvud är större än rutnätsstorleken eller mindre än 0. För att kontrollera kollisioner mot den övre och nedre väggen kommer vi att utföra samma kontroll men med y-koordinater.

    Därefter kommer vi att kontrollera om det finns kollisioner mot själva ormen; vi kommer att kontrollera om någon annan del av dess kropp intar den position som för närvarande upptas av huvudet. Om du kombinerar allt detta bör kroppen för checkForCllision-funktionen se ut så här:

     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;
      }
    }

    generera mat

    GenereraFood-funktionen använder en do-while loop för att leta efter en position för att placera mat som inte upptas av ormen. När den hittats registreras matpositionen och ritas på duken. Koden för funktionen generFood ska se ut så här:

    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);
    }

    flytta

    Flyttfunktionen börjar med att skapa en kopia av positionen för ormens huvud. Sedan, baserat på den aktuella riktningen, ökar eller minskar den värdet på x- eller y-koordinaten för ormen. Att till exempel öka x-koordinaten motsvarar att flytta åt höger.

    När det har gjorts, skjuter vi den nya headPosition till orm-arrayen. Vi ritar också den nya headPosition till duken.

    Därefter kontrollerar vi om ormen har ätit mat i det draget. Det gör vi genom att kontrollera om headPosition är lika med foodPosition. Om ormen har ätit mat kallar vi genereraFood-funktionen.

    Om ormen inte har ätit mat tar vi bort det första elementet i ormuppsättningen. Detta element representerar svansen och att ta bort den kommer att hålla ormens längd densamma samtidigt som den ger en illusion av rörelse.

    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);
      }
    }

    sväng

    Den sista stora funktionen vi kommer att täcka är turfunktionen. Denna funktion kommer att ta en ny riktning och ändra riktningsvariabeln till den nya riktningen. Ormen kan dock bara vända sig i en riktning som är vinkelrät mot den de just nu rör sig i.

    Därför kan ormen bara svänga åt vänster eller höger om den rör sig uppåt eller nedåt. Omvänt kan den bara vända upp eller ner om den rör sig åt vänster eller höger. Med dessa begränsningar i åtanke ser turnfunktionen ut så här:

    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 är en händelsehanterare som anropar svängfunktionen med den riktning som motsvarar piltangenten som har tryckts ned. Funktionen ser därför ut så här:

    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 kommer att anropas regelbundet för att hålla spelet igång. Denna funktion kommer att anropa flyttfunktionen och funktionen checkForCollision. Den kontrollerar också om kollisionen är sann. Om så är fallet stoppar den en intervalltimer som vi använder för att köra spelet och visar skärmen ”game over”. Funktionen kommer att se ut så här:

    function gameLoop() {
      move();
      checkForCollision();
    
      if (collided) {
        clearInterval(timer);
        gameOverScreen.style.display = "flex";
      }
    }

    Startar spelet

    För att starta spelet, lägg till följande kodrader:

    setUp();
    document.addEventListener("keydown", onKeyDown);
    let timer = setInterval(gameLoop, 200);

    Först anropar vi setUp-funktionen. Därefter lägger vi till ”keydown”-händelselyssnaren. Slutligen använder vi setInterval-funktionen för att starta timern.

    Slutsats

    Vid det här laget bör din JavaScript-fil se ut som den på min GitHub. Om något inte fungerar, dubbelkolla med repan. Därefter kanske du vill lära dig hur du skapar en bildreglage i JavaScript.