Wyjaśniono samouczek JavaScript Snake

W tym artykule wyjaśnię, jak stworzyć grę Snake przy użyciu HTML, CSS i JavaScript.

Nie będziemy korzystać z dodatkowych bibliotek; gra będzie działać w przeglądarce. Tworzenie tej gry to zabawne ćwiczenie, które pomaga rozciągać i ćwiczyć mięśnie rozwiązujące problemy.

Zarys projektu

Snake to prosta gra, w której kierujesz ruchami węża w kierunku pożywienia, unikając jednocześnie przeszkód. Kiedy wąż dotrze do pożywienia, zjada je i rośnie dłużej. W miarę postępów w grze wąż staje się coraz dłuższy.

Wąż nie powinien wpadać na ściany ani na siebie. Dlatego w miarę postępów w grze wąż staje się dłuższy i staje się coraz trudniejszy do grania.

Celem tego samouczka JavaScript Snake jest zbudowanie poniższej gry:

Kod do gry dostępny jest na moim GitHub. Wersja na żywo jest hostowana na Strony GitHuba.

Warunki wstępne

Zbudujemy ten projekt przy użyciu HTML, CSS i JavaScript. Będziemy pisać tylko podstawowy HTML i CSS. Skupiamy się głównie na JavaScript. Dlatego powinieneś już to zrozumieć, aby śledzić ten samouczek JavaScript Snake. Jeśli nie, gorąco polecam Ci zapoznanie się z naszym artykułem o najlepszych miejscach do nauki JavaScript.

Będziesz także potrzebował edytora kodu, w którym możesz napisać swój kod. Oprócz tego będziesz potrzebować przeglądarki, którą prawdopodobnie masz, jeśli to czytasz.

Konfiguracja projektu

Na początek skonfigurujmy pliki projektu. W pustym folderze utwórz plik Index.html i dodaj następujący znacznik.

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

Powyższy znacznik tworzy podstawowy ekran „Koniec gry”. Będziemy przełączać widoczność tego ekranu za pomocą JavaScript. Definiuje także element płótna, na którym narysujemy labirynt, węża i jedzenie. Znacznik łączy także arkusz stylów i kod JavaScript.

Następnie utwórz plik stylów.css dla stylizacji. Dodaj do niego następujące style.

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

W zestawie reguł „*” celujemy we wszystkie elementy i resetujemy odstępy. Ustawiliśmy także rodzinę czcionek dla każdego elementu i ustawiliśmy rozmiar elementów na bardziej przewidywalną metodę zmiany rozmiaru zwaną border-box. W przypadku bryły ustawiliśmy jej wysokość na pełną wysokość rzutni i wyrównaliśmy wszystkie elementy do środka. Nadaliśmy mu również niebieski kolor tła.

Na koniec wystylizowaliśmy ekran „Koniec gry”, nadając mu wysokość i szerokość odpowiednio 200 i 500 pikseli. Nadaliśmy mu również magentowy kolor tła i czarną ramkę. Ustawiamy jego położenie na absolutne, tak aby znajdowało się poza normalnym obiegiem dokumentów i było wyrównane do środka ekranu. Następnie wycentrowaliśmy jego treść. Ustawiliśmy jego wyświetlanie na brak, więc domyślnie jest ukryty.

Następnie utwórz plik Snake.js, który napiszemy w kolejnych kilku sekcjach.

Tworzenie zmiennych globalnych

Następnym krokiem w tym samouczku JavaScript Snake jest zdefiniowanie niektórych zmiennych globalnych, których będziemy używać. W pliku Snake.js dodaj na górze następujące definicje zmiennych:

// 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");

Zmienne te przechowują odniesienia do ekranu „Koniec gry” i elementów obszaru roboczego. Następnie stworzyliśmy kontekst, który posłuży do rysowania na płótnie.

Następnie dodaj te definicje zmiennych pod pierwszym zestawem.

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

Pierwsza określa wielkość siatki w pikselach. Drugi definiuje długość jednostki w grze. Ta długość jednostkowa będzie używana w kilku miejscach. Na przykład użyjemy go do określenia grubości ścian labiryntu, grubości węża, wysokości i szerokości pożywienia oraz przyrostów poruszania się węża.

Następnie dodaj następujące zmienne rozgrywki. Zmienne te służą do śledzenia stanu gry.

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

Zmienna węża śledzi pozycje aktualnie zajmowane przez węża. Wąż składa się z jednostek, a każda jednostka zajmuje miejsce na płótnie. Pozycja zajmowana przez każdą jednostkę jest przechowywana w tablicy węża. Współrzędne pozycji będą miały wartości x i y. Pierwszy element tablicy reprezentuje ogon, a ostatni reprezentuje głowę.

Gdy wąż się poruszy, będziemy przesuwać elementy na koniec tablicy. To przesunie głowę do przodu. Usuniemy również pierwszy element lub ogon z tablicy, aby długość pozostała taka sama.

Zmienna pozycji żywności przechowuje bieżącą lokalizację żywności przy użyciu współrzędnych x i y. Zmienna kierunku przechowuje kierunek, w którym porusza się wąż, podczas gdy zmienna kolidująca jest zmienną logiczną oznaczaną jako prawda w przypadku wykrycia kolizji.

Deklarowanie funkcji

Cała gra jest podzielona na funkcje, co ułatwia pisanie i zarządzanie. W tej sekcji zadeklarujemy te funkcje i ich cele. W poniższych sekcjach zdefiniowano funkcje i omówiono ich algorytmy.

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

