Ahogy minden egyéb játékmotor a Unity is indulásától kezdve egy nagy ciklusba kerül, ami addig fut, amíg le nem állítjuk a programot. Ezen ciklus minden egyes végrehajtásában renderel a motor egy képkockát.
Azt, hogy ez másodpercenként hányszor történik meg az FPS (Frame Per Second) mérőszám adja meg. Minél magasabb az FPS, annál rövidebb idő kell egy kép rendereléséhez.
Hogy a render ciklus hányszor fut le másodpercenként az változó. Vannak olyan gameengine-ek, amik fix képfrissítéssel dolgoznak, ám a legtöbb modern motor változó sebességgel működik. A Unity is ebbe a kategóriába tartozik. Ekkor a számítógép kapacitása, a monitor frissítési sebessége és egyéb korlátozó tényezők döntik el milyen gyakran fut le a ciklus. Ebben az esetben két ciklusvégrehajtás ideje eltérhet egymástól.
Az Update metódus
Egy játék egy erősen dinamikus szoftver. Az objektumok folyamatosan mozgásban vannak, reagálnak a felhasználó inputjára és egyéb játékbéli történésekre.
Az eddig tanult módszerekkel ezt nem tudjuk megoldani. Ha egy metódus elindult addig, amíg az fut, semmi más nem történik. Ezt láthatjuk, amikor végtelen ciklussal lefagyasztjuk az egész projektet.
Ahhoz hogy, ne csak a játék indításakor, de utána is folyamatosan lefussanak bizonyos részei a kódunknak az Update
metódust tudjuk használni.
Az Update
a Start
-hoz hasonlóan egy Unity specifikus MonoBechaviour üzenetmetódus. Ezek közös jellemzője, hogy a Unity automatikusan lefuttatja őket bizonyos események hatására minden betöltött scene minden GameObject-jének, minden komponensére.
Update
metódust a Unity meg fogja hívni minden képfrissítés előtt egyszer Ahhoz, hogy mindez megtörténjen fontos, hogy
- MonoBehaviour sckriptben írjuk meg a metódust,
- megfelelően adjuk meg a típusát, nevét és paramétereit,
- társítsuk a komponenst egy GameObject-hez a betöltött Scene-ben,
- valamint, hogy a GameObject és a komponens is bekapcsolt állapotban legyen.
void Update()
{
Debug.Log($"GameObject: {name} is updated!");
}
Az Update
, ahogy a Start
is egymás után fut le minden komponensre és ennek a sorrendje előrejelezhetetlen. Ez nem teljesen igaz, de erről később: MonoBehaviour-ok életciklusa
Mindig igyekezzünk olyan architektúrát tervezni, amit nem befolyásol a GameObject-ek végrehajtási sorrendje.
Képzeljük el, hogy pontosan 60 FPS-re készít valaki játékot. És van egy tárgy, amit egyenletes, beállítható sebességgel szeretnénk mozgatni az x tengely mentén.
Ekkor minden Update
-ben kiszámolhatjuk és beállíthatjuk az új x pozíciót:
[SerializeField] float speed; // Sebesség egység/másodpercben (unit/s) megadva
// Field (lokális változó) arra, hogy eltároljuk az aktuális x pozíciót:
float xPosition = 0; // A field-ek értéke nem veszik el különböző metódusok hívása közt.
void Update()
{
float movement = speed; // Másodpercenként ekkora elmozdulást kell megteni.
movement *= 1/60f; // Ennek egy Update metódusban csak az 1/60-ad része kell.
xPosition += movement; // Növeljük a pozíciót.
}
Így, vagy ehhez hasonló módon működik nem csak az egyenletes mozgás, de bármilyen érték folyamatos módosítása egy játékmotor alatt. Egy képfrissítési ciklus minden végrehajtásakor egy kicsit változtatunk a játékon és az új megváltoztatott jelenetből készítünk képet.
Ezen módosítások lehetőségei nagyon széles skálán mozognak, de kezdetben koncentráljunk csak az egyenes vonalú egyenletes mozgásra.
Time.deltaTime
A gond az, hogy az, hogy másodpercenként hányszor fut le az Update
az előrejelezhetetlen, a képfrissítési sebesség, az FPS értéke fogja szabályozni.
Ha számításigényesebb a játék vagy gyengébb hardware-en fut, akkor kisebb lesz a z FPS szám, ha egyszerűbb a játék vagy erősebb gépen fut, akkor magasabb. Eszerint előbbi esetben az Update is ritkábban fog megtörténni, míg utóbbiban gyakrabban.
Érzékelhető tehát, hogy emiatt ha a játékunk 60 helyett 120-szor frissít képet egy másodpercben, akkor a tárgy dupla akkora sebességgel fog haladni, mint a beállított, és ha 30 FPS-sel fut csak, akkor fele akkorával.
Jelen példában az (1/60)
azt írja le, hogy mennyi idő (hány másodperc) telik el két képfrissítés közt. Legalábbis erre számítunk. Ha a képfrissítések közti idő nem tudjuk előre sőt folyamatosan változik, ahogy az a valós életben történni is szokott, nem használhatunk egy fix számot.
Ehelyett a Time.deltaTime
-t kell használni. Ezzel az előző képfrissítés óta eltelt időt tudjuk lekérni másodpercben mérve.
Lásd a példában:
Ha gyakrabban történik képfrissítés, kisebb a Time.deltaTime
.
// Sebesség (unit/s):
[SerializeField] float speed;
// Az aktuális x pozíciót:
float xPosition = 0;
void Update()
{
float movement = speed * Time.deltaTime;
xPosition += movement; // Növeljük a pozíciót.
}
A jövőben ez lesz az ökölszabály. Minden olyan változásra, amit szeretnénk, hogy időben egyenletes legyen (egyenletes mozgás, egyenletes forgás, egyenletesen növekvő életcsík…) mindig szorozzunk Time.deltaTime
-mal, az Update
metódusban!
Komponens “kikapcsolása”
Ha az adott GameObject vagy a rajta lévő komponens kikapcsolt állapotban van, akkor nem fog meghívódni sem a Start sem az Update metódus.
Később bővebben és pontosabban tárgyaljuk ezt:
A LateUpdate metódus
Létezik egy másik Unity MonoBehaviour üzenetmetódus, ami képfrissítésenként mindig lefur egyszer minden komponensre, ez a LateUpdate
. A működése nagyon hasonló az Update
-éhoz. Az egyetlen különbség, hogy a LateUpdate
később történik. Egészen pontosan:
Garantált, hogy minden komponens Update
függvénye le fog futni, mielőtt az első komponens LateUpdate
függvénye lefutna.
Ez sok helyen hasznos lehet. Pl. ha a kamera követi a játékost, akkor a kamera játékost követő kódjától azt szeretnénk, hogy a játékos helyváltoztatása után fusson csak le. Különben a kamera mindig egy frame-es késésben lenne. Ez megoldható úgy, hogy a játékos mozgatását végző kódot annak Update
-jébe, míg a kamera mozgatását végző kódot annak LateUpdate
-jébe tesszük.
Time és TimeScale
Ha a játék kezdete óta eltelt időre vagyunk kíváncsiak, azt a Time.time
-mal tudjuk lekérni.
Ennek és minden egyéb Unityb-en időre értelmezett mérőszám szintén másodpercben értendő.
Lehetőség van arra, hogy a játékidő múlását meggyorsítsuk vagy lelassítsuk. Ehhez a Time.timeScale
statikus property-t kell módosítanunk. Ez egy szorzóérték, ami azt adja meg, hogy az idő a játékban milyen gyorsan fog telni a valós idő sebességéhez képest. Ha 1-nél nagyobb értéket adunk neki a játékidősebessége gyorsabban fog telni, ha alacsonyabbat, akkor lassabban.
A timeScale
az egész játékra vonatkozóan megegyezik. Nem rendelhetünk egyes scene-ekhez vagy gamObject-ekhez saját egyedi -t. Ha ehhez hasonló működést kívánunk elérni, akkor azt magunknak kell leprogramozni.
A Time.timeScale
többek közt módosítja a lekérhető Time.deltaTime
értékét is. Ha egy kódban kiváncsiak vagyunk az előző Update
hívás óta eltelt valódi időre, akkor a Time.unscaledDeltaTime
lekérdezést használhatjuk.
A képfrissítési sebesség korlátozása (Kiegészítő anyag)
Az Update metódus és a renderelés vagy képfrissítés szinkronban van egymással. Minden Update után új képet renderelünk mielőtt a Unity meghívná a következőt.
Az, hogy ez milyen sebességgel történik alapvetően a játékot futtató hardware szabja meg.
A rendereléshez szükséges számítások oroszlánrészét a videokártya végzi, míg az általunk megírt kódot teljes egészében a processzor.
Ha korlátozni szeretnénk a renderelés sebességét használhatjuk a VSync beállítást
Ezt a projekt minőségi beállításai közt találjuk: Felső menüsáv / Edit / Project Setting / Quality / VSync
A VSync bakapcsolása azt eredményezi, hogy a játék frissítését megpróbálja szinkronbahozni a monitor frissítési sebességével. Ha van egy 120 FPS-es monitorod, akkor a max képfrissítés 120 lesz.Ehhez persze kell az is, hogy a monitorod támogassa a technológiát.
A VSync használtának tehát az egyik előnye, hogy kevesebbet kell a számítógépnek dolgoznia, nem renderelünk akkor, ha nem tudjuk azt megjelen íteni.
Ezen felül a VSync kiküszöbölheti azt a jelenséget, amikor két félig renderelt kép látszik a képernyőn egy vízszintes határvonallal elválasztva.
Ez azért van, mert a játék a memória egy speciális területébe, a rasztertárba írja bele a megjelenítendő képadatokat a renderelés után. Innen olvassa ki azt a monitor.
Ha nincs szinkronban a játék és a monitor frissítése, akkor lehet, hogy a monitor megjelenít egy képet, aközben, hogy az újonnan renderelt kép másolása a rasztertárba még éppen rajlik és csak részben lett felülírva az előző frame.
Ezen felül kódból manuálisan is be lehet állítani még, hogy mi a cél frame-rate (FPS).
Application.targetFrameRate = 30; // 30-ra korlátozom az FPS-t
Ha ezt meghívod a futás elején pl. egy Start
metódusban valahol, akkor a Unity nem fogja a beállított érték fölé engedni a Framerate-et a játékban akkor sem, ha a számítógép képes lenne rá.
Ennek beállítása természetesen az Update metódusok hívási gyakoriságára is hatással lesz.
Ettől függetlenül, ha kell, akkor mindig a Time.deltaTime
-mal szorozzunk továbbra is.