Developedia
Developedia

Az állapotgép minden játék alapjaiban

Minden szoftvernek, ami némileg meghaladja a Hello World típusú példaprogramok méretét van belső állapota. Ez a belső állapot a memóriában létező objektumok összessége. Ezen számtalan objektumnak kell legyen valamilyen hierarchiája: Van amelyik fontosabb, mint a többi. Ha ez a hierarchia hiányzik, akkor nagy bajban vagyunk, ezért is most feltételezzük, hogy a képzeletbeli szoftverünk osztály/objektum hierarchiája létezik és jól átgondolt.

A hierarhiában nevezzük alacsony szintűnek az alapvető osztályokat és objektumaikat úgy, hogy egyre magasabb szintekre lépve egyre jelentéktelenebbek fognak szerepelni. Fordítva is válaszhatnánk ezt a terminológiát, de ebben a blogposztban ehhez fogom magam tartani, mivel ez a reláció jobban illeszkedik az épület-alap és a a fa-gyökér valós világ béli példáihoz.

Ha lelépkedünk a gyökérig ebben a hierarchiában elérve a legfontosab objektumot, akkor talán találunk valami olyasmit, mint egy állapotgép, ami néhány véges állapot közt váltogat. Egy játék esetében ezek az állapotok lehetnek pl.: Kezdőképernyő, Játékmenü, Pályaválasztó, Játék, Játék vége képernyő, Pause képernyő, Beállítás menü, Grafikai beállítás menü, stb…

Mint a fenti példa mutatja, ez a fő állapotgép nagyrészt a UI / UX navigációt fogja fedezni. Talán nem gondolunk erre gyakran úgy mint a legalapvetőbb részére a játéknak, de ha végiggondoljuk, akkor rájövünk, hogy mégis így van. Ez az az objektum, aminek a szoftver teljes futása alatt mindig léteznie kell.

Azt szeretném jelen blogposztban megosztani, hogy tapasztalatom szerint hogyan érdemes ezt az állapotgépet felépíteni ezen a lehető legalacsonyabb, azaz legfundamentálisabb szinten, ami a legtöbbször fedezi az igényeinket. Nem túl komplikált a válaszom, de sok kísérletezés és sok projekt erevménye, ami alapján kikristályosodott bennem, hogy ez a legcélszerűbb megközelítés a játékok sőt általánosabban a szoftverek nagyrészében.

💡

Szóval az egyszerű válaszom, hogy az állapotgép legyen egy Stack, vagy (ha valaki nagyon ragaszkodik a magyar terminológiához,) egy verem.

Oké. Lehet, hogy én vagyok lassú, hogy évek kellettek, hogy ezt felismerjem, de az is biztos, hogy vannak még mások is akik ezzel a kérdéssel kűzdenek. Ezért is a jelen poszt.

Hogy miket pakolunk ebbe a verembe, ami reprezentálja az egyes fő szoftverállapotokat, az mellékes. Lehetnek referenciák összetettebb objetumokra, de egyszerű enum-ok is. Egyedül azért könyörgök az olvasónak, hogy ne használjon string-eket. Ha ilyesmi bárkinek is megfordulna egyáltalán a fejében, azonnal felejtse el! Nehezemre esik nem kifelyteni, miért olyan rossz ötlet ez, de most fókuszálnom kell a témára.

Unity projekt esetén használhatunk állapotnak Scriptable Objekt-eket, amiknek mindig megvan az az előnye, hogy kódolás nélkül is létrehozhatók, és ami mégfontosabb, igény esetén beállítások adhatók hozzá. Viszont ahogy korábban jeleztem, sok esetbnen egy enum bőven elég a célra.

Támogassuk meg a vermünket egy Observer patternnel, azzaz indítsunk felíratkozható eseményt, amikor bármi változás történt az adatszerkezetünkön. A releváns információk, amit az eseménynek továbbítani kell, az, hogy…

  • push vagy pop művelet történt,
  • mi került a vermünk tetejére,
  • és mi volt ott (legfelül) előtte.

