2. Programozás Unity-ben

2.1. Szkriptek szerkesztése Visual Studio-ban

Unity-ben a programozás azt jelenti, hogy az objektumainkhoz programkódokat, úgynevezett szkripteket csatolunk. Ezt a kívánt objektum Hierarchy nézetben való kijelölése után tehetjük meg úgy, az Inspector-ban az Add Component >> New Script pontot választjuk ki. A szkript nevének megadása után megjelenik a programkód szerkesztő ablak, amelyben a szkriptünket megírhatjuk. A szkripteket C# nyelven kell megírnunk. Ez a nyelv eléggé összetett, de nekünk elég megtanulnuk az alapjait.

Az alábbiakban egy olyan szkriptet fogunk írni, amely a játékosunkat mozgatja. Hozzunk létre egy szkriptet PlayerMovement néven és nyissunk meg a kódszerkesztőben. A szkript forrása az alábbihoz lesz hasonló. Most nem szükséges megérteni ennek minden elemét, sőt, az első két sorra nem is lesz szükségünk, az törölhető. Egyelőre elég a Start() és Update() függvényekre koncentrálnunk.

 
img007 - 2.1 - 1.png

Start() függvény akkor fog lefutni, amikor a játékunk elindul. Ide érdemes olyan kódot írni, amelynek csak egyszer kell lefutnia a játék indulásakor (például pontszám nullázása). Az alábbi kód például a konzolra kiír egy naplóüzenetet.

 

    void Start () {
        Debug.Log("Indul a játék!");
    }

A konzolra kerülnek ki a fontosabb hibaüzenetek és figyelmeztetések a játék futása során, ezért érdemes időről időre figyelnünk, hogy nem-e jelenik meg itt működési hibára utaló üzenet.

Tegyük fel, hogy szeretnénk, ha a játék indláskor egy erő hatna a játékosunkra. Ezt kódból oldhatjuk meg, méghozzá a Start() függvény alábbi módosításával:

    public Rigidbody rb;

    void Start () {

       rb.AddForce(0, 200, 500);

    }

Itt a Start() függvény előtti sor azt jelenti, hogy felveszünk egy Rigidbody típusú, rb nevű változót, aminek a segítségével a szkriptünkben hivatkozhatunk arra az objetumra, amire vonatkoznia kell majd a kódnak. Magában a függvényben pedig meghívjuk az AddForce() függvényt, amely egy irányvektorral megadott erősségű erőt alkalmaz majd az objektumunkra. 

A szkript elmentése után az Inspector-ban megjelenik a felvett Rb nevű változónk, a játékosunkhoz tartozó Rigidbody-t össze kell rendelnünk ezzel a változóval a Ridigbody belehúzásával (lásd az ábrát).

img008 - 2.1 - 2.png

Ha elindítjuk a játékot, láthatjuk, hogy az alkalmazott erő "eldobja" a kocka által megszemélyesített játékosunkat, ahogy azt vártuk.

2.1.1. feladat

Kísérletezz az alkalmazandó erő irányával és erejével. Kapcsold ki-be a gravitációt, módosítd a test tömegét.

 

A fenti kód egyszeri erő alkalmazását valósítja meg. Ha folyamatosan akarunk erőt kifejteni egy objektumra, akkor az Update() függvényt kell használnunk. Ez a függvény minden egyes képkocka legenerálása után, másodpercenként akár többször is lefut. Hogy pontosan hányszor, azt a futtató számítógép teljesítménye határozza meg, Ezt a számot hívjuk FPS-nek (Frames per Second)

 

2.1.2. feladat

Írjuk át a kódunkat úgy, hogy ezútatl az Update() függvényben hívjuk meg az AddForce() függvényt. Milyen változást tapasztalunk? Milyen hatása lehet a számítógép teljesítményének az objektumok viselkedésére?

Tipp: ha a fizikát érintő parancsokat akarunk használni, akkor optimalizációs indokok miatt ezeket a parancsokat célszerübb az Update() függvény helyett a FixedUpdate() függvénybe tennünk.

2.2. Változók használata

A fenti példában az alkalmazandó erőt közvetlenül a kódban adtuk meg. Ha meg szeretnénk változtatni az értékét, át kell írnunk a kódot. Jobb lenne, ha ezt közvetlenül módosíthatnánk az Inspector nézetben. Ezt publikus változók felvételével tehetjük meg. Fentebb ezt tettük a Rigidbody típusú rb változóval, ugyanezt bármilyen célú és típusú (pl. int, float, string vagy bool) változóval megtehetjük, ha szükséges. Ha a kódban definiálunk egy publikus változót, az meg fog jelenni az Inspector-ban ahol közvetlenül beállíthatjuk a kívánt értékét.

Maradva az erő példájánál, az alábbi kódban felveszünk egy force nevű, int típusú publikus változót, amely a Z tengely irányában alkalmazandó erőt fogja megadni:

 

Ennek a hatására az objektumunk Inspector nézetében a szkript komponens így fog kinézni:

img009 - 2.2 - 1.png

 

