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é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ó
[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:
- 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.) - 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.
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á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:
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
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ípusok, Fü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
}
Grvitá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ás
[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;
}
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!
[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);
}
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 & 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.
[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);
}
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.