04. Zbieranie kyslíka

Aby astronautka Sandra vo vesmíre prežila, kým ju príde zachrániť nejaký raketoplán, potrebuje kyslík. My jej ho pridáme vo forme kyslíkových bômb, ktoré bude musieť vo voľnom priestore loviť. Popri tom použijeme prvý raz aj programátorskú vec zvanú cyklus.

Otvorte si svoj projekt alebo si stiahnite hotový: SpaceGame_03.zip

Môj prvý powerup

Kyslík bude voľne pohodený v priestore a Sandra sa za ním bude musieť dopraviť. Začneme tým, že si vytvoríme game objekt pre kyslík

Naimportujte do assetov sprajt kyslíkovej bomby: tank.png

Keď ho presunieme do scény, je trochu veľký, tak ho naškálujeme na 0.2 a nech sa nemýli s assetom tank, tak ho premenujme na Kyslik. A môžeme ho cez inšpektor v jeho komponente Sprite Renderer aj ofarbiť na belaso.

Sandra bude vo vesmíre prichádzať do kontaktu s takýmito kyslíkovými bombami, ale bude prichádzať do kontaktu aj s UFOm. Aby sme pri kontakte vedeli rozlíšiť, či sa stretla s niečím, čo vie zužitkovať, tak bude dobré mať zužitkovateľné objekty odlíšené. Použijeme na to Tag.

Vytvorte nový tag s názvom Consumable a priraďte ho game objektu Kyslík.

Tento tag bude znamenať, že objekt je skonzumovateľný. Keby sme v budúcnosti pridali do hry iné skonzumovateľné objekty (napríklad lekárničku) , tak môžeme použiť tento istý tag na ich označenie.

Nadýchni sa

A hneď aj naučíme Sandru skonzumovať kyslík. Budeme potrebovať zistiť, že Sandra prišla do kolízie s kyslíkom a zareagovať na to nejak. Čo potrebujeme, aby sa dalo s kyslíkom kolidovať? Hádate správne:

Pridajte na Kyslík komponent Capsule Collider 2D

Po pridaní collidera už Sandra naráža do kyslíkovej bomby, ako by to bola prekážka:

Čo chceme, aby sa stalo, keď narazí do kyslíka? Budeme chcieť, aby sa Sandre niekam pripočítal kyslík (toto zatiaľ nemáme) a aby nádrž zmizla. Tak urobme zatiaľ to zmiznutie. V prvom rade potrebujeme zareagovať nejak na to, že vôbec nastala kolízia. Na to máme preddefinované metódy. V prípade 2D kolízií je to metóda OnCollisionEnter2D(). Pridajme ju do skriptu pre astronautku:

public class Astronaut : MonoBehaviour
{
    public float speed = 1;
    public float rotationSpeed = 180;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.Translate(Vector2.up * speed * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.Q))
        {
            transform.Rotate(0, 0, rotationSpeed * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.E))
        {
            transform.Rotate(0, 0, -rotationSpeed * Time.deltaTime);
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        // Tu budu prikazy co sa ma stat, ked nastane kolizia
    }
}

Zatiaľ si do metódy pridajme len výpis do konzoly, cez ktorú budeme sledovať, či nám kolízia funguje:

    private void OnCollisionEnter2D(Collision2D collision)
    {
        Debug.Log("Kolizia!");
    }

Uložme skript a poďme skontrolovať, či funguje. Po spustení hry, keď Sandra narazí do Kyslíku (ale aj keď do Sandry narazí UFO) vypíše hra do konzoly „Kolizia!“.

Fajn, teraz potrebujeme rozlíšiť, či sa Sandra zrazila s UFOm alebo s kyslíkom. Na to použijeme práve tag, ktorý sme si skôr vytvorili. V kóde sa teda spýtame, či game objekt, s ktorým nastala kolízia má tag Consumable. A ak áno, tak ten game objekt zmizneme.

    private void OnCollisionEnter2D(Collision2D collision)
    {
        Debug.Log("Kolizia!");

        // Ak sme sa zrazili s konzumovatelnym kyslikom
        if (collision.gameObject.tag == "Consumable")
        {
            // Tak skonzumuj kyslik
            // (toto zatial nemame)

            // A zlikviduj kyslikovy objekt
            Destroy(collision.gameObject);
        }
    }

Takto podobne sme sa na tagy v kolízii pýtali už pri gulečníku, takže snáď nič nové. Nová je tu len funkcia Destroy(), ktorá robí to, že game objekt, ktorý jej dáme ako parameter, vymaže zo scény aj z hierarchie.

