Hogyan (ne) fejlesszünk játékot?
Milyen bugokba, problémákba lehet belefutni egy online játék készítése során? Bár a konkrét téma valószínűleg nem érint tömegeket, azért vannak olyan tanulságok, amiket minden fejlesztő levonhat és hasznosíthat magának. A tapasztalatok a Zandagort nevű PHP–MySQL–AJAX-ban írt, böngésző alapú MMO stratégiai játék fejlesztéséből származnak.
Externáliák
IE6-os problémákat (PNG-k kezelése és hasonlók) nem is mondok, szerencsére 2010-ben ez már-már múltidő. Mindenki más mintából indul ki. Én egy teljesen pre-javascriptes, viszonylag látogatott oldalon is 3% körüli IE6-ot kapok, ami már megközelíti a „le van ejtve” kategóriát.
Ennek ellenére bele lehet futni egy-két kifejezetten kliens oldali problémába. Ilyen volt például, amikor kaptam egy levelet, miszerint „az elelmiszertermelesem ertekenel egz folyamatosan valtozo telefonszam jeleneik meg minden belepesnel hogy hivjam. 4959004 4959111 meg hasonlok de ezek inaktiv telefonszamok”. Hát persze, hogy a Skype rondított bele az UI-ba.
Egy másik, sokkal sunyibb hibát a webszerver hiba naplójában találtam. Teljesen ad hoc módon kaptam POST-request, but content-length missing 411 hibákat, amikben két közös pont volt: a host név hiánya, és mind a Tor nevű IP-elrejtő proxytól érkeztek. Úgy látszik, a Tor valamiért néha lenyeli a HTTP kérés egy részét, pedig se a Content-length, se a host név nem olyan, amit el kellene rejteni diktatórikus rezsimek és az NSA elől.
Más eset, de szintén a fejlesztő hatáskörén kívülálló probléma a domén nevek lefoglalása. Az egy dolog, hogy talán a világon az egyetlen ország vagyunk, ahol várólista és hasonló bürokratikus akadályok vannak még az interneten is. De azért elég megdöbbentő volt, amikor két hét várakozás után visszadobták az aldebaran.hu igénylésemet azzal, hogy valakinek védjegye van rá. Az Aldebaran egy több, mint ezeréves arab név (az Alfa Tauri csillagé), amit egy Gerz nevű olasz nehézipari cég jegyzett be 1989-ben. Természetesen nem az övék az aldebaran.hu, mint ahogy a gerz.it vagy az aldebaran.it sem, és nyilván nincs is rá szükségük, de az ISZT mégis fenntartja nekik a lehetőséget. Így lett Zandagort a játék neve, amire azelőtt a Google nulla találatot adott.
Persze a bugok nagy részét mégiscsak a fejlesztő pottyantja el, a következőkben ilyeneket láthatunk.
Óraátállítás
Mivel a játékban a gyárak és egyebek adott idő alatt épülnek meg, ezeknél egy időbélyegben fel van jegyezve, hogy mikor készül el. Ez az év nagy részében hibátlanul működik, kivéve azon a két napon, amikor üzemszerűen megtörik a szerveridő, a téli és nyári időszámítás közti váltás miatt. Erre persze csak akkor jöttem rá, amikor egy játékos „panaszkodott”, hogy 25 helyett 24 óra alatt épült meg egy városa. A megoldás az lehet, ha kikapcsoljuk az automatikus átállást, vagy lecseréljük egy olyanra, ami ezeket az időzítő timestampeket is átírja.
Felhasználói bemenet
‘Did you really name your son
Robert'); DROP TABLE Students;--
?’
‘Oh, yes. Little Bobby Tables, we call him.’
‘Well, we've lost this year's student records. I hope you're happy.’Forrás: XKCD
Ilyen alapvető hibát persze (?) nem követ el az ember, de lehet ezt sokkal specifikusabban is űzni. A játék felületének fontos eleme, amikor be kell írni egy másik játékos valamelyik bolygójának a nevét (pl. megadni egy szabotázsakció célpontjának). Ehhez van is egy gépelésrásegítő a kényelem kedvéért. De mi van, ha egy bolygó neve japán karakterekből áll? Vagy egy szóközből, amit a PHP egyből trimmel, hogy (egyébként) ne legyen gond a véletlenül bemásolt whitespace-ekkel? Ilyenkor van az, hogy a mysql_real_escape_string
nem elég, muszáj egy whitelistet összeállítani a megengedhető (vagyis begépelhető) karakterekre.
Tesztelés
De nem ez az egyetlen példa, ami azt mutatja, hogy a (hardcore) játékosok értékes bétateszterek. Akár úgy, hogy jelentik, akár úgy, hogy kihasználják a bugokat és kiskapukat. Ilyen eset volt, amikor valaki rájött, hogyan lehet űrhajókat teleportálni a galaxis akármilyen távoli pontjai között. Kliens oldalon ugyan be volt építve a távolság alapú korlátozás, a szerver oldalról viszont kimaradt. Így elég volt Firebuggal megnézni, hogy pontosan milyen AJAX-kéréseket kell küldeni, és viszlát, fénysebesség! A csalót végül utolérte végzete, és egy elgépelt teleportnál a szövetsége legnagyobb flottája egy „fekete lyukban” kötött ki. Ez volt az a pont, amikor jelentette a bugot.
Szintén firebugos trükk, és azt mutatja, hogy még a látszólag ártatlan kiskapukat is könyörtelenül ki lehet használni, hogy a nyersanyagok transzportálásához használt energia (ún. teleporttöltés) round()
-dal kerekítve volt meghatározva. Ez azt jelentette, hogy nagyon kis mennyiséget ingyen is lehetett szállítani. És bár kényelmetlen lenne állandóan begépelni a kerekítési határnál kisebb mennyiséget és megnyomni a transzport gombot, elég csak egy apró szkript, és máris megrakott tehervagonok férnek át ezen a kiskapun.
Vannak persze olyan hibák is, amiket nem lehet kihasználni, teljesen véletlenszerű, hogy kit érint jól, és kit rosszul. A játék magja egy olyan szkript, ami percenként lefut. Ez számolja ki a termeléseket, a népességnövekedést, az ökoszféra fejlődését, a flották mozgását, vagyis ettől válik perzisztenssé a játék világa, ahol akkor is telik az idő, ha senki nincs bejelentkezve. A terhelés csökkentése érdekében mindig csak a bolygók 1/15 részét frissíti, vagyis a játékosok szemszögéből 15 percenként van termelés, de ez a körváltás minden bolygón máskor következik be. Az intenzív fejlesztési időszakban előfordult, hogy hiba került ebbe a szkriptbe, ami azt eredményezte, hogy csak egy pontig futott le, és ott kilépett. A számláló viszont, ami alapján eldőlt, hogy most a bolygók melyik része frissül, a szkript végén volt. Így történhetett meg az, hogy órákon keresztül, mire értesültem a hibáról és géphez tudtam jutni, a bolygók 1/15 részén negyed óra helyett percenként volt körváltás, a többi 14/15-ön pedig megállt az idő. (Az igazságosság persze megkövetelte, hogy visszaállítsuk a hiba előtti állapot.) (Dupla zárójeles megjegyzés: és csak most jutottam el oda, hogy a számlálót áttegyem a szkript legelejére…)
Túlterhelés
Igazi MMO-s probléma a túlterhelés. Az egyik eset, amikor nem valami rosszul indexelt tábla van a háttérben (bevallom, volt ilyen is), hanem szimplán az egyidejű kapcsolódások száma túl magas. A játékban van chat, a chat pedig általában kapcsolat igényes, akármilyen módszerrel oldjuk is meg (sőt, az egyébként kevésbé megterhelő long polling és társai még inkább). Az elkülönített chat szerveres megoldást a munkahelyi tűzfalak miatt inkább elvetettem, így találtam rá a Lighttpd-re, aminek kifejezetten az az ars poetica-ja, hogy nagyon sok szimultán kapcsolódást bír el (c10k problem). Szemben az Apache-csal, aminek alapesetben elég nagy a memory footprint-je. Működik is rendben, bár az még nem világos, hogyan érdemes konfigurálni a FastCGI folyamatok és gyermekfolyamatok számát. Az egyik logika szerint az a jó, ha sok anya van kevés gyerekkel, mert azok egymástól függetlenül tudnak meghalni, a másik logika szerint viszont opcode cache használata esetén (konkrétan APC) kevés anya sok gyerekkel a nyerő, mert minden anyának saját gyorstára van. Szívesen látom a visszajelzéseket ez ügyben.
Egy másik túlterheléses eset igazi algoritmus- és SQL-optimalizációs probléma. Ami az érdekessége, hogy teljesen meglepetésszerűen bukkant fel. „Fog of war”-nak hívják a játékokban azt a jelenséget, hogy a játékos nem lát mindent a térképen, csak azt, ami elég közel van hozzá vagy az épületeihez, bolygóihoz vagy egységeihez. Ez így teljesen rendben van, csak épp a megvalósítása nem triviális, ha a játék tele van mozgó célpontokkal és „látókkal”. Hiszen ekkor állandóan ki kell számolni egy csomó ellenséges és saját egység közti távolságot, hogy kiderüljön, az ellenségesek közül melyik látszódik épp, és melyik nem. Erre született egy valamilyen, alapvetően működő, de nem tökéletes megoldás.
Aztán eljött az a nap, amikor megérkezett a gonosz hódító, Zandagort, és az emberiségnek össze kellett (volna) fognia, hogy szembeszálljon vele. Az végülis a játékosok „magánügye”, hogy ez nem sikerült, az viszont nem, hogy a load a korábbi kellemesen alacsony tartományból ugrásszerűen megnőtt a használhatatlan szintre. Ami az amúgy is felfokozott hangulatban elég sok játékosnak az utolsó csepp volt a pohárban. És hogy miért volt ez? Hirtelen sokan lettek online, és ezt nem bírta a szerver? Abszolút nem. Volt ugyan növekedés, de nem vészes. Mindössze annyi történt, hogy míg korábban a játékosok az idejük nagy részében a bolygóikat igazgatták vagy egymással beszélgettek, most hirtelen mindenki átváltott a térképre, és azt kezdte el scrollozgatni, hogy lássa, mi történik, és hogy irányítani tudja a flottáit. Vagyis a korábban ritkán használt „fog of war”-os kód most főszerepbe került, és csúnyán lebőgött. Az áthidaló megoldás a funkció kikapcsolása lett, de ez nyilván csak rövidtávon elfogadható. A tanulság az, hogy még ha az ember figyeli is (pl. APC-ben), hogy melyik szkriptet mennyire gyakran hívják meg, és ez alapján optimalizál (ezzel csökkentve az egységnyi javulásra eső fejlesztési költséget), akkor is érhetik meglepetések.
Adatbázis-tervezés
A végére egy klasszikus SQL-tervezési hiba. Mindig is azt hittem, a jó mérnök onnan ismerszik meg, hogy nem kizárólag BIGINT-et használ, hanem minden mezőnél megbecsli a várható értéktartományt, és ahhoz választ megfelelő típust. Ez jól is hangzik, csakhogy közben a játékosok exponenciális ütemben fejlődnek, így hamar elkezdik átlépni a korábban reálisnak tartott kereteket, és jönnek a panaszok, hogy a tőzsdén legfeljebb 65535 hordó olajat lehet venni, miközben az igény ennél sokkal nagyobb. De a legszebb példa: az előző forduló végén elpusztult a Tejútrendszer, ezért a mostani forduló egy másik galaxisban játszódik. És úgy alakult, hogy ez nagyobb, méghozzá annyival, hogy az előző koordinátái belefértek 2 bájtba, a mostanié nem. Ez persze már a galaxis tervezésénél látszódott, ezért a bolygókat tároló táblában megtörtént a típusváltás. A flottáknál viszont nem, így csak akkor szembesültem a problémával, amikor eljutottak az első játékosok az űrhajózásig, és jött pár panasz, hogy van egy pont (valójában vonal) az űrben, amit képtelenek átlépni: ±32768 ugyebár. Igazából csoda, hogy nem csordult túl, és kötött ki a galaxis átellenes végében.
■
Köszi
És IGEN, sajnos előjött, hogy a jószándékot feltételezve nem lehet stabil programot írni. Azok az @X&# felhasználók (a karakterek jelentése: KEDVES :D ) egy része véletlenül, másik része szándékosan jön rá egy, s más libára amit ki lehet használni, a jobbik része a dolgoknak, ha csak 'semmire sem jó' dolgokat művelnek vele, de nem képesek megölni az adatokat és a szervert sem...
Köszi mégegyszer :)
Skype
Az ott leírt megoldás egyébként a legfrissebb Skype verzió ellen nem használ. Ez állítólag egy bug, amit javítani fognak majd, de nem törik össze magukat a sietségtől.
Distance
Erre való MySQL-ben a Distance függvény, nem?
GIS vs MEMORY table
Egy másik dolog, hogy a Distance csak két pont távolságát számolja ki gyorsan. És talán azon is lehetne optimalizálni, hogy a fog of war-nál a sok távolságból nem kell mindegyiket kiszámolni, mert van, amelyik "fedi" a másikat (koordinátánként B-tree index, és ebből Csebisev-távolság).
Ha kézzel kell...
Grat!
Gratula a cikkhez! :) Anno a
Anno a Thrillion Kincsei játék indulása elején (2002 november) mi is belefutottunk jónéhány hibába.
Az egyik legemlékezetesebb hiba számomra mind a mai napig az volt, hogy mivel X mennyiségű akciót kezdeményezhettek a játékosok, és ezt sessionben tároltuk, ezért párhuzamos session-ökkel ezt igen szépen ki lehetett játszani. Régi szép php4-es idők :)
Persze nálunk is a rendszeresen futó karbantartó kódok miatt voltak éjszakai felkelések az első időkben.
Örülök az ilyen tipusú,
Köszönet a cikkért, több
Én is köszönöm a cikket.
Igazából az van tervben, hogy
Bár régi cikk, de újra
Megnéztem volna a képét, amikor a fekete lyuk elnyeli a flottát :D :D :D