Ezáltal mindkét változó (Rb és Force) értéke közvetlenül az Inspector-ból lesz állítható.

Tipp: A Vector3 típus egy háromelemű tömbnek felel meg. Ha egy ilyen típusú force változót veszünk fel, akkor egyszerre adhatjuk meg az Inspector-ból a kívánt erő a három iránynak (X, Z és Z) megfelelő komonensét . 

2.3. Objektumok mozgatása kódból

Látható, hogy az objektumok akkor mozognak, ha erő hat rájuk. Ha mi szeretnénk meghatározni, hogy milyen irányba mozogkon egy objektum, akkor azt kell befolyásolnunk, hogy mekkoro erő hasson rá és milyen irányból. Ha a karakterünket billentyűzet vagy kontroller segítségével szeretnénk irányítani, akkor valamilyen módon érzékelnünk kell, hogy melyik billentyűt nyomtuk le és válaszul az annak megfelelő mozgást kiváltó erőt kell alkalmaznunk az objektumon. Ennek egy lehetséges módja az Input.GetKey(key) függvény használata, ami true-t ad vissza, ha a paraméterben kapott billentyű le van nyomva, egyébként pedig false értékkel tér vissza.

Próbáljuk ki az alábbi kódot, amely a játékosunkat mozgatja az A-D-W-S billentyűkkel:

 

A fenti módszer működik, de nem a leghatékonyabb a billentyűgombokkal való irányítás megvalósítására. Később megnézzük, hogy a fenti módszer milyen problémákkal jár és hogy lehet azokat orvosolni.

2.4. Anyagok és súrlódás

Megfigyelhetjük, hogy a kockánk mozgás közben nem siklik a talaj felületén, hanem pörög az élein. A kocka pörgését megakadályozhatjuk, ha a Box Collider komponensben a Freeze Rotation értkét tengelyenként true-ra állítjuk, de ez azt eredményezi, hogy a kockánk akkor sem fog pörögni, ha más objektummal vagy akadállyal ütközik. Ehelyett inkább lássuk, hogy mi a probléma gyökere: ez nem más, mint a kocka és talaj között fellépő súrlódás, amit a Unity játék közben valósághűen szimulál. A súrlódás mértéke egyebek mellett a súrlódó testek anyagától és súlyától függ. Az objektumunk anyagát a Box Collider >> Material tulajdonságban lehet megadni. Hozzunk létre a Project >> Assets nézetben egy új anyagot a Create >> Physic Material menüből, majd állítsuk be az Inspector nézetben a Dynamic Friction és Static Friction tulajdonságok értékeit 0-ra. Ezután húzzuk rá a létrehozott új anyagot a talajra. Azt fogjuk tapasztalni, hogy a viselkedése megváltozott: a súrlódás lenullázása miatt a kocka úgy mozog, mintha jégen csúszna.

 

2.4.1. feladat

Miután lecseréltük a talaj anyagát, a talaj sűrlódását megszüntettük, de a kocka súrlódása megmaradt, ezért egy idő után a csúszás megáll. Cseréljük le a kocka anyagát is a sűrlódásmentes anyagra és figyeljük meg, hogyan változik a talaj ás a kocka viselkedése.

 

2.5. A szereplő követése a kamera által

Korábban megtanultuk, hogy hogyan tudjuk mozgatni a szereplőnket, de a mozgás során a kamera nézőpontja és iránya statikus maradt. Leggyakrabban az az elvárt működés, hogy a kamera követi a szereplőnk mozgását. Az egyik lehetőség az, hogy a Hierarchy nézetben kamerát behúzzuk a játékosunk alá. Ezzel gyakorlatilag a kettőnek a pozícióját összekötjük. Ez jó lehet, de néha furcsa hatást kelthet, mert a kamera együtt fordul a szereplővel, amit rendszerint nem célunk. A másik megoldás az lehet, hogy szkriptből állítjuk be a kamera pozícióját.

Adjunk a kameránkhoz egy szkript komponenst és nevezzük el azt, mondjuk FollowPlayer-nek. A forráskódból töröljük ki a felesleges sorokat, csak az Update() függvényt hagyjuk meg. Mivel ez a kamera kódja és a pozícióját a szereplő pozíciójához szeretnénk kötni, szükségünk lesz egy változóra, amely a játékos Transform komponensére fog mutatni és amelyen keresztül fogjuk a játékos pozícióját kiolvasni. Hasonlóan a korábbi példákhoz ezt a következő sorral tehetjük meg:

public Transform player;

Ahogy korábban is, az Inspector nézetben kell összekötnünk az új változót a szereplővel, belehúzva azt a Hierarchy nézetből. A szereplő pozícióját a player.position tulajdonságból tudjuk kiolvasni és felhasználni, a kamara saját pozícióját pedig a transform.position-ból. A következő kódsor a kamera pozícióját beállítja a szereplő pozíciójával:

transform.position = player.position;

