3. Játékmentek kialakítása

3.1. Prefab-ok használata

Játékok készítésekor gyarkran előfordulhat, hogy a játékban több, hasonló kinézetű és viselkedésű objektumra lesz szükségünk. Például falak, ellenséges katonák, fák az erdőben, tekebábuk, lámpaoszlopok, stb.). Egy ilyen objektum nagyon összetett is lehet, sok időt elvehet, amíg egy ilyen objektumot létre hozunk. Ugyan készíthetünk egy objektumról másolatokat a Hierarchy nézetben, de ha az összes objektum valamilyen tulajdonságát egyszerre szeretnénk megváltoztatni (pl. a fák magasságát), akkor vévig kellene mennünk a Hierarchy nézetben az összes objektumon és mindegyiknél egyesével átállítani a megfelelő tulajdonságo(ka)t. Erre a problémára a Unity kínál egy megoldást, amit prefab-nak (előregyártott elemnek) hívnak. 

Ha már kidolgoztunk egy objektumot, amire majd több példányban lesz szükségünk a játékban, akkor a Hierachy nézetből az Assets nézetbe húzva az objektumot abból egy prefab-ot késztünk. Ezután az Assets nézetből a létrehozott prefab-ot testszőleges számúszor a Scene nézetbe húzhatjuk, ezzel létrehozva a prefab példányait. Ezzel egyidőben a létrehozott példány megjelenik a Hierarchy nézetben. A prefabok használatának előnye abból adódik, hogy a példányok öröklik a prefab minden jellenmzőjét. Ha módosítom a prefab egy jellemzőjét, akkor az összes példány "megörökli" azt a változást. Például ha módosítom a Scale X tulajdonság értékét 3-ra, akkor minden példány X tengely mentén mért mérete egyszerre háromszorosára változik az eredetihez képest.

 
 

3.1.1. feladat

A mintaprojektünkben akadályok talajon való elhelyezésével tudjuk illusztrálni a prefab-ok használatát. Dolgozzunk ki egy akadály objektumot, majd abból készítsünk egy prefab-ot az objektum Assets nézetbe húzásával. Ezután készítsük több (5-6) példányt belőle és helyezzük el azokat a pályán. Ha kell, hosszabb1tsuk meg a talajt a Z tengely mentén. Jelöljük ki a prefab-ot az Assets nézetben, próbáljuk meg módosítani a Scale tulajdonságát. Figyeljük meg, hogy hogyan módosulnak a példányok méretei.

 

Tipp: az akadályok pontos és gyors elhelyezéséhez a pályán célszerű felülnézetre váltani. A Scene nézet jobb felső sarkában található egy koordináta rendszer, amelynek ha a tengelyeira kattintunk, gyorsan válthatunk a tengelyekre merőleges nézetre. A rácsszerkezetre való igazítás is hasznos lehet a tárgyak pozicionálása során, az ezt segítő ablakocskát az Edit >> Snap Settings menüpont alól nyithatjuk meg.

A valósághűbb hatásért ködöt is adhatunk a játéktérhez, ezt a Window >> Lighting >> Settings ablakban tehetjük meg.

3.2. Interfész-elemek használata

Egy játék nem csak objektumokból áll: néha ki kell írnunk egy üzenetet a játékosnak vagy meg kell jelenítenünk a pontszámot, netán egy menüt. Ehhez felhasználói interfész (user interface - UI) elemeket fogunk használni.

 

Ennek a bemutatásához legyen az a feladatunk, hogy a minta-projektünkben jelenítsük meg folyamatosan az eddig ütközés nélkül megtett távolságot. Ehhez egy Text UI elemet fogunk használni. UI elemeket ugyanúgy tudunk létre hozni Unity-ben mint bármilyen más objektumot: a Hierarchy nézetben a jobb klikkre előugró menüből válasszuk a UI >> Text menüpontot. Ezzel létrehozunk egy vásznat (canvas) és rajta egy Text objektumot, amellyel szöveget tudunk megjeleníteni a vásznon. A vászon Render Mode tulajdonságának értékét hagyjuk Screen Space - Overlay értéken, a UI Scale Mode-ot viszont állítsuk át Scale with Screen Size értékre. A Scene nézetben váltsunk 2D-s nézetre és kattintsuk duplán a vásznon, hogy fókuszba kerüljön. Állítsuk be a Text komponensünk pozícióját, betütípusát és színét tetszésünk szerint. A Horizontal Overflow tulajdonság értékét célszerű Overflow-ra módosítani. A Scene és Game nézetekben az alábbihoz hasonló módon jelenik az új Text komponens.

 
img011 - 3.2 - 1.png

 

