Zůstaň doma a naprogramuj si hru 5

Ahoj! Včera to bylo o HTML a CSS. Máme připravené základy, na kterých teď budeme stavět. K HTML a CSS se ještě během následujících dní budeme vracet. Dnes to bude čistě o JavaScriptu, ve kterém si vytvoříme bludiště. Je toho dnes moc, ale neboj, je to hodně o tom, abys pochopil/a nutnou teorii. Jdeme na to!

Zvětšení herní plochy

Prozatím je náš canvas maličký a právě v JavaScriptu (zkráceně JS) mu určíme velikost. Jeho velikost budeme uchovávat v tzv. proměnné. Proměnná je jedna z nejzákladnějších věcí, která se v programování používá. Tak jako my si ukládáme naše myšlenky do paměti i počítač si musí někam uložit specifické hodnoty, aby na ně nezapomněl, a my s nimi mohli pracovat. Proměnnou si tedy představ, jakou nějakou krabičku v paměti počítače, kam se ukládají informace.

Proměnnou v JavaScriptu vytvoříme pomocí kouzelného slova let. Možná už znáš odjinud deklaraci proměnné pomocí slova var. Pro naše účely dělá let a var v podstatě tu stejnou věc, my budeme používat modernější a novější let.

Když chceme v JS vytvořit novou proměnnou, uděláme to tímto způsobem:

let myVariable;

Když do proměnné chceme vložit hodnotu, stačí napsat =, což je v JavaScriptu tzv. symbol přiřazení.

myVariable = 7;

Protože na začátku programu většinou potřebujeme založit proměnné a ihned do nich vložit nějaké startovní hodnoty, lze založení proměnné a přiřazení hodnoty udělat v jednom kroku (odborně se tomu říká deklarovat proměnnou):

let myVariable = 7;

Proměnnou pomocí slova let deklarujeme (založíme) pouze jednou. Potom už ji používáme a vkládáme do ní hodnotu pouze pomocí =.

let myVariable = 7;
	 
// a o chvíli později už jenom…
myVariable = 13;

Mimochodem, řádek začínající znaky // je komentář. Komentář je text, který se v programu ignoruje. Pomocí komentářů si můžeš do kódu dělat poznámky a spoustu poznámek najdeš i v ukázkovém kódu po každé lekci.

Pokud nemáš s programováním zkušenosti z dřívějška, tak si všimni několika věcí:

  • Každý příkaz končí v JavaScriptu středníkem.
  • Na velikosti písmen záleží. myVariable je něco jiného než myvariable.
  • Ve jménech proměnných nemohou být mezery. Když chceš vytvořit víceslovný název, použij zápis, kde prvníPísmenoKaždéhoDalšíhoSlovaBudeVelké.
  • Ve jménech proměnných jdou sice používat české znaky, ale je obvyklou praxí, že se používají jen písmena anglické abecedy bez háčků a čárek.

Vzpomínáš si, jak jsme si do elementu canvas vložili ID? Tak právě díky tomuto ID můžeme JavaScriptu říct o jaký canvas se jedná. Založme si další 2 proměnné width a height, do kterých uložíme informaci o výšce a šířce.

let width = 600;
let height = 600;

A nyní nezbývá už nic jiného, než tyto dvě proměnné přiřadit výšce a šířce canvasu.

canvas.width = width;
canvas.height = height;

Tímto doslova říkáme, že výška canvasu se rovná naší zadané výšce. Možná si říkáš, proč jsme tam tu hodnotu nenapsali rovnou, ale vkládáme ji tam skrz proměnnou. Je to nejlepší řešení s ohledem do budoucna, kdybychom někdy v budoucnu chtěli měnit velikost canvas v kódu, budeme mít jistotu, že se na začátku našeho JavaScriptu nachází výchozí velikost, kterou kdykoliv můžeme použít. Když obnovíš stránku, měl by se ti canvas zvětšit.

Generování herní mapy

Když si spustíš projekt, uvidíš, jak se nám herní plocha zvětšila. Je ale stále prázdná, chtělo by to vygenerovat herní mapu. Pojďme si udělat přípravu, než se vrhneme na samotné generování. Abychom takovou herní plochu byli schopni vygenerovat, musíme dát počítači další informace.

