InfoC adventi naptár
Láncolt listák

Hétvége lévén, legyen megint grafikus program!
A feladat: írjunk programot, amelyben egy repülővel repkedünk a képernyő alján. Haladjunk egy erdő felett, amelyben fák vannak. Jöjjenek szembe is repülők, amelyekre lehessen lőni a szóköz gombbal. Ha eltalálunk egy ilyet, akkor zuhanjon le (khm, tűnjön el), és ne legyen szabad nekimenni a szembejövő repülőknek sem.
1 A grafikai rész

A program grafikája azért persze nem lesz túlbonyolítva. A repülők a jobb oldalt látható módon színes téglalapokból rakhatók össze. Ezeket a megfelelő sorrendben egymás fölé rajzolva (előbb a testet, utána a szárnyat) néhány utasítással megrajzolhatóak. A játékos által irányított gép kék, az ellenségek pedig pirosak. A kék gép a képernyőn felfelé néz, a pirosak lefelé, ezért legegyszerűbb ehhez egy függvényt írni, amelynek paramétere, hogy saját vagy nem saját repülőgépről van szó; nem saját esetén pedig a szín átállításán kívül tükrözi is azt függőlegesen:
/* repulogepet rajzol. ha sajat, akkor felfele nez, ha nem, lefele */ void repgep_rajzol(SDL_Surface *bm, int x, int y, int sajat) { int dx=rand()%5; /* légcsavarhoz */ int s=sajat ? 1:-1; /* y tükrözéshez */ Uint32 szin=sajat ? 0x000080FF : 0x400000FF; /* 0xRRGGBBAA */ boxColor(bm, x-3, y-20*s, x+3, y+20*s, szin); /* test */ boxColor(bm, x-8, y+17*s, x+8, y+19*s, szin); /* farok */ boxColor(bm, x-20, y-10*s, x+20, y-3*s, 0xFFFFFFFF); /* szarny */ boxColor(bm, x-20, y-4*s, x+20, y-4*s, 0xC00000FF); /* szarny */ boxColor(bm, x-dx, y-24*s, x+dx, y-23*s, 0xFFFFFFFF); /* prop */ }
A légcsavar szélességét egy véletlenszám adja, így az mozog. Ezt a dx
változó
tárolja. A programban mindenhol az …RGBA
nevű függvények helyett
a …
nevű SDL_gfx függvények hívása szerepel – néha így kényelmesebb
megadni a színeket. Ennek az utolsó paramétere egy 32 bites, előjel nélküli integer,
amelyben 0xRRGGBBAA
alakban szerepelnek a színkomponensek bájtjai.

A fenyőfák a programban egy háromszögből és egy négyzetből állnak. Az SDL_gfx
filledTrigonRGBA()
függvénye rajzolja a háromszöget, a boxRGBA()
pedig a
téglalapot: Természetesen mindkét rajzoló függvény a koordináta szerint paraméteres, hiszen mind
repülőkből, mind fákból sok lesz a játékmezőn.
/* fat rajzol az adott koordinatara */ void fa_rajzol(SDL_Surface *bm, int x, int y) { filledTrigonColor(bm, x, y-20, x-12, y+8, x+12, y+8, 0x008000FF); boxColor(bm, x-2, y+9, x+2, y+13, 0x603000FF); }
2 Minden relatív
A valóságban a fák mozdulatlanok. Akár köthetnénk hozzájuk is a játékban használt koordinátarendszert. A játékos repülője hozzájuk képest egyik irányban (pl. negatív y irányban), az ellenségek pedig a másik (pozitív y irányban) mozognak.
Ez azonban nem feltétlenül kell így legyen. Ha a játékost az ablak koordinátarendszeréhez rögzítjük (ami logikus lépés, hiszen az ablakban mindig látszódnia kell annak a repülőgépnek), akkor nem csak az ellenségek y koordinátája, hanem a fák y koordinátája is változik. Ebben a játékban ez a legegyszerűbb megvalósítás, a látvány szempontjából pedig mindegy – ugyanaz lesz az eredmény.
A játéktéren lévő elemeket (ellenség repülőgépek, fák és kilőtt golyók) egy láncolt listába lehet tenni, és adott időközönként az y koordinátájukat módosítani. A repülőgépek a fáknál gyorsabban mozognak (pozitív y irányba); a kilőtt golyó az ellenségek felé megy, ezért az negatív y irányba, a képernyőn felfelé mozog. A láncolt listára azért van szükség, mert a játéktéren lévő objektumok száma folyamatosan változik: a képernyő tetején új fák és ellenségek jelennek meg, míg a képernyő alján kilépőket rendszeresen törölni kell a listából.

