Mikor mi úgy gondoljuk egy helyben állunk, akkor igazából ez csak abban a koordinátarendszerben igaz, amiben a Föld bolygó az alap. Ha a naprendszert választjuk alapul, akkor a föld tengelye körül folyamatosan forgunk, ha a galaxist, akkor még a naprendszeren belül is keringünk a csillagunk körül. Ha viszont képzeletbeli szemeinkkel a Galaxison kívülről nézzük le magunkra, akkor még annak a forgása is bonyolítja a pályát, amin haladunk.
Ebben a példánkban a lokális (földhöz képesti) pozíciónk nem változik, de a globális (univerzumon belüli) térben vett mozgásunk nagyon is összetett.
Ezáltal érzékelhető, hogy többféleképp választható meg a koodinátarendszer és az is, hogy nem feltétlenül mindig ugyanabban hasznos gondolkodni.
Szülő-gyerek kapcsolat GameObject-ek közt
A 3D-s szoftverek fontos fogalma az objektumok hierarchiája, azaz szülő és gyerek kapcsolata.
Ha egy Alfa objektumot a Hierarchy ablakban egy másik objektumra, Bétára ráhúzol, akkor Bétához kerül egy kis legördülő menü és Alfa ezen menün belül, Béta alatt, egy tab-bal beljebb fog elhelyezkedni. Teát Alfa a rész lesz Bétának. Béta tartalmazni fogja Alfát.
Ezt más fogalmakkal úgy is mondhatjuk, hogy Alfa objektum Beta gyereke lesz, és ugyanígy Beta objektum pedig szülője lesz Alfának. Senki ne gondoljon bele túl sokat ebbe az elnevezésbe. Mindennek semmi köze nincs a valós életben megszokott rokoni kapcsolathoz azon kívül, hogy minden objektumnak lehet bárhány gyereke és legfeljebb egy szülője. “Anya csak egy van”.
Ez ismételhető bármilyen mélységben. Azokat az objektumokat, aminek nincs szülőjük gyökér objektumoknak hívjuk.
Így egy faszerkezet épül fel a GameObject-ekből, olyanformán akár egy mappaszerkezet.
A fenti csillagászati példa esetében valahogy így nézne ki a Unity hierarchia:
Egy gyerek objektum “örökli” a szülője Transfom komponensének tulajdonságait. Ez azt jelenti, hogyha mozgatjuk, forgatjuk vagy méretezzük a szülőt, akkor a gyerek is mozogni, forogni és méreteződni fog a szülővel együtt.
Ha forog a föld vele forgunk. Ha odébb mozok, akkor megyünk vele mi is.
Talán úgy a legkönnyebb megérteni mindezt, hogy építünk egy pálcika-embert Unity-ben az objektumokból. A karakterünk teste a gyökér abból nőnek ki a végtagok és a fej, tehát ezek a gyerekei lesznek. A kézből nő ki az alkar, abból a tenyér és abból az ujjak, szóval az újak lesznek a legutolsó gyerekek, amik senkinek nem a szülei. Ha az egész embert mozgatni vagy forgatni szeretnénk a test objektumot mozgatjuk. Ekkor azt szeretnénk, hogy minden tagja vele együtt mozogjon. Ugyanez igaz arra, ha csak egy karját akarjuk mozgatni mondjuk egy integetés animálására. Ekkor nem akarjuk, hogy a test vele mozogjon, da az alkar, tenyér és ujjak igen.
Globális és Lokális tér
Ahogy azt már tárgyaltuk a Unity Scene-nek van egy origója és három speciális iránya, az X, Y és Z tengelyek. Mindez minden objektumra egyformán igaz. Ezt nevezzük globális térnek vagy világkoordinátarendszernek.
Ha egy szülő mozog, forog vagy méreteződik, a gyereke globális koordinátája vele változik, de a lokális koordinátája nem, mert a pozíciója, orientációja és mérete a szülőhöz képest nem változott.
Egy objektum lokális terének tengelyei sem feltétlenül egyeznek a globáliséval Ez akkor van így, ha a szülő el van forgatva egy vagy több tengely mentén.
A Scene ablak jobb felső sarkában lehet váltani, hogy a szerkesztőeszközök (mozgató, forgató és méretező gizmók) lokális vagy globális térben értelmezettek. Ezt az X gyorsbillentyűvel is tudjuk váltogatni oda és vissza.
Kódból egy Transform komponens minden tulajdonságának (pozíció, forgatás és skálázásnak), elérhető globális és lokális verziója is. Ezen tulajdonságok ugyanígy beállíthatók is kódból (egy kivétellel).
// Lekérdezés
Vector3 localPosition = transform.localPosition;
Vector3 position = transform.position;
Vector3 localScale = transform.localScale;
Vector3 lossyScale = transform.lossyScale;
Quaternion localRotation = transform.localRotation;
Quaternion rotation = transform.rotation;
// Beállítás
transform.localPosition = new Vector3 (1,2,3);
transform.position = new Vector3 (1,2,3);
transform.localScale = new Vector3 (1,2,3);
transform.lossyScale = new Vector3 (1,2,3); // Ilyen nincs! Ez nem álítható kódból
transform.localRotation = new Vector3 (1,2,3);
transform.rotation = new Vector3 (1,2,3);
Érdemes fejben tartani, hogy a lokális értékek lekérdezése és beállítása mindig jóval gyorsabb a globálisoknál. A Transform komponens mindig lokális értékeket tárol. A globális pozíciót, rotációt és skálázást mindig lekérdezéskor számolja ki a játékmotor.
Váltás lokális és globális terek közt
Egy objektum lokális előre iránya nem feltétlenül egyezik meg a globálissal.
Egy objektum fő lokális irányai (jobbra, fel, előre) viszont könnyen lekérdezhetők annak Transform komponensétől úgy, hogy az eredmény a globális térben értelmezett.
Ez akár gizmókkal ki is rajzolható a következőképp:
void OnDrawGizmos()
{
Vector3 pos = transform.position;
Vector3 right = transform.right; // Transform lokális jobbra iránya glob. térben
Gizmos.color = Color.red;
Gizmos.DrawLine(pos, pos + right);
Vector3 up = transform.up; // Transform lokális felfele iránya glob. térben
Gizmos.color = Color.green;
Gizmos.DrawLine(pos, pos + up);
Vector3 forward = transform.forward; // Transform lokális előre iránya glob. térben
Gizmos.color = Color.blue;
Gizmos.DrawLine(pos, pos + forward);
}
A fő irányokon (jobbra, fel, előre) kívül bármi egyebet is könnyedén át lehet konvertálni a lokális térből globálisba és visszafelé globálisból lokálisba is:
// Transform egy lokális irányának a globális térbe transzformálása
Vector3 worldDirection = transform.TransformDirection(new Vector3(1, 3, -2));
// Egy globális irány Transform lokális terébe transzformálása
Vector3 localDirection = transform.InverseTransformDirection(new Vector3(1, 3, -2));
Nem csak irányt, de vektort és pozíciót is lehet oda-vissza alakítani terek közt:
// Ha a szülők méretezése is számít:
// Transform egy lokális vektor a globális térbe transzformálása
Vector3 worldVector = transform.TransformVector(new Vector3(1, 3, -2));
// Egy globális vektor Transform lokális terébe transzformálása
Vector3 localVector= transform.InverseTransformVector(new Vector3(1, 3, -2));
// Ha a szülők méretezése és pozíciója is számít:
// Transform egy lokális pontjának a globális térbe transzformálása
Vector3 worldPosition = transform.TransformPoint(new Vector3(1, 3, -2));
// Egy globális pont Transform lokális terébe transzformálása
Vector3 localPosition = transform.InverseTransformPoint(new Vector3(1, 3, -2));
Hol lehet ennek haszna?
Hát, sok helyen... Vegyünk pár példát.
Képzeljük el, hogy egy “third person”, azaz külső nézetű játékot írunk. Ekkor a kamera a játékost kívülről mutatja. Sokféleképp meg lehet írni third person kamerát. Általában ekkor a játékos háta mögül nézzük őt, de ez nem mindig van így.
Az viszont minden esetben igaz, hogy a kamera nézet nem fog feltétlenül egy irányba állni a globális előre iránnyal. Ha ekkor megnyomjuk az “előre haladás” gombot, ami hatására a globális térben értelmezett előre irányba mozgatjuk a karaktert, az egyáltalán nem fogja azt jelenteni, hogy a mi nézetünkhöz képest is előre fog elindulni a játékos.
float speed = 5;
Vector3 forward = Vector3.forward; // GLOBÁLIS előre irány
Vector3 velocity = forward * speed; // Sebesség vektor
transform.position += velocity * Time.delaTime; // Haladás
Ehelyett a kamerához képesti előre irányt kéne venni a következőképp:
float speed = 5;
Vector3 forward = Camera.main.transform.forward; // A kamera LOKÁLIS előre iránya
forward.y = 0; // Függőleges kompones elhagyható (Nem mindig kéne)
forward.Normalize(); // Ekkor viszont normalizálni kell, hogy újra egységvektort kapjunk
Vector3 velocity = forward * speed; // Sebesség vektor
transform.position += velocity * Time.delaTime; // Haladás
Ekkor bárhogy is áll a kamera, ahhoz képest fogunk előre elindulni ezáltal a képernyőn is azt látjuk, amire intuitívan számítanánk: A játékos távolodik tőlünk. Ugyanez alkalmazható minden más irányra is, nem csak az előrére.
Vegyünk egy másik példát!
Van egy ágyúm, a csöve felfelé néz. Ezt valahogy elforgatjuk a 3D térben. Az ágyúgolyó sebességvektora ekkor úgy fog előállni, hogy az ágyú lokális felfelé irányát globális térbe konvertáljuk.
Ezt megtudjuk tenni egy TransformDirection
függvényhívással az ágyú Transform komponensén, de mivel egy fő irányról van szó, lekérdezhető még egyszerűbben is a up
property-vel.
Tranform cannonTransform;
// ...
Vector3 bulletDirection = cannonTransform.up;
// Alternatív megoldás, ami nem csak fő irányokra működik:
Vector3 localDir = new Vector3 (0,1,0);
bulletDirection = cannonTransform.TransformDirection(localDir);
Transzformációs mátrixok (Kiegészítő anyag)
A transzformációs mátrixok és azok matematikája túlmutat jelen kurzus anyagán. Annyit érdemes tudni róluk, hogy 3D grafikában egy lokális teret egy transzformációs mátrixszal reprezentálunk. A Unity is ezt teszi a háttérben.
Minden Transform komponens lokális beállításai egy transzformációs mátrixot határoznak meg. Egy transzformációs mátrix egyszerre tartalmaz egy eltolást, forgatást és skálázást.
Ezen transzformációs mátrixok matematikai objektumok és definiált rajtuk a “szorzás” művelet.
Ha egy gyerek objektum lokális terét ki akarjuk számolni, akkor magának és összes szülőjének transzformációs mátrixát kell összeszorozni egymással. Ezt végzi a Unity a háttérben.
A globális tér mátrixa az identitás: Matrix4x4.identity
.
(Az identitás a különböző matematikákban az az elem, amivel történő szorzás nem változtat az eredeti értéken. Az egyszerű számokon ez az érték az 1, mivel X (bármi) szorozva 1-gyel, az X.)
Gizmók és terek (Kiegészítő anyag)
A Gizmók kirajzolása globális térben történik, ám néha kényelmesebb lenne lokális térben rajzolni ki dolgokat. Sokszor így meg lehetne spórolni sok-sok átszámolást a terek közt. Ez megoldható azáltal, hogy átállítjuk a Unity Gizmo rajzoláshoz használatos transzformációs mátrixát. Ezt a következő módon tudjuk megtenni:
void OnDrawGizmos()
{
Gizmos.DrawWireSphere(Vector3.zero, 1); // Rajzolás globális térben
Gizmos.matrix = transform.localToWorldMatrix; // Átállítjuk a rajzolást lokális térbe
Gizmos.DrawWireSphere(Vector3.zero, 1); // Rajzolás lokális térben
Gizmos.matrix = Matrix4x4.identity; // Visszaállítjuk globális térbe
Gizmos.DrawWireSphere(Vector3.zero, 1); // Rajzolás újra a globális térben
}
Fontos, hogy a művelet végén ne felejtsük el visszaállítani a mátrixot az eredeti globális térre, mert ha ez elmarad, azzal más komponensek Gizmóit elronthatjuk.