Tipp: fonts.google.com oldalról tetszőleges betütípus letölthető, felvehető asset-ként és felhasználható a Text komponensben.

Ha elindítjuk a játékot, láthatjuk, hogy a Text mező folyamatosan meg lesz jelenítve, az értéke azonban nem változik. A célunk az, hogy a mező az ütközés nélkül megtett távolságot mutassa. Ezt természetesen programkód írásával érhetjük el. 

Vegyünk fel a Text objektumhoz egy pontszam nevű szkript komponenst az alábbi forráskóddal.

Mivel egy Text interfészelem tartalmát szeretnénk módosítani, szükségünk van az UnityEngine.UI könyvtár beemelésére. A szkriptből el kell érnünk a szereplő pozícióját (ami a játékunk esetében z tengelyen vett koordinátáját jelenti) ezért van szükség egy publikus Player változóra, amit az Inspector nézetben össze is rendelünk a szereplőnkkel. Szintén el kell érnünk a szkriptből a Text elemet is, hogy módosíthassuk a megjelenítendő  szöveget, ehhez szintén szükségünk lesz egy publikus Text változóra, amit ugyencsak összerendelünk az Inspector nézetben a Text elemünkkel.

Az Update() metódusban frissítjük a szöveget, mégpedig a szereplőnk pillanatnyi z koordinátájának kerekített és sztringgé konvertált értékével.  Mentés után a játékot kipróbálva tapasztalhatjuk, hogy minden az elvárásaink szerint működik.

3.3. A játék elvesztésének kezelése

Egy játék véget érhet azzal, hogy teljesítettük a kitűzött célt, vagy azzal, hogy elbuktunk. A játékunknak fel kell ismernie ennek az eseménynek a bekövetkezését és megfelelően le kell kezelnie azt.

 

A példaprojektünkben a játék célja, hogy minél gyorsabban végigmenjünk a pályán úgy, hogy a szereplőnk ne ütközzön akadályokkal és ne essen le a pályáról. Ha e két esemény bármelyike bekövetkezne, akkor újra kell indítanunk a szintet (vagy megtehetnénk azt is, hogy egyszerűen egy Game Over üzenetet jelenítünk meg). Korábban a Player objektumjoz hozzáadtunk egy szkriptet, ami figyelte, hogy falnak ütközünk-e és amikor ez bekövetkezett, akkor letíltotta a játékos mozgatásáért felelős szkriptet. Az a szkript azonban nem a legmegfelelőbb hely a játék szintjének újraindítására. Ehelyett inkább hozzunk létre egy új üres GameObject-et a Hierarchy nézetben a Create Empty menüpont kiválasztásával és nevezzük el GameManager-nek. Ő lesz felelős azért, hogy a játékmenet ez elképzelésünk szerint folyjon (összetettebb játékok esetén pl. a menü megjelenítése, a pontszám kiiratása, váltás a szintek között, játék befejezése, stb.). Adjunk az objektumhoz egy új szkript komponenst, legyen a neve szintén GameManager. A szkript tartalmazzon egy EndGame() függvényt, amelyet majd minden alkalommal meghívunk, ha akadállyal ütközünk, vagy lecsúszunk a pályáról. Egyelőre a függvény ne csináljon mást, csak írja ki a konzolra, hogy "VÉGE A JÁTÉKNAK!".

 

A PlayerCollission() függvényben, amely az ütközések detektálásáért felelős, ütközéskor meg kell hívnunk az EndGame() függvényt az alábbi módon:

A FindObjectOfType<GameManager>() megkeresi a létrehozott GameManager objektumot és meghívja annak az EndGame() függvényét. Ha ezzel a módszerrel hivatkozunk szkriptből egy külső objektumra, akkor elkerülhetjük a publikus változók használatát és az Inspector nézetben történő összerendelést.

 

Az  EndGame() függvényt akkor is meg kell hívnunk, ha lecsúszunk a pályáról. Ezt a PlayerMovement.FixedUpdate() eljárásban tehetjük meg, ugyanott, ahol a billentyűparansokat figyeljük. Az alábbi sorokat kell az említett eljáráshoz adnunk:

   if (rb.position.y < -1f) {
      FindObjectOfType<GameManager>().EndGame();    
   }

Figyeljük meg, hogy arról ismerjük fel, hogy a játékos lecsúszott a pályáról, hogy a pozíciójának y koordinátája -1-nél kisebb lesz.

A játékot kipróbálva azt tapasztaljuk, hogy az jól fut, de a konzolon nagyon sok "VÉGE A JÁTÉKNAK!' üzenet jelenik meg. Meg kellene oldanunk, hogy ha már egyszer vége lett a játéknak, akkor többé ne ismételjül meg a kiiratást. Ezt az GameManager kódjának alábbi módosításával érhetjük el:

A következő lépésben az üzenet kiiratása helyett ténylegesen újraindítjuk a szintet. Ezt a

       void Restart() {

      SceneManager.LoadScene(SceneManager.GetActiveScene().name);

   }

eljárás EndGame()-ből való meghívásával tehetjük meg, amihez szükségünk van a UnityEngine.SceneManagement könyvtár beemelésére is.

Ezen módosítások után a játékunk az elvárt módon fog működni, két dolgot kivéve: egyrészt a szint újrainditásakor megváltoznak a fényviszonyok. Ennek orvoslásához a Window >> Lighting >> Settings menüpont alatt elérhető ablakban ki kell kapcsolnunk az Auto Generate opciót. Másrészt, ütközés vagy pályaelhagyás esetén a szint újraindítása szinte azonnali, jó lenne azt egy-két másodpercel elodázni. Ehhez a Restart() függvény direkt hívása helyett az Invoke("Restart", 2f) parancsot használhatjuk, amely két másodperces késleltetéssel hívja meg a Restart() függvényt. A GameManager szkript ezek után így fog kinézni:

Természetesen a késleltetés mértékét kitehetnénk egy publikus változóba, hogy azt az Inspector nézetből lehessen módosítani.

3.4. Üzenetpanel megjelenítése szint teljesítése esetén

Hasonlóan a vesztes állás detektálásához azt is fel kell ismernünk, ha teljesítettük a játék szintjét. A példaprojektünkben a szint teljesítése azt jelenti, hogy elértük a pálya végét. Ennek detektálásához helyezzünk el a pálya végén keresztben egy LevelEnd nevű téglatestet, amelynek a mérete legyen (15, 5, 5) és a pozíciója (0, 3, x), ahol x a pályánk hossza plusz egy kevés. Tegyük láthatatlanná az objektumot a Mesh Renderer komponensének letiltásával vagy akár törlésével. Ha ezzel az objektummal ütközünk, az azt jelenti, hogy elértünk a pályánk végét anélkül, hogy kicsúsztunk vagy akadályba ütköztünk volna, azaz teljesítettük a pályát. A téglatest Box Collider komponensének Is Trigger tulajdonságát állítsuk be, ennek az lesz a hatása, hogy valódi ütközés nem fog történni, a szereplőnk csak áthalad a keresztbe tett téglatesten. 

A GameManager szkriptben vegyünk fel egy új eljárást, amely akkor fog lefutni, amikor elértük a pályánk végét. Egyelőre csak irassuk ki a konzolra, ha elértük ezt a pontot.

   public void EndLevel() {
        Debug.Log("LEVEL COMPLETE");
   }

 

Most a LevelEnd-hez is fel kell vennünk egy EndCollision nevű szkriptet, amely detektálja majd, ha áthalad rajta a játékos. Ennek a forráskódja egyszerű lesz:

 