Krótko mówiąc, funkcja setUp konfiguruje grę. Funkcja checkForCollision sprawdza, czy wąż zderzył się ze ścianą, czy z samym sobą. Funkcja DoesSnakeOccupyPosition przyjmuje pozycję określoną przez współrzędne x i y i sprawdza, czy jakakolwiek część ciała węża znajduje się w tej pozycji. Przyda się to, gdy będziesz szukać wolnego miejsca, do którego możesz dodać jedzenie.

Funkcja ruchu przesuwa węża w dowolnym kierunku, który wskazuje, natomiast funkcja obrotu zmienia ten kierunek. Następnie funkcja onKeyDown będzie nasłuchiwać naciśnięć klawiszy używanych do zmiany kierunku. Funkcja gameLoop przesunie węża i sprawdzi kolizje.

Definiowanie funkcji

W tej sekcji zdefiniujemy funkcje, które zadeklarowaliśmy wcześniej. Omówimy także sposób działania poszczególnych funkcji. Przed kodem pojawi się krótki opis funkcji i komentarze wyjaśniające wiersz po wierszu, jeśli to konieczne.

Funkcja konfiguracji

Funkcja konfiguracji wykona 3 rzeczy:

  • Narysuj granice labiryntu na płótnie.
  • Skonfiguruj węża, dodając jego pozycje do zmiennej węża i rysując go na płótnie.
  • Wygeneruj początkową pozycję żywności.
  • Dlatego kod będzie wyglądał następująco:

      // 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();

    robiSnakeOccupyPosition

    Ta funkcja przyjmuje współrzędne x i y jako pozycję. Następnie sprawdza, czy w ciele węża istnieje taka pozycja. Używa metody wyszukiwania tablicy JavaScript, aby znaleźć pozycję o pasujących współrzędnych.

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

    sprawdźForKolizja

    Ta funkcja sprawdza, czy wąż zderzył się z czymkolwiek i ustawia zmienną collided na true. Zaczniemy od sprawdzenia kolizji z lewą i prawą ścianą, górną i dolną ścianą, a następnie z samym wężem.

    Aby sprawdzić kolizje z lewą i prawą ścianą, sprawdzamy, czy współrzędna x głowy węża jest większa niż gridSize lub mniejsza niż 0. Aby sprawdzić kolizje z górną i dolną ścianą, wykonamy to samo sprawdzenie, ale z współrzędne y.

    Następnie sprawdzimy kolizje z samym wężem; sprawdzimy, czy jakakolwiek inna część jego ciała zajmuje pozycję zajmowaną obecnie przez głowę. Łącząc to wszystko, treść funkcji checkForCllision powinna wyglądać następująco:

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

    generujŻywność

    Funkcja generateFood wykorzystuje pętlę „do-while” do wyszukiwania miejsca, w którym można umieścić żywność niezajętą ​​przez węża. Po znalezieniu pozycja jedzenia jest rejestrowana i rysowana na płótnie. Kod funkcji generateFood powinien wyglądać następująco:

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

    przenosić

    Funkcja move rozpoczyna się od utworzenia kopii położenia głowy węża. Następnie, w oparciu o bieżący kierunek, zwiększa lub zmniejsza wartość współrzędnej x lub y węża. Na przykład zwiększenie współrzędnej x jest równoznaczne z ruchem w prawo.

    Gdy już to zrobimy, wypychamy nową pozycję headPosition do tablicy węża. Na kanwie rysujemy także nową pozycję headPosition.

    Następnie sprawdzamy, czy wąż zjadł w tym ruchu pożywienie. Robimy to sprawdzając, czy headPosition jest równa foodPosition. Jeśli wąż zjadł jedzenie, wywołujemy funkcję generateFood.

    Jeśli wąż nie zjadł jedzenia, usuwamy pierwszy element tablicy węża. Ten element reprezentuje ogon, a usunięcie go sprawi, że długość węża pozostanie taka sama, dając jednocześnie iluzję ruchu.

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

    zakręt

    Ostatnią ważną funkcją, którą omówimy, jest funkcja obrotu. Ta funkcja przyjmie nowy kierunek i zmieni zmienną kierunku w tym nowym kierunku. Jednakże wąż może skręcić tylko w kierunku prostopadłym do tego, w którym się aktualnie porusza.

    Dlatego wąż może skręcić w lewo lub w prawo tylko wtedy, gdy porusza się w górę lub w dół. I odwrotnie, może obracać się w górę lub w dół tylko podczas ruchu w lewo lub w prawo. Mając na uwadze te ograniczenia, funkcja turn wygląda następująco:

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

    naKeyDown

    Funkcja onKeyDown to procedura obsługi zdarzeń, która wywoła funkcję turn w kierunku odpowiadającym naciśniętemu klawiszowi strzałki. Funkcja wygląda więc następująco:

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

    graPętla

    Funkcja gameLoop będzie regularnie wywoływana, aby gra działała. Ta funkcja wywoła funkcję przenoszenia i funkcję checkForCollision. Sprawdza także, czy kolizja jest prawdziwa. Jeśli tak, zatrzymuje licznik czasu, którego używamy do uruchomienia gry i wyświetla ekran zakończenia gry. Funkcja będzie wyglądać następująco:

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

    Rozpoczęcie gry

    Aby rozpocząć grę, dodaj następujące linie kodu:

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

    Najpierw wywołujemy funkcję setUp. Następnie dodajemy detektor zdarzeń „keydown”. Na koniec używamy funkcji setInterval, aby uruchomić timer.

    Wniosek

    W tym momencie Twój plik JavaScript powinien wyglądać jak ten na moim GitHub. Jeśli coś nie zadziała, sprawdź ponownie w repo. Następnie możesz dowiedzieć się, jak utworzyć suwak obrazu w JavaScript.