InfoC adventi naptár
Síelés

Síeljünk! Itt egy program, ami három dimenzióban rajzolja ki a sípályát. A pályán, amelynek széleit egy halvány szürke vonal jelzi, bóják és fák vannak. Természetesen lejt is. A síelő ezen halad lefelé; egy fának nekimenve pontlevonást kap, bóját eltalálva pontot. A játékban kanyarodni a balra és jobbra nyilakkal lehet; hóekézni pedig a lefelé nyíllal.
A játék érdekessége, hogy programozási szempontból alig különbözik a múltkor bemutatott repülős játéktól – ugyanazok a problémák kerülnek elő itt is (változó számú objektumok, egymás átfedése stb.), mint annál.
1 A játék működése
A játékban hősünk elvileg egy lejtős pályán száguld lefelé a fák és bóják között a
völgy felé. Az „elvileg” itt nem töltelékszó, mert ez a programban nem feltétlenül kell így
történjen: végülis nem muszáj a programban a lejtős pályát számolni, éppen elegendő az is, ha az
eredmény úgy néz ki, mintha lejtene a pálya. Sőt hősünknek nem muszáj a fák és bóják
felé csúsznia sem: az is jó, ha a fák és a bóják jönnek felé. A látvány a monitoron ugyanolyan
lesz! Induljunk ki ebből a gondolatból, mert mint kiderül, a dolgunk sokkal egyszerűbb lesz, ha a
játékos a tér origójában van.
Tehát a program dolga a következő. Minden időlépésben a pálya összes elemének z
koordinátáját csökkenti, azaz mindent a játékos felé mozgat. Ha valamelyik pályaelem a játékos
mögé kerül, akkor azt már
el lehet dobni. Hátrafordulni úgysem fog, és hegynek felfelé csúszni sem. Így egyre fogynak az
akadályok a pályán – vagyis fogynának, ha a program nem generálna véletlenszerűen újakat jó messze
a játékostól. A main()
függvény ide vonatkozó része minden hosszegységenkénti
csúszás után (ami a programban tíz méter) generál néhány új pályaelemet.
Mivel az új elemek messze vannak, és a közelebbi fák eltakarják azokat, ez nem észrevehető.
(Ha az ELEMEK
konstans értékét túl alacsonyra állítjuk, akkor viszont nagyon is.)
Apropó, láthatóság! Ahogyan a repülős programban meg kellett oldani, hogy a repülők felül legyenek,
és a tájképesnél is figyelni kellett a négyszögek sorrendjére, itt is foglalkozni kell ezzel a
problémával. A közeli tárgyaknak el kell fedniük a távoliakat!
Egy kis trükkel ez itt egyszerűen megoldható: mivel az egyes pályaelemek távolságviszonya,
z
koordináta szerinti sorrendje nem változik, nem kell soha sorbarendezni őket. Ha
fogunk egy várakozási sort, és annak a végére kerülnek az új fák, bóják, a sor elején pedig azok
vannak, amelyek már a legközelebb vannak a játékoshoz, éppen olyan sorrendben lesznek benne az
elemek (hátulról előrefelé), ahogyan ki kell rajzolni azokat. A program láncolt listája ehhez
képest szándékosan pont fordítva van, mert akkor a láncolás sorrendje megegyezik a kirajzolás
sorrendjével. Így a lista elején a legtávolabbi elemek vannak, ezért az újakat oda
kell beszúrni; a kirajzoláshoz pedig előrefelé kell haladni a listában. Az elejére beszúrást meg
úgyis szeretjük, az a legegyszerűbb.
A main()
függvény v
változója tárolja a játékos sebességét és a
csúszásának irányát (szog
) is. Az utóbbinál a 90 fok jelenti az egyenesen előrét,
azaz a völgy irányát. Ezekből számolódik ki az, hogy éppen hol van a pályán – de csak az x
koordináta, xpos
, mivel az y
mindig nulla. A játék hangulatát
és látványosságát javítandó, a doles
változó azt a szöget tárolja, ahány fokkal a
játékos bedől a kanyarban. A kirajzoláskor ennyivel elforgatja a teljes nézetet. Ezen változók
kezelése az eseményhurokban történik. A dőlés 10 fokra, a mozgási irány eltérése a völgynek lefelétől
pedig 15 fokra van maximálva:
doles *= 0.7; if (key[SDLK_LEFT]) { /* balra kanyarodas - nyomva tartassal */ szog-=3; if (szog<75) /* limit */ szog=75; if (doles>-10) /* bedoles a kanyarban (z tengely szerinti forg.) */ doles-=1; }
A doles*=0.7
értékadás hatására a gomb elengedése után rövid időn belül visszatér
egyenesbe a nézet. A mozgás lelkét az alábbi a programrész adja:
lefele += -v*sin(szog*3.14/180); /* ha ennyit ment lefele, akkor uj HOSSZEGYSEGnyi meretet general a palyahoz */ if (lefele < -HOSSZEGYSEG) { uj_palyaresz(&lista, (ELORE-1)*HOSSZEGYSEG); lefele += HOSSZEGYSEG; } jelenet_mozgat(&lista, 0, 0, -v*sin(szog*3.14/180)); jelenet_feldolgoz(hatter, &lista, xpos, (szog-90)*3.14/180, doles*3.14/180, &pont);
Ez az előbb említett módon számolja, hogy mennyit csúszott lefelé a játékos, és szükség
esetén új pályaelemeket hoz létre: uj_palyaresz()
. Aztán mozgatja a játékos felé
a fákat, bójákat: jelenet_mozgat()
, és végül kirajzol mindent:
jelenet_feldolgoz()
. A feldolgozás része az is, hogy a játékos mögé került elemek
törlődnek. Ezt azért volt kényelmes így megoldani, mert a forgatás által is kerülhetnek negatív
z
koordinátára pályaelemek, a forgatás pedig a kirajzolás közben történik csak meg.
2 Emlékeztető a három dimenzióról
Minden egyes tárgy, amelyik a képernyőn megjelenik, először még három dimenzióban van,
x
, y
, és z
koordinátákkal is rendelkezik. Az x
tengely a vízszintes, az
y
a függőleges (de felfelé nő, nem lefelé), a z
tengely pedig a mélységet jelenti,
vagyis az átdöfi a monitort. A nagyobb z
koordinátájú tárgyak távolabb vannak. Ezeket a
három dimenziós koordinátákat kell leképezni a két dimenziós monitorra.
Minél messzebb van egy tárgy, annál kisebbnek kell látszódjon. A drótvázas testek kirajzolása
kapcsán már szerepelt az, hogyan képezhetőek le a koordináták:
Láttuk, hogy a leképezés képlete a két háromszög hasonlóságából vezethető le, és azt is, hogy a leképezett
tárgyak alakja függ a megfigyelő vetítési síktől (itt: y tengely) vett távolságától. A síelős programban
ez tovább egyszerűsödik, ugyanis ebben a játékos nem kívülről szemléli a teret, hanem benne lesz
abban. Ahogy az előbb már szerepelt: konkrétan ő lesz az origában, tehát d=0
.
Ettől a perspektívát leíró y'=d·y/(d+z)
képlet persze megbolondulna, úgyhogy tekintsünk inkább d=1
-et. Írjunk bele még egy ex-has
dolgot a képletbe. Döntsük el már most, hogy a programban tárolt koordináták méterben lesznek megadva. Például
ha egy fenyőfa négy méter magas, legyen annak koordinátája y=4
. Végülis mindegy, hogy milyen
arányokat választunk, ezért megtehetjük, hogy egy nekünk kényelmeset adunk meg. Hogy a képernyőn megjelenő
fenyőfa ne legyen négy pixeles, nagyítsuk fel a kapott képet. Vagyis térjünk át a játékbeli koordinátákról
(világkoordinátákról) képernyőkoordinátákra ezekkel a képletekkel:
xk = f.x/(f.z+1)*500 + kep->w/2; yk = -f.y/(f.z+1)*500 + kep->h/2;
A játékos origóba helyezése miatt a számítások nagyon leegyszerűsödnek, különösen a három
forgatás, amellyel a program számol. Ezek a következők. Először is, az világkoordináták szerint sík pályát meg
kell dönteni előrefelé, azaz
meg kell forgatni az x
tengely körül (pitch). Emiatt olyan, mintha lejtene az egész.
Meg kell forgatni az y
tengely körül is (yaw), mégpedig azért, mert ez adja a játékos
csúszásirányát. Végül pedig, kell egy forgatást végezni a z
tengely körül (roll), mert
ebből lesz a kanyarban bedőlés. Mindezt azután, hogy a kirajzolás közben a tárgyak koordinátáit
elmozdítottuk xpos
-zal, és még függőlegesen lefelé -1,7 méterrel. Miért? Mert az
a játékos szemmagassága:
f = pont3d_eltol(negyszog[i], xpos, -1.7, 0); /* sielo x pozicioja es szemmagassaga */ f = pont3d_forgat_x(f, 13*3.14/180); /* lejto dolese */ f = pont3d_forgat_y(f, irany); /* fordulas (merre nez) */ f = pont3d_forgat_z(f, doles); /* kanyarban doles */
Ha a -1.7
helyett -10
-et írunk, azt fogjuk látni, amit a repülős játékban
a pilóták láttak.
3 A kirajzolás trükkjei
A perspektíva képletével a gond ott kezdődik, ha olyan pont koordinátáit helyettesítjük be, amelyek a néző
mögött vannak (vagyis z<0
). Ilyen esetekre a képlet hamis eredményt ad: a negatív előjel miatt
fejjel lefelé fordítja a képet – azt a képet, amit elvileg a játékos nem is lát.
Egy félig előtte, félig mögötte lévő szakaszt nem lehet kirajzolni
egy egyszerű kétdimenziós, monitoron lévő szakaszként: annak egyik pontja helyesen számolódik, a másodikra
viszont helytelen az eredmény. A programban ezért csalni fogunk: az ilyen szakaszokat,
vagyis az ilyen sokszögeket egyszerűen eldobjuk. Előbb-utóbb minden tereptárgy mellett elhalad a síelő,
ezért az összes tárgy erre a sorsra jut.
A kirajzolt sokszögek egyébként a programban mind négyszögek. Minden tárgyhoz két négyszög tartozik, amelyek eltérő színűek lehetnek:
typedef struct Targy { enum { fa, boja, palyaszele } tipus; int nekiment; /* 1, ha mar megkapta erte a pontot */ Pont3D p0; /* referenciapont */ Pont3D n1[4], n2[4]; /* ket negyszog - rajzhoz */ Uint32 c1, c2; /* ket szin */ struct Targy *kov; /* lancolt listahoz */ } Targy;
A fenyőfa háromszöge, és a bója zászlója egyszerűen úgy van megcsinálva, hogy két-két pontjuk
nagyon közel van egymáshoz. A tárgyakat létrehozó függvények a fa_hozzaad()
és
boja_hozzaad()
. Ezeknek egy p0
pontot lehet megadni, amelyhez képest az új
tárgyat elhelyezik. Sok a csalás megint. :) A fenyőfa például teljesen lapos, csak mindig szinte
szemből látjuk. A megadott koordináták szerint függőlegesen, pontosan felfelé nő, az y
tengely irányába. Ezzel nem is lenne gond, ha nem forgatnánk el az egész pályát az x
tengely körül egy kicsit a kirajzoláskor. Látszik is valamennyire a játék közben, hogy
ettől ferdék valamennyire. Persze a fenyőfa létrehozásánál lehetne kompenzálni, ha a csúcsához a
p0
-nál valamilyen közelebbi pontot választanánk, de nem lényeges. A függvényekben
megadott koordinátákat kockás lapra lerajzolva egyébként szépen kiadódnának a rajzok. Például a
fa:
Targy *fa_hozzaad(Pont3D p0) { Targy *uj=(Targy *) malloc(sizeof(Targy)); uj->tipus=fa; uj->nekiment=0; uj->p0=p0; uj->n1[0]=pont3d_eltol(p0, -0.1, 0, 0); uj->n1[1]=pont3d_eltol(p0, -0.1, 0.3, 0); uj->n1[2]=pont3d_eltol(p0, 0.1, 0.3, 0); uj->n1[3]=pont3d_eltol(p0, 0.1, 0, 0); uj->c1=0x402020FF; /* barna */ uj->n2[0]=pont3d_eltol(p0, -0.8, 0.3, 0); uj->n2[1]=pont3d_eltol(p0, -0.1, 4.3, 0); uj->n2[2]=pont3d_eltol(p0, 0.1, 4.3, 0); uj->n2[3]=pont3d_eltol(p0, 0.8, 0.3, 0); uj->c2=0x008000FF; /* zold */ return uj; }
A sokszögeket az SDL fillPolygonColor()
függvénye rajzolja ki. Ennek bárhány csúcsból álló
sokszöget meg lehet adni, és kifesti a belsejét is, nem csak a körvonalait rajzolja meg. A forgatások
és a szemmagasság miatti eltolási transzformáció után ellenőrizzük, hogy a forgatott pont z
koordinátája
nem lett-e túl kicsi vagy negatív; ha az lett, akkor a negyszog_kepernyore()
függvény
nem rajzolja ki a poligont, hanem 1
-gyel tér vissza. Ezzel jelzi a hívó jelenet_feldolgoz()
függvénynek, hogy az adott tárgyat a listából el kell távolítani. Mivel a tárgyak közelednek a néző felé,
ha egyszer kicsi a z
koordinátájuk, akkor már később csak még kisebb lesz.
Az utóbbi függvény végzi egyébként az ütközések ellenőrzését is, amihez a pályaelemek p0
adattagját használja.
4 A program
A letölthető SDL-es program (advent20-si.c) a szokásos módon fordítható. A bemutatott trükkökkel együtt 320 kódsorba fért be a dolog.