Az SDL multimédiás könyvtár

Az SDL egy platformfüggetlen multimédiás függvénykönyvtár. Az egyes géptípusok, operációs rendszerek különbségeit elfedi, és a programozók számára egy egységes felületet biztosít a grafikus megjelenítéshez, hangok megszólaltatásához, billentyűk, egér és botkormányok kezeléséhez. Ez azt jelenti, hogy az SDL-lel megírt program működik különféle Windows verziókon, de Linuxokon, Mac OS X-en, és még néhány okostelefonon is.
Az alap SDL-ben nincsenek vonal, kör, és egyéb primitívek kirajzolásához függvények. Ahhoz további könyvtárakat kell telepíteni (pl. SDL_gfx). Ezen függvénykönyvtárak tudása viszont már elég nagy. Az SDL_ttf segítségével bármilyen betűtípust használva rajzolhatunk, az SDL_mixer több hang és zene megszólaltatását teszi lehetővé, az SDL_net pedig a hálózatprogramozás ügyes-bajos dolgait rejti egy platformfüggetlen réteg mögé. Az SDL_image nevű kiegészítő sokféle képformátumot (PNG, JPG) ismer; ilyen fájlokat lehet vele betölteni, és a programban kirajzolni.
Az SDL telepítéséről egy külön írásban olvashattok.
1 Az első program

Alább látható az első program. Ez kirajzol néhány kört a képernyőre, utána pedig addig vár, amíg a felhasználó be nem zárja az ablakot a nagy piros X-szel.
Az első lépés az SDL könyvtár inicializálása, ezt az SDL_Init()
nevű függvénnyel tehetjük meg. Az SDL alrendszerekből áll (grafika, hang, időzítés
stb.), ezek közül az első programban csak a grafikai alrendszerre van szükségünk,
ezért a függvény paramétere SDL_INIT_VIDEO
.
Ezután létrehozunk egy 440×360 képpont méretű ablakot az SDL_SetVideoMode()
hívással. Ennek harmadik paramétere a használandó színek számával kapcsolatos – nem
lényeges, állíthatjuk nullára; a negyedik pedig a grafikus alrendszer beállításait
adja meg, ez sem lényeges most. (SDL_ANYFORMAT
annyit jelent, hogy
bármilyen beállításokat elfogadunk, amit az operációs rendszer ad.)
Az SDL_SetVideoMode()
függvény visszatérési értéke egy SDL_Surface
típusú pointer. Az SDL_Surface
a kép típusa az alrendszernek: minden
függvény, ami valamit kirajzol, egy ilyet vár első paramétereként, hogy tudja, melyik
képre kell rajzolnia. Ezt a mutatót el kell mentenünk, mert később hivatkozni kell
rá. Ha NULL
pointert ad a függvény, az azt jelenti, hogy valami probléma
történt. Ha nem, akkor viszont indulhat a rajzolás!
#include <SDL.h> #include <SDL_gfxPrimitives.h> #include <math.h> int main(int argc, char *argv[]) { SDL_Event ev; SDL_Surface *screen; int x, y, r; /* SDL inicializálása és ablak megnyitása */ SDL_Init(SDL_INIT_VIDEO); screen=SDL_SetVideoMode(440, 360, 0, SDL_ANYFORMAT); if (!screen) { fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); exit(1); } SDL_WM_SetCaption("SDL peldaprogram", "SDL peldaprogram"); r=50; /* karika */ x=100; y=100; circleRGBA(screen, x, y, r, 255, 0, 0, 255); circleRGBA(screen, x+r, y, r, 0, 255, 0, 255); circleRGBA(screen, x+r*cos(3.1415/3), y-r*sin(3.1415/3), r, 0, 0, 255, 255); /* antialias karika */ x=280; y=100; aacircleRGBA(screen, x, y, r, 255, 0, 0, 255); aacircleRGBA(screen, x+r, y, r, 0, 255, 0, 255); aacircleRGBA(screen, x+r*cos(3.1415/3), y-r*sin(3.1415/3), r, 0, 0, 255, 255); /* kitoltott kor */ x=100; y=280; filledCircleRGBA(screen, x, y, r, 255, 0, 0, 255); filledCircleRGBA(screen, x+r, y, r, 0, 255, 0, 255); filledCircleRGBA(screen, x+r*cos(3.1415/3), y-r*sin(3.1415/3), r, 0, 0, 255, 255); /* attetszo kor */ x=280; y=280; filledCircleRGBA(screen, x, y, r, 255, 0, 0, 96); filledCircleRGBA(screen, x+r, y, r, 0, 255, 0, 96); filledCircleRGBA(screen, x+r*cos(3.1415/3), y-r*sin(3.1415/3), r, 0, 0, 255, 96); /* szoveg */ stringRGBA(screen, 110, 350, "Kilepeshez: piros x az ablakon", 255, 255, 255, 255); /* eddig elvegzett rajzolasok a kepernyore */ SDL_Flip(screen); /* varunk a kilepesre */ while (SDL_WaitEvent(&ev) && ev.type!=SDL_QUIT) { } /* ablak bezarasa */ SDL_Quit(); return 0; }
A program köröket rajzol, négyféleképpen. Az első három körnél egyszerűen kiszínezi azokat a képpontokat (pixel), amelyek a körívre esnek. A második háromnál ennél okosabb. Ahol a körív nem pont a képpontra esik, ott a szomszédos képpontok között színátmenetet képez. Ezt az eljárást úgy nevezik, hogy antialiasing. Így a rajz szebb, a körív nem annyira recegős.
A pozíció és a méret megadása után következik mindegyik függvénynél a szín megadása. Ezek három komponensből állnak: vörös, zöld és kék, mindegyik 0-tól 255-ig. 255, 0, 0 jelenti a vöröset, 255, 255, 255 pedig a teljesen fehéret. A legutolsó paraméter az átlátszatlanságot adja meg, amely ugyancsak egy 0 és 255 közötti érték. 0 jelenti a teljesen átlátszót, 255 pedig a teljesen átlátszatlant. Ez látszik az alsó köröknél, ahol a jobb oldali köröknél az érték 255 helyett csak 96. Így azok színei keverednek.
Miután elvégeztük az összes rajzolást, meg kell hívni az SDL_Flip
függvényt
a képernyőre. A rajzolások először csak a memóriában történtek, és igazából a hívás hatására
kerül ki minden az ablakba. Ez azért előnyös, mert így a felhasználó nem fogja látni, ahogy
egyesével jelennek meg az elemek, hanem csak a végeredményt – animációnál ez fontos lesz.
A további rajzolásokkal a meglévő képet módosítjuk; az eredmény pedig egy újabb
SDL_Flip
hatására jelenik meg.
Az SDL_gfx függvénykönyvtár néhány rajzeleme (grafikus primitíve):
pixelRGBA(kép, x, y, r, g, b, a)
– képpont rajzolása.lineRGBA(kép, x1, y1, x2, y2, r, g, b, a)
– szakasz.thickLineRGBA(kép, x1, y1, x2, y2, v, r, g, b, a)
– vastag szakasz.rectangleRGBA(kép, x1, y1, x2, y2, r, g, b, a)
– téglalap.boxRGBA(kép, x1, y1, x2, y2, r, g, b, a)
– kitöltött téglalap.circleRGBA(kép, x1, y1, x2, y2, r, g, b, a)
– kör.trigonRGBA(kép, x1, y1, x2, y2, x3, y3, r, g, b, a)
– háromszög.filledTrigonRGBA(kép, x1, y1, x2, y2, x3, y3, r, g, b, a)
– kitöltött háromszög.stringRGBA(kép, x, y, szöveg, r, g, b, a)
– szöveg.
A vonalas rajzokat (szakasz, kör, háromszög stb.) készítő függvényeknek mind van aa
-val
kezdődő párjuk is. Ezen függvények dokumentációja elérhető ezen az oldalon,
a teljes SDL_gfx függvénykönyvtáré pedig ezen az oldalon.
2 Események, eseményvezérelt programozás

