03. Klikanie na model
Interakcia s objektami v scéne je dôležitá súčasť 2D/3D aplikácie. Zatiaľ sme s objektmi interagovali tak, že sme do nich narážali (tágom do gulí) alebo že sme klikali na button (pri otvárani dvierok v minulej lekcii). Teraz vytvoríme interakciu, pomocou ktorej budeme vedieť klikať na konkrétne objekty v scéne.
Otvorte si svoj projekt alebo si stiahnite hotový: CupboardApp_02.zip
Kurzor, kurzor, kto sa pod tebou skrýva?
Kliknutie na button sa počítaču hľadá ľahko. Pozná X,Y súradnice kurzora myši, pozná X,Y súradnice buttonu, vie, že button je obdĺžnikový. Ale zistí, na ktorý objekt sme klikli, keď nejde o obdĺžnikový button ale o 3D objekt v scéne, ktorý môže byť rôzne pootočený, rôzne ďaleko od kamery a nemá práve obdĺžnikový tvar?
Unity má na toto zabudovaný mechanizmus, ktorý sa vola Raycast. Ray znamená lúč a cast znamená niečo vrhnúť. A tak aj funguje raycast. Do scény sa vrhne lúč a Unity spočíta, aké objekty ten lúč preťal. Ten mechanizmus sa využíva napríklad na zistenie, či strela zo zbrane zasiahla nepriateľa, alebo či nepriateľ cez prekážku vidí hráča. Alebo či lúč vyslaný z kurzora myši zasiahol nejaký objekt.
Vytvorte nový skript DoorController a priraďte ho na game objekt Skrinka.
Náš nový skript bude mať za úlohu zisťovať, či sa na dvere kliklo a otvoriť/zatvoriť ich. Zatiaľ je prázdny:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DoorController : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Na zistenie, či používateľ klikol sú dva prístupy. Buď prihlásiť svoj skript na „odoberanie“ informácií o myši od počítača. Vtedy monitorovanie myši obstaráva počítač. A keď používateľ stlačí myš, tak počítač o tom informuje všetky skripty, ktoré sa prihlásili na odber tejto informácie. Je to takzvaný eventovo-orientovaný prístup, lebo skript nič nerobí, len slušne čaká, kým od šéfa nedostane informáciu, že nastala želaná udalosť = event. Napríklad stlačenie myši.
My zatiaľ eventy nevieme, tak použijeme druhý prístup: nekonečný cyklus. Ten funguje takto:
Pýtanie sa na stav myši si náš skript robí sám, v donekonečna sa opakujúcom cykle. Unity pre nás taký cyklus už poskytuje: je to metóda Update(). Tá sa vykonáva stále dokola. V nej si sa budeme pýtať, či už používateľ stlačil myš:
void Update()
{
if (Input.GetMouseButtonDown(0))
{
}
}
Rozoberme si, čo sme napísali:
ifje podmienka, čiže nasledujúci kód v{...}sa vykoná, iba ak je splnená táto podmienka.Inputje trieda, cez ktorú zisťujeme rôzne vstupy z myši, klávesnice a pod.GetMouseButtonDown()je metóda triedyInput, ktorá zisťuje, či bolo stlačené myšacie tlačidlo,0je číslo tlačidla, na ktoré sa pýtame. Ľavé má číslo 0, pravé má číslo 1, prostredné má číslo 2.
No len zatiaľ sa po stlačení myši nič neudeje. Lebo v tele podmienky nič nie je. Ako zistíme, či funguje? Pridáme si tam len tak na testovacie účely jednoduchý výpis do konzoly:
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Debug.Log("Kliklo sa!");
}
}
Čo je konzola?
Pomocou príkazu Debug.Log() si vieme v Unity vypisovať do vývojárskej konzoly kontrolné hlášky, aby sme napríklad vedeli, či sa náš kód vykonáva alebo aká je hodnota nejakej vlastnosti a podobne. Konzola je okno v Unity v záložke vedľa Projektu:

Keď teraz uložíme skript a pustíme hru, tak pri kliknutí do hry by sa vždy malo do konzoly vypísať „Kliklo sa!“:

V konzole sa niekedy objavujú aj červené chybové hlášky, keď niečo nefunguje alebo nastala nejaká chyba. Alebo žlté upozornenia. Preto je dobré sa tam pozerať, keď vám niečo nefunguje. Zatiaľ možno nebudete rozumieť všetkému, čo vám tam Unity vypíše, ale časom sa stane konzola dôležitým pomocníkom.
Ak to funguje, tak vieme, žer sme dobre napísali podmienku na klikanie myšou. A môžeme sa posunúť k samotnému raycastu, aby sme vedeli zistiť, či kurzor zasiahol nejaký objekt.
Nasledujúci kód berte ako mágiu, nebudeme ho nateraz veľmi rozoberať:
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);
}
}
}
V skratke, čo sa tu deje, je, že sa vytvorí nový lúč s názvom ray a vytvorí sa tak, že prechádza cez pozíciu myši (Input.mousePosition). A v ďalšej podmienke sa potom zisťuje, či tento lúč niečo zasiahol a ak áno, tak sa ten zásah uloží do premennej hit. Netreba tomu veľmi rozumieť, podstatné pre nás bude, že game objekt, ktorý bol zasiahnutý, nájdeme v hit.transform.gameObject. No a aby sme vedeli, čo sme zasiahli, tak sme si tam pridali Debug.Log(), ktorý vypíše názov zasiahnutého objektu.
Uložme skript, pustime hru a kliknime na dvierka. Čo sa vypíše do konzoly?

