Developedia
Developedia

2D Platformer Kontroller (Félkész)

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-ekIngyenes 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.

image

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.

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.)

Az alábbiakban végig-veszem miért ezeket a beállításokat választottuk:

  • rigidbody.bodyType = RigidbodyType2D.Dynamic
  • Dinamikus Rigidbody szükséges az ütközésdetektáláshoz: Unity fizika és a RigidbodyUnity fizika és a Rigidbody

  • rigidbody2D.simulated = true;
  • Enélkül nem is mozgatná a fizika a karaktert.

  • rigidbody2D.drag = 0f; rigidbody2D.angularDrag = 0f;
  • Nincs szükségünk semmilyen közegellenállásra.

  • rigidbody2D.gravityScale = 0f;
  • A gravitációt manuálisan kódból fogjuk szabályozni, ezért kikapcsoljuk az automatikusat.

  • rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;
  • A fizikai szimuláció framerate ne befolyásolja a megjelenítés sebességét.

  • rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
  • 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.

  • rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;
  • 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.

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.

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:

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.

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 InputBillentyűzet és Gamepad Input

Mivel az ugrás egy pillanatszerű művelet, ezért az Update-ban kezeljük le.

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.

Wile E. Coyote vagy ahogy magyarul ismerjük: A prérifarkas
Wile E. Coyote vagy ahogy magyarul ismerjük: A prérifarkas

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ő)

Wall Jump

A teljes kód:

Platformer2DController.cs11.1KB
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
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...
	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...
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
}
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) { /* ... */ }
[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
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
}
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;
}