A játékot futtatva látjuk, hogy működik az elképzelésünk, a kamera folyamatosan követi a szereplőt. Igazából a játékot teljesen a szereplő szemszögéből látjuk. Ha kissé a szereplő fölé-mögé szeretnénk elhelyezni a kamerát, akkor a kamera pozíciójának koordinátáit módosítanunk kell némi eltolással. Az eltolást tárolására vegyünk fel egy Vector3 típusú, offset nevű változót és az Inspector nézetben adjuk meg a tengelyenkénti eltolás értékét. A kódot kissé módosítanunk kell, hogy ezek az értékek figyelembe legyenek véve: 

transform.position = player.position + offset;

Figeljük meg, hogy egyetlen operátorral össze tudjuk adni a vektorok mindhárom komponensét. A teljes kód így fog kinézni:

 

2.5.1. feladat

Szimuláljuk két kameraállást a játékunkban két különböző offset megadásával. A két nézet között lehessen váltani: a 'c' billentyű lenyomására ugorjunk az egyikbe, a 'v' billentyűvel pedig a másikba. Használjuk a korábban megismert if utasítást és az Input.GetKey() függvényt. Ha szeretnénk, hogy a kameránk oldalról nézze a jelenetet, akkor állítsuk be a pozícióját a fent leírt módon és az irányát módosítsuk az alábbi parancssal úgy, hogy a jelenet felé nézzen:

transform.rotation = Quaternion.Euler(0, rotationY, 0);

 

 

2.6. Ütközések detektálása

A játék során a szereplők mozognak és gyakran ütköznek (nekimegyünk a falnak, egy golyó eltalál egy szereplőt, a tekegolyó ledönti a bábukat, stb). Fel kell tudnunk ismerni az ütközés eseményét (mely szereplő mivel ütközött, milyen erővel, mi volt az ütközési pont, stb) és tudnunk kell megfelelően reagálni az eseményre.

Tételezzük fel, hogy az a feladat, hogy át kell jutnunk a játékosunkkal egy az alábbi ábra szerinti szűkített átjárón és a játéknak véget kell érnie, ha érintjük az átjáró falát. 

 
img010 - 2.6 - 1.png

 

A fenti példában legyen a két akadályt képező objektum neve ObstactleL és ObstacleR, az ezekkel való ütközést kellene detektálnunk. Ehhez vegyünk fel egy új, pl. PlayerCollision nevű szkriptet a szereplő számára, töröljük a felesleges Start() és Update() függvényeket és vegyünk fel egy void típusú OnCollisionEnter(Collision c) eseménykezelőt az alábbiak szerint: 

Az OnCollisionEnter(Collision c) eseménykezelő minden alkalommal meghívásra kerül, amikor a szereplőnk ütközik valamelyik másik objektummal, feltéve, hogy a szereplő rendelkezik Rigidbody és [Box] Collider komponensekkel. A Collision típusú, c nevű paraméter opcionális, ha használjuk, ebben kaphatjuk vissza az ütközés adatait. Ezek közül is a Collision.collider az egyik legfontosabb, ez azt az objektumot takarja, amellyel ütköztünk.

 

A fenti kódot kipróbálva láthatjuk, hogy az objektumunk a talajjal is ütközik. Az alábbi utasítással figyelmen kívül hagyhatjuk a talajjal való ütközést és koncentrálhatunk a fallal való ütközés detektálására:


       if (c.collider.name == "ObstacleL" || c.collider.name == "ObstacleR") {
      Debug.Log("Ütköztünk a fallal");

      }
  

Ez a módszer akkor lehet problémás, ha túl sok fal elemünk van: ekkor végig kellene vizsgálnuk egyesével, hogy annak az objektumnak a neve, amivel ütköztünk, egyike-e a fal elemeknek. Ehelyett a fal elemek minegyikhez hozzárendelhetjük ugyanazt a cimkét (tag-et) és azt vizsgálhatjuk, hogy annak az objektumnak, amivel ütköztünk, a cimkéje megegyezik-e a fal elemek cimkéjével. Ha a cimke pl. "Wall" volt, akkor a kód így módosul:

   if (c.collider.tag == "Wall") {
      Debug.Log("Ütköztünk a fallal");
   } 

Ezzel fel tudjuk ismerni, ha ütközünk a játékban. Ha azt szeretnénk, hogy a játékos ne mozgathassa tovább a szereplőt, akkor le kellene tiltanunk azt azt megvalósító szkriptet. Ezt megtehetjük kódból, miután felvettünk egy PlayerMovement típusú változót és azt az Inspector nézetben összekötöttük a Játékos objektum PlayerMovement szkriptjével. A kód így fog kinézni:

Ennek a kódnak köszönhetően ha ütközünk a fallal, a játékos elveszíti a szereplő feletti irányítást, mivel az azt megvalósító szkriptet letíltottuk az enabled tulajdonság false-ra való állításával.

Tipp: ha azt tapasztaljuk, hogy az ütközés-detektálás nem mindig pontos, precízebbé tehetjük azt azzal, ha az objektumok Rigidbody >> Collision Detection tulajdonságának értékét Continuous-ra állítjuk.