Mivel Unity-ben a kamera, mint azt már korábban megismertük egy hagyományos GameObject-hez csatolt egyszerű komponens ( Virtuális kamerák) , így a kamera mozgatása sem különbözik semmi egyéb GameObject pozicionálásától és forgatásától: Pozíció és mozgás, Forgatás
Nem az a kérdés tehát, hogyan lehet egy kamerát mozgatni. Erre könnyű a válasz: változtatjuk a Transform-jának a pozícióját és rotációját. Az igazi kérdés az, hogyan tesszük mindezt kényelmesen.
Ebben a leckében nem fogunk megismerni új eszközöket, függvényeket, osztályokat, csak a korábban már tárgyaltakat fogjuk felhasználni erre az egy konkrét feladatra.
Kamera pozíciója és rotációja
Gondoljuk végig milyen kérdések érdeklik igazán a game-dizájnert, mikor egy kamerát állít be!
Először is szinte sosem a globális pozíció az érdekes számunkra, hanem egy célponthoz viszonyított. Ez a célpont az, amire a kamera “ránéz”, a játékos figyelmének a központja: Általában a játékos virtuális karaktere.
Ezen célpont és az irányított Camera referenciáját vegyük fel mindenek előtt, mint beállítás, valamint egy ehhez viszonyított pozíciót is.
[SerializeField] Camera controlledCamera;
[SerializeField] Transform target;
[SerializeField] Vector3 distanceVector;
Ezután OnValidate
-ben és Update
-ben (vagy még inkább LateUpdate
-ben) is folyamatosan állítsuk be a camera pozícióját relatívan a célponthoz képest majd forgassuk is azt a megfelelő irányba.
A LateUpdate
azért jobb erre a célra, mint az Update
, mert ha a célpont a saját pozícióját Update
-ben frissítette, akkor nem garantált, hogy előbb fog az lezajlani, mint a kamera követéséért felelős Update
. Így előfordulhatna, hogy a kamera követés van maradva 1 frame-mel. Ezzel szemben minden LateUpdate
garantáltan később fut le, mint bármely Update
, így a csúszás kockázatát nagyban (vagy teljesen) csökkentjük.
void OnValidate()
{
FreshCamera();
}
void LateUpdate()
{
FreshCamera();
}
void FreshCamera() // Külön metódus feleljen a kamera pozíciójának frissítéséért.
{
if (target == null) return; // Ezen referenciák nélkül
if (controlledCamera == null) return; // nem tudunk semmit elvégezni
Transform cameraTransform = controlledCamera.transform;
cameraTransform.position = target.position + distanceVector; // Relatív pozció
cameraTransform.LookAt(target); // Célpontra néz
}
Ha az osztályt megjelöljük az [ExecuteAlways]
attribútummal (Hasznos Unity attribútumok ), akkor az OnValidate
el is hagyható.
Ez a [SerializeField] Vector3 distanceVector;
azonban nem a legkényelmesebben állítható. Ha belegondolunk nem szeretünk koordinátákban, sokkal kényelmesebb távolságokban. Egyelőre bontsuk szét tehát a distanceVector
-t két egyéb beállításértékre: vízszintes és függőleges távolság.
[SerializeField] Vector3 distanceVector;
[SerializeField] float horizontalDistance; // Vízszintes távolság
[SerializeField] float verticalDistance; // Függőleges távolság
...
Vector3 distanceVector = new Vector3(0, verticalDistance, horizontalDistance);
cameraTransform.position = target.position + distanceVector;
Ezen a ponton elvesztettük az x tengely beállításának lehetőségét, de ne aggódjunk előbb utóbb visszaszerezzük. 🙂
Ezen két távolságértéket (vízszintes és függőleges) most számoljuk ki két még relevánsabb adatból:
[SerializeField] float horizontalDistance; // Kamera vízszintes távolsága a célponttól
[SerializeField] float verticalDistance; // Kamera függőleges távolsága a célponttól
[SerializeField] float distance; // Kamera távolsága a célponttól
[SerializeField] float verticalAngle; // kamera függőleges dőlésszöge
Vegyük a szituáció oldalnézetét: A distance, horizontalDistance és verticalDistance egy derékszögű háromszöget adnak, aminek alulsó szöge a verticalAngle.
Egy derékszögű háromszögből kettő ismert érték alapján midig kiszámolható az összes többi gy csöpp matek használatával. Ígérem, nem lesz sok belőle.
Ehhez csak használni kell a Pithagorasz tételt és olyan trigonometriai függvényeket, mint: Szinusz, Koszinusz, Tangens.
Most az ismert adatok a következők:
- a háromszög egyik szöge (verticalAngle) és
- a háromszög átfogója (distance)
Ezkből szeretnénk kiszámolni a két befogót (horizontalDistance és verticalDistance)
Mivel egy szög szinusza megegyezik a szöggel szemközti befogó és az átfogó hányadosával és egy szög koszinusza pedig megegyezik a szög melletti befogó és az átfogó hányadosával, felírható:
Mindez programkóddal:
// Ne feledjük az átváltást fok-ból radiánba, ha trigonometriai függvényt használunk!
float verticalAngleRad = verticalAngle * Mathf.Deg2Rad;
float verticalDistance = Mathf.Sin(verticalAngleRad) * distance ;
float horizontalDistance = Mathf.Cos(verticalAngleRad) * distance ;
Vector3 distanceVector = new Vector3(0, verticalDistance, horizontalDistance);
cameraTransform.position = target.position + distanceVector;
Már csak annyi a feladat, hogy a vízszintes távolságot felbontsuk x és z komponensre: Ezt ugyanúgy a szinusz és koszinusz függvényekkel fogjuk végezni, viszont először szükségünk lesz egy vízszintes szögre is, mint beállítás.
A végleges kód:
[SerializeField] Camera controlledCamera;
[SerializeField] Transform target;
[SerializeField] float targetDistance; // Kamera távolsága a célponttól
[SerializeField] float verticalAngle; // kamera függőleges dőlésszöge
[SerializeField] float horizontalAngle; // kamera vízszintes szöge
void FreshCamera()
{
if (target == null) return;
if (controlledCamera == null) return;
float verticalAngleRad = verticalAngle * Mathf.Deg2Rad;
float verticalDistance = Mathf.Sin(verticalAngleRad) * targetDistance;
float horizontalDistance = Mathf.Cos(verticalAngleRad) * targetDistance;
float horizontalAngleRad = horizontalAngle * Mathf.Deg2Rad;
float xDistance = Mathf.Sin(horizontalAngleRad) * horizontalDistance;
float zDistance = Mathf.Cos(horizontalAngleRad) * horizontalDistance;
Vector3 distanceVector = new (xDistance, verticalDistance, zDistance);
controlledCamera.transform.position = target.position + distanceVector;
controlledCamera.transform.LookAt(target);
}
Próbáljuk most ki a beállítását a kamerának! Ugye, hogy intuitívabb, mint szimplán egy távolság-vektorral?
Ha igazán meggondoljuk, akkor még a távolság is egy kissé absztrakt beállítás egy game designer számára. Hogy ehelyett milyen beállításokat vehetünk fel és hogyan, azt a következő leckében olvashatjátok: A virtuális kamera látótere
Simított mozgás
Általában nem azt szeretnénk elérni egy célpont követő kamera esetén, hogy az egy az egyben kövessen egy pontot megadott logika szerint, hiszen az a mozgás nagyon szögletesnek hathat. Ehelyett valami féle simított kameramozgás a cél.
A simított kameramozgást valamiféle késleltetéssel érjük el, amikor a kamera nem követi pontosan a célpontot, hanem kicsit lemarad mögötte, majd fokozatosan beéri azt. Minél jobban beérte a kamera a célpontot annál lassabb a korrigáció sebessége.
Ehhez használjunk simított mozgást a kamerára: Simított mozgás
A fenti fejezetből leginkáb a smoothdamp és a Lerp alapú megoldások alkalmazhatók kamerákra.
Simított kameramozgás esetén probléma, hogy a kamera mindig kicsit lemarad a játékostól. Ezért egy kicsit nagyobb teret látunk be a játékos mögött, mint előtt. Ez game-dizájn szempontból nem túl szerencsés. Ezért gyakran a simítva mozgó kamera célpontja nem a karakter maga, hanem egy pont a karakter előtt.
Update/FixedUpdate szinkron probléma
Ha a követett tárgy, “darabosan” mozog a játékban, azaz úgy tűnik, mintha mozgás közben remegne, azt általában az okozza, hogy a kamera és az játékos objektum pozíciója nem szinkronban frissül. Egyik fizika szerint mozog, azaz minden FixedUpdate ciklusban kerül frissítésre, míg a másik elemet saját, kód szabályozza az Update ciklusban. (A LateUpdate is az Update ciklus része.)
Ekkor ha fizikát használtunk, akkor a legcélravezetőbb megoldás bekapcsolnia a Rigidbody interpolációt a megfelelő komponensen, hogy ezáltal a megjelenített pozíció mindig Update ciklusban frissüljön.
Manuális mozgatás esetén egyszerűen figyelni kell arra, hogy a pozíció módosítását csakis Update()
metódusban tegyük, akkor is ha a gyorsítás FixedUpate()
-ben történt.
Bővebben: A FixedUpdate és a gyorsuló mozgás
Kamera mozgatása egérrel
Ha szeretnénk forgatni a kamerát az egér mozgásával pélául például First és Third Person játékokban, akkor érdemes lekérni az előző képfrissítés (frame) óta megtett egérmozgást.
float mouseDeltaX = Input.GetAxis("Mouse X"); // Vízszintes tengely
float mouseDeltaY = Input.GetAxis("Mouse Y"); // Függőleges tengely
A fenti módszer előnye, hogy ha így kérjük le a mozgást, akkor mindig fogunk visszakapni nullától különböző input értéket, mikor az egeret fizikailag mozgatjuk. Még akkor is, amikor a kurzor nekiütközött a képernyő szélének és nem tud tovább menni.
Az eredményt képernyő koordinátában, azaz pixelben kapjuk meg. Ez nem ideális a legtöbb esetben, hisz lehetőségtől függően szeretnénk eszközfüggetlen eredményt kapni, márpedig különböző számítógépek más-más felbontással rendelkezhetnek. Általában a képernyő felbontásához képesti érték, azaz a Viewport koordináta sokkal hasznosabb nekünk.
mouseDeltaX /= Screen.width;
mouseDeltaY /= Screen.height;
Ezt az értéket lehet hozzáadni vagy kivonni a vízszintes és függőleges elfordulásunkhoz.
horizontalAngle += mouseDeltaX; // Összeadás eredményez megszokott érzetet
verticalAngle -= mouseDeltaY; // Kivonás eredményez megszokott érzetet
Ezt még több mint érdemes megszorozni egy beállítható szenzitivitás értékkel majd végül beszorítani a függőleges értéket két szintén beállítható minimum és maximum érték közé a Clamp
metódussal.
[SerializeField] float mouseSensitivity = 0.25f;
[SerializeField] float verticalAngleMin = 10;
[SerializeField] float verticalAngleMax = 60;
// ...
horizontalAngle += mouseDeltaX * mouseSensitivity;
verticalAngle -= mouseDeltaY * mouseSensitivity;
verticalAngle = Mathf.Clamp(verticalAngle, verticalAngleMin, verticalAngleMax);