Writing a Game Engine from Scratch – Part 1: Messaging

author
23 minutes, 52 seconds Read

A következő blogbejegyzést, hacsak másképp nem jelezzük, a Gamasutra közösségének egy tagja írta.
A kifejtett gondolatok és vélemények az író sajátjai, és nem a Gamasutra vagy annak anyavállalata.

1. rész – Üzenetkezelés
2. rész – Memória
3. rész – Adat & Cache
4. rész – Grafikus könyvtárak

Nagyszerű időket élünk fejlesztőként. A hatalmas mennyiségű, mindenki számára elérhető, nagyszerű AAA-minőségű Engine-ek segítségével az egyszerű játékok készítése olyan egyszerű lehet, mint a drag-and-drop. Úgy tűnik, manapság már egyáltalán nincs okunk engine-t írni. És a közvélekedés szerint: “Írj játékokat, ne motorokat”, miért is tennéd?

Ez a cikk elsősorban szólófejlesztőknek és kis csapatoknak szól. Feltételezek némi jártasságot az objektumorientált programozásban.

Az Engine-fejlesztés megközelítéséhez szeretnék némi betekintést nyújtani, és ennek illusztrálására egy egyszerű fiktív Engine-t fogok használni.

Miért írjunk Engine-t?

A rövid válasz a következő: Ne tedd, ha elkerülheted.

Az élet túl rövid ahhoz, hogy minden játékhoz egy motort írjunk (Sergei Savchenko 3D Graphics Programming című könyvéből vettük)

A jelenlegi kiváló Engine-ek, mint például a Unity, Unreal vagy CryEngine, olyan rugalmasak, amilyet csak remélni lehet, és nagyjából bármilyen játék elkészítéséhez használhatók. Speciálisabb feladatokra természetesen léteznek speciálisabb megoldások, mint az Adventure Game Studio vagy az RPG Maker, hogy csak néhányat említsek. Már még a kereskedelmi forgalomban kapható Engine-ok ára sem érv.

Már csak néhány hiánypótló ok maradt a saját engine írására:

  • Meg akarod tanulni, hogyan működik egy motor
  • Szükséged van bizonyos funkciókra, amelyek nem állnak rendelkezésre, vagy a rendelkezésre álló megoldások instabilak
  • Hiszed, hogy jobban/gyorsabban meg tudod csinálni
  • Az irányításod alatt akarod tartani a fejlesztést

Mindegyik teljesen jogos ok, és ha ezt olvasod, akkor valószínűleg az egyik ilyen táborhoz tartozol. A célom az, hogy ne menjek bele egy hosszadalmas “Melyik motort használjam?” vagy “Írjak-e motort?” vitába itt, és rögtön bele is ugrok. Kezdjük tehát.

Hogyan bukjunk el egy Engine megírásakor

Várj. Először azt mondom, hogy ne írj egyet, aztán elmagyarázom, hogyan kell elbukni? Nagyszerű bevezetés…

Mindenesetre rengeteg dolog van, amit figyelembe kell venni, mielőtt egyetlen sor kódot is írsz. Az első és legnagyobb probléma, amivel mindenki szembesül, aki játékmotort kezd írni, a következőkre vezethető vissza:

Minél gyorsabban akarok játékmenetet látni!

Mennél gyorsabban rájössz, hogy sok időbe fog telni, amíg valóban látni fogsz valami érdekeset, annál jobban fogod tudni megírni a motorodat.

A legnagyobb ellenséged ezen a ponton az, ha arra kényszeríted a kódodat, hogy minél gyorsabban mutasson valamilyen grafikát vagy játékmenetet, csak azért, hogy legyen valami vizuális megerősítése a “haladásnak”. Vegyük. A tiéd. Time!

Ne is gondolj arra, hogy a grafikával kezdd. Valószínűleg rengeteg OpenGL / DirectX oktatóanyagot és könyvet olvastál, és tudod, hogyan kell renderelni egy egyszerű háromszöget vagy sprite-ot. Azt gondolhatod, hogy egy rövid kódrészlet egy kis háló renderelése a képernyőn egy jó hely a kezdéshez. Nem az.

