Wprowadzenie

W ramach odpoczynku od klasycznych wpisów i szeroko pojętej kwarantanny zdecydowałem się na przygotowanie poradnika o tym jak stworzyć swoją pierwszą grę w oparciu o JavaScript. Na warsztat weźmiemy jeden z najbardziej popularnych tytułów mojego dzieciństwa, tj. snake.

Gra ukazała się w roku 1997 na telefonie Nokia 6110. Możecie się również domyślać dlaczego zdecydowałem się na ten tytuł…

Niczego prostszego (w teorii) chyba nie znajdziemy. Przechodzimy zatem do konkretów i implementacji tej gry.

Snake

W praktyce moglibyśmy przygotować jeden plik .html w którym umieścilibyśmy całą zawartość gry. Użyjemy jednak naszego środowiska Visual Studio 2019, utworzymy nowy projekt ASP.NET Core 3.1 oraz dokonamy wstępnej konfiguracji na bazie poprzedniego wpisu, który możecie znaleźć tutaj.

Jako, że jest to nasza pierwsza gra (a nie ich zbiór) wykorzystamy domyślny kontroler Home a cała zawartość gry dostępna będzie od razu po uruchomieniu naszego projektu, tj. na widoku Index.
Domyślne modyfikacje widoku polegają na dodaniu kontenera w którym będzie odbywała się nasza gra, przycisku rozpoczęcia oraz osiągniętego wyniku. Spójrzcie poniżej:

@{
    ViewData["Title"] = "Snake";
}

<div class="text-center">
    <h1 class="display-4">Snake</h1>
</div>

<div class="row">
    <div class="col-md-8">
        // Element ‘canvas’ pozwala na dynamiczne renderowanie kształtów i obrazów
        // Jest nam niezbędny do utworzenia dynamicznej gry w 2D
        <canvas width="625" height="625" class="game-panel">
        </canvas>
        <input type="button" class="start-game-button" value="Rozpocznij grę" />
    </div>
    <div class="col-md-4">
        <p class="current-score">Wynik: <span id="snake-game-score">0</span></p>
    </div>
</div>

// Definicja użytych styli - w idealnym świecie przenosimy do osobnego pliku .css
<style type="text/css">
    .game-panel {
        background-color: black;
        display: block;
        margin-left: auto;
        margin-right: auto
    }

    .start-game-button {
        display: block;
        width: 150px;
        margin-left: auto;
        margin-right: auto;
        margin-top: 20px
    }

    .current-score {
        height: 500px;
        line-height: 500px;
        font-size: 25px;
    }
</style>
Całość nie prezentuje się jeszcze okazale – to jednak dopiero początek: ASP.NET Core: snake

Kod JavaScript

Element canvas pozwala na dynamiczne renderowanie kształtów przy użyciu języka JavaScript. Musimy zatem przygotować sekcję odpowiedzialną za obsługę logiki weża oraz poprawne wyświetlanie obrazu na naszym panelu.
Czego potrzebujemy do obsługi naszej gry? (pomijamy narazie moment rozpoczęcia rozgrywki oraz wyświetlania wyników)
Niezbędne elementy to:

  • kontekst panelu canvas;
  • nasłuchwanie zdarzeń z klawiatury – czyli kliknięcia odpowiednich przycisków;
  • odpowiednie renderowanie panelu zgodnie z ruchami gracza.
Sekcja skryptów prezentuje się następujco:
<script type="text/javascript">
    var canvas
    var context;

    window.onload = function () {
        canvas = document.getElementById("snake");
        // Metoda getContext() zwraca nam obiekt, który udostępnia metody i właściwości 
        // niezbędne do rysowania na kanwie (tj. naszym panelu) 
        context = canvas.getContext("2d");

        // Nasłuchujemy wciśnięcia przycisku na klawiaturze 
        document.addEventListener("keydown", keyDownEvent);

        // Odswieżanie naszego widoku będzie następowało 10 razy na sekundę 
        const t = 10; 
        setInterval(refresh, 1000 / t);
    }