Uložiť a vyskúšať. Zmizne kyslík po stretnutí so Sandrou? Zmizne. Super!

No a čo bude Sandra dýchať ďalej. Jedna kyslíková bomba nestačí, vyrobme jej ich do zásoby viac.

Kyslík pre všetkých!

Ak chceme z jedného game objektu vyrábať kópie, je dobrý nápad robiť to cez prefaby. Keď potom niečo chceme zmeniť (pridať na game objekt nejaký skript, zmeniť farbu a pod.), stačí to zmeniť na prefabe. Urobme to tak aj teraz.

Vytvorte v assetoch adresár Prefabs a pretiahnite do neho z hierarchie Kyslík.

Z hierarchie môžeme teraz vymazať game objekt Kyslik, máme ho uložený v prefaboch.

Keď sme v gulečníku potrebovali generovať náhodne gule, tak sme použili už napísaný skript na generovanie gulí. Ale my sme už velkáči a napíšeme si ten skript sami.

  • Vytvorte nový prázdny game objekt Powerupy
  • Vytvorte nový skript PowerupGenerator
  • Priraďte ho na game objekt Powerupy

Aké informácie potrebuje taký skript vedieť, aby nám vedel vygenerovať kyslíkové bomby vo vesmíre na nejaké náhodné pozície?

  • Koľko toho má vygenerovať (počet)
  • Kde to má generovať (rozsah X a Y hodnôt)
  • Čo to má generovať (ktorý prefab)

Počet objektov, ktoré treba vygenerovať, je celé číslo. To znamená že vlastnosť, v ktorej bude táto informácia uložená, bude typu int. A chceme to vedieť nastavovať v editore, takže vlastnosť bude public. A meno tej vlastnosti dajme nejaké zrozumiteľné, napríklad count:

public class PowerupGenerator : MonoBehaviour
{
    public int count;

Podobne vytvorte aj vlastnosti xMin, xMax, yMin, yMax, ktoré budú určovať v akom rozmedzí sa majú náhodne generovať pozície kyslíkov. Tieto hodnoty budú desatinné, takže to bude typ float.

Vytvorte aj vlastnosť objectPrefab, v ktorej bude uložené, čo vlastne má generátor vytvárať. Bude typu GameObject, lebo to je typ, ktorý sa vzťahuje na všetky game objekty aj prefaby. A táto vlastnosť bude tiež bude verejná.

Teraz môžeme v inšpektore na game objekte Powerupy vyplniť príslušné hodnoty. Povedzme, že kyslíkov budeme chcieť 10 a majú sa generovať (podobne ako target pre UFO) v rozsahu -7.5 až 7.5 pre X a -4 až 4 pre Y. Do vlastnosti objectPrefab pretiahneme z asssetov prefab Kyslik:

Pre kontrolu, takto vyzerá skript:

public class PowerupGenerator : MonoBehaviour
{
    public int count;

    public float xMin;
    public float xMax;
    public float yMin;
    public float yMax;

    public GameObject objectPrefab;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Ako vygenerovať objekt?

Skript už teda má potrebné informácie, ešte ale nemá potrebné príkazy. Poďme mu prikazovať. Zatiaľ vygenerovanie jedného kyslíka.

Vyššie sme si ukázali, že zlikvidovať objekt vieme príkazom Destroy(). K nemu náprotivok je príkaz Instantiate(). Keď takému príkazu podsunieme nejaký prefab, vyrobí z neho do scény kópiu – inštanciu. Veď skúsme.

My chceme, aby sa kyslíky vygenerovali na začiatku, takže budeme písať do metódy Start():

    void Start()
    {
        Instantiate(objectPrefab);
    }

Tento príkaz teda urobí to, že zoberie prefab, ktorý je uložený vo vlastnosti objectPrefab, a vyrobí z neho inštanciu. Tá sa volá ako prefab, ale má v mene dodatok “ (Clone)“.

Pozícia, na ktorú sa prefab vygeneroval, je defaultná. A to nám nevyhovuje, lebo my chceme, aby sa vygeneroval na náhodnú pozíciu. My by sme možno aj vedeli game objektu nastaviť jeho pozíciu, len kde je ten vygenerovaný game objekt. Ako sa k nemu dostaneme v skripte?

Metóda Instantiate nie len vygeneruje nový objekt s prefabu, ale aj vráti ako svoju výstupnú hodnotu vygenerovaný game objekt. A tak sa my k nemu dostaneme. Vyrobíme si nejakú premennú, kam sa uloží a cez ňu máme k nemu prístup:

    void Start()
    {
        GameObject powerup = Instantiate(objectPrefab);
    }

Čo sa tu deje: Vytvorili sme si takú dočasnú premennú s názvom powerup. Nie je to plnohodnotná vlastnosť triedy, to by musela byť zadefinovaná na úrovni triedy. Táto je zadefinovaná na úrovni Start() a inde o nej tým pádom nikto nevie. A do tejto premennej powerup si ukladáme, čo nám vygeneroval Instantiate().

A keď už máme k dispozícii game objekt, môžeme mu nastaviť jeho pozíciu na nejakú náhodnú hodnotu z rozsahu xMin až xMax a yMin až yMax:

    void Start()
    {
        GameObject powerup = Instantiate(objectPrefab);
        powerup.transform.position = new Vector2(Random.Range(xMin, xMax), Random.Range(yMin, yMax));
    }

Tu už sa nedeje nič neznáme. Pozícia game objektu sa nastavuje v jeho vlastnosti transform.position a vygenerovovať nový náhodný vektor vieme už keď sme generovali náhodný cieľ cesty pre UFO.

Keď teraz vyskúšame hru, zistíme, že pri každom spustení sa kyslík vygeneruje na inú pozíciu.

No a teraz to isté, len 10-krát

Ak by sme chceli takto vygenerovať 10 kyslíkov, teoreticky by sme mohli nakopírovať tieto 2 riadky 10-násobne. A technicky to aj možné je:

void Start()
{
    GameObject powerup = Instantiate(objectPrefab);
    powerup.transform.position = new Vector2(Random.Range(xMin, xMax), Random.Range(yMin, yMax));

    GameObject powerup2 = Instantiate(objectPrefab);
    powerup2.transform.position = new Vector2(Random.Range(xMin, xMax), Random.Range(yMin, yMax));

    GameObject powerup3 = Instantiate(objectPrefab);
    powerup3.transform.position = new Vector2(Random.Range(xMin, xMax), Random.Range(yMin, yMax));

    ... 

Ale za prvé, je to otrava. Za druhé, čo by sme robili, keby niekto chcel 50 objektov? Za tretie, keby sme chceli niečo zmeniť (ako napríklad pridať každému kyslíku aj nejakú náhodnú rotáciu), tak by sme museli tú istú zmenu robiť 50x. Fuj!

V programovacích jazykoch poznáme konštrukt, pomocou ktorého vieme sekvenciu príkazov opakovať koľkokrát chceme. Volá sa to cyklus alebo loop. Cyklov je niekoľko typov, my teraz použijeme cyklus, ktorý sa volá for (z anglického for = pre, ako „pre konkrétny počet opakovaní“).

V tomto bode nemusíte rozumieť, pokojne to berte ako mágiu, ale funguje to takto:

    void Start()
    {
        for (int i = 0; i < count; i++)
        {
            GameObject powerup = Instantiate(objectPrefab);
            powerup.transform.position = new Vector2(Random.Range(xMin, xMax), Random.Range(yMin, yMax));
        }
    }

Naše príkazy na inštancovanie kyslíka a nastavenie jeho polohy sme zavreli do brčkatých zátvoriek { ... }, dali sme ich do bloku, do takzvaného tela cyklu. Podobne ako zatvárame do brčkatých zátvoriek blok príkazov v prípade podmienky if.

Pri podmienke sa blok vykoná ak je podmienka splnená.

Pri cykle sa blok vykoná toľkokrát, koľko zadefinujeme vo for(...)

Definícia cyklu for má 3 časti, oddelené bodkočiarkami. Nás teraz zaujíma tá prostredná časť, ktorá hovorí, že koľkokrát sa má cyklus opakovať a to je naša hodnota count.

Keď takýto kód spustíme, hra vygeneruje 10 kyslíkov. Toľko, koľko sme jej zadali v inšpektore. Skúste zmeniť v game objekte Powerupy v inšpektore počet count na 50 a kyslíkov bude 50:

Zhrnutie

  • Keď chceme generovať viac rovnakých objektov, používame na to prefaby a cykly
  • V skripte sa objekt sa z prefabu vyrába príkazom Instantiate()
  • Keď chceme opakovať nejakú sekvenciu príkazov, použijeme cyklus for a príkazy zavrieme do jeho tela.

Na domácu úlohu

Doplňte do cyklu, aby sa každý vygenerovaný powerup aj náhodne natočil o nejakú hodnotu v rozsahu 0 až 360. Ako natočiť objekt ste si vyskúšali pri Sandre. Len teraz budete natáčať o náhodný uhol:

Prejdite si na W3Schools cvičenia na cykly

Hotový projekt na stiahnutie: SpaceGame_04.zip