Developedia
Developedia
Célpontkövető kamera

Célpontkövető kamera

Mivel Unity-ben a kamera, mint azt már korábban megismertük egy hagyományos GameObject-hez csatolt egyszerű komponens ( Virtuális kamerákVirtuá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ásPozíció és mozgás, ForgatásForgatá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.

Ha az osztályt megjelöljük az [ExecuteAlways] attribútummal (Hasznos Unity attribútumokHasznos 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.

image

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.

Futurama - The Prisoner of Benda

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ó:

sin(α)=vd/dsin(α)∗d=vdsin(α) = vd/d\\sin(α) * d = vdsin(α)=vd/dsin(α)∗d=vd
cos(α)=hd/dcos(α)∗d=hdcos(α) = hd/d\\cos(α) * d = hdcos(α)=hd/dcos(α)∗d=hd

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:

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ótereA 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ásSimí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.)

“Darabos” / “Szaggató” játékos mozgás

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ásA 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);
Logo

Főoldal

Blog

Elmélet

3D Studio

Adatvédelmi nyilatkozat

GY.I.K.

Házirend

Szerző: Marosi Csaba / marosi.csaba@3d-studio.hu

DiscordGitHubLinkedIn
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
}
[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);               
}