</script>
Powyższa sekcja skryptów to wstęp do dalszej implementacji. W pierwszym kroku przygotujemy się na obsługę odpowiednich klawiszy, tj. brakująca funkcja keyDownEvent.

Obsługa klawiatury

Wspomniana powyżej funkcja keyDownEvent będzie wyłapywała odpowiednie zdarzenia nadchodzące z klawiatury. Mam tutaj na myśli klawisze nawigacyjne, tj. strzałki. Wciśnienie przycisków będzie się również wiązało z poruszeniem węża w odpowiednią stronę. Spójrzcie poniżej:

function keyDownEvent(e) {
    // Lewo: kod przycisku 37;
    // Góra: kod przycisku: 38;
    // Prawo: kod przycisku: 39;
    // Dół: kod przycisku: 40;

    switch (e.keyCode) {
        case 37:
            movementX = -1;
            movementY = 0;
            break;
        case 38:
            movementX = 0;
            movementY = -1;
            break;
        case 39:
            movementX = 1;
            movementY = 0;
            break;
        case 40:
            movementX = 0;
            movementY = 1;
            break;
    }
}

Definicja ‘snejka’

Przygotowaliśmy już planszę dla naszej rozgrywki oraz obsługę klawiszy nawigacyjnych. Musimy jeszcze dodać definicję węża tak, aby go zmaterializować na naszym panelu. Potrzebujemy określić jego pozycję po rozpoczęciu gry, długość ogona (wraz ze zjedzeniem każdego jabłka jego długość rośnie) oraz szlak po którym się porusza – pamiętacie, że wchodząc w swój własny ogon przegrywamy i zaczynamy od początku.

Dodajemy poniższy kod do sekcji naszych skryptów:

// defincja węża
var snakeTailSize = 5;
var snakeTrail = [];

// ustawiamy pozycję początkową węża na mapie
var snakePositionX = 12;
var snakePositionY = 12;

Panel gry

Wspomniany wielokrotnie wcześniej panel canvas to miejsce po którym będziemy się poruszać. W naszym przypadku jest to kwadrat o długości i wysokości 625 pikseli. Na tak zdefiniowanej planszy wąż byłby niezmiernie mały a gra trwałaby bardzo długo. Ustawimy wielkość pojedynczego ‘piksela’ naszej planszy na 25. Dzięki temu ograniczymy znacząco ilość ruchów oraz powiększymy naszego węża. Dodajmy poniższy kod do sekcji naszych skryptów:

// definicja planszy
var pixelSize = 25;
var nextMovX = 0
var nextMovY = 0;

Jabłuszko…

Wraz z każdym zebraniem, ogon węża ulegnie wydłużeniu a jabłuszko znajdzie się w innej (losowej pozycji). Musimy jednak ustalić, gdzie znajduje się po uruchomieniu gry. W tym celu zdefiniujemy podstawe kordynaty:

// definicja jabłuszka
var applePositionX = 20;
var applePositionY = 7;

Gra

W powyższych krokach dokonaliśmy definicji niezbędnych parametrów. Pora zatem zobaczyć jak prezentuje się nasza gra. W pierwszej kolejności chcielibyśmy zobaczyć czy wąż pojawia się na planszy i reaguje na nasze polecenia dotyczące kierunku podróży. Aby dokonać testów musimy zaimplementować funkcję refresh().

Pierwszy krok to zmiana pozycji:

// zmiana pozycji węża
snakePositionX += nextMovX;
snakePositionY += nextMovY;
Drugi to pomalowanie tła panelu oraz węża wraz z jego ogonem:
// Każdy ruch węża wymaga w pierwszej kolejności zamalowania całego tła
// Wiecie dlaczego? Panel generowany jest dynamicznie a wąż porusza się w każdej chwili zmieniając swoją pozycje
// Zmiana pozycji wiąże się z malowaniem kolejnych pikseli definicujących węża
// Zakomentowanie poniższych dwóch linii spowoduje, że tło nie zostanie odswieżone a my będziemy zamalowywać każdy kolejny ‘piksel’ na zielono
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);

