2013. február 28., csütörtök

Érthetőbben már!

Volt már veletek olyan, hogy próbáltatok tapasztaltabbaktól tanácsot kérni, de a választ egyszerűen nem értettétek? Volt hogy google-ben szétkutattátok az agyatokat egy-egy egyszerűbb (már akinek) matekpélda után? Mi a tök az a LoS, mi az a normalizálás? Vagy hogy a fenébe lehet egy egyszerű távolságot számolni egy level 58-as paladin és egy 28-as szintű ÜberSkeletonWarrior között?

Szeretnétek tudni érthetően, képletekkel ezeket a 2d játékfejlesztésben fontos dolgokat? Hát én is... :D

Viccet félretéve, meg kellett tapasztalnom, akármennyire is szar vagyok matekból, hogy MUUUHHHSZÁÁÁJJJ valamilyen szinten érteni ehhez a tárgyhoz, főleg ha játékokat akarok fejleszteni hosszabb távon is.

Vegyünk több egyszerű példát és menjünk sorjában a dolgokkal. Itt lesz pár olyan alapvető 2d matek példa -ÉRTHETŐEN - ami elengedhetetlen a 2d játékfejlesztéshez. Vágjunk is bele.

Legyen az elképzelt játékunk egy... hmmm... space shoot 'em up! Igen, az jó lesz... :D 
Legyen a játékunk sprite alapú, semmi 3d, semmi modell. Felülnézetes, nem izometrikus. (Először Diablo2-n akartam szemléltetni az egészet, de még jobban érthető egy nem izometrikus játéknál). Überszájbarágós magyarázás jön. Programozásbeli dolgokat nem magyarázok, csak és kizárólag dedómatekot.


Remélem az egyértelmű, hogy a sprite-oknak van egy X és egy Y koordinátája. X a vízszinte poziciót, Y a függőlegeset jelöli. A képen látható koordináták a sprite bal felső sarkát jelöli, tehát ha te kirajzolsz az X:296-ra és az Y:489-re egy 70x70 -es képet, akkor annak jobb alsó sarka X:366 és Y:559 lesz...

Addig nem történik semmi gond, amíg mi azt szeretnénk, hogy a player lövedéke függőlegesen menjen felfele (a lövedék Y értéke folyamatosan csökken) és az enemy lövedékei pedig haladnak a képernyő alja felé (Y érték folyamatosan nő). Az ellenséges ürhajók is menjenek folyamatosan lefele, a playert pedig mindig oda pozicionáljuk, ahol az egerünk van.

Ekkor már örülünk, hogy hát itt az új évezred legjobb játéka, és ez fantasztikus szórakozást nyújt... Legalább öt percig bizonyosan, mert hát a statikusan közlekedő enemiket gyorsan megunjuk, rakéta vagy kamikaze enemy sincs, ami követne minket. Ha az engine amit használunk még automatikus ütközésdetektort sem tartalmaz, akkor az is előfordulhat, hogy a betaláló lövedékeink is csak áthaladnak az enemin, látványos ütközések helyett pedig csak elsiklik egymáson a két űrhajó. Hát ez lehet még sem csúcsjáték így ...

(Az Andorra2D nekem biztosított ütközésdetektálót sprite-sprite és pixel-pixel alapút is, de ha nektek nincs ilyen szerencsétek, itt jön a példa, hogy hogyan írhattok sajátot).

Először is, szükségünk van egy listára, ami tartalmazza a képen látható 4 enemy-t (a 4.-nek ott az orra felül, mielőtt azt hinnétek, hogy elszámoltam), vagyis egy dinamikus listára, amiben tároljuk a játéktéren lévő enemyket. Ezután minden iterációkor megvizsgáljuk a player és az enemy-k közötti távolságot.
Ehhez először is ismernünk kell a sprite-unk középpontját. Ez egyszerű, hisz ha van egy 70x70 -es űrhajónk, akkor a csak az X és Y értékhez hozzá kell adnunk a kép méretének a felét, tehát 35-öt.


