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. :)




Nincsenek megjegyzések:

Megjegyzés küldése