// malowanie węża wraz z jego ogonem
context.fillStyle = "green";
for (var i = 0; i < snakeTrail.length; i++) {
    context.fillRect(
        snakeTrail[i].x * pixelSize,
        snakeTrail[i].y * pixelSize,
        pixelSize,
        pixelSize
    );
}
A trzeci to zapisanie pozycji określających położenie naszego węża. Poniższa tablica będzie nam potrzebna do określenia czy wąż przypadkiem nie ugryzł swojego ogona:
// Zapisujemy gdzie w danej chwili znajduje się wąż
snakeTrail.push({ x: snakePositionX, y: snakePositionY });

// Poniższy warunek pozwala nam na przechowywanie lokalizacji każdego piksela (weża) na naszej mapie
// Waż określony jest przez swoją długość - dlatego po spełnieniu poniższego warunku usuwamy pierwszy element z naszej listy
// W tablicy znaduje się ilość wierszy zgodnych z długością węża
while (snakeTrail.length > snakeTailSize) {
    snakeTrail.shift();
}
Cały dotychczasowy kod:
@{
    ViewData["Title"] = "Snake";
}

<script type="text/javascript">
    var canvas
    var context;

    // definicja planszy
    var pixelSize = 25;
    var nextMovX = 0
    var nextMovY = 0;

    // defincja węża
    var snakeTailSize = 5;
    var snakeTrail = [];

    // ustawiamy pozycję początkową węża na mapie
    var snakePositionX = 10;
    var snakePositionY = 10;

    // definicja jabłka
    var applePositionX = 20;
    var applePositionY = 7;

    window.onload = function () {
        canvas = document.getElementById("snake");
        // Metoda getContext() zwraca nam obiekt, który udostępnia metody i właściwości 
        // niezbędne do rysowania na kanwie (tj. naszym panelu) 
        context = canvas.getContext("2d");

        // Nasłuchujemy wciśnięcia przycisku na klawiaturze 
        document.addEventListener("keydown", keyDownEvent);

        // Odswieżanie naszego widoku będzie następowało 10 razy na sekundę
        var t = 10;
        setInterval(refresh, 1000 / t);
    };


    function refresh() {
        // zmiana pozycji węża
        snakePositionX += nextMovX;
        snakePositionY += nextMovY;

        // Każdy ruch węża wymaga w pierwszej kolejności zamalowania całego tła
        // Wiecie dlaczego? Panel generowany jest dynamicznie a wąż porusza się w każdej chwili zmieniając swoją pozycje
        // Zmiana pozycji wiąże się z malowaniem kolejnych pikseli definicujących węża
        // Zakomentowanie poniższych dwóch linii spowoduje, że tło nie zostanie odswieżone a my będziemy zamalowywać każdy kolejny ‘piksel’ na zielono
        context.fillStyle = "black";
        context.fillRect(0, 0, canvas.width, canvas.height);

        // malowanie węża wraz z jego ogonem
        context.fillStyle = "green";
        for (var i = 0; i < snakeTrail.length; i++) {
            context.fillRect(
                snakeTrail[i].x * pixelSize,
                snakeTrail[i].y * pixelSize,
                pixelSize,
                pixelSize
            );
        }

        // Zapisujemy gdzie w danej pozycji znajduje się wąż
        snakeTrail.push({ x: snakePositionX, y: snakePositionY });

        // Poniższy warunek pozwala nam na przechowywanie lokalizacji każdego piksela (weża) na naszej mapie
        // Waż określony jest przez swoją długość - dlatego po spełnieniu poniższego warunku usuwamy pierwszy element z naszej listy
        // W tablicy znaduje się ilość wierszy zgodnych z długością węża
        while (snakeTrail.length < snakeTailSize) {
            snakeTrail.shift();
        }
    }

    function keyDownEvent(e) {
        // Lewo: kod przycisku 37;
        // Góra: kod przycisku: 38;
        // Prawo: kod przycisku: 39;
        // Dół: kod przycisku: 40;

        switch (e.keyCode) {
            case 37:
                nextMovX = -1;
                nextMovY = 0;
                break;
            case 38:
                nextMovX = 0;
                nextMovY = -1;
                break;
            case 39:
                nextMovX = 1;
                nextMovY = 0;
                break;
            case 40:
                nextMovX = 0;
                nextMovY = 1;
                break;
        }
    }