Az egyszerű, konzolos programok lineárisan működnek: a
printf()
-fel mondhatunk valamit a felhasználónak, a
scanf()
-fel pedig kérdezhetünk tőle valamit. Nem gond
az, hogy a scanf()
megakasztja a programot, mert amíg
nincs meg a bemenő adat, addig úgysem tudna továbbhaladni a program.
Egy játéknál, meg általában a grafikus programoknál ez nincs így. A
programnak itt egyszerre több bemenete van: a billentyűzetre és az
egérre is reagálnia kell, arról nem is beszélve, hogy ha a
felhasználó épp nem nyúl semelyikhez, akkor is folytatódnia kell a
képernyőn látható eseményeknek. Nem akadhat meg a játék attól, hogy
éppen nem nyomtuk meg egyik gombot sem!
Ezért találták ki az eseményvezérelt programozást. Az SDL a programhoz beérkező eseményeket (billentyűzet, egérmozdulatok, időzítések, ablak bezárása) összegyűjti, és azokat keletkezésük sorrendjében adja nekünk. Ezt a programnak egy eseményhurokban (event loop) kell feldolgoznia, amely nagyon egyszerű:
SDL_Event event; while (fut_a_program) { SDL_WaitEvent(&event); /* várunk a következő eseményre */ switch (event.type) { /* esemény típusa szerinti esetszétválasztás */ ... /* esemény feldolgozása */ } }
Az SDL_WaitEvent()
függvény addig vár, amíg meg nem történik
a következő esemény; amint az bekövetkezik, akkor az adatait beteszi az
event
nevű, SDL_Event
típusú struktúrába (azért
veszi át cím szerint, hogy ezt meg tudja tenni). Ezután az esemény feldolgozhatjuk,
annak típusa szerint:
case SDL_QUIT:
kilépés, a felhasználó az ablak bezárása piros x-re kattintott;break;
case SDL_MOUSEMOTION:
egérmozdulat;break;
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP:
egérgomb kattintás és elengedés;break;
case SDL_KEYDOWN: case SDL_KEYUP:
billentyűzet események;break;
Az event
struktúra az esemény típusától függően további információkat
tartalmaz. Egérmozgás esetén az
event.motion
struktúrát tölti ki az SDL_WaitEvent()
a koordinátákkal: event.motion.x
a vízszintes, event.motion.y
a függőleges koordináta.
Kattintásnál az
event.button
struktúra adattagjai vesznek fel értékeket: az event.button.button
adattag
mutatja, hogy melyik gombról van szó SDL_BUTTON_LEFT
, SDL_BUTTON_MIDDLE
,
SDL_BUTTON_RIGHT
.
Az alábbi C programban rajzolni lehet az egérrel. A működést a kód közepén lévő eseményhurok
irányítja. A bal gombbal lehet rajzolni, a jobb gombbal pedig törölni az ablak tartalmát.
Az eseményvezérlés kellemes vonása, hogy a program gyakorlatilag semennyire sem terheli le
a számítógépet. Amíg nincs esemény, addig ugyanúgy alszik, ahogyan azt egy scanf()
-re
várakozás esetén is teszi.
#include <SDL.h> #include <SDL_gfxPrimitives.h> #include <math.h> int main(int argc, char *argv[]) { SDL_Event event; SDL_Surface *screen; int elozox, elozoy, click, quit, rajzoltam; /* SDL inicializálása és ablak megnyitása */ SDL_Init(SDL_INIT_VIDEO); screen=SDL_SetVideoMode(360, 360, 0, SDL_ANYFORMAT); if (!screen) { fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); exit(1); } SDL_WM_SetCaption("SDL esemenyek", "SDL esemenyek"); /* az esemenyvezerelt hurok */ quit = 0; click = 0; elozox = 0; elozoy = 0; while (!quit) { SDL_WaitEvent(&event); rajzoltam = 0; switch (event.type) { /* eger kattintas */ case SDL_MOUSEBUTTONDOWN: if (event.button.button == SDL_BUTTON_LEFT) { click = 1; elozox = event.button.x; elozoy = event.button.y; } else if (event.button.button == SDL_BUTTON_RIGHT) { boxRGBA(screen, 0, 0, 359, 359, 0, 0, 0, 255); rajzoltam = 1; } break; /* egergomb elengedese */ case SDL_MOUSEBUTTONUP: if (event.button.button == SDL_BUTTON_LEFT) { click = 0; } break; /* eger mozdulat */ case SDL_MOUSEMOTION: if (click) { aalineRGBA(screen, elozox, elozoy, event.motion.x, event.motion.y, 255, 255, 255, 255); rajzoltam = 1; } /* a kovetkezo mozdulat esemenyhez */ elozox = event.motion.x; elozoy = event.motion.y; break; /* ablak bezarasa */ case SDL_QUIT: quit=1; break; } if (rajzoltam) SDL_Flip(screen); } SDL_Quit(); return 0; }
Maga az eseményhurok ennél a progamnál tulajdonképpen egy
állapotgép. Na nem azért, mert switch()
van benne (az
csak az események típusának megállapításához kell), hanem mert az
egyes események jelentése eltérő attól függően, hogy mik történtek a
múltban. Például az egérmozdulatnál csak akkor rajzolunk, ha
előzőleg egy kattintás eseményt már feldolgoztunk, és minden
mozdulatnál megjegyezzük a koordinátákat, hogy a legközelebbi
ugyanilyen eseménynél tudjuk, honnan hova kell húzni a vonalat.
3 Az időzítők használata