Ha megvan a középpont, akkor a két középpont közötti távolságot kiszámoljuk. 
Ja hogy ez csúnya, meg wtf, meg itt be is zárnád az oldalt?
Vegyük végig lépésről-lépésre, és úgy már nem is lesz olyan félelmetes.
A képlet első zárójeles része a vízszintes távolságot adja vissza, a második zárójel pedig a függőlegeset. És az egész a gyök alatt adja vissza a tényleges távolságot.

Számoljuk először a vízszintes távolságot:
DistanceX := (X2 - X1) * (X2 - X1);
érthetőbben
Vízszintes távolság = (Enemy.X - Player.X) * (Enemy.X - Player.X)

Majd a függőlegeset:
DistanceY := (Y2 - Y1) * (Y2 - Y1);
Függőleges távolság = (Enemy.Y - Player.Y) * (Enemy.Y - Player.Y)

Megfigyelhetitek, hogy a négyzetreemelést nem beépített SQR fügvénnyel végzem, hanem saját magával szorzom meg. Elég sokat olvasgattam a témában és többen is írták, az SQR fügvények van hogy 50x lassabbak, mint egy sima szorzás, ezért az optimálisabb mód az, ha a négyzetreemelést így oldjátok meg.

Megvan tehát a vízszintes és a függőleges távolságunk is.
Ezután jön a kettő összege a gyök alatt:
Distance := SQRT(DistanceX + DistanceY);

És tessék, ezzel már tudjuk is a két sprite közötti távolságot.
Igen ám, de ezzel még nem érünk el semmit, hacsak nem vizsgáljuk az ütközést.

Tegyük fel, hogy az enemy-k egymással nem tudnak ütközni, csak a player tud ütközni enemy-vel. Ekkor tehát megvizsgáljuk az enemy és a player közötti távolság, majd HA a távolság kisebb, mint Z, akkor meghívjuk az előre megírt ütközést kezelő algoritmusunkat.

Kicsit azért lehet csalóka ez így, mert lehetséges, hogy grafikailag nem történik meg az ütközés, csak nagyon közel van a két hajó egymáshoz. Ez azért van, mert az egymáshoz viszontított középpontjaikat vizsgáljuk. Lehet ezzel trükközni, belőni pontos értéket, vagy olyan feltételeket biztosítani, hogy az enemy és a player egymáshoz képest elfoglalt poziciója micsoda.

Nézzük meg újra ezt a képet:


Amikor vizsgáljuk a player helyzetét az aktuális enemy-vel, akkor IF-ek sorozatával belőhetjük, hogy az enemy hol helyezkedik el. Ebben az esetben az fog kijönni, hogy az Enemy.X < Player.X és Enemy.Y < Player.Y. (emlékezzünk, hogy bal felső képpontokat nézünk ebben az esetben, de nézhetjük akár a két középpontot is).
Ekkor távolságként ne a két sprite középpontját vizsgáljuk, hanem a player középpontját és az enemy jobb oldalát.


Ebben az esetben pedig az enemy orrától a player orráig, mert valószínűsíthető, hogy ha a player még kicsit jobra megy, akkor ütközni fognak és akkor grafikailag is pontosan fog kinézni az ütközés.

Igy az iromány kb. felénél látom, hogy nem biztos, hogy ezek voltak a legjobb képek, amiket találhattam, hiszen nekünk nem éppen tipikus "téglalap" űrhajóink vannak, aminél az ütközések kezelése ezzel a módszerrel jól néz ki, hanem a pixel-pixel ütközéstesztelő miatt nagyon pontos ütközéseket láthatunk. :D
Mindegy, szerintem érthető mire akarok kilyukadni...

Igy hogy már ismerjük a távolság kiszámolását, ami nem is annyira vészes, valamint le tudunk írni pár IF-es feltételt arra vonatkozólag, hogy a két űrhajó milyen poziciót foglal el egymáshoz viszonyítva, egy egész jó és gyors "collisiondetectiont" tudunk létrehozni.