</script>

<div class="text-center" >
     <h1 class="display-4" >Snake</h1>
</div>

<div class="row">
    <div class="col-md-8">
        // Element ‘canvas’ pozwala na dynamiczne renderowanie kształtów i obrazów
        // Jest nam niezbędny do utworzenia dynamicznej gry w 2D
        <canvas id="snake" width="500" height="500" class="game-panel">
        </canvas>

        <input type="button" class="start-game-button" value="Rozpocznij grę" />
    </div>
    <div class="col-md-4">
        <p class="current-score">Wynik: <span id="snake-game-score">0</span></p>
    </div>
</div>

// Definicja użytych styli - w idealnym świecie przenosimy do osobnego pliku .css
<style type="text/css">
    .game-panel {
        background-color: black;
        display: block;
        margin-left: auto;
        margin-right: auto
    }

    .start-game-button {
        display: block;
        width: 150px;
        margin-left: auto;
        margin-right: auto;
        margin-top: 20px
    }

    .current-score {
        height: 500px;
        line-height: 500px;
        font-size: 25px;
    }
</style>
Spójrzmy jak wygląda nasza rozgrywka:

Udało nam się osiągnąć to co założyliśmy – wąż reaguje na nasze polecenia i porusza się po mapie. Pod koniec nagrania opuścił jednak planszę a my straciliśmy nad nim kontrolę. To dlatego, że nie zdefiniowaliśmy granic naszej kanwy dla dynamicznie poruszającego się węża.

Dodatkowo musimy dodać "pare" funkcjonalności. Brakujące jabłuszko, rosnący ogon, sprawdzenie czy wąż nie ugryzł sam siebie, obliczanie bieżącego wyniku oraz rozpoczęcie gry i komunikat o jej zakończeniu. Tym zajmiemy się w kolejnej sekcji wpisu.

Brakujące funkcjonalności

W pierwszej kolejności skupimy się na przekroczeniu rozmiarów planszy – efektem będzie przerwanie rozgrywki. Każde odświeżenie panelu musi się wiązać ze sprawdzeniem czy głowa węża nie wyszła poza rozmiar planszy. Znając rozmiar naszej planszy nie jest to szczególnie skomplikowane, spójrzcie na poniższy kod wraz z komentarzami:

function refresh() {
...
...
...

// 1. Sprawdzamy czy wąż nie wyszedł poza ramy naszego panelu
// 2. Funkcja cleatInterval(...) zatrzymuje ciągłe wywoływanie funkcji refresh()
// 3. Kończmy grę, resetujemy pozycję węża i wyświetlamy stosowany komunikat
if (snakePositionX < 0 || snakePositionX > pixelSize - 1) {
    clearInterval(timer);
    resetSnakeGame();
    return;
}

if (snakePositionY < 0 | snakePositionY > pixelSize - 1) {
    clearInterval(timer);
    resetSnakeGame();
    return;
}

// 1. Resetowanie pozycji węża do startowej
// 2. Malowanie całego panelu na kolor czarny
// 3. Czyszczenie tablicy pozycji naszego węża i wpisanie punktu startowego
// 4. Pomalowanie punktu startowego w którym rozpoczynamy swoją rozgrywkę
function resetSnakeGame() {
snakePositionX = 12;
snakePositionY = 12;

context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
snakeTrail = [];

// Zapisujemy gdzie w danej pozycji znajduje się wąż
snakeTrail.push({ x: snakePositionX, y: snakePositionY });

// malowanie węża wraz z jego ogonem
context.fillStyle = "green";
for (var i = 0; i < snakeTrail.length; i++) {
        context.fillRect(
            snakeTrail[i].x * pixelSize,
            snakeTrail[i].y * pixelSize,
            pixelSize,
            pixelSize
        );
    }

    alert("Gra została zakończona. Jeżeli chcesz ponownie rozpocząć wciśnij przycisk 'Rozpocznij grę!'");

    }
...
...
...
}
Zobaczymy czy wąż porusza się zgodnie z naszymi założeniami:

Skoro udało nam się ograniczyć poruszanie węża w granicach kanwy możemy pójść o krok dalej. Sprawdźmy czy wąż nie ugryzł swojego ogona. Tym razem konsekwencją będzie zmniejszenie długości ogona do wartości domyślnej.

Pamiętacie tablicę w której przechowujemy pozycję każdej części węża? Dzięki niej będziemy mogli sprawdzić czy bieżąca lokalizacja węża pokrywa się z jakąś wartością z tablicy. W tym celu przygotowałem poniższy kod:

// malowanie węża wraz z jego ogonem
context.fillStyle = "green";
for (var i = 0; i < snakeTrail.length; i++) {
    context.fillRect(
        snakeTrail[i].x * pixelSize,
        snakeTrail[i].y * pixelSize,
        pixelSize,
        pixelSize
    );

    // malując poszczególne piksele składające się na całego węża możemy sprawdzić czy wąż nie wszedł w samego siebie
    // w tym celu wykorzystamy tablicę opisującą położenie węża
    if (snakeTrail[i].x == snakePositionX && snakeTrail[i].y == snakePositionY) {
        tailSize = defaultTailSize;
    }
}
Zanim sprawdzimy efekty powyższej aktualizacji dodamy jeszcze jeden istotny "szczegół". Tym razem będzie to definicja jabłka – mam tutaj na myśli zaznaczenie na mapie oraz to czy został zjedzony przez węża.

W pierwszym kroku definiujemy lokalizację początkową (wspomniałem o tym nieco wcześniej):

// definicja jabłka
var applePositionX = 20;
var applePostionY = 7;

W drugim sprawdzamy czy wąż był w stanie zjeść jabłko – pomyślnym efektem jest zwiększenie długości ogona oraz wylosowanie nowej pozycji jabłka na mapie:

// 1. Sprawdzamy czy pozycja węża jest zgodna z pozycją jabłka na mapie
// 2. Jeżeli tak zwiększamy jego długość o 1 punkt
// 3. Losujemy nową pozycję jabłka na mapie, która mieści się w obrębie naszej planszy
// 4. Zamalowanie odpowiedniej pozycji dokonujemy po zamalowaniu całej planszy (spójrzcie na komentarz poniżej)
if (snakePositionX == applePositionX && snakePositionY == applePositionX) {
    snakeTailSize++;

    applePositionX = Math.floor(Math.random() * pixelSize);
    applePositionX = Math.floor(Math.random() * pixelSize);
}

Ostatni krok (na tym etapie) to zaznacznie/pomalowanie jabłka na planszy:

// malowanie jabłka
context.fillStyle = "red";
context.fillRect(applePositionX * pixelSize, applePositionY * pixelSize, pixelSize, pixelSize);

Spójrzcie na kod, który napisaliśmy do tej pory:

@{
    ViewData["Title"] = "Snake";
}