Potřebujeme si nadefinovat způsob, jakým bude canvas vykreslovat objekty. Možností je několik, nám bohatě postačí 2D vykreslování, jelikož se jedná o 2D hru. Pojďme si vytvořit novou proměnnou, kterou si nazveme ctx a pomocí funkce getContext určíme způsob vykreslování:

let ctx = canvas.getContext("2d");

Následně se nám jistě bude hodit velikost každého bloku a předmětu v bludišti, pomocí nové proměnné blockSize si nadefinujeme jednotnou velikost:

let blockSize = 30;

A nyní už se můžeme zamýšlet nad tím, kam uložíme tu mapu jako takovou. Pojďme se naučit další novinku, které se říká v programování pole.

Pole

Zatím jsme se naučili, co je to proměnná. Zatím víme, že si do takové proměnné můžeme uložit jen jednu konkrétní hodnotu nebo informaci. Co když jich potřebujeme víc? Třeba seznam jmen apod. Na tyto věci se nám hodí datová struktura s názvem pole. Pole je např. toto:

// pole textových řetězců
let names = ['Jana', 'Adam', 'Eliška', 'Jirka'];

Najednou máme v proměnné names uložených několik hodnot – jmen, se kterými mohu pracovat. Tady je další ukázka pole:

// pole čísel
let numbers = [7, 13, 31, 53, 67];
         

V tomto poli je zase uloženo několik čísel. Všimni si, že takové pole začíná a končí hranatou závorkou a jednotlivé hodnoty oddělujeme čárkou (mezera za čárkou není nutná, slouží čistě k větší přehlednosti). Jak ale počítač pozná s jakou hodnotou chceme pracovat, když tam není jen jedna hodnota? Každá hodnota v poli má svůj tzv. index, to je číslo, které nám označuje pozici hodnoty v tom poli. Každý pole začíná indexem 0. Když se znovu podíváme na pole names, tak vidíme že:

  • Na indexu 0 je Jana
  • Na indexu 1 je Adam
  • Na indexu 2 je Eliška
  • Na indexu 3 je Jirka

A celková délka toho pole je 4, protože uchovává 4 hodnoty. Možná je to trochu matoucí, že se začíná od 0 a ne od 1, ale ber to prosím jako fakt a pamatuj na to. Kdybychom index spletli, mohlo by nám to s naším programem hezky zamávat. A teď kontrolní otázka! Jaká je celková délka pole numbers? Jaké má indexy? Jaké hodnoty na daných indexech jsou?

Tady jsou odpovědi:

  • Celková délka pole je 5, uchovává 5 hodnot
  • Má indexy v rozsahu 0-4
  • Na indexu 0 je 7
  • Na indexu 1 je 13
  • Na indexu 2 je 31
  • Na indexu 3 je 53
  • Na indexu 4 je 67

Není to tak těžké ne? 😉

Toto je jednoduché pole. Z obrázků je ale jasné, že nám toto pole asi stačit nebude. Mapa je čtvercová a některým už asi došlo, že se mapa bude skládat z bloků, jejichž velikost je 30 (proto ta proměnná blockSize). A když víme, že mapa má celkovou velikost 600x600, jednoduchou matematikou si spočítáme, že musíme pomocí polí vytvořit datovou strukturu 20x20, kde budou umístěné jednotlivé bloky. Záměrně říkám polí, protože místo číselné hodnoty nebo textové hodnoty, můžeme vložit do pole další pole. :D

Tady je malá ochutnávka:

let board = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1]
]

Máme pole board, který v sobě má další 2 pole, jedno na indexu 0 a jedno na indexu 1. Každé z těchto polí má v sobě hodnoty 0 a 1. Nám pak už jen stačí počítači říct, že pokud v poli narazí na jedničku, tak vykreslí zeď a pokud na 0, nechá blok jako cestu. Vzpomeňte si, jak jste hráli lodě a umisťovali plavidla do herní plochy (čtverečkovaný papír). My toto akorát přeneseme do počítačové podoby a využíváme pole. Celá proměnná board bude vypadat takto a tu si vložme do hry:

let board = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1],
    [1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1],
    [1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1],
    [1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1],
    [1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1],
    [1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1],
    [1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1],
    [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1],
    [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1],
    [1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
];

