Sorting Layer
Korábban már beszéltünk a Sorting Layer-ekről a 2D képalkotás-ról szóló leckében.
Láttok, hogy a Sorting Layer és egy rétegen belül az Order in Layer határozza meg, hogy milyen sorrendben legyenek kirajzolva az elemek rendereléskor. Ennek haszna főleg a 2D grafikában van, ahol csak objektumonként végzünk mélységvizsgálatot. Ha az egyik objektum közelebb van a kamerához, mint egy másik, akkor az előrébb lesz rajzolva. Ezzel szemben a 3D mesh-ek kirajzolásánál pixelenként végez mélységvizsgálatot a renderer, és nincs szükség extra módon szabályozni a kirajzolás sorrendjét.
Ha módosítani akarjuk egy SpriteRenderer Sorting Layer-ét azt egy legördülő menüben tehetjük meg a komponens beállításai közt. Itt láthatjuk, hogy felvehetünk új Sorting Layer-eket is. Ha erre a lehetőségre kattintunk, az megjelenít egy menüt ahol a Tag-eket, Layer-eket és SortingLayer-eket tudjuk beállítani. Ez a lehetőség a Project Settings-ben is elérhető. Ez is mutatja, hogy ezen beállítások egész projekt szintűek, nem csak egy Scene-re vonatkoznak.
Tetszőleges számú Sortink Layert vehetünk fel, és azon belül is bármilyen Sorting Order számot adhatunk egy megjelenített elemnek.
Layer
Azért tekintettük át újra a Sorting Layer fogalmát, mert ahogy azt a fent említett menüben már láthattuk is, a Unity használ egy nagyon hasonló nevű fogalmat valami egészen másra, ez szimplán a Layer (vagy a réteg) nevet viseli.
Minden Unity GameObject tartozik egy és pontosan egy réteghez. Ez egy frissen létrehozott objektum esetében a Default, azaz alapértelmezett réteg.
A beállítást meg lehet tekinteni a GameObject Instpector ablakának jobb felső sarkában, ahol változtatni is tudunk a beállításon egy legördülő menüből válogatva a lehetőségek közül. Itt hozzá is tudunk adni új rétegeket a projekthez.
Feltűnhet, hogy legfeljebb 32 réteget tudunk felvenni egy projektben, egyel sem többet. Ennek speciális technikai oka van, amiről ugyanezen leckében később a kiegészítő anyagok közt olvashatsz.
A rétegek igazából csak int számok, de a 32 réteget többségét mi magunk evezhetjük el minden projektben.
Rétegek maszkolása
Készíthetünk egy úgynevezett rétegmaszkot is, ami a rétegek egy csoportját reprezentálja. Ehhez használjuk a Unity beépített típusát a LayerMask
-ot. Ez akár szerializált mezőként is működik, ekkor egy legördülő menüben tudjuk összeválogatni mely layer-ek legyenek a maszk résezi és melyek ne.
[SerializeFiled] LayerMask mask;
Megállapítható, hogy egy réteg egy maszkon belül van-e. Ez a művelet kivételesen gyorsan elvégezhető és ezért is-ok beépített Unity funkció használja a maszkokat arra, hogy szűrje a rétegek egy bizonyos csoportját. Különösen a fizikai szimuláció és renderelés közben támaszkodik a motor a rétegmaszkolásra, mivel ezek a folyamatok különösen erőforrásigényesek, úgyhogy minden apró optimalizáció sokat számíthat.
Például minden kamerára beállítható egy úgy nevezett “Culling mask”. Ez azon rétegek felsorolása lesz, amit a kamera megjelenít. Minden olyan objektumot, aminek rétegét nem tartalmaza a maszk, a kamera a rendereléskor “kiselejtez”. Ehhez hasonló maszk tartozik minden fényforráshoz, azaz light komponenshez is. Ami nincs benne ebben a maszkban, arra nem fog hatni a fényforrás.
Az is layer-enként határozható meg hogy mely objektumok ütközzenek és melyek ne. A fizikával kapcsolatos beépített felhasználási módokat később tárgyaljuk bővebben: Pl.: Raycast, Ütközés: Collider-ek és Trigger-ek
Maszkolás manuálisan (kiegészítő anyag)
A műveletet, mi magunk is el tudjuk végezni manuálisan, azonban nem ajánlom, hogy túlzottan hagyatkozzunk a rétegek egyedi saját célra történő felhasználására, ha csak valami nagyon jó okunk nincs rá. Ez a bizonyos nagyon jó ok általában csak az optimalizálás.
Mivel a Unity már így is több mindenre használja a ugyanazt az egy dolgot, a rétegeket, nem szerencsés, hogy még több funkciót adjunk ennek az egy beállításnak. Mindez különösen igaz annak tükrében, hogy a rétegek elégé sok technikai gyökerű korlátozással járnak:
- Maximum 32 lehet belőlük egy projektben. (A Unity ebből is lefoglal párat)
- Egy GameObject-hez csak egy réteg tartozhat.
- A rétegek igazából csak int számok, ezért nem tudjuk referenciaként tárolni őket.
Ezek együtt ahhoz vezethetnek, hogy ha túlzottan hagyatkozunk a rétegekre, akkor a projektünk merevvé és nehezen karbantarthatóvá válik.
Miután tisztáztuk a rétegmaszk manuális használatának veszélyeit, mégsem kizárt, hogy mégis szeretnénk kihasználni a művelet sebességéből adódó előnyt. Ekkor a következő műveletet tudjuk használni, ami megállapítja egy rétegről, hogy egy maszk része-e:
LayerMask mask;
uint layer; // Előjel nélküli int
//...
bool doMaskConstinLayer;
doMaskConstinLayer = mask.value & layer != 0; // Megoldás bitenkénti ÉS-sel
doMaskConstinLayer = mask.value | layer == layer; // Megoldás bitenkénti VAGY-gyal
Ennek technikai részleteiről később.
A LayerMask.NameToLayer()
metódussal kérdezhetjük le kódból, hogy egy réteg névhez, melyik int
szám tartozik:
string layerName = "Name of the Layer";
int layer = LayerMask.NameToLayer(layerName);
Tag-ek
(Ejtsd: teg-ek)
A tag-ek egy újabb módja annak, hogy valami extra identitást rendeljünk egy GameObject-hez. Ezen identitás alapján tudunk keresni objektumokat. Mondjuk felvehetünk egy “Enemy” (ellenség) tag-et. Ezután bizonyos GameObject-eket megjelölhetek ezzel a tag-gel. Végül lekérhetjük az összes ellenség objektumot a betöltött játékban a GameObject.FindGameObjectsWithTag
metódussal.
Ennek mikéntjéről nem is kívánok itt több szót ejteni, ugyanis a Tag-eket egyáltalán nem tartom hasznos eszköznek a e Unity-n belül. Véleményem szerint történelmi oka van csak annak, hogy a szoftver részei.
Van néhány beépített Unity tag, amik közül néhány módosítja a GameObject viselkedését:
- MainCamera: Ha legalább egy kamera GameObject meg van jelölve ezzel a tag-gel, akkor a
Camera.main
statikus lekérdezéssel bárhonnan elérhető. - EditorOnly: Ez ezzel megjelölt objektumok a végleges Build-ben törlődnek.
A többi beépített tag-et itt nem sorolom fel, ugyanis a Unity nem vette a fáradságot, hogy dokumentálja őket. És úgy tűnik, hogy semmi hatásuk nincs a játékra. Ez is arra enged következtetni, hogy a Unity sem javasolja a használatukat, még ha ezt nem is jelentik ki explicit módon.
Bővebben itt írok arról, hogy miért nem tartom szerencsésnek a Tag-ek használatát és mit javaslok helyette: GameObject-ek azonosítása
Alternatíva: Tag-elés ScriptableObject-tel
Rétegek és maszkok technikai háttere (Kiegészítő anyag)
A két kérdésnek, hogy miért tud olyan gyors lenni a layer-ek maszkolása, és miért csak épp 32 lehetséges értéket vehet fel egy réteg, ugyanaz a technikai gyökere.
Egy réteget egy előjel nélküli integer szám tárol, ami 32 bitből áll.
Mikor egy hagyományos egész számot és nem pedig egy réteget tárolunk el int típusú változóban, akkor az a szám felvehet, különböző értéket. Ez azért van így mert egy egész számot úgy tárolunk el a memóriában, hogy a kettes számrendszertben leírt számjegyei egy-egy bitet foglalnak el. Ez egy nagyon magas érték. Ha az int előjeles verzióját nézzük, akkor az el tud tárolni minden egész számot -2147483648 és 2147483647 közt. Ez előjel nélküli uint
esetben 0-tól 4,294,967,295.
Ez jóval több mint a maszk által felvehető 32 érték. Ennek oka, hogy a réteg más logikával működik, mint egy sima int változó. Rétegek esetén az int mind a 32 bitjéhez tartozik egy és pontosan egy réteg. Ha egy adott réteget tárolunk el egy int változóban, akkor a 32-ből csak az ehhez a bizonyos réteghez tartozó bit lesz 1 és az összes többi pedig 0. Például, ha a 17-es layer-t ábrázolom, akkor a 32 bit közül pontosan a 17-es indexű veszi csak fel az 1-et, minden egyéb 0 lesz.
A maszkot szintén 32 biten ábrázoljuk, és szintén minden rétegnek megfelel egy bit. Míg egy réteg pontosan egy bitje 1-es, addig egy maszkban tetszőleges számú bit veheti fel az pozitív értéket. Ha egy rétegmaszk például az 1-es, 2-es és 13-as réteget tartalmazza, akkor ezek a bitek veszik fel az 1-et, a többi pedig nulla marad.
Egy layer-t és egy layermask-ot tehát egyaránt egy 32 bites int tartalmaz. Végrehajthatunk bizonyos műveleteket ezen adatok közt, amit bitenkénti-és valamint bitenkénti-vagy műveletnek nevezzük. Ez nem egyezik meg a logikai ÉS és VAGY műveletekkel. Ebben az esetben a processor az első változó minden egyes bitjét függetlenül összehasonlítja a másik változó minden egyes bitjével egy és/vagy művelet alapján. Az eredmény összes bitje ezen független összehasonlításokból áll össze. Példák:
01100101 ÉS
11001011 =
01000001
01100101 VAGY
11001011 =
11101111
Bitenkénti ÉS művelet: Az a bit lesz 1, ahol mindkét bemenet is 1-es volt. Operátor: &
Bitenkénti VAGY művelet: Az a bit lesz 1, ahol legalább egy bemenet 1-es volt. Operátor: |
Ha ezen adatok egyike egy layer és másik pedig egy maszk, akkor könnyen eldönthető, hogy az adott maszk tartalmazza-e a layer-t.
Layer: 00000100 ÉS
Maszk: 11001101 =
Eredmény: 00000100
Benne van a layer a maszkban, ha az eredmény nem nulla.
Layer: 00000100 VAGY
Maszk: 11001101 =
Eredmény: 11001101
Benne van a layer a maszkban, ha az eredmény egyezik a layer-rel.
Unity kód:
LayerMask mask;
uint layer; // Előjel nélküli int
//...
bool doMaskConstinLayer;
doMaskConstinLayer = mask.value & layer != 0; // Megoldás bitenkénti ÉS-sel
doMaskConstinLayer = mask.value | layer == layer; // Megoldás bitenkénti VAGY-gyal
Tehát szükségünk van egy logikai bitműveletre valamint egy összehasonlításra és már meg is mondtuk, hogy a maszk része-e a réteg. Ez nagyon gyorsan elvégezhető, mivel mindegyik fenti művelet megtalálható a legtöbb processzorarchitektúra utasításkészletében. Ezáltal tud a Unity fizikai és render motorja másodpercenként akár sok millió maszkolást is elvégezni.