És ha most az egészet úgy mondom, hogy megtanultuk használni "Pitagorasz-tétel távolság formuláját" ?

Most már közelebb állunk ahhoz, hogy ha nem is az évtized játékát csináljuk meg, de egy N+1. spaceshootert összerakjunk. Mi lenne, ha feldobnánk ezt azzal, hogy jönnek kamikaze űrhajók, hőkövető rakéták, és esetleg a szétlőtt űrhajókból különböző fejlesztést biztosító orbok is kihullanának a roncsok közül, amik bizonyos közelségbe érve automatikusan belerepülnek a playerbe.

Hogy ez igy hirtelen nagy ugrás? Hogy biztos sok kód és mégtöbb matek? Mindjárt meglátjuk, hogy ez sem olyan borzalmasan nehéz dolog.

Mire is van itt szükségünk? Arra, hogy minden képkockával egyre közelebb jöjjön az a bizonyos dolog a playerünkhöz. Hiszen a kamikaze űrhajó is közelít, a "hőkövető" rakéta is közelít, az orb is közelít, csak ott egy feltételnek is teljesülnie kell, mégpedig annak, hogy elég közel legyen egymáshoz a player és az orb.

Kezdjük a kamikaze-val:
Fontos tudnunk az előbb megtanult távolságszámítást. Az algoritmussal két kis lebegőpontos számot kapunk vissza, amivel módosítanunk kell a mozgó sprite (kamikaze, vagy rakéta, stb.) pozicióját.

Nézzük először, hogy mit mondanak az okosok meg a képletek:

dX = X + speed * cos(alpha*rad)
dY = Y + speed * cos(alpha*rad)

Hell yeah! Ezzel lóf*szt nem érünk így, ugye?
Nekünk ennél jóval egyszerűbb kell!

Alább az egyszerűbb, érthetőbb képlet:
mx := (Player.X - Kamikaze.X) / Távolság
my := (Player.Y - Kamikaze.Y) / Távolság

mx := mx * (TimeGap * Kamikaze.Sebesseg);
my := my * (TimeGap * Kamikaze.Sebesseg);

Kamikaze.X := Kamikaze.X + mx;
Kamikaze.Y := Kamikaze.Y + my;

Magyarázat:
Fogjuk a két sprite X értékét, kivonjuk egymásból. Ugyanez a helyzet az Y értékkel. 
Ezután kell a sprite sebessége (speed) és az az érték, ami a game loop-ban két ciklusfutás között számolódik. C típusú nyelveknél Tick-nek is hívják, nálam Delphi-ben az Andorra ezt TimeGap-nek hívja, de pl. Lord Crusare kolléga deltaGap-nek hívta, XNA-ban meg elapsedTime.

Tehát az első két értékünket (mx; my) még fel kell szorozni a TimeGap és a SPEED eredményével. Ezt a számot hozzá kell adni a mozgó sprite X és Y értékéhez és megvan az új pozició.

Viszont ez így még nem jó, mert azt fogja eredményezni, hogy minél közelebb van a Kamikaze, annál lassabban fog haladni. Ekkor jön képbe a / Távolság.
Semmit nem segítő képletként legyen itt ez:


Ugye mennyire okosabbak lettünk ezzel? Naugye...

Ide jön egy kis beszélgetésem, részben annak az okán is írom ezt a blogbejegyzést.
Egy nálam jóval nagyobb tudású programozó sráctól kérdeztem (név, cím a szerkesztőségben), hogy mi a tökömért nem jó a régi, általam csak "hőkövető algoritmus"-nak hívott borzalmam. Azt mondja: "Hát hozzad normál alakra". Oké, de azt hogy csináljam? "Hát izé... Hozd normál alakra, másképp ezt nem tudom neked elmondani." Anyádat. Te sem tudod még ezt sem, csak van egy beépített "normalize" eljárásod, és visszakapod az értéket.

Anno mikor ez a beszélgetés volt, lehet idegességemben nem találtam meg gugliban egyből a választ, vagy nem volt türelmem sok oldal magyarázatot elolvasni, viszot így, hogy már tudom mit kell keresni, egyből megvan.

