Developedia
Developedia
Simított mozgás

Simított mozgás

Gyakori feladat egy videojátékban, hogy azt szeretnénk, hogy egy objektum kövessen egy másikat vagy csak szép egyenletesen másszon a egy célpozícióba.

Erre a feladatra már megismertünk egy módszert Vector2.MoveTowards(), Vector3.MoveTowards() Bővebben: A Towards függvényekA Towards függvények

Lineáris mozgás

A Towards típusú függvények mindegyikéről azt tanultuk, hogy 3 bemenete és egy visszatérése van:

  • Paraméter 1: Kezdő állapot
  • Paraméter 2: Cél állapot
  • Paraméter 3: Maximum változás mennyisége:
    • Ez általában: Változás sebessége * Delta Time
  • Visszatérés: Új állapot

Ezen függvények bármelyikét Update() metódusban használva. Egyenletes, lineáris változást eredményez, legyen szó pozícióról, színről, elfordulásról, vagy akár csak egy szimpla float-ról.

Így használjuk a Vector3.MoveTowards()-t célpont követésre:

[SerializeField] Transform target;
[SerializeField] float speed = 10f;

void Update()
{
    float maxStep = speed * Time.deltaTime;
    transform.position =
				Vector3.MoveTowards(transform.position, target.position, maxStep);        
}

A lineáris pozíció változás, amit itt használunk nem jelent azonban sima mozgást. Az elindulás, megérkezés és az irányváltás is éles módosulást fog eredményezni a sebességben.

Mozgás Lerp függvénnyel

Gyakori megoldásként találkozik az ember neten Lineáris intepoláció alapú célpontkövető megoldásokkal, annak ellenére, hogy a Lerp függvényt nem erre találták ki: Lineáris és simított interpolációLineáris és simított interpoláció

[SerializeField] Transform target;
[SerializeField] float tValue = 10f;

void Update()
{
		transform.position =
			Vector3.Lerp(transform.position, target.position, tValue * Time.deltaTime);
}

Ami itt történik az a következő: Minden egyes update-ben a jelenlegi és a célpont közé tesszük az objektumot úgy, hogy az arányszám: tValue * Time.deltaTime. Ez általában egy alacsony érték mivel szoroztunk Time.deltaTime-mal. Ezért minden update-ben ez a köztes pont jóval közelebb lesz a kiinduláshoz.

A megoldás egész szép eredményt ad, amiben annál gyorsabb a mozgás, minél távolabbi a célpont.

A gyakorlatban ez úgy néz ki, hogy az objektum gyorsan pillanat szerűen lódul meg a célja irányába viszont lassan simán érkezik meg.

Mindazonáltal nem probléma nélküli ez a megoldás:

  1. Először is a tValue csak egy arányszám. Ha nagyobb, akkor gyorsabb a mozgás, ha kisebb, akkor lassabb, de nem takar valós fizikai értéket. Nem sebesség, nem gyorsulás. (Ez a kisebbik gond.)
  2. A nagyobb probléma azzal van, hogy a megoldás nem FPS-független. Senkit ne tévesszen meg a Time.deltaTime használata. Ebben az esetben a vele történő szorzás nem ment meg minket.
  3. Ezért használata pusztán vizuális feladatokra javasolható, de ha gameplay szempontból is jelentősége van a mozgásnak, sose használjuk ezt a módszert.

Ezt a második problémát megoldja az, hogyha áttesszük a logikát FixedUpdate()-be, ekkor viszont vizuálisan tehetjük darabossá a mozgást: (Bővebben: A FixedUpdate és a gyorsuló mozgásA FixedUpdate és a gyorsuló mozgás)

Az ideálishoz legközelebbi megoldás az lehet, ha összevonjuk a Lerp-es megoldást a lineárissal.

Ekkor ki kell bővítenünk a beállításainkat egy max sebességgel is, és a gyorsulást FixedUpdate()-ben a mozgást pedig Update()-ben kell végezzük.

[SerializeField] Transform target;
[SerializeField] float tValue = 10f;
[SerializeField] float maxSpeed = 20f;

Vector3 targetPosition;

void FixedUpdate()
{
	targetPosition =
		Vector3.Lerp(transform.position, target.position, tValue * Time.fixedDeltaTime);
}

void Update()
{
	float maxStep = maxSpeed * Time.deltaTime;
	transform.position =
		Vector3.MoveTowards(transform.position, targetPosition, maxStep);
}