Előbb arról volt szó, hogy a program futásának nem szabad megszakadnia amiatt, mert eseményre vár – és aztán jött egy program forráskódja, amely nem csinál semmit, azaz alszik az események között. Hogy fog akkor a játék tovább futni, amíg a felhasználó nem nyúl se a billentyűzethez, se az egérhez? Nagyon egyszerű: létre kell hozni egy időzítőt, amely adott időközönként generál egy eseményt. Ha létrejön az esemény, annak hatására fel fog ébredni az eseményhurok – de fel fog ébredni a billentyűzet vagy az egér hatására is.
Időzítőt létrehozni az SDL_AddTimer()
függvénnyel lehet.
Ennek paraméterei a következők: 1) mennyi idő múlva hívódjon meg
(ezredmásodperc), 2) melyik függvény hívódjon meg, 3) egy tetszőleges
mutató, amit paraméterként meg fog kapni a függvény. (Ha ez nem kell
semmire, akkor lehet NULL.) A függvény visszatérési értéke egy
SDL_TimerID
típusú azonosító, amivel hivatkozhatunk az
időzítőre (pl. az SDL_RemoveTimer()
-nek paraméterként adva
letilthatjuk azt.) A hívás tehát így néz ki:
id = SDL_AddTimer(20, idozit, NULL);
A paraméterként adott függvény fejléce kötött, ilyen kell legyen:
Uint32 idozit(Uint32 ms, void* param);
Vagyis az SDL időzítője által meghívott függvény megkapja paraméterként
azt, hogy milyen időközökre lett beállítva, és a tetszőleges felhasználói
paramétert. Visszatérési értéke pedig egy egész szám, hogy legközelebb
hány ezredmásodperc múlva hívódjon meg. Legegyszerűbb, ha egy
return ms;
sorral fejezzük be a függvényt, amiben általában
amúgy sincs más, csak egy felhasználói típusú esemény létrehozása, és
beillesztése a várakozási sorba:
Uint32 idozit(Uint32 ms, void* param) { SDL_Event ev; ev.type = SDL_USEREVENT; SDL_PushEvent(&ev); return ms; /* ujabb varakozas */ }
Az eseménykezelő hurkot tehát ki kell egészíteni az SDL_USEREVENT típusú esemény(ünk) feldolgozásával. A labdát pattogtató program így néz ki:
#include <SDL.h> #include <SDL_gfxPrimitives.h> /* ez a fuggveny hivodik meg az idozito altal. * betesz a feldolgozando esemenyek koze (push) * egy felhasznaloi esemenyt */ Uint32 idozit(Uint32 ms, void* param) { SDL_Event ev; ev.type = SDL_USEREVENT; SDL_PushEvent(&ev); return ms; /* ujabb varakozas */ } int main(int argc, char *argv[]) { enum { ABLAK=360, GOLYO_R=10 }; struct Golyo { int x, y; int vx, vy; }; SDL_Event event; SDL_Surface *screen; int quit; SDL_TimerID id; struct Golyo g; /* SDL inicializálása és ablak megnyitása */ SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER); screen=SDL_SetVideoMode(ABLAK, ABLAK, 0, SDL_ANYFORMAT); if (!screen) { fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); exit(1); } SDL_WM_SetCaption("SDL idozites", "SDL idozites"); quit = 0; g.x=ABLAK/2; g.y=ABLAK/3; g.vx=3; g.vy=2; /* idozito hozzaadasa: 0 ms mulva hivodik meg eloszor */ id = SDL_AddTimer(20, idozit, NULL); /* szokasos esemenyhurok */ while (!quit) { SDL_WaitEvent(&event); switch (event.type) { /* felhasznaloi esemeny: ilyeneket general az idozito fuggveny */ case SDL_USEREVENT: /* kitoroljuk az elozo poziciojabol */ filledCircleRGBA(screen, g.x, g.y, GOLYO_R, 0, 0, 0, 255); /* kiszamitjuk az uj helyet */ g.x+=g.vx; g.y+=g.vy; if (g.x<GOLYO_R || g.x>ABLAK-GOLYO_R) g.vx *= -1; if (g.y<GOLYO_R || g.y>ABLAK-GOLYO_R) g.vy *= -1; /* ujra kirajzolas, es mehet a kepernyore */ filledCircleRGBA(screen, g.x, g.y, GOLYO_R, 128, 128, 255, 255); SDL_Flip(screen); break; case SDL_QUIT: quit=1; break; } } /* idozito torlese */ SDL_RemoveTimer(id); SDL_Quit(); return 0; }
Itt nagyon fontos, hogy csak a kép teljes megrajzolása után hívjuk meg az SDL_Flip()
-et. Ha a törlés után is meghívnánk, akkor az animáció villódzna (flicker), így viszont
szép, folytonos a megjelenítés. Törölni viszont kell, hiszen mindig az előzőleg megrajzolt képet
módosítjuk.
Nem csak egy, hanem akár egyszerre több időzítőt is létrehozhatunk. Hogy ezeket meg
lehessen különböztetni, az általuk generált eseményeknek érdemes külön azonosítót adni. Az
események típusa, az event.type
adattag nem felsorolt típus, hanem egy egyszerű
egész szám. Az SDL dokumentációja pedig azt mondja, hogy az SDL_USEREVENT
konstans
értékétől fölfelé bármilyen saját eseményt definiálhatunk. Ezért ezeket használhatjuk akár úgy
is, hogy az egyik időzítőnk SDL_USEREVENT+1
, a másik SDL_USEREVENT+2
stb. típusú eseményeket generál.
4 Képfájlok beolvasása