A normalizálás az, mikor a fenti mx és my értékeket elosztod a két sprite távolságával.
Másnéven ekkor megkapjuk az egységvektort.


Ekkor már tökéletesen fog működni és szép egyenesen belerepül a playerbe a Kamikaze.


Jeee!

Hogy újra borzoljam a kedélyeket, leírom az egészet egyben:
"Az egyenes vonalú egyenletes mozgás a kinematika tárgykörébe tartozó legegyszerűbb mozgásforma."
Valamint megtanultuk, hogy hogyan kell egy 2d vektort normalizálni. Hogy megint wtf? Az "mx" és a "my" értékeket mikor megkaptuk, a távolsággal való osztás adja vissza a vektor normál értékét, vagyis az "egységvektort"... Ezt miért nem lehet ilyen egyszerűen leírni, ahogy én tettem most? Ne kérdezzétek. 

Végezetül jöjjön a csodaorb, ami bizonyos távolság esetén belerepül a playerbe. 
Leírom pascalban, de ott lesz a magyarázat is mellette.
Megint csak annyi kell, hogy folyamatosan számoljuk a távolságot és legyen mondjuk 3 féle konstansunk.
Legyen 300-as távolság, 200-as és 100-as.

"1". distance := GetDistance(Player, Orb); 
"2". case distance of
"3". 300..201 : Orb.getPlayer := true;
"4". 200..101 : inc(Orb.speed, 25);
"5". 100..0 : inc(Orb.speed, 50);
"6". end;

1. sor: Meghívjuk a GetDistance függvényt, paraméterként átadjuk a playert és az orbot. A függvény a fent tanultak alapján kiszámolja a távolságot és visszaadja az értéket.
2. sor: Egy case-ben vizsgáljuk a distance-t.
3. sor: Ha 300 és 201 között van a távolság, akkor az Orb getPlayer logikai változóját true-ra rakjuk. Az Orbnak legyen mondjuk 25 az alap speedje és vizsgálja, hogy ha a getPlayer változója true, akkor az előbb tanult egyenes vonalú mozgással haladjon a player felé.
4. sor: Ha 200 és 101 közötti a distance, akkor növeljük az orb sebességét 25-el.
5. sor: Ha 100 és 0 közötti az érték, akkor pedig még ennél is gyorsabb legyen az orb, adjunk a sebességéhez 50-et.
6. sor: case vége

Ezzel a jópofa trükkel el tudjuk érni azt, hogy a ha a player szétlő egy ellenséges űrhajót, és abból kihullik pár orb, akkor azok ne automatikusan repüljenek a playerbe. Tegyük fel, hogy ott van még körülötte 3 ellenséges gép, de nekünk nagyon kell az a pár orb, mert pont szintlépés közelében vagyunk. Ekkor meg kell kockáztatni, hogy az orbok közelébe kerüljünk, amik azzal "hálálják" ezt meg, hogy szépen gyorsítva haladnak a player felé.
Ez egy kis extra "challenge" a játékosnak, nekünk nagyon tetszett. :)

És akár a hőkövető rakétát is meg lehet így csinálni. A rakéta mindig a hozzá legközelebb eső sprite-ot fogja követni, de tegyük fel egy kamikaze pont akkor hasit el a pálya közepén, hopp, egyből új célja van a rakétának, elindul arrafele. Ha pedig bizonyos ideig nincs mondjuk 320-as distance-en belül senki, akkor felrobban.

A most megszerzett tudással ezeket már gyerekjáték megcsinálni, és ezeket lehet bárhogy variálni.

Remélem segítséget tudtam nyújtani és jobban meg tudtam veletek értetni ezt a pár alap egyenletet.
Legközelebb a Line of Sight-ról, sugárról, lőtávról, és hasonló jópofa magyarázattal jövök, REMÉLHETŐLEG már új dizájnnal, mert ez a kopasz blog kezd zavaró lenni. :D