<script type="text/javascript">
    var canvas
    var context;
    var timer;

    // definicja planszy
    var pixelSize = 25;
    var nextMovX = 0
    var nextMovY = 0;

    // defincja węża
    var snakeTailSize = 5;
    var snakeTrail = [];

    // definicja jabłka
    var applePositionX = 20;
    var applePositionY = 7;

    // ustawiamy pozycję początkową węża na mapie
    var snakePositionX = 12;
    var snakePositionY = 12;

    window.onload = function () {
        canvas = document.getElementById("snake");

        // Metoda getContext() zwraca nam obiekt, który udostępnia metod i właściwosci
        // niezbędne do rysowania na kanwie (tj. naszym panelu)
        context = canvas.getContext("2d");

        // Nasłuchujemy wciśnięcia przycisku na klawiaturze
        document.addEventListener("keydown", keyDownEvent);

        // Odswieżanie naszego widoku będzie następowało 10 razy na sekundę
        var t = 10;
        timer = setInterval(refresh, 1000 / t);
    };

    function refresh() {
        // zmiana pozycji węża
        snakePositionX += nextMovX;
        snakePositionY += nextMovY;

        // 1. Sprawdzamy czy wąż nie wyszedł poza ramy naszego panelu
        // 2. Funkcja cleatInterval(...) zatrzymuje ciągłe wywoływanie funkcji refresh()
        // 3. Kończmy grę, resetujemy pozycję węża i wyświetlamy stosowany komunikat
        if (snakePositionX < 0 || snakePositionX > pixelSize - 1) {
            clearInterval(timer);
            resetSnakeGame();
            return;
        }

        if (snakePositionY < 0 || snakePositionY > pixelSize - 1) {
            clearInterval(timer);
            resetSnakeGame();
            return;
        }

        // 1. Sprawdzamy czy pozycja węża jest zgodna z pozycją jabłka na mapie
        // 2. Jeżeli tak zwiększamy jego długość o 1 punkt
        // 3. Losujemy nową pozycję jabłka na mapie, która mieści się w obrębie naszej planszy
        // 4. Zamalowanie odpowiedniej pozycji dokonujemy po zamalowaniu całej planszy (spójrzcie na komentarz poniżej)
        if (snakePositionX == applePositionX && snakePositionY == applePositionY) {
            snakeTailSize++;

            applePositionX = Math.floor(Math.random() * pixelSize);
            applePositionY = Math.floor(Math.random() * pixelSize);
        }

        // Każdy ruch węża wymaga w pierwszej kolejności zamalowania całego tła
        // Wiecie dlaczego? Panel generowany jest dynamicznie a wąż porusza się w każdej chwili zmieniając swoją pozycje
        // Zmiana pozycji wiąże się z malowaniem kolejnych pikseli definicujących węża
        // Zakomentowanie poniższych dwóch linii spowoduje, że tło nie zostanie odswieżone a my będziemy zamalowywać każdy kolejny 'piksel' na zielono
        context.fillStyle = "black";
        context.fillRect(0, 0, canvas.width, canvas.height);

        // malowanie węża wraz z jego ogonem
        context.fillStyle = "green";
        for (var i = 0; i < snakeTrail.length; i++) {
            context.fillRect(
                snakeTrail[i].x * pixelSize,
                snakeTrail[i].y * pixelSize,
                pixelSize,
                pixelSize
            );

            // malując poszczególne piksele składające się na całego węża możemy sprawdzić czy wąż nie wszedł w samego siebie
            // w tym celu wykorzystamy tablicę opisującą położenie węża
            if (snakeTrail[i].x == snakePositionX && snakeTrail[i].y == snakePositionY) {
                snakeTailSize = 5;
            }
        }

        // malowanie jabłka
        context.fillStyle = "red";
        context.fillRect(applePositionX * pixelSize, applePositionY * pixelSize, pixelSize, pixelSize);

        // Zapisujemy gdzie w danej pozycji znajduje się wąż
        snakeTrail.push({ x: snakePositionX, y: snakePositionY });

        // Poniższy warunek pozwala nam na przechowywanie lokalizacji każdego piksela (weża) na naszej mapie
        // Waż określony jest przez swoją długość - dlatego po spełnieniu poniższego warunku usuwamy pierwszy element z naszej listy
        // W tablicy znaduje się ilość wierszy zgodnych z długością węża
        while (snakeTrail.length > snakeTailSize) {
            snakeTrail.shift();
        }
    }

    function keyDownEvent(e) {

        // Lewo: kod przycisku 37;
        // Góra: kod przycisku: 38;
        // Prawo: kod przycisku: 39;
        // Dół: kod przycisku: 40;

        switch (e.keyCode) {
            case 37:
                nextMovX = -1;
                nextMovY = 0;
                break;
            case 38:
                nextMovX = 0;
                nextMovY = -1;
                break;
            case 39:
                nextMovX = 1;
                nextMovY = 0;
                break;
            case 40:
                nextMovX = 0;
                nextMovY = 1;
                break;
        }
    }

    // 1. Resetowanie pozycji węża do startowej
    // 2. Resetowanie pozycji jabłka do startowej
    // 3. Malowanie całego panelu na kolor czarny
    // 4. Czyszczenie tablicy pozycji naszego węża i wpisanie punktu startowego
    // 5. Pomalowanie punktu startowego w którym rozpoczynamy swoją rozgrywkę
    // 6. Malowanie pozycji startowej w której znajduje się jabłko
    function resetSnakeGame() {
        snakePositionX = 12;
        snakePositionY = 12;

        applePositionX = 20;
        applePositionY = 7;

        context.fillStyle = "black";
        context.fillRect(0, 0, canvas.width, canvas.height);
        snakeTrail = [];

        // Zapisujemy gdzie w danej pozycji znajduje się wąż
        snakeTrail.push({ x: snakePositionX, y: snakePositionY });

        // malowanie węża wraz z jego ogonem
        context.fillStyle = "green";
        for (var i = 0; i < snakeTrail.length; i++) {
            context.fillRect(
                snakeTrail[i].x * pixelSize,
                snakeTrail[i].y * pixelSize,
                pixelSize,
                pixelSize
            );
        }

        // malowanie pozycji jabłka
        context.fillStyle = "red";
        context.fillRect(applePositionX * pixelSize, applePositionY * pixelSize, pixelSize, pixelSize);

        alert("Gra została zakończona. Jeżeli chcesz ponownie rozpocząć wciśnij przycisk 'Rozpocznij grę!'");
    }