Ez nagyon egyszerű feladat: az SDL_image nevű függvénykönyvtárnak
van egy IMG_Load()
nevű függvénye. Ennek egyetlen paramétere
a betöltendő kép, ami elég sokféle formátumú lehet (az SDL_image
dokumentációja szerint BMP, GIF, JPEG, LBM, PCX, PNG, PNM, TGA, TIFF, WEBP, XCF, XPM és XV).
A függvény visszatérési értéke egy SDL_Surface*
, vagyis egy
mutató a betöltött képre. Ezzel tudunk később hivatkozni rá, mert bent
maradt a gép memóriájában. Ha már nincs rá szükség, fel kell
azt szabadítani, az SDL_FreeSurface()
függvénnyel. Ha
ezt nem tesszük meg, a betöltött képek miatt a programunk egyre több
memóriát foglal. Úgyhogy ez fontos!
A betöltött képpel sincsen nehéz dolgunk: az SDL_BlitSurface()
függvény tud kép(részlet)et másolni egyik SDL_Surface
-ről a másikra.
Ennek a paraméterei: forráskép, forrás téglalap, cél kép, cél téglalap (bal
felső sarka). Vagyis nem csak a teljes képet tudja másolni, hanem annak
csak egy részletét is, a cél kép tetszőleges pozíciójára. A pozíciókat és
a méreteket SDL_Rect
típusú struktúrákkal kell megadni; ezekre
a függvény pointereket vesz át:
SDL_Rect forrasterulet = { forras_x, forras_y, forras_szelesseg, forras_magassag }; SDL_Rect celpozicio = { cel_x, cel_y, 0, 0 }; SDL_BlitSurface(forraskep, &forrasterulet, celkep, &celpozicio);
Ha a teljes forrás képet szeretnénk másolni, akkor a forrás téglalapra mutató pointer lehet
NULL
; ha a cél kép bal felső sarkába szánjuk a képet, akkor pedig a cél téglalap pointere
helyett elfogadott NULL
pointert adni. (A struktúra adattagjai: x
, y
bal felső sarok, w
, h
szélesség és magasság.)

pieces.png (klikk a letöltéshez)
Az alábbi programban kihasználjuk azt, hogy a kép egy részét is lehet másolni. A program
tartalmaz egy felsorolt típust, amely a fenti képen látható figurák sorrendjében nevezi meg
azokat. A mezo_rajzol()
függvényen belül kiszámolódik a fenti képen belüli
koordináták (melyik figuráról van szó), és a cél koordináták is (melyik mezőre kerül). Ez a kép
egyébként átlátszó képpontokat is tartalmaz, az SDL ezt is támogatja. A fájlt le kell tölteni,
és a futtatható (.exe) mellé tenni pieces.png
néven.
A program futása közben bármikor létrehozhatunk egyéb képeket is az
SDL_CreateRGBSurface()
függvényhívással (lásd a
dokumentációt).
Ez arra jó, ha munka képrészletekkel szeretnénk dolgozni; pl. egy háttérképet
bonyolult műveletekkel megrajzolunk, és később már csak a megrajzolt képet másolgatjuk
az ablakba. Csak arra kell figyelni, hogy minden külön létrehozott képet
szabadítsunk is fel az SDL_FreeSurface()
függvénnyel, ha már nem kell.
A letöltött képfájlt (pieces.png) a Code::Blocks projekt mappájába kell tenni.
#include <SDL.h> #include <SDL_image.h> #include <SDL_gfxPrimitives.h> #include <math.h> enum { MERET = 52, KERET = 26 }; /* mezon allo figura. ugyanolyan sorrendben vannak, mint a kepen, * igy a kapott egesz szamok megegyeznek a png-beli indexekkel */ typedef enum Babu { Ures = -1, VKiraly, VVezer, VBastya, VFuto, VHuszar, VGyalog, SKiraly, SVezer, SSastya, SFuto, SHuszar, SGyalog } Babu; typedef Babu Tabla[8][8]; /* uj allassal tolti ki a parameterkent kapott tablat */ void uj_allas(Tabla tabla) { int x, y; for (y=0; y<8; y++) for (x=0; x<8; x++) tabla[y][x]=Ures; tabla[0][0]=SSastya; tabla[0][1]=SHuszar; tabla[0][2]=SFuto; tabla[0][3]=SVezer; tabla[0][4]=SKiraly; tabla[0][5]=SFuto; tabla[0][6]=SHuszar; tabla[0][7]=SSastya; for (x=0; x<8; x++) tabla[1][x]=SGyalog; tabla[7][0]=VBastya; tabla[7][1]=VHuszar; tabla[7][2]=VFuto; tabla[7][3]=VVezer; tabla[7][4]=VKiraly; tabla[7][5]=VFuto; tabla[7][6]=VHuszar; tabla[7][7]=VBastya; for (x=0; x<8; x++) tabla[6][x]=VGyalog; } /* kiszamolja, hogy milyen koordinatan van a kepernyon az adott mezo */ int palyapos(int koord) { return MERET*koord + KERET; } /* kirajzol egy mezot; a forras a betoltott png, a cel nevu kepre rajzol. * melyik babut, milyen koordinatakra: melyik, x, y. */ void mezo_rajzol(SDL_Surface *forraskep, SDL_Surface *celkep, Babu melyik, int x, int y) { /* a forras kepbol ezekrol a koordinatakrol, ilyen meretu reszletet masolunk. */ SDL_Rect src = { 12+(melyik%6)*62-2, 13+60*(melyik/6)-2, MERET, MERET }; /* a cel kepre, ezekre a koordinatakra masoljuk. (0, 0 lenne a meret, de az nem szamit, * a masolando kepreszlet meretet az elozo struct adja meg. */ SDL_Rect dest = { x*MERET + KERET, y*MERET + KERET, 0, 0 }; /* mezo alapszine */ if (x%2 != y%2) boxRGBA(celkep, palyapos(x), palyapos(y), palyapos(x+1)-1, palyapos(y+1)-1, 0xcc, 0xad, 0x99, 255); else boxRGBA(celkep, palyapos(x), palyapos(y), palyapos(x+1)-1, palyapos(y+1)-1, 0xe6, 0xd1, 0xc3, 255); if (melyik==Ures) return; /* kepreszlet masolasa */ SDL_BlitSurface(forraskep, &src, celkep, &dest); } /* kirajzolja az egesz tablat. forraskep a betoltott png, celkep ahova rajzol. */ void tabla_rajzol(Tabla tabla, SDL_Surface *forraskep, SDL_Surface *celkep) { int x, y; /* az egeszet kitolti */ boxRGBA(celkep, 0, 0, celkep->w-1, celkep->h-1, 0x90, 0xe0, 0x90, 255); rectangleRGBA(celkep, palyapos(0)-1, palyapos(0)-1, palyapos(8), palyapos(8), 0, 0, 0, 128); /* kirajzolja a mezoket */ for (y=0; y<8; y++) for (x=0; x<8; x++) mezo_rajzol(forraskep, celkep, tabla[y][x], x, y); } int main(int argc, char *argv[]) { SDL_Event event; SDL_Surface *screen; SDL_Surface *babuk; Tabla tabla; SDL_Init(SDL_INIT_VIDEO); screen=SDL_SetVideoMode(MERET*8+KERET*2, MERET*8+KERET*2, 0, SDL_ANYFORMAT); if (!screen) { fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); exit(1); } SDL_WM_SetCaption("SDL kepek", "SDL kepek"); /* kep betoltese */ babuk = IMG_Load("pieces.png"); if (!babuk) { fprintf(stderr, "Nem sikerult betolteni a kepfajlt!\n"); exit(2); } /* uj allas letrehozasa es kirajzolasa */ uj_allas(tabla); tabla_rajzol(tabla, babuk, screen); SDL_Flip(screen); while (SDL_WaitEvent(&event) && event.type!=SDL_QUIT) { } /* nincs mar ra szukseg: felszabaditjuk a memoriat */ SDL_FreeSurface(babuk); SDL_Quit(); return 0; }
5 Szövegek megjelenítése
Az SDL_gfx stringRGBA()
függvénye meg tud jeleníteni szövegeket
(lásd az első példaprogramot), de sajnos a használt betűk nagyon kicsik, és
nem ismeri a magyar ékezetes betűket sem (árvíztűrő tükörfúrógép).
SDL_TTF függvénykönyvtár megoldja mindkét problémát.
(A dokumentációja itt
érhető el.) Ez tetszőleges
True Type betűtípust be tud olvasni (Arial, Trebuchet stb.), és helyesen
tudja kezelni az ékezetes betűket is.
Bár az angol ábécé betűit kódoló ASCII szabvány gyakorlatilag mára egyeduralkodóvá vált a világon, az ékezetes betűket és egyéb karaktereket kódoló szabványokról ez sajnos nem mondható el. Több kódtáblát, azaz betű→szám táblázatot is használnak elterjedten a világon; az egységes Unicode kódolás még nem szorította ki a többit. (Ezekről bővebben a Rémtörténet a karakterkódolásokról írásban olvashatsz.) A többféle kódtábla miatt az SDL_TTF-ben minden szövegrajzoló függvénynek három változata van: 1) a Latin-1 kódolású szöveget, 2) a Unicode kódolású szöveget, és 3) az UTF-8 kódolású szöveget váró függvény.
A használat menete a következő. A használandó betűtípus fájlt
először meg kell nyitni az TTF_OpenFont()
függvényhívással.
Ilyenkor meg kell adni a betűk méretét is. A függvény visszatérési
értéke egy TTF_Font
típusú mutató, amellyel hivatkozni
lehet a betűtípusra (ilyenből több is lehet), és amelyet a TTF_CloseFont()
függvénynek a program végén oda kell adni, hogy felszabadítsa a memóriaterületet.

LiberationSerif-Regular.ttf (klikk a letöltéshez)
Minden alkalommal, amikor egy szöveget meg kell rajzolni, azt valamelyik
TTF_Render…()
függvénnyel kell tenni
(lásd itt),
függően a rajzolás kívánt minőségétől és a karakterkódolástól. A függvények
visszatérnek egy SDL_Surface
típusú mutatóval, mivel a rajzolások
kimenete egy kép, amelyben meg van rajzolva a felirat. Ezt a képet lehet
átmásolni a kívánt helyre a képernyőre az SDL_BlitSurface()
függvényhívással; egy feliratot akár többször, több helyre is. Ha már nincs
rá szükség, akkor pedig fel kell szabadítani a hozzá tartozó memóriaterületet
az SDL_FreeSurface()
hívással. (Természetesen ha sok, különféle
felirat van, akkor ehhez a műveletsorhoz érdemes saját függvényeket írni. A
programozásban nem copy-pastelünk!)
A rajzolások módja a következő lehet, bár a kép mindent elmond:
TTF_Render…_Solid
: gyors, de a betűk széle recegős.TTF_Render…_Shaded
: nem recegős. A háttér egy megadott szín.TTF_Render…_Blended
: nem recegős. A háttér átlátszó.
Betűtípusokat a Windows C:\Windows\Fonts
mappájában, vagy a Linux
/usr/share/fonts/truetype
mappájában lehet találni. Meg a neten egy
csomó helyen, csak sajnos az ingyenes betűtípusokból hiányozni szokott az ő és az ű
betű. A lenti program a Liberation Serif nevű betűtípust használja. A linkre
kattintva letölthető fájlt a Code::Blocks projekt mappájába kell tenni.
#include <SDL.h> #include <SDL_gfxPrimitives.h> #include <SDL_ttf.h> int main(int argc, char *argv[]) { SDL_Color feher = {255, 255, 255}, piros = {255, 0, 0}; SDL_Rect hova = { 0, 0, 0, 0 }; SDL_Event event; SDL_Surface *screen; TTF_Font *font; SDL_Surface *felirat; int i; SDL_Init(SDL_INIT_VIDEO); screen=SDL_SetVideoMode(480, 200, 0, SDL_ANYFORMAT); if (!screen) { fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); exit(1); } SDL_WM_SetCaption("SDL betutipusok", "SDL betutipusok"); /* hatter */ for (i=0; i<500; ++i) filledCircleRGBA(screen, rand()%screen->w, rand()%screen->h, 10+rand()%5, rand()%256, rand()%256, rand()%256, 64); /* betutipus betoltese, 32 pont magassaggal */ TTF_Init(); font = TTF_OpenFont("LiberationSerif-Regular.ttf", 32); if (!font) { fprintf(stderr, "Nem sikerult megnyitni a fontot! %s\n", TTF_GetError()); exit(2); } /* felirat megrajzolasa */ felirat = TTF_RenderUTF8_Solid(font, "TTF_RenderUTF8_Solid()", feher); /* felirat kep masolasa a kepernyore */ hova.x = (screen->w-felirat->w)/2; hova.y = 20; SDL_BlitSurface(felirat, NULL, screen, &hova); /* a feliratot tartalmazo kepre nincs mar szukseg */ SDL_FreeSurface(felirat); felirat = TTF_RenderUTF8_Shaded(font, "TTF_RenderUTF8_Shaded()", feher, piros); hova.x = (screen->w-felirat->w)/2; hova.y += 40; SDL_BlitSurface(felirat, NULL, screen, &hova); SDL_FreeSurface(felirat); felirat = TTF_RenderUTF8_Blended(font, "TTF_RenderUTF8_Blended()", feher); hova.x = (screen->w-felirat->w)/2; hova.y += 40; SDL_BlitSurface(felirat, NULL, screen, &hova); SDL_FreeSurface(felirat); felirat = TTF_RenderUTF8_Blended(font, /* ez az utf8 szoveg azert nez ki ilyen rosszul, * mert szinte csak ekezetes betu van benne */ "\xC3\xA1rv\xC3\xADzt\xC5\xB1r\xC5\x91 " "t\xC3\xBCk\xC3\xB6rf\xC3\xBAr\xC3\xB3g\xC3\xA9p " "\xE2\x98\xBA \xE2\x82\xAC", feher); hova.x = (screen->w-felirat->w)/2; /* kozepre vele */ hova.y += 40; SDL_BlitSurface(felirat, NULL, screen, &hova); SDL_FreeSurface(felirat); /* nem kell tobbe */ TTF_CloseFont(font); SDL_Flip(screen); while (SDL_WaitEvent(&event) && event.type!=SDL_QUIT) { } SDL_Quit(); return 0; }
A TTF_Init()
függvényhívást elég egyszer megtenni a
program legelején. A TTF_OpenFont()
által beolvasott
betűtípus pedig akárhányszor használható – annyi szöveget írhatunk
ki vele, amennyit csak szeretnénk. Akár többféle betűtípus is lehet
betöltve egyszerre. Ha valamelyikre nincs már szükségünk, csak akkor
kell felszabadítani a hozzá tartozó memóriaterületet egy
TTF_CloseFont()
hívással.
6 A billentyűzet kezelése
A billentyűzet kezelése SDL-ben nem nagy ördöngősség: SDL_KEYDOWN
eseményt kapunk egy billentyű megnyomásánál, SDL_KEYUP
eseményt az
elengedésénél. Az esemény adatait tároló strktúrában az
alábbi adattagok érhetőek el:
event.key.keysym.sym
: a lenyomott billentyű azonosítója, ebből a táblázatból.event.key.keysym.mod
: módosító billentyűk (shift, ctrl stb.) ebből a táblázatból. Mivel egyszerre több módosító is le lehet nyomva, egy bitenkénti ÉS&
művelettel kell megvizsgálni azt, amelyik érdekes. A módosítók lenyomásakor külön esemény is érkezik.event.key.keysym.unicode
: a karakter UNICODE kódja, ha van. (Pl. a shift lenyomásakor nincs: 0.) Csak akkor van kitöltve, ha előzetesen egySDL_EnableUNICODE(1)
hívást kiadtunk, és csak a billentyű lenyomásakor, a felengedésekor nem.
Játékokban, ahol arra vagyunk kíváncsiak, hogy nyomva van-e tartva egy billentyű,
nekünk kell külön megjegyezni azt. Ez egyszerűen megoldható egy logikai típusú
változóval, amelynek értékét SDL_KEYDOWN
esemény esetén igazra,
SDL_KEYUP
esemény esetén pedig hamisra állítjuk.
Az alábbi példaprogram egy szöveg beolvasását végző függvényt tartalmaz. Ez példa a UNICODE karakterek kezelésére is. Ehhez is kell a LiberationSerif-Regular.ttf nevű fájl, amelyet az előző program is használt.
#include <SDL.h> #include <SDL_gfxPrimitives.h> #include <SDL_ttf.h> #include <math.h> /* Beolvas egy szoveget a billentyuzetrol. * Ehhez rajzol egy zold keretet x, y, sz, m helyen, 'hatter' szinnel * es 'szin' szinu betukkel. * A rajzolashoz hasznalt font es a kepernyo surface-e az utolso parameterek. * Az elso a tomb, ahova a beolvasott szoveg kerul. * A visszateresi erteke logikai igaz, ha sikerult a beolvasas. * Ha nem kell UNICODE text, akkor a dest tipusa char * legyen, a * TTF_RenderUNICODE_Blended() fuggvenyhivas pedig TTF_RenderText_Blended-re cserelheto. */ int input_text(Uint16 *dest, int x, int y, int sz, int m, SDL_Color hatter, SDL_Color szin, TTF_Font *font, SDL_Surface *screen) { SDL_Rect forras = { 0, 0, sz, m}, cel = { x, y, sz, m }; SDL_Surface *felirat; SDL_Event event; int hossz, kilep, enter; hossz = 0; dest[hossz] = 0x0000; /* lezaro 0 */ SDL_EnableUNICODE(1); enter = 0; kilep = 0; while (!kilep && !enter) { /* szoveg kirajzolasa */ boxRGBA(screen, x, y, x+sz-1, y+m-1, hatter.r, hatter.g, hatter.b, 255); felirat = TTF_RenderUNICODE_Blended(font, dest, szin); SDL_BlitSurface(felirat, &forras, screen, &cel); SDL_FreeSurface(felirat); rectangleRGBA(screen, x, y, x+sz-1, y+m-1, 0, 255, 0, 255); /* updaterect: mint az sdl_flip, de csak a kepernyo egy darabjat */ SDL_UpdateRect(screen, x, y, sz, m); SDL_WaitEvent(&event); switch (event.type) { case SDL_KEYDOWN: switch (event.key.keysym.unicode) { case 0x0000: /* nincs neki megfelelo karakter (pl. shift gomb) */ break; case '\r': case '\n': /* enter: bevitel vege */ enter = 1; break; case '\b': /* backspace: torles visszafele, ha van karakter */ if (hossz>0) dest[--hossz] = 0x0000; break; default: /* karakter: tombbe vele, plusz lezaro nulla */ dest[hossz++] = event.key.keysym.unicode; dest[hossz] = 0x0000; break; } break; case SDL_QUIT: /* visszatesszuk a sorba ezt az eventet, mert * sok mindent nem tudunk vele kezdeni */ SDL_PushEvent(&event); kilep = 1; break; } } /* 1 jelzi a helyes beolvasast; = ha enter miatt allt meg a ciklus */ return enter; } int main(int argc, char *argv[]) { SDL_Color feher = {255, 255, 255}, fekete = { 0, 0, 0 }; SDL_Rect hely; Uint16 szoveg[100]; SDL_Event event; SDL_Surface *screen, *felirat; TTF_Font *font; int i; SDL_Init(SDL_INIT_VIDEO); SDL_WM_SetCaption("SDL szoveg bevitele", "SDL szoveg bevitele"); screen=SDL_SetVideoMode(480, 200, 0, SDL_ANYFORMAT); if (!screen) { fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); exit(1); } TTF_Init(); font = TTF_OpenFont("LiberationSerif-Regular.ttf", 32); if (!font) { fprintf(stderr, "Nem sikerult megnyitni a fontot! %s\n", TTF_GetError()); exit(2); } SDL_EnableKeyRepeat(500, 30); /* szoveg beolvasasa */ for (i=0; i<500; ++i) lineRGBA(screen, rand()%screen->w, rand()%screen->h, rand()%screen->w, rand()%screen->h, rand()%256, rand()%256, rand()%256, 64); SDL_Flip(screen); input_text(szoveg, 40, 80, 400, 40, fekete, feher, font, screen); /* szoveg kirajzolasa */ if (szoveg[0]!=0x0000) { boxRGBA(screen, 0, 0, screen->w, screen->h, 0, 0, 0, 255); for (i=0; i<100; ++i) filledCircleRGBA(screen, rand()%screen->w, rand()%screen->h, 20+rand()%5, rand()%256, rand()%256, rand()%256, 64); felirat = TTF_RenderUNICODE_Blended(font, szoveg, feher); hely.x = (screen->w - felirat->w)/2 + 2; hely.y = (screen->h - felirat->h)/2 + 2; SDL_BlitSurface(felirat, NULL, screen, &hely); SDL_FreeSurface(felirat); SDL_Flip(screen); while (SDL_WaitEvent(&event) && event.type!=SDL_QUIT) ; } TTF_CloseFont(font); SDL_Quit(); return 0; }
7 Többmodulos projektek és az SDL-es programok futtatása
Van két apróság, amit tudni kell az SDL-es projektek fordításáról és futtatásáról.
Az egyik a többmodulos projektekkel kapcsolatos. A Code::Blocks beépített SDL projekt varázslója egy
olyan projektet hoz létre, amelyben a fő forrásfájl neve main.cpp
, ami a
kiterjesztése miatt alapértelmezetten nem C, hanem C++ fordítóval fordul. Ha újabb modult adunk
hozzá, annak pedig .c
a kiterjesztése, azt a C fordító fogja kapni. Ugyan
megoldható, hogy a kettővel együtt dolgozzunk, de ennek technikai részletei túlmutatnak a
Prog1/Szoftlab1 tárgyakon. A több modulos programhoz ezért inkább az eredeti
main.cpp
-t töröljük ki, és adjunk a projekthez egy új, üres .c
-t,
amiben a főprogram lesz. (A fentebb letölthető InfoC alap SDL projekteknél nincs ilyen
probléma, azok .c
fájlból indulnak ki.)
A másik az SDL-es programok futtatása. Az SDL könyvtár lefordított függvényei nem
kerülnek be a mi programunk futtatható, .exe
fájljába, hanem külön fájlokban
vannak. A végleges linkelést nem a fordító végzi, hanem az elindított program „húzza be”
az indításakor a szükséges programrészeket. Ezt dinamikus linkelésnek (dynamic linking)
nevezzük. Ezek a függvénykönyvtárak a *.dll
fájlokban (dynamic link library)
vannak. Ha az SDL-es programot el szeretnénk indítani a Code::Blockson kívülről, akkor
vele egy mappába kell tenni az SDL *.dll
fájljait is. Ezek a fent letölthető
ZIP fájl bin
mappájában vannak. A nagyobb programok telepítőinek (installer)
egyébként éppen ez a feladata, hogy a program futásához szükséges dinamikusan betöltött
könyvtárakat is a megfelelő helyre másolják.
Fel szokott merülni az a kérdés is, hogyan kell olyan SDL-es programot csinálni, amelynek nem
nyílik külön konzol ablaka az indításkor. A Code::Blocks eltérő fordítási beállítások mellett tud
.exe
fájlt készíteni. Ha fent, a menüsor alatt a „Release” mód van kiválasztva a „Debug”
helyett, akkor olyan .exe
fájlt készít, amely nem nyit magának konzol ablakot. Az
elkészült .exe
a projekt mappájában, a \bin\Release
almappa alatt található.
Windowson alapvetően a grafikus programoknál nem szokás az, hogy írjanak a szabványos
kimenetükre. Ezért ott az SDL azt szokta csinálni, hogy bezárja a szabványos kimenetet,
és megnyit helyette egy stdout.txt
nevű fájlt, amibe ezután a printf()
-elt
szövegek kerülnek. (Ugyanez történik a szabványos hibakimenettel is.) Ezért hiába
printf()
-elünk az SDL programokban, nem fog az a konzol ablakban megjelenni,
még ha nyílt is hozzá. Ezt az SDL FAQ-ban is megemlítik,
itt. Ennek elkerülésére azt javasolják,
hogy újra kell nyitni a konzol ablakot. Ezt a main()
függvény elejére, de
az SDL_Init()
hívás után az alábbi módon lehet megtenni:
#ifdef __WIN32__ freopen("CON", "w", stdout); freopen("CON", "w", stderr); #endif
Ezután már lehet printf()
-elni. A __WIN32__
makrót egyébként
az SDL definiálja. Mivel az csak a windowsos fordításoknál létezik, a fenti négy sort nyugodtan
betehetjük a programba, platformfüggetlen marad.