Köszönöm a mindenkori segítséget Bitsculptor-nak és Lord_Crusare-nek, valamint a meg nem nevezetteknek. :)




2013. január 9., szerda

Kezdjük hát!

Itt az új év, itt az új post, ahogy azt ígértem.
Viszont egy új dizájn nem ártana, mert már nem a Starship Prototype-ról (később: Dimension Leak) fog szólni a blog, hanem inkább saját magamról és kalandozásaimról a játékfejlesztők világában. :D Ezen felül még a hozzám hasonló haladó(bb) fejlesztőknek próbálok tanácsokat adni, ahogy eddig is. Tök zéró kezdőknek is ajánlom az olvasgatást, mert néha úgy érzem, nem sok választ el engem is ettől a státusztól. Minél többet tanul az ember és minél tapasztaltabbak között dolgozik, annál inkább szarnak érzi magát. Ez ilyen úgy látszik. :)

Ez a bejegyzés pedig nem született meg volna, ha hülye fejemmel nem felejtem el felmásolni pendrive-ra a melóhelyi munkát, mert most is azt a kódot túrnám, mint jó disznó a moslékot. Arra még visszatérek később, hogy a kódomat miért nevezem mosléknak a jelen állapotában.

Azzal gyorsan megbarátkoztam, hogy igencsak nulla vagyok a többi kollégához képest, de hát el kell kezdeni valahol. Ezt próbálom azzal kompenzálni, hogy az egy hétre tervezett 42 óra munkaidőt én simán kitolom 60-65 -re. Ez nem (csak) azért van, mert ilyen munkamániás vagyok, hanem mert nekem jóval több idő megcsinálni egy-egy fejlesztést, vagy beépíteni egy új feature-t, mint más kollégának. Ekkor jön az, hogy addig kell csinálni, amíg nem működik. Ha kétszer annyi ideig tart, akkor annyi ideig, de meg kell csinálni.

Kezdjük ott hogy prototípus fejlesztés. Ebben már egész jónak éreztem magam, és tájékozódtam is a témában. Első szabály az volt, hogy minél gyorsabban, a kódot leginkább elhanyagolva csináljunk látható eredményt, hogy le lehessen tesztelni futás közben. Ezután ha marad a feature, akkor újratervezünk és újraalkotjuk a kódot. Na igen, de mi van ha a nagy siettségben az ember ezt a refactoring részt kihagyja? Hát kurva nagy gáz. :D

Jelenleg a tényleges munkaidőn kívüli kódolásom abban merül ki, hogy szépítgetem, csinosítom, újraírom a spagettit, mert ha jön valami változtatás, tuti borul az egész, és akkor (ahogy ma is) nem a tényleges átírással fog foglalkozni az ember, hanem a bugok kergetésével, amit gyűlölök. Úgy ültem neki a mai munkának is, hogy 1-2 óra alatt megvan (előző nap negyed órára tettem ezt a munkát), erre 7 óra elteltével sikerült mindennel elkészülni, úgy hogy kínkeservesen kiírtottam a bugokat, de a kód cserébe úgy néz ki, mint egy rakás szar. Holnap pedig nem a további feature-ök fejlesztésével kezdem a napot, neeem, hanem ennek a kódnak a csinosításával, mert bent hagytam a melót...

Megtanulhatunk tehát egyből két leckét is:
1. A munkát akkor is vidd haza, ha úgy gondolod, hogy nem fogsz otthon kódolni. Ha kell töltsd fel ftp-re, rakd fel pendrive-ra, küldd el magadnak e-mailben, bármi, de a munka MINDIG legyen elérhető!

Verziókövetővel tessék megbarátkozni! Én a TortoiseHG-t használom. Hogy miért? Mert ezt ismerem. :D Viccet félretéve, szerintem jó kis program és megkönnyíti a munkát, még ha a setupolása elsőre körülményesnek is hathat. MEGTÉRÜL! Higyjétek el!