</script>

<div class="text-center">
    <h1 class="display-4">Snake</h1>
</div>

<div class="row">
    <div class="col-md-8">

        // Element 'canvas' pozwala na dynamiczne renderowanie kształtów i obrazów
        // Jest nam niezbędny do utworzenia dynamicznej gry w 2D
        <canvas id="snake" width="625" height="625" class="game-panel">
        </canvas>

        <input type="button" class="start-game-button" value="Rozpocznij grę" />
    </div>

    <div class="col-md-4">
         <p class="current-score">Wynik: <span id="snake-game-score">0</span></p>
    </div>
</div>

// Definicja użytych styli - w idealnym świecie przenosimy do osobnego pliku .css
<style type="text/css">
    .game-panel {
        background-color: black;
        display: block;
        margin-left: auto;
        margin-right: auto
    }

    .start-game-button {
        display: block;
        width: 150px;
        margin-left: auto;
        margin-right: auto;
        margin-top: 20px
    }

    .current-score {
        height: 500px;
        line-height: 500px;
        font-size: 25px;
    }
</style>
    
Obecnie rozgrywka prezentuje się tak:

Podsumowanie

W tym wpisie udało nam się przygotować pierwszą grę przy wykorzystaniu HTML, CSS oraz JavaScript. Wiem, nie jest to żaden zaawansowany projekt

Zakładam jednak, że większość z Was pracuje jako Full-Stack Developer a taka odskocznia od codziennej pracy jest całkiem przyjemna. Przyznam szczerze - nigdy wcześniej nie używałem kontrolki canva. Mam jednak nadzieję, że taki wpis pozwala nieco zrozumieć podstawy dynamicznego tworzenia obrazu przy wykorzystaniu języka skryptowego.

Czy to już koniec? W powyższym projekcie nie wszystko działa: brakuje zliczania punktów oraz uruchamiania gry za pomocą przycisku. Można również upodobnić grę do tej, która pojawiła się na telefonie Nokia 6610. W ramach pracy domowej warto spróbować dodać powyższe funkcjonalności, aby osiągnać w pełni funkcjonalną grę: