A 2D platformer játékunk magja a karakter controller. Ez fogja vezényelni a játékosunk mozgását.
GameObject és komponensek
Mindenek előtt szükségünk van egy GameObject
-re. Hozzunk létre egy üreset és nevezzük el Player-nek.
Legcélszerűbb Unity fizikát használni az ütközésdetektálásra, minden mást manuálisan programozunk le.
Adjunk tehát hozzá a GameObject
-hez egy Rigidbody2D
-t, valamint egy 2D Collider-t, ami a karakter alakját adja meg.
Javaslom, hogy használjunk CapsuleCollider2D
-t vagy BoxColluder2D
-t.
Mivel teljesen manuálisan szeretnénk szabályozni a játékos működését, hozzunk létre egy PhysicsMaterial2D
fájlt. Állítsuk be, hogy a pattanóssága és a súrlódása is nulla legyen és kössük rá a Collider-re. Ezáltal sem pattogni sem a felületi súrlódástól lelassulni nem fog a játékos.
A játékos karaktert meg is kell jeleníteni valahogy a felhasználónak. Erre a célra én most SpriteRenderer
-t használok, de nyugodtan lehetett volna MeshRenderer
is.
A SpriteRenderer
-nek beállítom a Sprite-ját:
Jelen esetben én a Ingyenes Asset-ek közül választottam egy karaktert.
Ne felejtsük el beállítani a Collider méretét a megfelelő képhez.
A működés logikáját mi programozzuk le. Erre a célra létre hozunk egy új MonoBehaviour
osztályt Platformer2DController
néven és hozzáadjuk a Player GameObject
-hez.
A referenciák bekötése a kódban
Kezdjük azzal az osztályunkat, hogy bekötjük a szükséges referenciákat. A komponensnek ismernie kell majd:
Rigidbody2D
komponenst: Mozgás szabályozása- Visuals
GameObject
Transform
komponensét: Irányba forgatás
Ezek [SerializeField]
beállítások. Automatikusan kössük be őket az OnValidate()
Unity metódusban.
using UnityEngine;
class Platformer2DController : MonoBehaviour
{
// Player komponnes referenciái:
[Header( "References" )]
[SerializeField] new Rigidbody2D rigidbody2D; // Vezényelt Rigidbody2D
[SerializeField] Transform visuals; // Transform ami a vizuális részeket tartalmazza
void OnValidate() // Autómatiks beállítások az OnValidate metódusban
{
if (rigidbody2D== null) // Ha nincs Rigidbody2D
rigidbody2D= GetComponent<Rigidbody2D>(); // Keresd meg
if (visuals == null) // Ha nincs meg visuals Transform
visuals = treansform; // Keresd meg
SetupRigidbody(); // Rigidbody paraméterek beállítása
// Később írjuk meg...
}
// Tovább...
Figyeljünk arra, hogy valóban a Visuals GameObject
legyen bekötve a visuals
field-hez. Ha kell módosítsuk manuálisan.
A Rigidbody helyes beállítása
Ezután kódból automatikusan beállíthatjuk a Rigidbody2D
-t a SetupRigidbody()
metódusban, amit az OnValidate()
-ből hívunk. (Ezt a beállítást lehetne manuálisan is végezni az Inspector felületen, de az alábbi megoldás kevesebb esélyt ad későbbi hibák elkövetésére.)
void SetupRigidbody() // Rigidbody paraméterek beállítása
{
if (rigidbody == null) return;
rigidbody2D.bodyType = RigidbodyType2D.Dynamic;
rigidbody2D.simulated = true;
rigidbody2D.drag = 0f;
rigidbody2D.angularDrag = 0f;
rigidbody2D.gravityScale = 0f;
rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;
rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;
}
// Tovább...
Az alábbiakban végig-veszem miért ezeket a beállításokat választottuk:
- rigidbody.bodyType = RigidbodyType2D.Dynamic
- rigidbody2D.simulated = true;
- rigidbody2D.drag = 0f; rigidbody2D.angularDrag = 0f;
- rigidbody2D.gravityScale = 0f;
- rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;
- rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
- rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;
Dinamikus Rigidbody szükséges az ütközésdetektáláshoz: Unity fizika és a Rigidbody
Enélkül nem is mozgatná a fizika a karaktert.
Nincs szükségünk semmilyen közegellenállásra.
A gravitációt manuálisan kódból fogjuk szabályozni, ezért kikapcsoljuk az automatikusat.
A fizikai szimuláció framerate ne befolyásolja a megjelenítés sebességét.
Ez a legpontosabb ütközésdetektálás. Lévén a fő karakterről van szó, hasznos a legjobbat használni, még ha kicsikét több erőforrást vesz is igénybe.
Nem akarjuk, hogy a karakter el tudjon forogni, ezért lelockoljuk.
Földön állunk-e?
Állapítsuk meg az aktuális állapotunkat! Éppen állunk valamin vagy a levegőben esünk?
Ezt és még néhány egyéb információ tárolására hozzunk létre field-eket:
// Current State
Collider2D _platform = null; // A platform referenciája, amin állunk
float _groundingStateChangedTime = 0f; // Az állapot megváltozásának ideje
// Egy property-t is létrehozunk, ami megmondja, hogy éppen földön állunk-e az alapján,
// hogy a platform változóban van-e valami érték a null-on kívül.
bool IsGrounded => _platform != null;
Az OnCollisionEnter2D
és OnCollisionExit2D
metódusokat fogunk használni hogy mikor találkozunk és mikor távolodunk el platformtól.
void OnCollisionEnter2D(Collision2D collision) // Ütközéskezelés
{
_platform = collision.collider; // Eltároljuk, hogy melyik platformon vagyunk
_groundingStateChangedTime = Time.time; // Eltároljuk, hogy mikor értük el a földet
}
void OnCollisionExit2D(Collision2D collision) // Mikor elvállunk egy Collidertől
{
if(collision.collider != _platform) // Ha nem a platform-tól távolodunk el,
return; // amin épp állunk, akkor kilépünk a metódusból
// Egyébként:
_platform = null; // Töröljük az eltárolt platformot
_groundingStateChangedTime = Time.time; // Eltároljuk, hogy mikor válltunk el
}
Ezt az egy egyszerű megoldást a talajdetektálásra majd később bővítjük.
Input
Ez a kontroller ugrani, és jobbra mozogni lesz képes. A megfelelő billentyűket bekötjük [SerializeField]
változókba.
[Header( "Input" )]
[SerializeField] KeyCode leftKey = KeyCode.A; // Balra gomb
[SerializeField] KeyCode rightKey = KeyCode.D; // Jobbra gomb
[SerializeField] KeyCode jumpKey = KeyCode.Space; // Ugrás gomb
Folyamatos mozgás szimulálása
A mozgás szimulálását a FixedUpdate( )
Unity MonoBechaviour
életciklus metódusban végezzük.
void FixedUpdate() // Fix időközönként: Itt kezeljük a fizikai szimulációt
{
Vector2 v = rigidbody2D.velocity; // Jelenlegi sebesség
v.x = HandleHorizontal(v.x); // Vízszintes sebesség kezelése
v.y = HandleVertical(v.y); // Függőleges sebesség kezelése (Később)
rigidbody2D.velocity = v; // Sebesség visszaállítása
}
float HandleHorizontal(float x) { /* ... */ }
float HandleVertical(float x) { /* ... */ }
A fenti kódban az aktuális sebességet két komponensre bontjuk: Vízszintes és függőleges. Utána függetlenül számításokat végzünk ezen a két float
számon két saját függvény segítségével majd végül visszaírjuk a rigidbody2D.velocity
-be.
Vízszintes mozgás
Először is vegyük fel a vízszintes mozgáshoz szükséges beállításokat.
Ezek a mi esetünkben a következők lesznek:
[Header( "Horizontal Movement" )]
[SerializeField, Min(0)] float maxMoveSpeed = 5f; // Max vízszintes sebesség
[SerializeField, Min(0)] float groundAcceleration = 25f; // Földön való gyorsulás
[SerializeField, Min(0)] float airAcceleration = 10f; // Légben való gyorsulás
[SerializeField, Min(0)] float groundDrag = 10f; // Földön való lassulás
[SerializeField, Min(0)] float airDrag = 1f; // Légben való lassulás
A földön és a levegőben a platformer játékok nagy részében máshogy viselkedik a karakter, ezért mi is különválasztjuk ezt a két esetet.
Utána megírjuk a HandleHorizontal
metódust, amit FixedUpdate()
-ből hívtunk
HandleHorizontal
egy minden FixedUpdate()
-ban újra számolja tehát a vízszintes sebességet az aktuális helyzet alapján.
float HandleHorizontal(float x) // Vízszintes sebesség kezelése
{
bool left = Input.GetKey(leftKey); // Balra gomb nyomva van-e?
bool right = Input.GetKey(rightKey); // Jobbra gomb nyomva van-e?
// Acceleration
if (left ^ right) // Ha pontosan az egyik gomb van megnyomva, akkor mozgunk
{
// Gyorsulás mértéke attól függően, hogy a milyen irányba haladnánk
float acceleration = IsGrounded ? groundAcceleration : airAcceleration;
// A sebesség változása a gyorsulás mértékével
if (right)
x += acceleration * Time.fixedDeltaTime;
else
x -= acceleration * Time.fixedDeltaTime;
// Fordulás kezelése (jobbra és balra)
visual.rotation = right ? Quaternion.identity : Quaternion.Euler(0, 180, 0);
// Maximumális vízszintes sebesség kezelése
if (Mathf.Abs(x) > maxMoveSpeed)
x = maxMoveSpeed * Mathf.Sign(x);
}
else // Ha nem nyomjuk a haladás gombot valamilyen irányba
{
// Kezeljük a lassulást függően attól, hogy a földön vagy a levegőben vagyunk
float drag = IsGrounded ? groundDrag : airDrag;
x *= 1f - (drag * Time.fixedDeltaTime);
}
return x; // Visszaadjuk a módosított vízszintes sebességet
}
Gravitáció kezelése
Esés beállításai:
[Header( "Vertical Movement" )]
[SerializeField, Min(0)] float gravity = 15f; // Gravitációs gyorsulás
[SerializeField, Min(0)] float maxFallSpeed = 15f; // Max lefelé esési sebesség
Esés logikáját a HandleVertical
metódusban írjuk meg. Emlékezzünk rá, hogy ezt a metódust a FixedUpdate()
-ból hívtuk.
float HandleVertical(float y) // Függőleges sebesség kezelése
{
// Gravitáció kezelése
y -= gravity * Time.fixedDeltaTime;
// Maximumális függőleges sebesség kezelése
if (y < -maxFallSpeed)
y = -maxFallSpeed;
return y; //Visszaadjuk a módosított függőleges sebességet
}
Az ugrás kezelése
Egyelőre egy beállítás fog tartozni az ugráshoz, a kezdősebesség. Ezt először is vegyük fel [SerializeField]
-ként.
[Header( "Jumping" )]
[SerializeField, Min(0)] float jumpStartJumpVelocity = 5f; // Ugrás kezdő sebessége
Emlékezzünk, hogy az Input
osztály pillanatszerű lekérdezéseit csak az Update
és LateUpdate
metódusokban érdemes hívni és nem a FixedUpdate
-ben. Ellenkező esetben hibás eredményt adhat: Billentyűzet és Gamepad Input
Mivel az ugrás egy pillanatszerű művelet, ezért az Update
-ban kezeljük le.
void Update()
{
Vector2 velocity = rigidbody2D.velocity; // Jelenlegi sebesség
velocity.y = HandleJumpStart(velocity.y); // Ugrás kezdésének kezelése
rigidbody2D.velocity = velocity; // Sebesség beállítása
}
float HandleJumpStart(float y) // Ugrás kezdésének kezelése
{
if (!Input.GetKeyDown(jumpKey)) // Ha nincs ugrás gomb megnyomva,
return y; // akkor ne csinálj semmit
bool canJump = IsGrounded // Tudunk ugrani, ha a földön vagyunk.
if (canJump) // Ha tudunk ugrani,
y = jumpStartJumpVelocity; // akkor beállítjuk a függőleges sebessége
return y;
}
A folyamatos ugrás
TODO
AirJump: Ugrás a levegőben
TODO
Kifinomultabb módja a földet érés megállapításának
TODO
Coyote Time
A kengyelfutó gyalogkakukk című népszerű Warner Bros. rajzfilm egyik főszereplője a prérifarkas vagy eredeti nevén Wile E. Coyote. Akik megfelelő figyelemmel nézték hősünk kezdettől bukásra ítélt cselszövéseit, tudják, hogy a gravitáció leggyakrabban nem akkor kezd el lefelé gyorsítani egy prérifarkast, amikor az alatt nincs talaj, hanem csak akkor amikor az állat lenéz, sőt néha még csak azután hogy felemel egy táblát és pislog kettőt. Igazság szerint a fizika eléggé inkonzisztens a prérifarkasokkal.
Ezért kapta erről az állatról a nevét a platformer játékfejlesztés egy fontos koncepciója, a Coyote Time (Prérifarkas idő)