Ahhoz hogy a térben mozgassunk egy GameObject-et elengedhetetlen a 3D-s tér, a vektorok és műveleteik megértése: 2D és 3D vektorok, A 3D-s tér Unity-ben
Minden GameObject
-nek van egy Transform komponense, ami tartalmazza az objektum helyzetét, elforgatását és skálázását (méretezését)
Minden beállítást, amit az Inspector ablakon látunk, azt kódból is elérjük. Ehhez kell legyen egy referenciánk az adott Transform
komponenshez.
Ezt a következő módon tudjuk lekérni:
void Update()
{
Transform t = transform;
}
A fenti kódban egy Transform típusú változóban eltároltam a “saját” Transform-ját a szkriptünknek.
A “saját” Transfom itt azt jelenti, hogy egyazon GameObject-en lévő Transform komponens.
Mivel minden GameObject-nek van Transform-ja, így biztosak lehetünk benne, hogy minden MonoBehaviour
komponenshez tartozni fog egy Transform komponens.
Egy Transform pozícióját lekérhetjük és be is állíthatjuk. Ez minden esetben egy Vektor3 típusú objektum lesz.
Például az alábbi szkript egy nagyon egyszerű dolgot végez: Meggátolja, hogy a transform y koordinátája 0 alá csökkenjen.
void Update() // Minden képfrissítés előtt elvégezzük:
{
Vector3 position = transform.position; // Lekérjük a pozíciót
// (Most a saját Transform-ot nem tettem külön változóba)
if(position.y < 0) // Ha a GameObject lejjebb van, mint 0,
position.y = 0; // akkor állítsuk 0-ra a magasságát.
transform.position = position; // Beállítjuk a pozíciót
}
(Ennek működését Play módban ki lehet próbálni.)
Egyenletes haladás
Most próbáljuk meg egyenletes sebességgel mozgatni a komponenst!
Először is nézzük meg, hogyan tudunk odébb tenni egy objektumot egy fix vektorral. Ehhez a vektor-összeadás műveletét fogjuk használni.
Hogy miért összeadás műveletet használtunk, arról bővebben itt volt szó: 2D és 3D vektorok
Vector3 originalPosition = transform.position;
Vector3 offset = new Vector3(1,2,3); // Eltolás vektora
Vector3 newPosition = originalPosition + offset; // Új pozíció kiszámítása összeadással
transform.position = newPosition;
A fentiek jóval rövidebben is leírhatók a +=
operátorral:
Vector3 offset = new Vector3(1,2,3); // Eltolás vektora
transform.position += offset; // Eltoljuk a pozíciónka egy vektorral
Ezt fogjuk használni az egyenletes sebességgel történő mozgáshoz is azáltal, hogy minden egyes képfrissítés előtt, azaz minden egyes Update
metódusban egy kicsivel eltoljuk a Transform komponens pozícióját a fenti módszerrel.
Ha ezt adott sebességgel és adott irányba szeretnénk megtenni, ahhoz szükségünk van egy sebességvektorra.
A sebességvektor az a vektor, ami leírja egy mozgásnak mind az irányát, mind a mértékét (hosszát) egy adott pillanatra.
Ha a sebességvektor állandó, akkor egy másodperc alatt pontosan a vektornyival kerülne odébb a mozgatott objektum.
Ez a vektor időben folyamatosan változhat. A sebesség mértéke a vektor hosszával egyenlő. Ezen sebesség mértékegysége (Unity hossz egység/másodperc)-ben értendő.
Ezen sebességvektort egyelőre csak egy [SerializeField]
változóban fogjuk beállítani manuálisan:
[SerializeField] Vector3 velocity;
Habár a sebességvektorunk iránya pontosan megegyezik a kívánt iránnyal, ahhoz, hogy a mozgásunk egyenletes legyen és valóban azzal a sebességgel történjen, amit beállítottunk, nem tehetjük minden egyes Update-ben az egész sebességvektorral odébb az objektumot.
Ehelyett frame-enként mindig csak a sebességvektor egy tört részével kell odébb mozognunk. Ezt a tört részt előállíthatjuk a sebességvektor osztásával, vagy egy alacsony számmal történő szorzással.
Vector3 offset;
offset = velocity /= 10; // Sebességvektort tizedét kapjuk meg
offset = velocity *= 0.1f; // ugyanaz szorzással
Na de hányadrészére csökkentsük a vektort, hogy pontosan a kívánt sebességet érjük el? Más szóval azt szeretnénk, hogy az objektum sebessége (egység/másodperc) mértékegységben mérve pontosan akkora legyen, mint a vektor hossza.
Hogy ezt hogy tudjuk elérni, azt itt tárgyaltuk általánosan: Az idő múlása & Update metódus. Most nézzük meg mindezt speciálisan a mi feladatunkhoz, az egyenletes mozgáshoz illesztve!
Tegyük fel, hogy tudjuk, hogy a játékunk pontosan 100 FPS-sel fut, azaz másodpercenként pontosan 100 képfrissítés történik. Ekkor minden képfrissítésben pontosan a sebességvektor egy 100-ad részével kéne odébb tenni az objektumot. Ezáltal egy teljes másodperc múlva az pontosan egy sebességvektornyival kerülne odébb. Épp ezt szeretnénk elérni.
Sajnos nem tudhatjuk előre, milyen gyors lesz a képfrissítés minden egyes lehetséges számítógépen, ami futtatja a játékot. Nem is beszélve arról, hogy az FPS érték folyamatosan változik még egy másodpercen belül is. Ennek ellenére a fenti példa mégis segíthet nekünk a tovább haladásban.
A 100 FPS esetén minden Update-ben vettük a sebességvektor 100-ad részét. Ez azt jelenti, hogy 100-zal osztottuk, vagy 0,01-gyel szoroztuk azt. Ez a szorzó szám, a 0,01 pont meg fog egyezni két képfrissítés közti idővel. Ezt fogjuk felhasználni akkor is, amikor nem lesz ilyen szép kerek állandó FPS érték. Ugyanis azt, hogy mennyi idő telt el az előző képfrissítés óta, azt le tudjuk kérdezni a Time.deltaTime
-mal.
Tehát ha egyenletes képfrissítés független sebességet akarunk elérni, akkor szoroznunk kell az előző képfrissítés óta eltelt idővel.
A fenti képletben a pozíciók valamint a sebesség mind 2D vagy 3D vektorok (Vector2
/Vector3
), az eltelt idő pedig egy skalár, azaz (float
) szám.
[SerializeField] Vector3 velocity;
void Update()
{
transform.position += velocity * Time.deltaTime;
// Minden Update-ben a pozíciót növeljük a sebességvektor irányába.
}
Most érjük el, hogy a sebességvektort a felhasználói input alapján állítsuk elő.
Ezt többféleképpen is megtehetjük. Íme egy példa:
float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");
Vector3 velocity1 = new Vector3(x, y, 0);
// Máshogy is összeállíthattuk volna a sebességvektort. Pl.:
Vector3 velocity2 = new Vector3(x, 0, y); // (X/Z) azaz vízszintes síkban mozgunk.
A felhasználói inputról bővebben itt volt szó: Billentyűzet és Gamepad Input
Ha azt szeretnénk, hogy a játékos pontosan egyenletes sebességgel mozogjon függetlenül az iránytól, akkor a sebességvektornak fix hosszúnak kell lennie, bármilyen irányba is mutat.
Ez a fix sebesség egyelőre legyen pontosan 1 egység. Más szóval egységvektort kell csinálni. Ez könnyen elérhető normalizálással. Ezt a vektort nevezzük pusztán irányvektornak (direction), mivel pontosan egység hosszú azaz hosszinformációt nem tartalmaz, csak irányt.
void Update()
{
float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");
Vector3 direction = new Vector3(x, y, 0);
direction.Normalize(); // Normalizálom a vektort
transform.position += Time.deltaTime * direction;
}
Azért volt szükség erre a fenti esetben, mert ha átlósan haladunk, akkor a vektor x és y koordinátája is külön-külön 1 egység hosszú. Emiatt az egész vektor valamivel hosszabb lett, mint egy egység. (Egészen pontosan hosszú, de ez most nem érdekes.)
Átlós irányban ezért normalizálás nélkül gyorsabban mozognánk, mint a fő irányokban, jobbra, balra, fel vagy le.
Ha sebességet is szeretnénk állítani, akkor fel tudunk venni erre a célra egy float típusú [SerializeField]
beállítást. Ezt a skalár sebesség értéket szoroznunk kell a Vector2
vagy Vector3
típusú irányt reprezentáló egységvektor-ral. Ennek a szorzásnak az eredményeként áll elő a sebességvektor, ami egyszerre tartalmazza a sebesség irányát és mértékét.
void Update()
{
float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");
Vector3 direction = new Vector3(x, y, 0).normalized; // Haladás iránya (normalizált)
Vector3 velocity = speed * direction; // Haladás vektora
Vector3 step = velocity * Time.deltaTime; // Egy frame alatt megtett út
transform.position += step; // Egy lépés
}
Tehát egy szorzás művelettel tudtuk egy skalár (flaot
) hosszból és egy egységvektor irányból előállítani a sebességvektorunk.
A módszer akkor is működik, ha a sebességvektor iránya folyamatosan változik.
Bár a sebességvektor azt írja le, hogy egy másodperc múlva mennyivel lenne eltolva az az objektum, ami ezzel a sebességvektorral halad, ez nem jelenti azt, hogy a másodperc leteltével tényleg oda lyukadunk is ki. Ez csak akkor lenne így, ha nem változtatnánk ez idő alatt a sebességvektoron. Amint változtatjuk a vektort, egyből változik a mozgás is és vele a vártható jövendő beli pozíció.
Ennek a legkisebb egysége, ha Update metódusban végezzük a lépkedést, pontosan egy képfrissítésnyi idő, azaz egy frame. Mindig csak egy frame-nyit lépünk előre.
Más GameObject-ek Transform-jai
Egyéb objektum Transform-jának állapotát is lekérhetjük vagy módosíthatjuk.
Egy másik objektum Transform-jához hozzáférhetünk azáltal, hogy egy [SerializeField]
beállításmezőként felvesszük azt. Ekkor persze manuálisan be kell állítani (be kell kötni) ezt a Transform-ot a megírt komponens Editor felületén.
A jövőben több módszerrel is hozzá fogunk férni más GameObject-ek Transform-jához és egyéb komponenseihez: Unity Objektumok referenciái
A következő szkript például arra fog szolgálni, hogy a GameObject, amihez ezt a komponenst hozzáadjuk, kövesse a megadott célt fix sebességgel.
public class Follower : MonoBehaviour
{
[SerializeField] Transform target; // Követendő cél transform
[SerializeField] float speed; // Sebesség
void Update()
{
Vector3 selfPos = transform.position; // Saját pozíció
Vector3 targetPos = target.position; // Cél pozíciója
Vector3 direction = targetPos - selfPos; // Cél felé mutató vektor
direction.Normalize(); // Normalizálás (hossz: 1)
float stepDistance = speed * Time.deltaTime; // Lépés hossza egy frame-ben
transform.position = direction * stepDistance; // Pozíció frissítése
}
}
Ennek a szkriptnek az lesz a hibája, hogy túl tud futni a célon. Ahhoz, hogy ezt megoldjuk, le kell tesztelni, hogy a lépés hossza nagyobb-e, mint a két objektum távolsága és ha igen, akkor csökkenteni a lépés méretét.
void Update()
{
Vector3 selfPos = transform.position; // Saját pozíció
Vector3 targetPos = target.position; // Cél pozíciója
Vector3 velocity = targetPos - selfPos; // Cél felé mutató vektor
float distance = velocity.magnitude; // Cél távolsága
velocity.Normalize(); // Normalizálás (hossz: 1)
float stepDistance = speed * Time.deltaTime; // Egy lépés távolsága egy frame-ben
if (distance < stepDistance) // Ha a cél közelebb van,
stepDistance = distance; // lépés távolságát felülírjuk
transform.position = velocity * stepDistance; // Pozíció frissítése
}
A fentire fogunk egy egyszerű alternatívát is tanulni: A Towards függvények
Transform.Translate (Kiegészítő anyag)
Egy alternatív módja is van Transform eltolásának: a Transform komponens Translate
metódusa, ami paraméterben várja az eltolás vektort.
Vector3 offset = speed * Time.deltaTime * direction;
transform.position += offset; // Ez a sor gyanazt teszi, ...
transform.Translate(offset, Space.World); // ... mint ez a sor.
Egy opcionális paramétere is van a Translate
metódusnak, ebben megmondhatjuk, hogy globális vagy lokális térben történjen a mozgás. Ezt később tanuljuk: A lokális és globális tér. Ha nem írjuk ki külön ezt a paramétert, akkor automatikusan a lokális térben fog történni a mozgás.
Addig a pontig, amíg még nem értitek a különböző terek közti különbséget, nem is javaslom, hogy a Translate
metódussal mozogjatok. (Ami azt illeti utána sem nagyon.)