Codemas - 16. den

Vánoce se kvapem blíží, už nemáme moc času na jejich záchranu, ale zatím nám to jde celkem dobře. Včera se nám povedlo upravit padající dárek tak, aby zmizel když dopadne na zem. Navíc je naše hra připravena na libovolný počet dárků, jen je musíme ve vhodnou chvíli do hry přidat.

Dnes chceme:

  1. Přidávat do hry nový dárek každých několik vteřin, nejlépe náhodně, aby hra nebyla předvídatelná.
  2. Nové dárky přidávat na herní plochu v místě, kde jsou zrovna sáně, aby to vypadalo, že dárek padá z nich.

Náhodná čísla

Ve většině her se hodně pracuje s náhodnými čísly, aby hra nebyla předvídatelná. Naše sáně se pohybují konstantní rychlostí sem a tam a kdyby dárek ze saní padal v pravidelných intervalech stejnou rychlostí, mohl by hráč padající dárky přesně předvídat a pouze jezdit robotem na předem známá místa a na dárky prostě počkat. To by nebyla žádná zábava.

Naše hra musí být alespoň trochu náhodná, aby na ni hráč musel reagovat a nemohl ji předvídat.

Tzv. generování náhodných čísel je v JavaScriptu velmi jednoduché, ale bohužel programátorsky ne příliš přívětivé pro většinu běžných použití. V naší hře si vytváření náhodných čísel vylepšíme vlastní funkci, která bude přesně vyhovovat našim účelům.

JavaScript má v sobě zabudovaný objekt Math (velké M, na velikosti písmen záleží), který obsahuje různé matematické funkce. Jednou z těch funkcí je Math.random(), která vrátí náhodné desetinné číslo v rozmezí 0 (včetně) až 1 (ale nikdy ne 1), tj. 0 až 0.999999999999999.

My ale v našem programu chceme náhodná čísla jako 7, 19, 43, … Jak to uděláme?

Stačí násobit. Když původní náhodné číslo padá od 0 do 1 a my přitom chceme číslo do 10, stačí nám napsat Math.random() * 10. Dostaneme náhodné desetinné číslo od 0 do 10, které může být 0 ale nikdy nebude 10, tj. 0 až 9.9999999.

Tak co kdybychom číslo, kterým násobíme, zvětšili o 1? Z Math.random() * (10 + 1) dostaneme náhodná desetinná čísla od 0 do 11, ale nikdy ne 11, tj. 0 až 10.999999.

Kdybychom teď celý výpočet zaokrouhlili směrem dolů, aby nikdy neměl desetinnou část, dostali bychom náhodná celá čísla od 0 do 10. Zaokrouhlení směrem dolů nabízí opět objekt Math a jeho metoda floor. Takže Math.floor( Math.random() * (10 + 1)).

A když budeme chtít náhodné číslo jiné než od nuly? Kdybychom například chtěli náhodné číslo v rozmezí 6 - 10? Není to stejné, jako kdybychom chtěli náhodné číslo od 0 do 4 a přičetli k němu 6? Takže náš výpočet by byl 6 + Math.floor( Math.random() * (10 - 6 + 1)).

Když to zobecníme:
dolniLimit + Math.floor( Math.random() * (horniLimit - dolniLimit + 1))

Už víte, proč jsem na začátku mluvil o tom, že pro většinu použití je generování náhodných čísel v JavaScriptu programátorsky ne příliš přátelské?

Protože budeme náhodná čísla potřebovat na víc místech, pojďme si z našeho vzorce udělat krátkou funkci, která bude přijímat dva parametry, horní a dolní hranici, a vrátí náhodné číslo v požadovaném rozsahu.

Když chceme z funkce vrátit nějakou hodnotu, použijeme slovo return hodnota.

Někam na konec našeho souboru script.js dopíšeme:

// funkce generuje náhodné číslo od dolniLimit do horniLimit (oba včetně)

function nahodneCislo(dolniLimit, horniLimit) {
  return dolniLimit + Math.floor(Math.random() * (horniLimit - dolniLimit + 1));
}

Když budeme chtít naši funkci použít, jednoduše ji zavoláme s požadovanými parametry:

// náhodné číslo od 1 od 6
let hodKostkou = nahodneCislo(1, 6);

// od 0 do 100
let procenta = nahodneCislo(0, 100);

// od 20 do 30
let rychlost = nahodneCislo(20, 30);

Náhodné vypouštění dárků

Funkci pro rychlé generování náhodných čísel v daném rozsahu máme připravenou na použití, pojďme teď vymyslet, jak zařídíme, aby jednou za čas začal padat ze saní další dárek.

My v pravidelném intervalu (50× za vteřinu) voláme funkci aktualizujHru, která nám posouvá sáně i dárky a zároveň kontroluje, zda dárky už nedopadly na zem.

Co kdybychom do hry přidali proměnnou, do které vygenerujeme náhodné číslo v nějakém intervalu a které bude představovat čas, za jak dlouho má ze saní spadnout další dárek. Toto číslo vždy při volání funkce aktualizujHru snížíme o 1 a když dojdeme až k 0, tak přidáme do hry další dárek a vygenerujeme novou náhodnou hodnotu.

Do našeho objektu hra, který už máme vytvořený někde na začátku našeho programu, přidejme vlastnost dalsiDarek, kde budeme odpočítávat čas. Výchozí hodnotu nastavme na 150:

// objekt pro hru
let hra = {
  element: document.getElementById('hra'),
  sirka: 900,
  vyska: 600,
  dalsiDarek: 150
}

Nyní přidáme funkci cekejNaDalsiDarek, která se bude starat o generování náhodného času, odpočítávání do nuly a následné přidání nového dárku do hry.

Na konec script.js dopíšeme:

// funkce odpočítává čas do vyhození nového dárku
function cekejNaDalsiDarek() {

  if (hra.dalsiDarek === 0) {

    // odpočet je na 0, do hry přidáme nový dárek
    pridejDarek();

    // vygenerujeme náhodný čas v rozmezí 1 - 5 vteřin
    // odpočítává se 50x za vteřinu, takže potřebujeme
    // číslo 50 - 250
    hra.dalsiDarek = nahodneCislo(50, 250);

  } else {

    // odpočet ještě není na 0, tak ho snížíme o 1
    hra.dalsiDarek--;

  }

}

Když je počitadlo na nule, přidá se do hry dárek (funkci už máme z předchozích dnů) a vygeneruje se nový čas pro odpočítávání. Chceme-li, aby nový dárek spadl někdy během příštích 1-5 vteřin, potřebujeme vygenerovat náhodné číslo mezi 50 a 250. Ke snížení počitadla dochází cca 50× za vteřinu, takže každých 50 znamená jednu vteřinu.

Když počitadlo ještě není nula (větev else podmínky), tak pouze snížíme počitadlo o 1.

Funkci máme napsanou, ale aby vše fungovalo, musíme ji pravidelně volat. Přidejme volání funkce cekejNaDalsiDarek do funkce aktualizujHru. Celá funkce bude vypadat takto:

// funkce pro aktualizování polohy objektů na obrazovce
// spouští se 50× za vteřinu
function aktualizujHru() {
  // posuneme sáně
  posunSane();

  // posuneme padající dárky
  posunDarky();

  // otestujeme padající dárky
  otestujDarky();

  // odpocitavame do dalsiho darku
  cekejNaDalsiDarek();
}

A ještě poslední věc, uvnitř funkce startHry máme stále řádek, na kterém přidáváme do hry jeden testovací dárek. Z funkce startHry smaž řádek, na kterém je:

// testovací přidání dárku na obrazovku, později vymažeme
pridejDarek();

Když teď hru spustíme, na levé straně herní plochy začne každých cca 1-5 vteřin padat nový žlutý dárek. Zatím nepadá ze saní, ale to hned opravíme.

Ve funkci pridejDarek, když vytváříme herní objekt nového dárku, tak zatím napevno nastavujeme jeho souřadnici x na 0. My ale chceme, aby se dárek objevil někde uprostřed saní. Prostředek saní můžeme spočítat jako sane.x + sane.sirka / 2.

