06. Klikanie na zložitejší model
Pridáme klikaciu animáciu aj hojdaciemu kreslu, nech máme peknú interaktívnu scénu. Popri tom narazíme na nejaké chyby a ukážeme si, čo s nimi.
Otvorte si svoj projekt alebo si stiahnite hotový: CupboardApp_05.zip
Z minula už vieme, že stoličku – teda presnejšie StolickaContainer – budeme rotovať okolo osi Z, a tým ju rozhojdáme. Vytvorme si teda takú animáciu.
- Vytvorte v assetoch nový animator controller a nazvite ho ChairAnimator.
- Pridajte game objektu StolickaContainer nový komponent typu Animator a priraďte do neho ChairAnimator.
- Vytvorte novú animáciu s názvom ChairPush a priraďte ju do animátora ChairAnimator.
- Premenujte ju v animátore na Push.
- Pridajte do animátora aj prázdny stav, ktorý nastavíte ako default stav.

StolickaContainer teraz môžeme rozanimovať tak, aby sa otáčal okolo Z ako keď sa hojdá kreslo:

Keď sa v editore animácii prepnete do režimu Curves, dostanete možnosť upravovať aj easing (hladkosť akcelerácie) animácie. Ja som tu napríklad na začiatku animácie urobil priebeh krivky strmý, lebo to viac zodpovedá pohybu, keď do stoličky niekto strčí silou (alebo klikne myšou).
Animáciu by ste už mali zvládnuť vytvoriť aj sami. Tá nie je jadrom tejto lekcie. Poďme radšej dorobiť klikanie na stoličku.
Ako kolidovať so stoličkou?
Keď sme chceli urobiť klikateľnými dvierka, pridali sme na ne BoxCollider. Dvierka sú hranaté a majú tvar natiahnutej krabice. BoxCollider ich tak vie presne obopnúť:

Hojdacie kreslo má ale značne ne-krabicovitý tvar a keby sme ho aj opísali krabicovým BoxColliderom, bude to dosť nepresné:

Napríklad oblasť, kde leží kurzor v obrázku vyššie, je taká, že vizuálne nepatrí do kresla, ale keby sme na ňu v hre klikli, tak to Unity vyhodnotí ako kliknutie na kreslo. Pretože sme zasiahli BoxCollider kresla.
Našťastie Unity má k dispozícii aj špeciálny collider, ktorý pracuje s geometriou importovaného 3D modelu.
(Ak ste si skúšali pridávať na kreslo BoxCollider, tak ho teraz odstráňte).
Pridajte na StolickaContainer komponent MeshCollider.
Skúsme teraz spustiť hru a kliknúť na stoličku. Čo sa stane? Nič.
To je v poriadku, zatiaľ sa ani nič nemalo stať. Lebo zatiaľ sme nenaskriptovali to, aby sa po kliknutí na kreslo prehrala jeho animácia. Ale vieme nejak zistiť, či nám funguje klikanie na kreslo?
Konzola moja každodenná
Spustime hru, zapnime si konzolu a kliknime na dvierka:

Keď klikneme na dvierka, vypíše sa „Kliklo sa!“ a vypíše sa názov game objektu, na ktorý sme klikli. Kliknime teraz na dlážku (ktorú sme pridali v domácej úlohe). Čo sa vypíše:

Výpis „Kliklo sa!“ znamená, že nám funguje klik myšou ako takou. Ďalej sa vypísal názov game objektu Dlazka (tak som si ho nazval ja, váš sa volá možno inak). A ešte sa tam vypíše aj červená chybová hláška. Tu sa teraz na moment pristavíme.
NullReferenceException
Jednou z najčastejších chýb, ktoré zažijete, je NullReferenceException. Bez toho, aby sme zachádzali do technických podrobností, znamená to, že program narazil v kóde na miesto, kde očakával, že niečo bude a ono tam nie je nič. Je tam null.
Keď sa pozrieme bližšie na text chybovej hlášky, je tam aj uvedené, kde a v ktorom skripte k tejto chybe došlo: DoorController.cs, riadok 27.
Tak si DoorController otvorme a poďme chybu opraviť:
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Debug.Log("Kliklo sa!");
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log(hit.transform.gameObject.name);
// Podla toho aky je stav open
if (hit.transform.gameObject.GetComponent<DoorStatus>().open == true)
{
// Prehraj bud toto
hit.transform.gameObject.GetComponent<Animator>().Play("CloseDoor");
// A nastav open na false
hit.transform.gameObject.GetComponent<DoorStatus>().open = false;
}
else
{
// Alebo toto
hit.transform.gameObject.GetComponent<Animator>().Play("OpenDoor");
// A nastav open na true
hit.transform.gameObject.GetComponent<DoorStatus>().open = true;
}
}
}
}
V tomto riadku 27 sa pýtame, či zasiahnutý game objekt má vo svojom komponente DoorStatus uložený stav „otvorené“. Čo sa stane ak sme klikni na dlážku? Má ona svoj komponent DoorStatus? Nemá. Lebo ani nechceme, aby sa otvárala ani sme jej tento komponent nepridali. Preto počítač namiesto komponentu DoorStatus našiel nič: null.
A taký null nemá žiadne vlastnosti, ani vlastnosť open. Preto počítač nevie pokračovať a sťažuje sa. Vieme to ošetriť jednoduchou podmienkou, do ktorej tela uzavrieme celý blok kódu, ktorý sa týka animácii dverí:
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Debug.Log("Kliklo sa!");
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log(hit.transform.gameObject.name);
// Uistime sa, ci naozaj mame k dispozicii DoorStatus
if (hit.transform.gameObject.GetComponent<DoorStatus>() != null)
{
// Podla toho aky je stav open
if (hit.transform.gameObject.GetComponent<DoorStatus>().open == true)
{
// Prehraj bud toto
hit.transform.gameObject.GetComponent<Animator>().Play("CloseDoor");
// A nastav open na false
hit.transform.gameObject.GetComponent<DoorStatus>().open = false;
}
else
{
// Alebo toto
hit.transform.gameObject.GetComponent<Animator>().Play("OpenDoor");
// A nastav open na true
hit.transform.gameObject.GetComponent<DoorStatus>().open = true;
}
}
}
}
}
Do kódu sme pridali podmienku, ktorá overí, či kliknutý objekt obsahuje komponent DoorStatus a telo tejto podmienky sa vykoná len ak pri hľadaní DoorStatus počítač dostane použiteľný komponent a nie null.
Uložiť, spustiť hru, otestovať:

Klikanie na dlážku už negeneruje chyby. Neviem či si to uvedomujete, ale práve sme odstránili náš prvý bug!
Vedeli ste o tom, na čo narazila pri debugovaní veľká pani programátorov, Grace Hopper? Čitajte tu. Ak nepoznáte Grace Hopper, tak to musíte dobehnúť.
No a prečo nejde klikať na kreslo?
Tí zvedavejší už asi odhalili, že keď klikáme na kreslo, tak konzola buď nevypíše žiadne meno objektu, alebo vypíše Dlazka či niektoré dvierka. Ako keby náš klik preletel skrz kreslo. Prečo to?
Asi je nejaká chyba s našim MeshColliderom, keďže neodchytáva kliky. Je to tým, že Unity nemá vygenerovanú geometriu pre správne vytvorenie MeshCollidera. Samotná geometria modelu (mesh) na to nestačí, lebo tá je niekedy príliš zložitá na výpočty Raycastu. Tak si Unity generuje na základe importovaného 3D modelu jednoduchší mesh. Upravme preto import assetu chair.
Zaškrteme voľbu „Generate Colliders“ a aplikujeme zmeny v importe (Apply)

Spomeňme si, že čo zmeníme v importe assetu sa automaticky prejaví aj na modeloch v scéne, ktoré sú vytvorené z tohoto assetu.
Vyskúšajme teraz spustiť hru a klikať na kreslo:

Konzola už obsahuje konkrétne názvy game objektov a keď si prelezieme hierarchiu, zistíme, že sú to názvy súčiastok nášho kresla:

Prečo sú to názvy jednotlivých súčiastok kresla, keď MeshCollider sme dali na game objekt StolickaContainer? Game objekt StolickaContainer nemá žiadnu svoju vlastnú geometriu. Obsahuje len potomka Stolicka. Ale ani ten nemá žiadnu svoju geometriu, je to len ďalši kontajner v ktorom sú jednotlivé súčiastky stoličky (tak bol tento 3D model jeho autorom vytvorený). Až tieto súčiastky majú nejakú geometriu (t.j. obsahujú komponent MeshRenderer), a preto sa v klikaní pomocou Raycastu objavujú až ony a nie ich kontajner.
Spúšťame animáciu na kresle
Už vieme klikať na kreslo, máme vytvorený animátor aj s animáciou. Poďme to spojazdniť. Doplníme si kód na spúšťanie animácie do DoorControllera. (Áno, áno, názov DoorController nemá nič s kreslami, ale berme to ako skúsenosť, že nabudúce si budeme skripty pomenúvať všeobecnejšie).
// Uistime sa, ci naozaj mame k dispozicii DoorStatus
if (hit.transform.gameObject.GetComponent<DoorStatus>() != null)
{
// Podla toho aky je stav open
if (hit.transform.gameObject.GetComponent<DoorStatus>().open == true)
{
// Prehraj bud toto
hit.transform.gameObject.GetComponent<Animator>().Play("CloseDoor");
// A nastav open na false
hit.transform.gameObject.GetComponent<DoorStatus>().open = false;
}
else
{
// Alebo toto
hit.transform.gameObject.GetComponent<Animator>().Play("OpenDoor");
// A nastav open na true
hit.transform.gameObject.GetComponent<DoorStatus>().open = true;
}
}
// Ak nemame k dispozicii DoorStatus
else
{
// Tak spustime animaciu potlacenia kresla
hit.transform.gameObject.GetComponent<Animator>().Play("Push");
}
Za telo podmienky, ktorá kontroluje, či máme DoorStatus sme pridali časť else { ... }, ktorá definuje, čo sa má vykonať, ak DoorStatus nenájdeme. A tu sme zadali príkaz, aby sa na kliknutom objekte prehrala animácia "Push".
Uložiť, vyskúšať. Nefunguje. Konzola, pomôž!

Konzola sa sťažuje, že sme klikni na game objekt pasted__bezier1_polySurface24 ale na ňom nie je žiadny animátor. To je pravda. Animátor nemáme na každej jednej súčiastke kresla, ale máme ho až na StolickaContainer.
Na toto v Unity mysleli tiež a využijeme namiesto metódy GetComponent<Animator>()metódu GetComponentInParent<Animator>():
// Ak nemame k dispozicii DoorStatus
else
{
// Tak spustime animaciu potlacenia kresla
hit.transform.gameObject.GetComponentInParent<Animator>().Play("Push");
}
Metóda GetComponentInParent nám pohľadá komponent Animator nie len na konkrétnom game objekte (v našom prípade nejaká súčiastka stoličky) ale ide v hierarchii aj o úroveň vyššie – game objekt Stolicka – a ak ho nenájde ani tam, tak ide zase vyššie a vyššie. V našom prípade to skončí na úrovni game objektu StolickaContainer, ktorý už animátor má. A tento animátor má aj animáciu Push (lebo sme mu ju tam dali) a tým pádom všetko funguje ako má:
No… a posledná chyba. Kreslo sa rozhojdá iba raz. Keď na neho klikneme druhýkrát, už sa nerozhojdá. Prečo?
Ako prehrať animáciu viackrát?
Je to dosť neintuitívne, ale animácie v Unity, keď dohrajú a nie sú automaticky napojené na ďalšiu animáciu (šípkami v animátore) tak ostanú stáť na svojom konci a nejde ich tak ľahko pustiť znova. Treba ich „pretočiť na začiatok“. Vedeli by sme k tomu animáciu donútiť drobnou úpravou skriptu, ale dosť už skriptovania a poďme zase niečo vyklikať.
Ovorme si animátor ChairAnimator:

V ňom sme si na začiatku vytvorili aj prázdny stav New State, aby sa animácia Push neprehrávala automaticky po spustení hri. (V animátore vždy musí byť nejaký default stav alebo default animácia, ktorá je napojená na bod Entry)
Animácia Push nie je napojená na nič. Preto po jej prehratí ostane animátor v akomsi vákuu a nevie ako pokračovať. Ak ju napojíme na bod Exit, tak tým povieme animátoru, že všetko korektne skončilo. A vďaka tomu bude animátor vedieť nabudúce znova pustiť Push.
Pravý klik na Push > Make Transition > Natiahnuť šípku na Exit


Takto upravený animátor už funguje ako chceme:
Tu je hotový projekt: CupboardApp_06.zip