Végül hasznos lehet az alapvető Push, Pop és Peak műveleteken kívül még egy-két hasznos segítő metódust a veremhez adni, és már kész is vagyunk.

  • bool Contains(State): Tartalmaz-e egy konkrét State-et lekérdezés
  • void PopUntil(State): Visszalépés egy konkrét State-ig

Unity estében érdemes lehet, ha ez az állapotgép egy Don’tDestroyOnLoad Singleton MonoBehaviour. Ezáltal könnyen tudjuk menedzselni a játék indítását és kikapcsolását.

  • Induláskor hozzáadjuk az üres veremhez a kezdőállapotot
  • Befejezéskor egyenként felülről lefelé kivesszük a még veremben lévő elemeket.

De miért pont verem? Miért korlátozzuk le magunkat egy ilyen korlátolt adatstruktúrára?

  1. A veremmel egy memóriát építünk, amivel mindig tudjuk, hogy hova tudunk visszalépni. Nem csak a mostani állapotot tudjuk, hanem az összes lejjebbit is megfelelő sorrendben. Gondoljuk végig: egy menü, almenü, akár csak egy igen/nem kérdésbuborék is mindig automatikusan tudja, mi történik ha egyszerűen kilépünk belőle, hova kell mennie. Ezt a viselkedést komplikált lenne hagyományos állapotgéppel elérni.
  2. Egy State-nek nem csak aktív és nem-aktív állapota van, hanem még egy köztes lehetősége is mikor a State a veremben van, de nem legfelül. Úgy tapasztaltam, hogy ez meglepően hasznos lehet számtalan esetben. Pl.: egy olyan objektum, mint a játékos karakter, akkor szünteti meg magát, hogyha a Gameplay állapot kikerült a veremből, viszont hibernálja magát (nem csinál semmit), ha valami rákerült a Gameplay állapot tetejére a veremben, pl. egy Pause vagy arre még egy Settings menü.
  3. Többszörös visszalépkedés esetén nem ugorhatunk át állapotokat. Ez hasznos lehet, hogyha egy objektum egy állapotból való kilépés esetén szeretne maga után valamennyit takarítani.
  4. A verem korlátozó természete jelen esetben kifejezetten hasznos a felhasználói élmény szempontjából is. A verem egy egyértelmű UX flow-t ad a szoftvernek. Persze lehetne sokkal ugrálósabb is a program, de minek? Ha a játék videobeállítás menüjéből átugorhatunk a pályaválasztóba az lehet, hogy spórol 1-2 másodpercet a felhasználónak, de összezavarhatná a navigációban. Sokkal tisztább azt a logikát követni, hogy ha egy állapotba belépünk, abból ki is kell lépni valamikor.
  5. Illegális állapotlépéseket sok esetben automatikusan tudunk detektálni és jelezhetjük a fejlesztőnek, hogy valamit elrontott. (Pl. ha push-olni akarunk egy állapotot, ami már korábban szerepel a veremben.)
  6. Ékegyszerű ez a megoldás. Egyszerű implementálni, egyszerű átlátni és egyszerű egy debug tool-lal vizualizálni. (Nem kell irányított gráfokkal szórakozni.) Ezeket a tulajdonságokat szeretem látni a korábban említett fontossági osztály/objektum hierarchiám alján.

Mindezért manapság egy új projekt írásakor gondolkodás nélkül ezzel a patternnel indítok. Természetesen nem fogja ez az egy állapotgép lefedni minden igényünket az adott szoftver fejlesztésében, viszont magasabb szinteken egyéb hasonló vagy teljesen más viselkedésű állapotgépek bármikor beépíthetők. Ezt a legalsó elemet azonban legtöbb esetben hasznos megtartani olyan egyszeűnek, mint egy függőleges lyuk a földben.

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