Ekkor nagy sebességnél a mozgás lineáris és ami a legfontosabb FPS független lesz, és a FixedUpdate okozta esetleges darabosság csak alacsony sebességnél jelentkezik, amikor sokkal kevésbé kiszúrható. Viszont ezen az alacsony sebességen is megmarad az FPS függetlenség.

SmootDamp

Ha azt szeretnénk, hogy a mozgás induláskor és megálláskor is sima legyen, arra a legegyszerűbb Unity-be épített, előre implementált megoldást a SmoothDamp függvény jelenti.

Ez hasonlóan működik, mint a Towards függvények. Létezik verzió belőle float-ra, és minden vektor típusra is. Minden esetben hasonló a paraméterezés logikája és sorrendje. Most nézzük ezt meg Vector3-ra:

A SmoothDamp nem változtat az objektum sebességén szögletesen, pillanat szerűen sem induláskor, sem kanyarodáskor sem pedig célba érkezéskor. A sebességvektor változása mindig sima, hívásonként kis módosításokkal történik. Ahhoz, hogy ezt a SmoothDamp meg tudja tenni szüksége van egy mindenkori velocity, azaz sebességvektor változóra. Ha nem tudná mi volt korábban a sebesség, nem tudná, hogy milyen mértékben módosít rajta.

Ezt a sebességet minden hívásnál számításba veszi a SmoothDamp ahhoz, hogy kiszámolja a következő pozíciót, valamint a függvény módosít is ezen a velocity értéken: Lassít vagy gyorsít a megfelelő irányba.

Mivel egy függvénynek nincs memóriája, nem tudja megjegyezni, mi volt a velocity értéke előző hívásnál. Ezért a sebességvektort a hívó félnek kell eltárolnia és minden egyes hívásnál paraméterben átadni.

Ez a paraméter adat típusú mégis referenciaként kerül átadásra a ref kulcsszó segítségével. Ezzel érhető el, hogy ne csak felhasználni tudja a függvény az értéket a saját számításaihoz, hanem módosítani is azt és ezen módosításnak legyen hatása a függvény hívói oldalon is.

Erről bővebben itt: Referencia- és értéktípusokReferencia- és értéktípusok, Függvények több kimenő adattalFüggvények több kimenő adattal

Használata követésre:

[SerializeField] Transform target;
[SerializeField] float smoothTime = 0.3f;
[SerializeField] float maxSpeed = 25f;

Vector3 velocity;

void Update()
{
	transform.position =Vector3.SmoothDamp(
		transform.position,
		target.position,
		ref velocity,
		smoothTime,
		maxSpeed);
    // Utolsó paramétert nem írom ki mert a default value is Time.deltaTime
}

Gravitáció szerű mozgás

Ha sima indulást és sima érkezést szeretnénk valamint egy kis rugózást is megérkezéskor, akkor lefejleszthetünk egyedi fizikai alapú mozgást, amiben gravitáció szerűen gyorsítjuk az objektumot a célpont irányába és emellett alkalmazunk egy közegellenállást is.

Részletek: A FixedUpdate és a gyorsuló mozgásA FixedUpdate és a gyorsuló mozgás

Rugó mozgás

Most az előző megoldást módosítsuk annyival, hogy a gyorsulás mértéke arányos legyen a távolsággal. Ezzel hasonló viselkedést érünk el, mintha egy rugó húzná az objektumot a célpontba.

Adjunk egy maximum sebességet is!

Egyedi mozgás

Ne higgyük, hogy a fenti megoldási lehetőségek az egyedüliek.

Ha programozásról van szó, akkor -bármilyen közhelyes is- , de a lehetőségeinknek valóban csak a képzelet szab határt. Kitalálhatunk és lefejleszthetünk egy teljesen egyedi, saját mozgást a teljesen egyedi és saját elképzeléseinkhez.

Ne feledjük, hogy az AnimationCurve típussal saját vizuálisan szerkeszthető függvényt vehetünk fel.

Bővebben: AnimationCurve & GradientAnimationCurve & Gradient

Általa tetszés szerint alakíthatunk át bármilyen paramétert egy másik függvényében.

  • Sebességet
  • Távolságot a célponttól,
  • Gyorsulást,
  • Közegellenállást
  • Rugóállandót
  • …

Például az alábbi kódban a fenti gravitációs megoldást módosítottam úgy hogy a gyorsulás mértéke és a közegellenállás is egyaránt függnek a távolságtól egy-egy manuálisan beállítható görbe szerint. Ezt aztán beállítottam úgy, hogy ha közel a célpont, akkor fokozatosan bekapcsol az ellenállás és kikapcsol a gyorsulás.

‣
A fenti példákban használt kód, amivel a célpontot kattintásra teleportáltam itt olvasható:

LateUpdate

A fenti megoldások mindegyikére igaz, hogyha mozgó célpontot követ az objektum, akkor érdemes Update() függvény helyett LateUpdete()-et használni. Ekkor garantáljuk, hogy a követett objektum előbb lép, minthogy mi elkezdenénk haladni az irányába.

Ha ezt elmulasztjuk, akkor előfordulhat, hogy a követett pozíció egy freme-mel le lesz maradva a célpont valódi pozíciójától. Ez persze nem hangzik egy nagy dolognak, és őszintén szólva a legtöbb esetben nem is az. A hatása azonban pontatlanságot eredményez ezért érdemes kerülni.

Az ilyen pontatlanságok mindig akkor a legészrevehetőbbek, ha kameramozgás esetén használjuk.

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
Vector3 nexPosition = Vector3.SmoothDamp(
		Vector3 current,                     // jelenlegi pozíció
		Vector3 target,                      // cél pozíció
		ref Vector3 currentVelocity,         // jelenlegi sebességvektor mint REFERENCIA
		float smoothTime,                    // Nagyjából mennyi idő alatt érjen célba
		float maxSpeed = Mathf.Infinity,     // Opcionális. Alapesetben végtelen
		float deltaTime = Time.deltaTime);   // Opcionális. Alapesetben a Time.deltaTime
[SerializeField] Transform target;
[SerializeField] float acceleration = 15f;
[SerializeField] float drag = 1f;

Vector3 velocity;

void Update()
{
  // Mozgás
	transform.position += velocity * Time.deltaTime;        
}

void FixedUpdate()
{
  // Gyorsulás
	Vector3 direction = target.position - transform.position;
	Vector3 accelerationVector = direction.normalized * acceleration;
	velocity += accelerationVector * Time.fixedDeltaTime;

	// Közegellenállás
	Vector3 dragVector = -velocity * drag;
    velocity += dragVector * Time.fixedDeltaTime;
}
[SerializeField] Transform target;
[SerializeField] float spring = 150f;
[SerializeField] float drag = 5f;
[SerializeField] float maxSpeed = 30f;

Vector3 velocity;

void Update()
{
  // Mozgás
	transform.position += velocity * Time.deltaTime;        
}

void FixedUpdate()
{
	Vector3 direction = target.position - transform.position;
	float distance = Vector3.Distance(transform.position, target.position);

  // Gyorsulás
	float acceleration = spring * distance;
	Vector3 accelerationVector = direction.normalized * acceleration;
	velocity += accelerationVector * Time.fixedDeltaTime;

	// Közegellenállás
	Vector3 dragVector = -velocity * drag;
  velocity += dragVector * Time.fixedDeltaTime;

	// Sebesség limit
	velocity = Vector3.ClampMagnitude(velocity, maxSpeed);
}
[SerializeField] Transform target;
[SerializeField] float accelerationBase = 10f;
[SerializeField] AnimationCurve accelerationOverDistance;
[SerializeField] float dragBase = 0.1f;
[SerializeField] AnimationCurve dragOverDistance;
[SerializeField] float maxSped = 10f;

Vector3 velocity;

void Update()
{
		// Mozgás
		transform.position += velocity * Time.deltaTime;
}

void FixedUpdate()
{
		Vector3 direction = target.position - transform.position;
		float distance = Vector3.Distance(transform.position, target.position);

		// Gyorsulás
		float acceleration = accelerationBase * accelerationOverDistance.Evaluate(distance);
		Vector3 accelerationVector = direction.normalized * acceleration;
		velocity += accelerationVector * Time.fixedDeltaTime;

		// Közegellenállás
		float drag = dragBase * dragOverDistance.Evaluate(distance);
		Vector3 dragVector = -velocity * drag;
		velocity += dragVector * Time.fixedDeltaTime;

		// Sebesség limit
		velocity = Vector3.ClampMagnitude(velocity, maxSped);
}