Komponensek és GameObject
Mikor írunk egy Unity MonoBehaviour scriptet, szinte mindig szükségünk van arra, hogy abból a kódból kommunikáljunk egy másik GameObject-tel vagy komponenssel főleg metódushívások, formájában vagy a egyes paraméterek beállításaán keresztül. Ez megtehető, ha ismerjük azt a bizonyos másik GameObject vagy komponens referenciáját, amivel kommunikálni szeretnénk.
A referencia nem más mint egy mutató az adott dologra. Amikor egy osztály típusú változónk van, akkor az mindig csak a referenciát tartalmazza az objektumra és nem az adatot magát. Ezeket nevezzük referencia típusoknak.
Bővebben itt tanulunk majd erről: Referencia- és értéktípusok
A GameObject és minden kompones referencia típus.
Ha egy olyan egyszerű scriptet veszünk például, mint egy egyenletes sebességgel mozgató szkript, még ott is muszáj, hogy legyen egy referenciánk az egyazon GameObject-en lévő Transform komponensre.
Ebben az esetben a Transform komponens referenciájához egyszerűen úgy férünk hozzá, hogy leírjuk a transform
szót. Ehhez hasonló módon egy MonoBehavior saját GameObject-jéhez is ilyen könnyedén férünk hozzá.
Transform selfTransform = transform;
GameObject selfGameObejct = gameObject;
// Figyeljük meg: A típus nagybetűs, a példány kisbetűs.
Minden C# osztály adott (éppen kódot futtató) példányát pedig a this
kulcsszóval érjük el:
Bővebben itt: Változók, tagok és a this kulcsszó
MyClassName selfInstance = this;
A következőkben azt nézzük meg, mi módon tudunk hozzáférni más típusú vagy más GameObject-en lévő komponensekhez.
[SerializeField]
Az egyik módja, hogy egy komponensből egy másik GameObejct vagy komponens referenciájához hozzáférjünk az, hogy a már jól ismert módon [SerializeField]
attribútummal jelölünk meg egy mezőt. Ekkor az editorból beállítható lesz a referencia.
[SerializeField] GameObject someOtherGameObject;
[SerializeField] Transform someTransform;
[SerializeField] Collider someCollider;
[SerializeField] Rigidbody someRigidbody;
// ...
Komponenseket ez után be kell kötni manuálisan a Unity Editor felületén. Több módszer van erre.
- Drag’n Drop módszerrel: Egérrel megragadjuk és behúzzuk a komponenst/GameObject-et. (Komponens esetén lehet a GameObject-et is, amin a komponens van).
- A mező melletti körre kattintva felnyílik egy menü, amiből kiválasztható a megfelelő komponens/GameObject.
OnValidate
metódussal automatikusan elintézzük a “bekötést” kódból. (Erről később bővebben)
A null érték
A C# összes típusa felbontható két nagy kategóriára.
- Érték típusok,
- Referencia típusok
Pl.: int, float, bool, Vector2, Vector3, Quaternion, Color, KeyCode
Pl.: string, Transform, GameObject, MonoBechaviour, Minden script, amit írtunk
Sok fontos különbség van a kettő közt. Ami számunkra fontos, hogy milyen értékeket vehetnek fel.
Minden típusnak van egy default vagy alapértelmezett értéke. Ez érték típusok esetén mindig egyedileg meghatározott. Ezt a default értéket kapja minden field változó, ha nem állítjuk be külön.
Pl.: int és float számoknak ad 0 az alapértéke, bool típus esetén a “false” érték, 2D és 3D vektoroknál pedig csupa nulla minden tengelyen.
A referencia típusok mindegyikének szintúgy van egy defalut-ja, náluk viszont ez minden esetben ugyanaz az egy speciális érték, amit null
-nak nevezünk.
A null
érték azt jelenti, hogy nincs a változóban semmi, nem mutat a változó semmilyen referenciára.
Például a string egy referenciatípus, így az ő alapértelmezett értéke is null
.
A GameObject és a komponensek mindegyike referenciatípus. Tehát a default értékük null
. Eszerint, ha [SerializeField]
-del felveszünk egy komponenst, az null
lesz addig, amíg ezt manuálisan az Editor felületen át nem állítjuk.
A null
-teszt
Ha nem vagyunk biztosak abban, hogy egy referencia változó értéke null
-e, akkor könnyedén ellenőrizhetjük a jól megszokott ==
(egyenlőségteszt) és !=
(egyenlőtlenségteszt) operátorokkal.
[SerializeField] Transform someComponent;
void Update()
{
if(someComponent != null) // Ha nem null a váéltozó értéke,
{
// Akkor kezdünk vele valamit:
Vector3 direction = (someComponent.position - transform.position).normalized;
transform.Translate(direction * Time.deltaTime);
}
}
GetComponent
A [SerializeField]
roppant hasznos és rugalmas eszköz, ám nem mindig célravezető sőt sok esetben nem is megoldható, hogy manuálisan kössünk be minden komponenst.
Ha kódból szeretnénk elkérni egy GameObject egy adott típusú komponensét, akkor a GetComponent
függvényt használhatjuk.
[SerializeField] GameObject someGameObject;
void Start()
{
Collider c = someGameObject.GetComponent<Collider>();
MeshRenderer mr = someGameObject.GetComponent<MeshRenderer>();
Transform t1 = someGameObject.GetComponent<Transform>();
// Az utolsó sor helyett a már jól ismert egyszerűsitést is lehet használni:
Transform t2 = someGameObject.transform;
}
A GetComponent
-nek meg kell adni, hogy milyen típusú komponenst akarunk lekérni relációs jelek (kacsacsőrök) között. A függvény visszatérési értéke meg fog egyezni a lekért típussal.
(Az ehhez hasonló függvényeket, amiknek egy vagy több típust is meg kell adni, mint paraméter, generikus függvényeknek nevezzük.)
Ha nem egy másik GameObject komponenseire vagyunk kíváncsiak, hanem a sajátunkéra (Ugyanaz a GameObject, aminek komponense a lekérdezést végző MonoBehaviour szkript), akkor nem kell referencia a megfelelő GameObject-re, csak szimplán leírni a GetComponent
függvényt:
var c = GetComponent<Collider>();
var m = GetComponent<MeshRenderer>();
var t = GetComponent<Transform>();
// Az utolsó sor helyett lehet:
var t2 = transform;
Ha az adott GameObject-nek nincsen komponense a lekérdezett típusból, akkor nem kapunk hibát, a függvény visszatérése egyszerűen null
lesz.
Ha viszont egy null értéket tartalmazó változón próbálok műveletet végezni, az futási idejű error-t okoz. Tehát ha nem vagyunk biztosak abban, hogy a lekért komponens létezik, végezzünk nulltesztet:
Camera cam = go.GetComponent<Camera>();
if(cam == null)
Debug.Log("Nincs Camera komponens a go GameObject-en!");
else
cam.fieldOfView = 60; // Nullteszt nélkül ez error-t okozhatna!
Optimalizációs tipp TryGetComponent
Ha egy olyan szituációban kéred le egy GameObject komponensét, amiben arra számítasz, hogy meg is találod azt, akkor a fenti GetComponent
-et a legcélszerűbb és hatékonyabb használni.
Viszont vannak olyan helyzetek, amikor ez nem így van. Gyakran arra számíthatsz, hogy csak az esetek egy bizonyos hányadában fogsz találni egyet a keresett komponenstípusból az átvizsgált GamObject-en. Ekkor szerencsésebb egy másik metódust, a TryGetComponent
-et használni.
bool foundComponentOnSelf = TryGetComponent(out MeshRenderer meshRendererFound);
bool foundComponentOnOther = other.TryGetComponent(out MeshRenderer meshRendererFound);
// Ha csak az érdekel, hogy van-e
bool foundComponentOnSelf = TryGetComponent<MeshRenderer>(out _);
Ez a függvény egy bool-lal tér vissza, ami megmondja, hogy sikerült-e megtalálni a keresett komponenst. Emellett van e függvénynek egy out
kimenő paramétere is. Ebben adja vissza a megtalált komponenst. Az out
paraméterrel rendelkező metódusokról itt olvashatsz bővebben:
Függvények több kimenő adattal
A GetComponent
valamivel gyorsabb, ha van találat, viszont a TryGetComponent
nem foglal le fölöslegesen memóriát akkor ha nincs találat. Egyik sem tragédia, de hosszútávon érdemes ezekre figyelni.
A Try
-jal kezdődő függvények nem ritkák a programozásban Unityn belül és kívül sem. Ez egy gyakori fejlesztési minta megvalósítása. A Try függvények mindig megpróbálnak elvégezni/visszaadni valamit és egy bool típussal térnek vissza, amiből kiderül, hogy ez sikerült-e.
A Try függvényeket gyakran használják egyből egy if
feltételeként. Pl.:
// Ha van "testedGameObject"-en SpriteRenderer, ...
if (testedGameObject.TryGetComponent(out SpriteRenderer spriteRenderer))
spriteRenderer.color = Color.red; // ... akkor az legyen piros
GetComponentInChildren, GetComponentInParent
A GameObject-ek hierarchiájában minden objektumnak lehet egy szülője és tetszőleges számú gyereke. A szülő gyerek kapcsolatok információit technikailag nem a GameObject, hanem a Transform komponens tartalmazza, ezért tőle is tudjuk lekérni.
Bővebben itt: A 3D-s tér Unity-ben
Azt is lekérdezhetjük, hogy egy adott GameObject szülein vagy gyerekein van-e komponens egy bizonyos típusból. Ehhez a GetComponentInParent
és a GetComponentInChildren
metódusokat tudjuk használni.
MeshRenderer meshRenderer = GetComponentInParent<MeshRenderer>();
MeshFilter meshFilter = GetComponentInParent<MeshFilter>()
Camera cam = GetComponentInChildren<Camera>();
BoxCollider boxCollider = GetComponentInChildren<BoxCollider>();
A GetComponent
, a GetComponentInChildren
, és a GetComponentInParent
függvényekre is igaz az, hogy ha lehetőségük van több komponenst is visszaadni, akkor a legelőször megtaláltal térnek vissza. Emiatt a GetComponentInChildren
és a GetComponentInParent
esetén érdemes tudni, milyen sorrendben nézi végig az GameObject-eket a Unity.
Mindkét esetben a saját GameObjet-tel kezdünk, és utána haladunk tovább.
Vegyük példának a következő GameObject hierarchiát:
🔽 Aaa
🔽 Bbb - (Aaa gyereke)
⏹️ Ccc - (Bbb gyereke)
⏹️ Ddd - (Bbb gyereke)
🔽 Eee - (Aaa gyereke)
⏹️ Fff - (Eee gyereke)
Ha Aaa-ban hívok GetComponentInChildren
lekérdezést akkor az átnézett GameObject-ek sorrendje:
Aaa, Bbb, Ccc, Ddd, Eee, Fff
(Ezt nevezzük mélységi bejárásnak)
Ha Fff-ben hívok GetComponentInParent
lekérdezést akkor az átnézett GameObject-ek sorrendje:
Fff, Eee, Aaa
Csak azokat a GameObject-eket fogja átnézni a függvény, amelyek be vannak kapcsolva, azaz aktívak.
Lekérhető nem csak egy darab komponens, de több komponens tömbje is a következő módon:
GameObject g;
MonoBechaviour[] scripts = g.GetComponents<MonoBechaviour>(); // Adott GameObject-en
RigidBody[] rigidBodies = g.GetComponentsInChildren<RigidBody>(); // + Minden gyerekén
Collider[] colliders = g.GetComponentsInParents<Collider>(); // + Minden szülőjén
Nem feltétlenül kell pontos típust megadnunk, egy ősosztályt is lekérhetünk. Például a GetComponentInChildren<Collider>()
lekérdezés az összes Collider-t visszaadja, legyen az BoxCollider, SphereCollider, vagy bármi más, aminek a Collider
az ősosztálya.
A GetComponentsInChildren
itt is mélységi bejárásban megy végig a faszerkezeten, tehát nem csak a közvetlen gyerekeket adja vissza, hanem a gyerekek gyerekeit is és a gyerekek gyerekeinek gyerekeit is, és a gyerekek gyerekeinek gyerekeinek gyere…
Transform.GetChild
Le tudjuk kérdezni egy GameObject közvetlen gyerekeit is a Transform.GetChild()
függvénnyel, ami egy int paramétert vár, a gyerek objektum indexét.
Minden Transform
tól a gyerekei számát is lekérdezhetjük a transform.childCount
property-bel.
Ezek alapján írhatunk egy ciklust, ami végigiterrál a egy Transform
gyerekein.
//Egy tömbbe gyűjtöm egy transform összes KÖZVETLEN gyerek objektumát
Transform[] children = new Transform[transform.childCount];
for (int i = 0; i < transform.childCount; i++)
{
children[i] = transform.GetChild(i);
}
//Egy listába gyűjtöm egy transform összes KÖZVETLEN gyerek objektumát
List<Transform> children = new List<Transform>();
for (int i = 0; i < transform.childCount; i++)
{
Transform childTransform = transform.GetChild(i);
children.Add(childTransform);
}
Lekérhetjük egy Transform minden (nem csak közvetlen) gyerekét is mélységi bejárással:
Ehhez használhatjuk a már megismert GetComponentsInChildren
metódust:
Transform[] children = GetComponentsInChildren<Transform>();
Figyeljünk oda, hogy ebben az esetben benne lesz a szülő transform is a tömmben a [0]
-ás indexe.
FindObjectOfType
Egy másik módja is van komponens lekérdezésének. Ez arra az esetre való, ha a faszerkezettől függetlenül akarunk találni az összes GameObject küzül egy konkrét komponenst.
Ekkor a FindObjectOfType<T>()
metódust használhatjuk.
Ennek is van tömböt visszaadó változata is, ami pedig az betöltött jelenetekben található összes komponenst adja vissza egy adott típusból: FindObjekct
s
OfType<T>()
Camera camera = FindObjectOfType<Camera>(); // Egy kamerát ad vissza
RigidBody[] rigidBodies = FindObjectsOfType<RigidBody>(); // Minden RigidBody-t
A fenti függvények mindig az aktív Komponensek közt keresnek. ha ezt módosítani szeretnénk, akkor az egy opcionális paraméterrel megtehető
FindAnyObjectByType
A FindObject
Of
Type
és FindObject
sOf
Type
-ra Unity 2021.3.18 felett alternatívák lehetnek a következő függvények:
Find
Any
Object
By
Type
- Egy darab
FindObject
sBy
Type
- Több darab (Tömb)
Find
First
Object
By
Type
- Egy darab
Ezek gyorsabbak mint a korábbi verziók, viszont nem garantált, hogy a hierarchiában látható sorrend szerint adják vissza a z eredményt.
Ezen felül a Find
First
Object
By
Type
Ez utóbbi teljesen úgy működik, mint a korábbi FindObject
Of
Type
, de illeszkedik az új nevezési módhoz.
Ha a fent megjelölt Unity verzióban vagy attól újabban dolgozunk, akkor javasolt mindenhol ezen új függvényváltozatokat használni.
FindObjectOfType
, mind a FindAnyObjectByType
műveletek használata azok viszonylag lassú végrehajtása miatt csakis inicializáláskor javasolt: Awake
, Start
, OnValidate
, stb. és nem pedig ciklikusan lefutó üzenetmetódusokban: Update
, FixedUpdate
, OnCollisionStay
és társai.Mikor érdemes lekérdezni a komponenseket?
Komponenseket lekérdezni lehet MonoBehaviour osztályon belül bárhonnan, akár közvetlenül a lekért komponens használata előtt is. Viszont, ha tudjuk, hogy a lekérdezés eredménye mindig ugyanaz lesz, akkor érdemes egyszer elintlézni a GetComponent hívást és az eredményt eltenni későbbre egy osztályváltozóba vagy más néven field-be. Ezzel jelentősen tudunk spórolni a processor használaton.
Erre ideális pont lehet a Start metódus:
[SerializeField] Color colorA, colorB; // Beállítások
SpriteRenderer spriteRenderer; // Ebben a field-ben tartom a szükséges komponenst.
void Start() // Start-ban egyszer lekérem és eltárolom a komoponenst.
{
spriteRenderer = GetComponent<SpriteRenderer>();
}
void Update() // Minden egyes Update-ban használom a komponenst valamire.
{
float t = Time.time % 1;
Color color = Color.Lerp(colorA, colorB, t);
spriteRenderer.color = color;
}
Mindez akár még korábban, a játék futása előtt OnValidate
-ben is elintézhető. Persze ekkor szükséges, hogy a mező, amiben tároljuk a komponenst, egy [SerializeField]
mező legyen, hiszen egyéb esetben a Unity nem mentené el a változást.
[SerializeField] Color colorA, colorB; // Beállítások
[SerializeField] SpriteRenderer spriteRenderer;
void OnValidate() // OnValidate-ben egyszer lekérem és eltárolom a komponenst.
{
if(spriteRenderer == 0)
spriteRenderer = GetComponent<SpriteRenderer>();
}
void Update() // Minden egyes Update-ban használom a komponenst valamire.
{
float t = Time.time % 1;
Color color = Color.Lerp(colorA, colorB, t);
spriteRenderer.color = color;
}
Az összes komponens kereső művelet használata kerülendő Update
és FixedUpdate
ciklusokban mivel igen lassúak tudnak lenni. Helyette próbáljunk inkább cach-elni ezeket az értékeket, amint tudjuk.
RequireConponent
A Startban vagy OnVallidate metódus beli lekérdezéshez persze tudni kell, hogy a lekérdezett komponens jelen van egyazon GameObject-en, mint amin a lekérdezést végző MonoBehaviour komponens is szerepel.
Ha biztosak akarunk lenni abban, hogy egy saját szkript mellet ugyanazon GameObject-en szerepel egy meghatározott egyéb komponens akkor felhasználhatjuk a [RequireComponent]
attribútumot, amit a következő módon lehet használni:
using UnityEngine;
[RequireComponent(typeof(Camera))] // Elvárom, hogy legyen Camera a GameObject-en.
public class CameraMover : MonoBehaviour
{
[SerializeField] Transform target;
[SerializeField] float distance = 10f;
[SerializeField] Camera cam; // Változó a Camera eltérolására
void OnValidate()
{
if (cam == null) // Ha nincd még eltárolva Camera,
cam = GetComponent<Camera>(); // lekérdezem és elmentem.
}
void Update() // Minden egeyes Update-ban használom a komponenst valamire.
{
cam.transform.position = transform.position - transform.forward * distance;
}
}
Ha e módon jelzem a Unity felé az elvárást, hogy mindenképp szeretnék egy X komponenst ott, ahol a szkriptem is van, akkor ha egy GameObject-hez hozzáadom az adott szkriptet, automatikusan hozzáadja az X komponenst is, sőt nem fogja engedni törölni azt mindaddig, amíg a szkript (a [RequireComponent]
-tel) rajta van.
GameObject.Find
Végül említsük meg hogy lehet keresni konkrét GameObject-re név alapján is a következő módon:
GameObject go = GameObject.Find("NameOfTheObject");
Ezen függvény használatát azonban egyáltalán nem javaslom. A megoldás kifejezetten kerülendő!
Az a gond a GameObject.Find
-dal, hogy felesleges redundanciát hoz be a projetbe azáltal, hogy a kód és a Scene is tartalmazni fogja ugyanazokat a string-eket, az objektumok neveit.neveket. Használatát nem javaslom!
Ha egy konkrét, ismert, scene-ben jelenlévő GameObject referenciájára van szükségünk használjuk inkább a [SerializeField]
-et manuális bekötéssel. Ha pedig kódból szeretnénk keresni egy GameObject-et, azt tegyük inkább annak egy komponense szerint a GetComponent
függvény valamely verziójával!
Bővebben a különböző lehetőségeinkről és a velük felmerülő problémákról: GameObject-ek azonosítása