Naše pole board má délku 20 a indexy od 0 do 19. Na každém indexu je pole, které má délku 20 a indexy od 0 do 19.

Máme návrh mapy a vysvětlili jsme si princip, jakým budeme s polem pracovat. Teď ho ale reálně potřebujeme vykreslit. V prvé řadě si vložme do kódu k proměnným tyto dva řádky:

let wall = new Image();
wall.src = "images/wall.png";

Na prvním řádku vytváříme novou proměnnou s názvem wall, do ní vkládáme informaci o tom, že v ní chceme uchovávat nějaký obrázek. Na druhém řádku, pak specifikujeme přesně ten obrázek, který do proměnné chceme uložit a který využijeme při vykreslování mapy.

Samotnou mapu budeme vykreslovat pomocí kódů ve funkci generateBoard. Funkce je taky novinka, kterou si dnes vysvětlíme.

Funkce

Možná jsi o funkcích už slyšel/a, ale krátké opakování určitě neuškodí. Funkce je skupina příkazů, kterou jsme nějak pojmenovali a kterou můžeme v programu volat opakovaně. V programování bychom se měli snažit držet principu DRY - Don't Repeat Yourself, neboli neopakuj se.

Provádíme-li v programu stejnou věc několikrát, je tento kousek kódu ideálním kandidátem na vytvoření funkce. Z našeho opakovaného kódu vytvoříme funkci, kterou pak z různých míst programu jenom voláme.

Funkce nám umožňují zjednodušit a zpřehlednit kód. Jsou prostě skvělé jako vánoční dárky! No, možná ne úplně, ale jsou tomu blízko. :)

Funkci začneme vždy slovem function, za kterým následuje jméno funkce (jak si my funkci pojmenujeme). Za jménem jsou kulaté závorky, ve kterých jsou tzv. parametry funkce. Pokud funkce parametry nemá, zůstanou závorky prázdné (ale být tam musí). A pak už jen do složených závorek {…} napíšeme příkazy, které chceme ve funkci mít.

Když chceme vykonat příkazy uvnitř funkce, stačí funkci zavolat jejím jménem s kulatými závorkami na konci. Do závorek se píšou parametry (víc později) nebo nic.

Základní kostra funkce generateBoard vypadá takto:

function generateBoard() {
// zde budou příkazy, které chceme vykonat po zavolání funkce
}

Co do ní ale napíšeme? Pokud jsme správně pochopili princip, musíme postupně procházet celé to velké pole. Ručně to ale rozhodně dělat nebudeme, použijeme něco, čemu se říká cyklus. Cyklus nám dokáže opakovat určité bloky kódy, když je to potřeba. Máme několik různých cyklů (např. for, while, do-while), my v naší hře využijeme primárně cyklus for. Ten vypadá následovně:

for (let i = 0; i < 10; i++) {
// zde budou příkazy, které chceme opakovat v cyklu

}

Začneme klíčovým slovem for a v klasický závorkách cyklus nastavíme, nastavení máme rozdělené do tří částí (oddělují se středníkem):

  • let i = 0 – vytváříme řídící proměnnou i
  • i < 10 – definujeme podmínku, do kdy se cyklus bude opakovat (v tomto případě, dokud bude i menší jak 10)
  • i++ - to je tzv. inkrement, aby se nám z cyklu nestal nekonečný cyklus, potřebujeme měnit hodnotu řídící proměnné – i++ nám zajistí, že se po každém opakování zvýší hodnota proměnné i o 1.

Vše to, co je mezi složenými závorkami se bude opakovat, jakmile se projedou všechny příkazy, program přeskočí zpět ke kulatým závorkám, zkontroluje, jestli hodnota řídící proměnné splňuje podmínku a zvýší hodnotu řídící proměnné o 1. Takhle se cyklus bude opakovat, dokud bude splněná podmínka.

Pojďme si do kódu přidat náš cyklus, který bude procházet pole board:

for (let y = 0; y < board.length; y++) {
}

První část cyklu je nám známa, nastavíme si řídící proměnnou y s počáteční hodnotou 0. V podmínce říkáme, dokud je menší než board.length. Schválně jsem v sekci o polích mluvil i o jejich délce, přesně v takových případech se nám to hodí, board.length nám vrátí hodnotu délky pole board. Někdo možná namítne, že známe velikost pole board - 20. Každý program by měl být ale co nejvíce obecný a připravený na změny, pokud bychom změnili velikost pole na 21, musel bychom v tu chvíli myslet na to, abychom to hodnotu všude změnili, což není v lidských silách, proto se snažte vždy psát obecné programy a myslet na tyto případy.

První cyklus máme, ale jistě ti došlo, že sice procházíme pole, ale už neprocházíme ta pole, která se nacházejí na těch indexech. Proto musíme vložit do cyklu další cyklus, který se o to postará.

for (let y = 0; y < board.length; y++) {
    for (let x = 0; x < board[y].length; x++) {
    }
}

Druhý cyklus je dost podobný, ale je tam pro nás trochu neznámý zápis - board[y].length. board[y] si vytáhne data z toho konkrétního indexu, tedy další pole. Tyto cykly nám zajistí, že se projde každý chlíveček v každém poli a my budeme schopni vykreslit mapu, nezbývá nic jiného něž vložit do druhého cyklu podmínku, která bude říkat – pokud bude v daném chlívečku 1, pak vykresli zeď, pokud bude v chlívečku 0, nech blok prázdný.

for (let y = 0; y < board.length; y++) {
    for (let x = 0; x < board[y].length; x++) {
        if (board[y][x] === 1) {
            ctx.drawImage(wall, x * blockSize, y * blockSize, blockSize, blockSize);
        }
    }
}

Klasickou podmínku definujeme pomocí klíčového slova if a do závorek napíšeme logický výraz, ze kterého vzejde pravda nebo nepravda. V podmínce reagujeme pouze na 1, protože 0 nás nijak nezajímá (nestane se při ní nic).

Naše funkce generateBoard by měla vypadat následovně:

function generateBoard() {
    for (let y = 0; y < board.length; y++) {
        for (let x = 0; x < board[y].length; x++) {
            if (board[y][x] === 1) {
                ctx.drawImage(wall, x * blockSize, y * blockSize, blockSize, blockSize);
            }
        }
    }
}

Pro vykreslení použijeme funkci drawImage. Jako parametr přijímá obrázek, pozici na ose x, pozici na ose y, výšku a šířku (přesně v tomto pořadí a oddělené čárkou).

Nyní už nám zbývá jen celou funkci zavolat. Na to taky půjdeme chytře. Budeme chtít tuto funkci zavolat ve chvíli, kdy se načte stránka. JavaScript na tuto a podobné události dokáže velice hezky reagovat, tzv. poslouchat. K „poslechu“ využijeme tzv. Poslouchač událostí.

Poslouchač událostí

Poslouchač událostí bude čekat (poslouchat), zda k události náhodou nedošlo a pokud ano, tak zavolá funkci, o které jsme mu řekli, že je tzv. ovladač události (event handler).

Posluchače události přidáváme pomocí: objekt.addEventListener('událost', ovladač).

  • Objekt je objekt, ke kterému chceme ovladač události připojit.
  • Událost je textový řetězec s názvem události, na kterou se má čekat.
  • Ovladač je název funkce, která se spustí, když k události dojde.

Vypadá to složitě, ale zas tak složité to není. Ukažme si to na konkrétním příkladu a to na našem kódu ze včerejška:

window.addEventListener("load", generateBoard);

Tímto řádkem jsme JavaScriptu řekli, že objekt window (okno prohlížeče) bude čekat, dokud v něm nebude všechno načtené (událost load). Když se načte kompletní obsah stránky včetně všech obrázků, fontů apod., tak dojde k události a zavolá se funkce generateBoard. Tedy ta funkce, která je zodpovědná za aktualizaci veškerého dění ve hře.

Na závěr

Dnes toho bylo hodně, hlavně teorie, ale i to je úděl programátora. Naučili jsme se spoustu nových věcí, které ostatní proberou za mnohem delší dobu, to je super ne? Mimo to máme na světe krásné bludiště! 😊 Celý kód z dneška najdeš zde. Doufám, že se ti to líbilo a uvidíme se zase zítra.

Další den

Předcházející den