Figyeljük meg, hogy mivel triggerként használtuk az objektumot és nem valódi ütközést szerettünk volna, a használt eseménykezelő az OnTriggerEnter() lett az OnCollisionEnter() helyett. Ha ezután kipróbáljuk a játékunkat, azt látjuk, hogy a játék vége helyesen lesz felismerve, már csak azt kell megoldanunk, hogy tájékoztassuk a játékost a szint sikeres teljesítéséről és betöltsük a következő szintet. A tájékoztatás célját az alábbi egyszerű, a vásznunkon létrehozott panel fogja szolgálni:

img012 - 3.4 - 1.png

A fenti panelt ne felejtsük el letiltani az Inspector nézetben, ha nem tesszük, akkor a játek során látható marad ezzel eltakarva a játékteret. Az alábbi pár sort hozzáadva a GameManager szkriptjéhez és a Complete Panel-t összekötve Inspector nézetben a LevelComplete objektummal elérjük a kívánt működést:

   public GameObject completePanel;

   public void EndLevel() {
      completePanel.SetActive(true);
   }

3.5. Egyszerű animáció létrehozása

A sikeres szint teljesítését jelző panel jókor jelenik meg, de túl hirtelen. Tetszetősebb lenne, ha egy animált átmenetben jelenne meg. Animációk készítését a Window >> Animation menüpontból nyíló ablakban tudunk készíteni. Jelöljük ki a LevelComplete objektumot a Hierarchy nézetben, majd az Animation ablakban hozzunk létre egy új animációt ugyancsak LevelComplete néven (esetleg erre a célra egy Animations mappát létrehozva a projekt könyvtárában).

Az animációnk abból fog állni, hogy a LevelComplete panel áttetszőségét 0 és 30/60-ad mp között 100%-ról 0%-ra csökkentjük és a két Text objektum (Level és Complete) áttetszőségével ugyanezt tesszük, de 25/60 és 50/60 mp között (az animáció idővonal (timeline)) hatvanad másodperces beosztással rendelkezik). Az áttetszőséget az Color tulajdonság ALpha komponensét jelenti. Ha ezeket beállítottuk, a képernyőnk hasonló módon fog kinézni az alábbi ábrához:

 
img013 - 3.5 - 1.png

Ha elkészültünk az animációnkkal, a LevelComplete objektumot tiltsuk le annak érdekében, hogy a játék indulásakor az ne legyen látható. Ezúttal ha sikerül végigmennünk a pályán, az animáció megjeleníti a panelt, de nem áll le, folyamatosan ismétlődni fog.

3.6. Következő szint betöltése

A folyamatos ismétlést kiküszöbölhetjük, hogy ha letiltjuk az objektumot, vagy ha a játék következő szintjét töltjük be az animáció lejátszása után. Ahhoz, hogy ez utóbbit megtehessük, 

az Animation nézetben kb 2 mp-nél hozzunk létre egy eseményt (event-et). Amikor ez az esemény bekövetkezik, azaz az animáció az idővonalán ehhez a ponthoz ér, meghívásra kerül az az eljárás, amely az új szintet be fogja tölteni. Ezt az eljárást hozzuk létre a LevelComplete objektum szkriptjében, amit szintén LevelComplete-nek nevezhetünk el:

 

A UnityEngine.SceneManagement könyvtárból a SceneManager.LoadScene() függvény fogja betölteni az aktív szint után következő szintet. Ha a következő szint nem elérhető, értelemszerűen hibával ér véget majd a futás. A játék szintjeinek sorrendjét  (más, a build folyamatot befolyásoló beállítások mellett) a File >> BuildSettings képernyőn lehet beállítani.

img014 - 3.6 - 1.png

 

A fentiek után még ki kell jelölnünk az Animation ablakban a létrehozott eseményt és az Inspector ablakban kiválasztanunk a LoadNextLevel() függvényt. Ha mindent jól csináltunk, az első szint sikeres teljesítése után a játékunk betölti és elindítja a következő szintet.

3.6.1. feladat

Dolgozd ki részletesen a játék 2-3 szintjét. Ügyelj arra, hogy a szintek teljesíthetők legyenek, de ne legyenek unalmasak. Gondolt át, hogy mit lehetne még javítani a játékmeneten.