Když souřadnici x dárku nastavíme na tuto hodnotu, bude na prostředku saní levá strana dárku. My chceme, aby tam byl prostředek dárku, takže od výsledku naopak zase odečteme polovinu šířky dárku, což je 20 pixelů. Celý výsledek pro jistotu zaokrouhlíme pomocí Math.floor, aby nám souřadnice nevycházela na desetinné pixely.

Podobně to uděláme se souřadnicí y dárku. Chceme, aby se objevil zhruba uprostřed saní, takže sane.y + sane.vyska / 2. Tentokrát nám nevadí, že uprostřed saní na výšku bude vršek dárku, takže to nechme jen takto.

Protože chceme být obzvláště záludní, můžeme každému dárku nastavit náhodně i rychlost, s jakou padá. Zvolme náhodně z rozmezí 1-4.

Po všech těchto úpravách bude založení nového herního objektu uvnitř funkce pridejDarek vypadat takto:

// vytvoříme objekt nového dárku
let novyDarek = {
  element: obrazek,
  x: Math.floor(sane.x + sane.sirka / 2 - 20),
  y: Math.floor(sane.y + sane.vyska / 2),
  sirka: 39,
  vyska: 44,
  rychlost: nahodneCislo(1, 3)
};

Ještě by se nám líbilo, kdyby všechny dárky nebyly stejné. Ve složce obrazky máme připravené 4 různé obrázky dárků očíslované darek1.png, darek2.png, darek3.png a darek4.png.

Pokaždé, když nastavujeme zdroj obrázku pro nový dárek, mohli bychom náhodně vybírat jeden z nich. Upravme nastavení atributu src ve funkci pridejDarek takto:

obrazek.src = 'obrazky/darek' + nahodneCislo(1, 4) + '.png';

Když nyní hru spustíme, uvidíme, že nám ze saní padají náhodně vybrané dárky, padají v náhodných intervalech a náhodnou rychlostí.

Když se podíváme pozorně, tak uvidíme, že pokaždé když se přidá nový dárek do hry, problikne nejprve v levém horním rohu, než se objeví u saní. To je proto, že my sice přidáme do hry obrázek dárku, ale jeho skutečnou polohu nastavíme až o padesátinu vteřiny později, když se znovu zavolá funkce aktualizujHru.

Doplňme na konec funkce pridejDarek ještě řádek, který zaktualizuje polohu nově přidaného dárku ihned:

// ihned umístíme dárek na správnou pozici na obrazovce
umistiObjekt(novyDarek);

Celá funkce pridejDarek nyní vypadá takto:

// funkce pro vytvoření nového dárku
// nový dárek se přidá do pole darky[]
function pridejDarek() {
  // vytvoříme nový element pro obrázek dárku
  let obrazek = document.createElement('img');
  obrazek.src = 'obrazky/darek' + nahodneCislo(1, 4) + '.png';

  // přidáme obrázek dárku na herní plochu
  hra.element.appendChild(obrazek);

  // vytvoříme objekt nového dárku
  let novyDarek = {
    element: obrazek,
    x: Math.floor(sane.x + sane.sirka / 2 - 20),
    y: Math.floor(sane.y + sane.vyska / 2),
    sirka: 39,
    vyska: 44,
    rychlost: nahodneCislo(1, 3)
  };

  // nový dárek přidáme do seznamu
  darky.push(novyDarek);

  // ihned umístíme dárek na správnou pozici na obrazovce
  umistiObjekt(novyDarek);
}

Na závěr

Dnes to bylo hodně složité (hlavně na sledování, co kde upravit), ale snad jste to všichni zvládli. Na ploše už nám padají dárky ze saní a když dopadnou na zem, tak zmizí. Robot může jezdit vpravo i vlevo, a i když nemůže zatím dárky chytat, můžeme už si alespoň zkusit, jak by se hra hrála, až bude hotová.

Vidíme, že asi budeme muset doladit časové rozmezí, v jakém nové dárky padají, abychom neudělali hru příliš těžkou. Ale to nechám na každém z vás, abyste si už upravili hru podle sebe.

Pro případ, že se ti dnes něco nepovedlo, kompletní kód hry po dnešních úpravách si můžeš prohlédnout zde.

Máš-li dotazy, diskutuj u příspěvku pro dnešní den ve facebookové události. Těšíme se na tebe opět zítra.