Logikus gondolat lenne a játéktér elemeit sorban kirajzolni, utána pedig megrajzolni még pluszba a játékos által vezérelt repülőgépet. (Az utóbbi tulajdonképp nem is kell része legyen a láncolt listának, úgyis minden szempontból különleges.) Azonban itt van egy buktató. Mivel a repülőgépek és a fák eltérő sebességgel mozognak, előfordulhat, hogy a láncolt listában egy előrébb szereplő elem a képernyőn valójában hátrébb van. Tipikusan azáltal, hogy egy repülőgép megelőzött egy fát. Ha ezt nem vesszük figyelembe, a kirajzolás eredménye az lehet, hogy a repülőgép a fa alatt van. Ilyet lehet, sokan szeretnének karácsonyra, de a játékban ez nem mutat jól. Figyelni kell a sorrendekre! A legegyszerűbb megoldás az, ha többször járjuk be a listát: előbb a fákat, később a repülőgépeket rajzoljuk ki. Így a kirajzolt fák képpontjait a repülőgépek „felülírják”, és nem néznek úgy ki, mintha alatta lennének. Az ilyesmit szokták „két és fél dimenziós” grafikának is hívni. Valójában a program nem tárol magasságkoordinátákat, de valamilyen szinten mégis foglalkoznia kell ezzel a kérdéssel.
/* ket es fel dimenzios grafika: alulra a fak, felulre a repulok */ for (iter = tlista->kov; iter != NULL; iter = iter->kov) if (iter->tipus == fa) fa_rajzol(bm, iter->x, iter->y); for (iter = tlista->kov; iter != NULL; iter = iter->kov) if (iter->tipus == ellenseg) repgep_rajzol(bm, iter->x, iter->y, 0); for (iter = tlista->kov; iter != NULL; iter = iter->kov) if (iter->tipus == golyo) golyo_rajzol(bm, iter->x, iter->y);
3 A lista kezelése
A listából elegendő egy egyszeresen láncoltat választani. Gyakori a listához fűzés, amikor új elemek jelennek meg – mivel a kirajzolás típusonként halad, mindegy, hogy a listába hova kerülnek az elemek, így tesszük az elejére. Jól jön viszont, ha van strázsa, mivel könnyen és gyakran kell a lista elejéről törölni elemet.
Van egy listaművelete a programnak, amely azonban nem teljesen triviális. Nevezetesen az, amikor sikerül kilőnie a játékosnak egy ellenséges gépet. Ahogyan változnak a kilőtt golyó és az ellenségek koordinátái, figyelni kell, hogy mikor kerül egy golyó egy géphez túl közel. Ehhez minden ellenség esetén meg kell vizsgálni az összes golyót a listában:
iter=………; /* a repülő pointere */ for (iterg = tlista->kov; iterg != NULL; iterg = iterg->kov) if (iterg->tipus == golyo) { dx = iter->x - iterg->x; dy = iter->y - iterg->y; if (dx*dx+dy*dy < 15*15) { /* …… ha eltaláltuk …… */ } }
Ha eltaláltunk egy gépet, akkor törölni kell azt a listából; és törölni kell a golyót is,
hiszen az nem lenne túl realisztikus, ha menne tovább, és letarolná a többi útjába kerülő gépet
is. Az adott golyó, vagyis az iterg
által mutatott listaelem törlése,
free(iterg)
azonban megzavarná a for()
ciklust, hiszen annak
utótevékenységében használjuk az iterg->kov
mutatót. Ezt ránézésre kivédhetjük
azzal, ha ezt kimentjük belőle törlés előtt:
iterg=tlista->eleje->kov while (iterg!=tlista->vege) { Targy *kovetkezo=iterg->kov; // mutató a ciklus számára if (iterg->tipus==golyo) { dx=iter->x - iterg->x; dy=iter->y - iterg->y; if (dx*dx+dy*dy < 15*15) { ……… töröl(iterg); /* a golyót */ ……… töröl(iter); /* az ellenséget */ } } iterg=kovetkezo; }
Azonban lehet, hogy ez sem elegendő: a listából törölt repülőgép lehet, hogy éppen az
iterg->kov
volt! Ilyen esetben a for()
ciklusnak az
iterg->kov->kov
címen kellene folytatódnia:
Ezt hogy oldjuk meg? Vagy írunk erre is egy plusz if()
-et, és lekezeljük külön;
vagy inkább úgy, hogy a törlendő elemeket egyszerűen csak megjelöljük ebben a ciklusban, és az
összes feldolgozás után töröljük csak ki véglegesen azokat. A programban az utóbbi megoldás
szerepel; ezért lett a Targy
struktúrában egy torlendo
nevű adattag is.
4 A program
A program többi része lényegében magától értetődő. Az eseményhuroknál a szokásos
dolgok láthatóak: egy időzítő létrehozása, és az időzítő által generált események alapján
a játék vezérlése. Külön magyarázatot talán csak a billentyűzet kezelése érdemel: mivel
a golyó kilövése a szóköz megnyomásához, az oldalirányú mozgás pedig a kurzorbillentyűk
nyomva tartásához kötődik, ezt eltérő módon kell a programban kezelni. Az SDL
SDL_GetKeyState()
függvénye visszaad egy tömbre mutató pointert, amely
tömb az egyes billentyűk lenyomott állapotát tárolja. Ez a tömb ugyanazokkal a konstansokkal
indexelhető, mint amelyek egy esemény .key.keysym.sym
adattagjában is vannak:
pl. az SDLK_LEFT
-edik elem logikai igaz, ha a balra gombot a felhasználó épp
lenyomva tartja.
A program többi apró részlete leginkább csak kozmetikai célokat szolgál: milyen gyakran jelenjenek meg új fák a listában, milyen gyakran jöjjenek új ellenségek stb. Ezeket kísérletezéssel lehet beállítani. A letölthető forráskód: advent15-repulo.c. Lefordítani szokásos módon lehet – segítség az Érdekességek menüpont alatt található, az SDL-es írásnál.