NIČ! Teda vypíše sa, že sme klikli. Ale nevypísal sa názov kliknutého objektu. Aj keď sme klikali na dvierka. Čo to znamená?
To znamená, že pri vykonávaní skriptu sa program dostal po výpis Debug.Log("Kliklo sa!") ale už sa nedostal po ďalší výpis Debug.Log(hit.transform.gameObject.name);. To znamená, že podmienka nebola splnená, čiže lúč nezasiahol žiadny objekt.
Ako funguje Raycast?
V scéne môže byť veľmi veľa rôznych objektov. U nás je to skrinka s 3 dvierkami, ale ak by nám v scéne poletovali tisíce lístkov na stromoch a tisíce stebiel trávy a počítač by mal overovať, že či sme na to náhodou neklikli na niektorý z tých tisícov objektov, tak by sa zavaril. Preto sa Raycast nepočíta na všetkých objektoch, ale využíva fyzikálne collidery. Presne tie, ktoré sme používali v billiarde.
Ak nejaký objekt nemá collider, tak ho Raycast ignoruje. My sme dvierkam zatiaľ collider nepridali, čo vysvetľuje, prečo if podmienka s Raycastom nie je splnená a prečo sa nám teda do konzoly nevypisuje názov kliknutého objektu.
Pridajte na game objekt Door2 komponent BoxCollider
Teraz už bude tento game objekt viditeľný pre Raycast a po kliknutí na neho sa do konzoly vypíše jeho meno:

Dvere otvorte sa!
Konečne môžeme otvoriť dvierka na kliknutie myšou. V minulej lekcii sme naviazali animáciu otvorenia dvierok na button bez kúska kódu, len klikaním v Unity editore. Teraz už budeme potrebovať pracovať so skriptom. Ale nebude to tak zložité vyskladať, ak vieme že:
- dotknutý game objekt je uložený v
hit.transform.gameObject - animácie ovláda komponent Animator
- prehratie sa povie Play
- animácia, ktorú chceme prehrať, sa volá „OpenDoor“.
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);
hit.transform.gameObject.GetComponent<Animator>().Play("OpenDoor");
}
}
}
Uložiť. Spustiť. Vyskúšať. Potešiť sa!
Dvere zatvorte sa!
No a teraz to isté len zavrieť. Ha! Ale ako? Kam napíšeme príkaz na prehranie animácie DoorClose? Hneď za príkaz na prehranie DoorOpen? To by sa spustili obe animácie naraz a to by nefungovalo. Keď sme animácie ovládali buttonmi Open a Close, tak to bolo jednoduché. Jeden button robil jedno, druhý button druhé. Ale teraz máme klik len jeden. A chceme aby taký istý klik raz dvere otvoril a inokedy zase zavrel.
Budeme sa teda musieť vedieť v skripte rozhodnúť, či sú dvere otvorené alebo zavreté a podľa toho vykonať takú alebo onakú animáciu. Už vieme, že na rozhodovanie sa slúžia podmienky if. Len čo do tej podmienky napíšeme? Potrebujeme nejak vedieť, v akom stave sa dvierka nachádzajú.
Uchovávame si stav
Stav nejakého game objektu je vlastne nejaká jeho vlastnosť. Game objekty majú niektoré štandardné vlastnosti: majú Name, majú Tag, v komponente Transform majú vlastnosti pre polohu, rotáciu a veľkosť. Bohužiaľ, game objekty štandardne nemajú vlastnosť „otvorený/zavretý“.
Nič nám ale nebráni takú vlastnosť ich naučiť.
- Vytvorte nový skript DoorStatus a priraďte ho na dvierka Door2.
- V tomto skripte pridajte do triedy
DoorStatusnovú vlastnosť s názvomopen - Typ tejto vlastnosti bude
bool - Počiatočná hodnota bude
false
Premenné a vlastnosti typu bool môžu mať len 2 možné hodnoty: true alebo false. Zapnuté-vypnuté. Sú ako modrá kontrolka na palubnej doske auta, ktorá indikuje, či máte zapnuté diaľkové svetlá.
Takto nejak teda vyzerá náš DoorStatus:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DoorStatus : MonoBehaviour
{
public bool open = false;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Vďaka komponentu DoorStatus si game objekt Door2 vie pamätať, či je otvorený alebo zatvorený a môžeme podľa toho zostaviť podmienku if a podľa nej prehrať príslušnú animáciu. V triede DoorController:
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");
}
else
{
// Alebo toto
hit.transform.gameObject.GetComponent<Animator>().Play("OpenDoor");
}
}
}
}
Keď toto vyskúšame spustiť, dvierka sa otvoria – ako doteraz. Ale stále sa nezatvárajú. Prečo?
Vlastnosť open má na začiatku hodnotu false. Ale mení sa niekde na true? Počítač sám od seba nevie, že ju má zmeniť na true, keď sa dvere otvoria. Musíme mu na to niekde dať príkaz.
Je viacero prístupov, kde a ako nastavovať vlastnosti podľa animácií. Mohli by sme urobiť skript, ktorý počká, až dohrá animácia „DoorOpen„, a vtedy nastaviť vlastnosť open na true. Alebo priamo do animácie nastaviť, aby po svojom skončení nastavila open na true. Ale nekomplikujme si to.
Na teraz nám postačí, ak do DoorControllera k príkazu na prehratie animácie doplníme príkaz, ktorý túto vlastnosť prepne podľa potreby na true alebo false:
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;
}
}
}
}
Dvierka si už teraz pamätajú svoj stav a DoorController sa podľa toho stavu rozhoduje, či prehrá animáciu na zatvorenie alebo na otvorenie dvierok.
Hotový projekt je tu: CupboardApp_03.zip