2. Még ha prototípust is fejlesztünk, törekedjünk a tiszta kódra. Szedjünk szét mindent a lehető legkisebb modulokra, függvényekre. Használjunk mindenhol konstansokat, ahol csak lehet, és kommentezzünk ezerrel. 
Én nagyon szeretem az objektumorientált tervezést és programozást és szinte mindent modulokra, külön unitokra, osztályokra szedek, amit csak lehet, de így is beleesek abba, hogy a nagy prototypingolásban (szép szó) úgy írok meg egy 50 soros eljárást, hogy azt később akár 3-4 függvényre szétszedem és máshol is használom ezeket a részleteket. Ehelyett, mivel gyorsabbnak érzem elsőre, ennek az 50 sorosnak a részleteit ollózom ide-oda, ha máshol is szükség van rá. Ennek az szarnak egésznek az lesz a vége, hogy az az idő, amit erre rászántam megduplázódik, sőt, megtriplázódik, mikor javításra vagy refactoringra kerül sor, mert ugyanaz a kódrészlet más helyeken is megtalálható lesz és mire kipucoljátok a kódot, ott hagytok, hogy bárcsak el se kezdtétek volna.

Haladjunk tovább!

Próbáltatok már úgy fejleszteni, hogy nektek mondjuk Win7 van a gépen és 24"-os monitoron dolgoztok, egyik társatoknak Linuxa van laptopon, a másiknak pedig asztali gép alaplapi videókártya és Win7?
Mi ebben az érdekes? 


Az egyik a méretezés:

Engem mindig átver a nagy monitor és az hogy szabadon csinálhatok akármilyen méretű editorokat hetvennyolc felugró ablakkal, külön szerkesztő modulokkal, stb. mert ELFÉR. Mikor kész vagy jó pár nap után a munkával és mondjuk egy pályaszerkesztő vázát odaadod a kollégának, aki használni fogja, az felrakja a laptopjára, ami max 1360x768 -as, abnormális méretet tud. A programot elindítva az természetesen nem fér ki neki és minden felugró újabb ablak elveszik az éterben.

A másik a plugin, ami esetünkben OGL vagy/és DX.
Amikor egyik kolléga gépe csak OGL-t szeret, a másik pedig csak DX-et, hát az fantasztikus. A legjobb mikor olyannál derül ki (pont a mai nap), hogy az asztali gépen, ahol alaplapi gpu van, ami nem is a grafikus megjelenítés, hanem a PÁLYAMENTÉS ?!

Jó, erre lehet azt mondani, hogy miért használok még mindig Delphi-t és Andorra2D-t, mikor ez az OGL/DX hiba egyértelműen a fejlesztői eszközök miatt van, vagyis leginkább a grafikus könyvtártól.
Szándékomban áll minél gyorsabban HTML5-re áttérni, főleg hogy a cég leginkább böngészőkre fejleszt.

Igy a harmadik leckénk mára:
3. Pályaszerkesztő és/vagy játékkliens fejlesztésének kezdetekor, ha tudjátok, hogy lesz kolléga, aki használni fogja a programot, kérdezzétek meg, hogy milyen gépen, és milyen oprendszer alatt fogja használni a cumót, különben hasonló csapdába eshettek, mint én.

Mivel még nem írtam soha hálózati játékot, és a játékaim sem használtak még nagyon netes adatbázist, ezért hát természetesen az első "hivatalos" játékomnak egyből ilyennek kell lennie a THX-nél. Sájsze. :D
Szerencsére azért SQL-t meg helyi adatbázisokat már használtam, de hát gyorsan kiderült, hogy nem ártana tudni a PHP/JavaScript/HTML5/Json/CSS csoportot. Amint lesz két szusszanásnyi időm, el is kezdem a HTML5-öt, mert eszméletlen, hogy miket lehet már megcsinálni egyszerű browserben...

Erről viszont majd a következő postban fogok részleteket írni, valamint könyvbemutató is lesz, hogy milyen programozással kapcsolatos könyveket sikerült beszerezni karácsonykor potom összegekért. ;)