Igen, a kezdeti fejlődésed elképesztő lesz. A fenébe is, egy nap alatt végigfuthatsz egy kis szinten First-Personban, különböző tutorialokból és a Stack Overflow-ból másolt kódrészleteket másolva. De garantálom, hogy 2 nap múlva minden egyes sorát törölni fogod annak a kódnak. Sőt, ami még rosszabb, még az is előfordulhat, hogy elkedvetlenedsz az Engine írásától, hiszen nem motiváló, hogy egyre kevesebbet és kevesebbet látsz.

A második nagy probléma, amivel a fejlesztők szembesülnek az Engines írása közben, az a feature creep. Mindenki szeretné megírni az Engines szent grálját. Mindenki azt a tökéletes Engine-t akarja, ami mindent tud. First Person Shooterek, taktikai RPG-k, amit csak akarsz. De az egyszerű tény az, hogy nem lehet. Még nem. Csak nézd meg a nagy neveket. Még a Unity sem tud minden játékműfajt tökéletesen kiszolgálni.

Ne is gondolj arra, hogy olyan Engine-t írj, ami egynél több műfajra képes elsőre. Don’t!

Hol is kezdjük valójában az Engine írását

Egy Engine írása olyan, mintha egy igazi motort terveznél egy autóhoz. A lépések tulajdonképpen elég nyilvánvalóak, feltéve, hogy tudod, milyen játékon (vagy autón) dolgozol. Itt vannak:

  1. Pontosítsd meg, hogy mire kell képesnek lennie a motorodnak ÉS mire nem kell képesnek lennie.
  2. Rendezd az igényeket Rendszerekbe, amelyekre a motorodnak szüksége lesz.
  3. Tervezd meg a tökéletes Architektúrát, amely összeköti ezeket a Rendszereket.
  4. Ismételd meg az 1. – 3. lépéseket, amilyen gyakran csak lehet.
  5. Kódolj.

Ha (= ha és csak ha) elég időt és energiát fordítasz az 1. – 4. lépésekre, és a játéktervezés nem változik hirtelen horrorjátékból nyerőgéppé (olvasd: Silent Hill), a kódolás nagyon kellemes vállalkozás lesz. A kódolás még mindig messze nem lesz könnyű, de tökéletesen kezelhető, még a szóló fejlesztők számára is.

Ez az oka annak, hogy ez a cikk elsősorban az 1. – 4. lépésekről szól. Gondolj az 5. lépésre úgy, mint “Az üres helyek kitöltésére”. 50.000 LOC üres hely”.

Az egésznek a legfontosabb része a 3. lépés. Erőfeszítéseink nagy részét ide fogjuk összpontosítani!

1. lépés. Határozzuk meg a szükségleteket és a szükségtelenségeket

Ezek a lépések elsőre meglehetősen triviálisnak tűnhetnek. De valójában nem azok. Azt gondolhatnánk, hogy egy First Person Shooter Engine fejlesztésének 1. lépése a következőkre egyszerűsíthető le:

El kell töltenem egy szintet, a játékosok fegyverét, néhány AI-val rendelkező ellenséget. Kész, tovább a 2. lépéshez.

Ha csak ilyen egyszerű lenne. A legjobb módja az 1. lépésnek az, hogy az egész játékot kattintásról-kattintásra, akcióról-akcióra végigmegyünk az asztalon lévő ikonra kattintástól a Credits gurítása után a Kilépés gomb megnyomásáig. Készíts egy listát, egy nagy listát arról, hogy mire van szükséged. Készíts egy listát arról, amire biztosan nincs szükséged.

Ez valószínűleg így fog lezajlani:

Elindítom a játékot, és egyenesen a főmenübe megy. A menü statikus képet fog használni? Egy vágott jelenetet? Hogyan tudom vezérelni a főmenüt, egérrel? Billentyűzet? Milyen GUI elemekre van szükségem a főmenühöz? Gombok, űrlapok, görgetősávok? Mi a helyzet a zenével?

És ezek még csak makró-megfontolások. Menj bele olyan részletesen, amennyire csak lehet. Ha úgy döntesz, hogy Gombokra van szükséged, az szép és jó, de azt is fontold meg, hogy mit tud egy gomb.

A gomboknak 4 állapota legyen: Fel, Hover, Le, Le, Letiltva. Szükségem lesz hangra a gombokhoz? Mi a helyzet a speciális effektekkel? Animáltak-e az üresjárati állapotban?

Ha a Szükségletek és nem szükségletek listája a Főmenü végére csak kb. 10 elemet tartalmaz, akkor valamit rosszul csináltál.

Ebben a szakaszban azt csinálod, hogy szimulálod a motort az agyadban, és leírod, hogy mit kell csinálni. Az 1. lépés minden egyes iterációval egyre világosabb lesz, ne aggódj amiatt, hogy az első alkalommal valamit kihagysz.

2. lépés. Rendszerezd az igényeket Rendszerekbe

Szóval, megvan a listád a szükséges és nem szükséges dolgokról. Itt az ideje, hogy rendszerezd őket. Nyilvánvaló, hogy a GUI-val kapcsolatos dolgok, mint például a gombok, valamilyen GUI rendszerbe kerülnek. A rendereléssel kapcsolatos dolgok a Graphics System / Engine-be kerülnek.

Mint ahogy az 1. lépésnél is, annak eldöntése, hogy mi hova kerül, a második iterációnál, a 3. lépés után már egyértelműbb lesz. Az első lépésnél csoportosítsd őket logikusan, mint a fenti példában.

A legjobb referencia a “mi hova kerül” és “mi mit csinál” témában kétségkívül Jason Gregory Game Engine Architecture című könyve.

Kezdd el a funkciók csoportosítását. Kezdj el gondolkodni azon, hogyan lehet őket kombinálni. Nincs szükséged Camera->rotateYaw(float yaw) és Camera->rotatePitch(float pitch)-ra, ha össze tudod kombinálni őket Camera->rotate(float yaw, float pitch)-ban. Maradjon egyszerű. A túl sok funkcionalitás (ne feledd, feature creep) később ártani fog neked.

Gondold át, hogy milyen funkcionalitásnak kell nyilvánosan láthatónak lennie, és milyen funkcionalitásnak kell csak magában a Rendszerben tartózkodnia. Például a Rendererednek minden átlátszó Sprite-ot szét kell válogatnia rajzolás előtt. Az ezeket a sprite-okat rendező funkciót azonban nem kell nyilvánosságra hozni. Ön tudja, hogy az átlátszó Sprite-okat rajzolás előtt rendezni kell, nincs szüksége egy külső Systemre, hogy ezt megmondja Önnek.

3. lépés. Az architektúra (Vagy a tényleges cikk)

A cikket akár itt is kezdhettük volna. Ez az érdekes és fontos rész.

A motorod egyik legegyszerűbb lehetséges Architektúrája az, hogy minden Rendszert egy osztályba helyezel, és a Main Game Loop meghívja az alprogramjaikat. Ez valahogy így nézhet ki:

while(isRunning)
{
Input->readInput();
isRunning = GameLogic->doLogic();
Camera->update();
World->update();
GUI->update();
AI->update();
Audio->play();
Render->draw();
}

Előre teljesen ésszerűnek tűnik. Minden alapot lefedtél, Input -> feldolgozás Input -> Output.

És valóban, ez elegendő egy egyszerű Játékhoz. De fájdalmas lesz a karbantartása. Ennek oka nyilvánvalónak kell lennie: Függőségek.

Minden rendszernek valamilyen módon kommunikálnia kell más rendszerekkel. A fenti Játékhurokban erre nincs semmilyen eszközünk. Ezért a példa egyértelműen jelzi, hogy minden egyes Rendszernek rendelkeznie kell valamilyen Referenciával a többi Rendszerről ahhoz, hogy bármi értelmeset tehessen. A GUI-nak és a játéklogikának tudnia kell valamit a bemenetünkről. A Rendererünknek tudnia kell valamit a Játéklogikánkról ahhoz, hogy bármi értelmeset megjelenítsen.

Az építészeti csodához vezet:

Ha spagettiszagú, akkor az spagetti. Határozottan nem az, amit mi akarunk. Igen, könnyű és gyors a kódolás. Igen, elfogadható eredményeket fogunk kapni. De karbantartható, az nem. Ha valahol megváltoztatunk egy kis kóddarabot, az pusztító hatással lehet az összes többi rendszerre, anélkül, hogy tudnánk róla.

Mégis mindig lesz olyan kód, amihez sok rendszernek kell hozzáférnie. Mind a GUI-nak, mind a Renderernek Draw hívásokat kell végeznie, vagy legalábbis hozzáféréssel kell rendelkeznie valamilyen interfészhez, ami ezt kezeli helyettünk. Igen, megadhatnánk minden rendszernek a jogot, hogy közvetlenül hívja az OpenGL / DirectX függvényeket, de a végén rengeteg redundanciát kapnánk.

Megoldhatnánk ezt úgy, hogy összegyűjtjük az összes rajzolási függvényt a Renderer rendszeren belül, és azokat a GUI rendszerből hívjuk meg. De akkor a Rendering Systemnek speciális függvényei lesznek a GUI számára. Ezeknek nincs helyük a Rendererben, és ezért ellentétes az 1. és 2. lépéssel. Döntések, döntések.

Ezért az első dolog, amit meg kell fontolnunk, hogy a motorunkat rétegekre osztjuk.

Motor Lasagne

A lasagna jobb, mint a spagetti. Legalábbis programozási szempontból. Renderer példánknál maradva, azt szeretnénk, hogy az OpenGL / DirectX függvényeket anélkül hívjuk meg, hogy közvetlenül a System. Ez olyan szagú, mint egy Wrapper. És nagyrészt az is. Az összes rajzolási funkciót egy másik osztályon belül gyűjtjük össze. Ezek az osztályok még egyszerűbbek, mint a mi Rendszereink. Nevezzük ezeket az új osztályokat Frameworknek.

Az ötlet e mögött az, hogy absztraháljuk az alacsony szintű API hívások nagy részét, és valami olyanná formáljuk őket, ami a játékunkra szabott. Nem akarjuk beállítani a Vertex Buffer-t, beállítani az Index Buffer-t, beállítani a Textures-t, engedélyezni ezt, letiltani azt csak azért, hogy egy egyszerű rajzhívást végezzünk a Renderer rendszerünkben. Tegyük az összes ilyen alacsony szintű dolgot a keretrendszerünkbe. És a keretrendszer ezen részét egyszerűen csak “Draw”-nak fogom hívni. Miért? Nos, ez csak annyit tesz, hogy mindent beállít a rajzoláshoz, majd lerajzolja azt. Nem érdekli, hogy mit rajzol, hol rajzol, miért rajzol. Ez a Renderer Systemre marad.

Ez furcsa dolognak tűnhet, hiszen sebességet akarunk a motorunkban, igaz? Több absztrakciós réteg = kevesebb sebesség.

És igazad is lenne, ha a 90-es években lennénk. De nekünk szükségünk van a karbantarthatóságra, és a legtöbb résznél együtt tudunk élni az alig észrevehető sebességveszteséggel.

Hogyan kellene akkor megtervezni a Draw Frameworkünket? Egyszerűen fogalmazva, mint a saját kis API-nk. Az SFML egy remek példa erre.

Fontos dolgokat tartsunk szem előtt:

  • Tartsuk jól dokumentálva. Milyen függvényeink vannak? Mikor hívhatók? Hogyan hívjuk őket?
  • Maradjon egyszerű. Az olyan egyszerű függvények, mint a drawMesh(Mesh* oMesh) vagy a loadShader(String sPath) hosszú távon boldoggá tesznek.
  • Tartsd funkcionálisnak. Ne légy túl specifikus. drawButtonSprite helyett legyen egy drawSprite funkció, és hagyd, hogy a hívó kezelje a többit.

Mit nyerünk? Rengeteget:

  • A keretrendszerünket csak egyszer kell beállítanunk, és minden szükséges rendszerben használhatjuk (GUI, Renderer….)
  • A mögöttes API-kat könnyen megváltoztathatjuk, ha úgy döntünk, anélkül, hogy minden rendszert újra kellene írnunk. OpenGL-ről DirectX-re váltani? Nem probléma, csak írjuk át a Framework osztályt.
  • A rendszerünk kódja tiszta és feszes marad.
  • A jól dokumentált interfész azt jelenti, hogy egy ember dolgozhat a Framework-en, míg egy másik ember a System Layer-en.

Valószínűleg valami ilyesmivel fogunk végezni:

Az én ökölszabályom, hogy mi kerüljön a Framework-be, elég egyszerű. Ha meg kell hívnom egy külső könyvtárat (OpenGL, OpenAL, SFML…) vagy vannak adatstruktúrák / algoritmusok, amelyekre minden rendszernek szüksége van, akkor azt a keretrendszerben kell megtennem.

Már kész az első réteg a lasagnából. De még mindig ott van fölötte ez a hatalmas gombóc Spagetti. Foglalkozzunk vele legközelebb.

Messaging

A Nagy Probléma azonban továbbra is fennáll. A rendszereink még mindig össze vannak kapcsolva. Ezt nem akarjuk. Számos módja van a probléma kezelésének. Események, üzenetek, absztrakt osztályok függvénymutatókkal (Milyen ezoterikus)…

Maradjunk az üzeneteknél. Ez egy egyszerű koncepció, amely még mindig nagyon népszerű a GUI programozásban. A mi Engine-ünk számára is jól használható egyszerű példaként.

Úgy működik, mint a posta. Az A vállalat üzenetet küld a B vállalatnak, és kéri, hogy tegyenek valamit. Ezeknek a cégeknek nincs szükségük fizikai kapcsolatra. Az A vállalat egyszerűen feltételezi, hogy a B vállalat valamikor meg fogja tenni. De egyelőre az A vállalatot nem igazán érdekli, hogy a B vállalat mikor és hogyan teszi meg. Csak meg kell csinálni. A fenébe is, a B vállalat akár úgy is dönthet, hogy átirányítja az üzenetet a C és D vállalathoz, és hagyja, hogy ők intézzék el.

Mehetünk még egy lépéssel tovább, az A vállalatnak még csak nem is kell elküldenie valakinek konkrétan. Az A vállalat egyszerűen feladja a levelet, és bárki, aki felelősnek érzi magát, feldolgozza azt. Így a C és a D vállalat közvetlenül feldolgozhatja a kérést.

Nyilvánvaló, hogy a vállalatok egyenlőek a mi Rendszereinkkel. Nézzünk egy egyszerű példát:

  1. A keretrendszer értesíti a beviteli rendszert, hogy az “A” billentyűt megnyomta
  2. A bevitel lefordítja, hogy az “A” billentyűleütés “Leltár megnyitása”, és üzenetet küld, amely tartalmazza a “Leltár megnyitása”
  3. .

  4. GUI kezeli az üzenetet és megnyitja a leltár ablakot
  5. Game Logic kezeli az üzenetet és szünetelteti a játékot

Input nem is érdekli, hogy mi történik az üzenetével. A GUI-t nem érdekli, hogy a Game Logic is feldolgozza ugyanazt az Üzenetet. Ha mindezek összekapcsolódnának, akkor az Inputnak meg kellene hívnia egy függvényt a GUI rendszerben és egy függvényt a Game Logicban. De erre már nincs szükség. Ezt sikeresen szét tudtuk választani az Üzenetek segítségével.

Hogyan néz ki egy Üzenet? Legalább valamilyen típusnak kell lennie. Például a leltár megnyitása lehetne valami OPEN_INVENTORY nevű enum. Ez elegendő az ilyen egyszerű Üzenetekhez. A fejlettebb Üzeneteknek, amelyeknek adatokat kell tartalmazniuk, szükségük lesz valamilyen módon tárolni ezeket az adatokat. Ennek megvalósítására számtalan mód van. A legegyszerűbben egy egyszerű térképszerkezet használatával valósítható meg.

De hogyan küldjük el az Üzeneteket? Természetesen egy Message Buson keresztül!

Hát nem gyönyörű? Nincs többé spagetti, csak a jó öreg sima lasagna. Szándékosan az Üzenetbusz másik oldalára helyeztem a Játéklogikánkat. Mint látható, nincs kapcsolata a Framework réteggel. Ez azért fontos, hogy elkerüljük a kísértést, hogy “csak azt az egy függvényt hívjuk meg”. Higgye el, előbb-utóbb ezt akarja majd, de ez tönkretenné a tervünket. Van elég rendszerünk, amelyik a keretrendszerrel foglalkozik, nincs szükség erre a játéklogikánkban.

A Message Bus egy egyszerű osztály, amelyik minden rendszerre hivatkozik. Ha van egy üzenet a sorban, a Message Bus egy egyszerű handleMessage(Msg msg) hívással elküldi azt minden Systemnek. Cserébe minden Systemnek van egy hivatkozása a Message Busra, hogy üzeneteket küldhessen. Ez nyilvánvalóan tárolható belsőleg vagy átadható függvény argumentumként.

Minden Rendszerünknek tehát örökölnie kell vagy a következő formájúnak kell lennie:

class System
{
public:
void handleMessage(Msg *msg);
{
switch(msg->type)
{
//// Example
//case Msg::OPEN_INVENTORY:
// break;
}
}

private:

MessageBus *msgBus;
//// Usage: msgBus->postMessage(msg);
}

(Igen, igen, nyers mutatók…)

A játékkörünk hirtelen úgy változik, hogy egyszerűen hagyjuk, hogy az Üzenetbusz üzeneteket küldjön körbe. Továbbra is szükségünk lesz arra, hogy periodikusan frissítsük az egyes rendszereket valamilyen update() híváson keresztül. De a kommunikációt másképp fogjuk kezelni.

Mégis, mint a keretrendszerünk esetében, az Üzenetek használata túlterhelést okoz. Ez egy kicsit lelassítja a motort, ne áltassuk magunkat. De minket ez nem érdekel! Tiszta és egyszerű dizájnt akarunk. Egy tiszta és egyszerű architektúrát!

És a legkirályabb rész? Elképesztő dolgokat kapunk ingyen!

A konzol

Minden üzenet nagyjából egy függvényhívás. És minden Message nagyjából mindenhová elküldésre kerül! Mi lenne, ha lenne egy olyan Rendszerünk, amely egyszerűen kiírna minden elküldött Üzenetet valamilyen Kimeneti ablakba? Mi lenne, ha ez a Rendszer az általunk beírt Üzeneteket is el tudná küldeni ebbe az ablakba?

Igen, épp most szültünk egy Konzolt. És ehhez mindössze néhány sornyi kódra volt szükségünk. Az agyam már akkor elszállt, amikor először láttam ezt működés közben. Még csak be sincs kötve semmibe, egyszerűen csak létezik.

A konzol nyilvánvalóan nagyon hasznos a játék fejlesztése során, és egyszerűen kivehetjük a kiadáskor, ha nem akarjuk, hogy a játékosnak ilyen jellegű hozzáférése legyen.

In-Game Cinematics, Replay & Debugging

Mi van, ha üzeneteket hamisítunk? Mi lenne, ha létrehoznánk egy új rendszert, ami egyszerűen csak üzeneteket küld egy bizonyos időpontban? Képzeljük el, hogy valami olyasmit küld, mint MOVE_CAMERA, majd ROTATE_OBJECT.

És Voila, megvan a játékbeli mozgókép.

Mi lenne, ha egyszerűen rögzítenénk a játék során küldött bemeneti üzeneteket, és elmentenénk őket egy fájlba?

És Voila, megvan a visszajátszás.

Mi lenne, ha egyszerűen rögzítenénk mindent, amit a játékos csinál, és amikor a játék összeomlik, elküldenénk ezeket az adatfájlokat nekünk?

És Voila, megvan a játékosok akcióinak pontos másolata, ami az összeomláshoz vezetett.

Multi-Threading

Multi-Threading? Igen, Multi-Threading. Minden rendszerünket szétválasztottuk. Ez azt jelenti, hogy akkor dolgozzák fel az üzeneteiket, amikor akarják, ahogy akarják, és ami a legfontosabb, ahol akarják. Az üzenetbuszunk eldöntheti, hogy az egyes rendszerek melyik szálon dolgozzanak fel egy üzenetet -> Multi-Threading

Frame Rate Fixing

Túl sok üzenetünk van ennek a keretnek a feldolgozásához? Nem probléma, tartsuk őket az üzenetsorban (Message Bus Queue) és küldjük el a következő Frame-et. Ez lehetőséget ad nekünk arra, hogy a játékunk zökkenőmentes 60 FPS sebességgel fusson. A játékosok nem fogják észrevenni, hogy az AI-nak néhány Frame-rel tovább tart a “gondolkodás”. Viszont észreveszik majd a képkockasebesség csökkenését.

Az üzenetek királyak.

Nagyon fontos, hogy minden üzenetet és annak paramétereit aprólékosan dokumentáljuk. Kezeljük úgy, mint egy API-t. Ha ezt jól csinálod, minden fejlesztő dolgozhat különböző rendszereken anélkül, hogy bármit is elrontana. Még ha egy Rendszer offline vagy építés alatt állna is, a Játék akkor is futni fog és tesztelhető lesz. Nincs audiorendszer? Semmi gond, vizuális rendszerünk még mindig van. Nincs Renderer, az rendben van, használhatjuk a konzolt…

De az üzenetek nem tökéletesek. Sajnos.

Mindenesetre néha szeretnénk tudni egy üzenet eredményét. Néha szükségünk van arra, hogy azonnal feldolgozzuk őket. Életképes lehetőségeket kell találnunk. Erre egy megoldás a Speedway. Egy egyszerű postMessage funkció mellett megvalósíthatunk egy postImmediateMessage funkciót, amelyet azonnal feldolgoznak. A visszatérő üzenetek kezelése sokkal egyszerűbb. Ezeket előbb-utóbb elküldjük a handleMessage funkciónknak. Csak emlékeznünk kell erre, amikor üzenetet küldünk.

Az azonnali üzenetek nyilvánvalóan megtörik a Multi-Threading és a Frame Rate Fixing, ha túlzásba visszük. Ezért létfontosságú, hogy korlátozzuk magunkat, hogy korlátozzuk a használatukat.

De a legnagyobb probléma ezzel a rendszerrel a késleltetés. Ez nem a leggyorsabb architektúra. Ha egy First Person Shooter-en dolgozol, amelynek twitch-szerű válaszideje van, ez lehet egy deal breaker.

Visszatérve az architektúránk megtervezéséhez

Úgy döntöttünk, hogy rendszereket és üzenetbuszokat használunk. Pontosan tudjuk, hogyan akarjuk felépíteni a motorunkat.

Eljött az ideje a tervezési folyamat 4. lépésének. Iteráció. Lehet, hogy néhány függvény nem fér el egyetlen Systemen belül sem, erre kell megoldást találnunk. Néhány függvényt sokszor kell hívni, és eltömítené az üzenetbuszokat, megoldást kell találnunk.

Ez időt vesz igénybe. De hosszú távon megéri.

Eljött végre a Kódolás ideje!

4. lépés. Hol kezdjük el a kódolást?

Mielőtt elkezdenél kódolni, olvasd el Robert Nystrom Game Programming Patterns című könyvét/cikkét.

Mindezeken kívül felvázoltam egy kis útitervet, amit követhetsz. Ez messze nem a legjobb út, de eredményes.

  1. Ha Message Bus típusú Engine-t választasz, először a Console és a Message Bus kódolását fontold meg. Ha ezek implementálva vannak, akkor bármilyen, még nem kódolt Rendszer létezését színlelheted. A fejlesztés minden szakaszában folyamatos kontrollt gyakorolhatsz az egész motor felett.
  2. Gondolkodj el azon, hogy legközelebb a GUI-val foglalkozz, valamint a szükséges Draw funkcionalitással a keretrendszeren belül. Egy szilárd GUI a konzollal párosítva lehetővé teszi, hogy minden más rendszert még könnyebben hamisítson. A tesztelés gyerekjáték lesz.
  3. A következőnek a Frameworknek kell lennie, legalábbis a felületének. A funkcionalitás később következhet.
  4. Végül térjünk át a többi rendszerre, beleértve a játékmenetet is.

Meglátod, a játékmenethez kapcsolódó dolgok tényleges renderelése lehet az utolsó dolog, amit csinálsz. És ez egy jó dolog! Sokkal kifizetődőbb érzés lesz, és motivált leszel, hogy befejezd a motorod utolsó simításait.

A játékterveződ azonban lelőhet téged e folyamat során. A játékmenet tesztelése konzolparancsokon keresztül körülbelül olyan szórakoztató, mint Counter Strike-ot játszani az IRC-n keresztül.

Következtetés

Nyugodj rá, hogy találj egy szilárd architektúrát, és ragaszkodj hozzá! Ez az a tanács, amit remélem, hogy megfogadsz ebből a cikkből. Ha így teszel, akkor a nap végére egy tökéletesen jó és karbantartható Motor felépítésére leszel képes. Vagy az évszázadot.

Személy szerint jobban élvezem a motorok írását, mint a játékmenetet. Ha bármilyen kérdésed van, nyugodtan keress meg a Twitteren keresztül @Spellwrath. Jelenleg egy másik Engine-t fejezek be, amiben a cikkben leírt módszereket használom.

A 2. részt itt találod.

Similar Posts

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.