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

Czirkos Zoltán · 2017.07.13.

Grafikus programozás az SDL multimédiás könyvtárral.

Az SDL egy platformfüggetlen multimédiás függvénykönyvtár. 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, miközben az egyes géptípusok, operációs rendszerek különbségeit elfedi. Így 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.

Ez az írás tartalmaz néhány olyan információt (pl. a többmodulos programokkal kapcsolatban), amelyek csak egy későbbi előadás után lesznek teljesen érthetőek. De addig is használhatóak az instrukciókat pontosan követve. Az SDL telepítéséről egy külön írásban olvashattok.

1. Az első program

SDL: grafikus primitívek

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 (később majd az SDL_INIT_TIMER is kell).

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 | SDL_INIT_TIMER);
    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, R, 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 felül minden függvénynek van egy nem RGBA-ra, hanem Color-ra végződő nevű párja: az utóbbiak a négy, színt megadó paraméter helyett csak egyetlen egyet várnak. Ez az egyetlen Uint32 típusú paraméter 0xRRGGBBAA formában tartalmazza a színkomponenseket és az átlátszóság információt. Tehát mind a vörös, zöld, kék komponensnek, mind az átlátszóságnak egyetlen egy bájt jut. Így egy szín egyetlen egy változóban is eltárolható. A 32 biten megadott színkód bitműveletekkel állítható elő (r<<24 | g<<16 | b<<8 | a). Például az alábbi sorok teljesen ekvivalensek, mindegyik félig átlátszó lila kört rajzol:

filledCircleRGBA(screen, 320, 240, 100, 255, 0, 255, 128);
filledCircleRGBA(screen, 320, 240, 100, 0xFF, 0, 0xFF, 0x80);
filledCircleColor(screen, 320, 240, 100, 0xFF00FF80);

A rajzoló 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

Események kezelése: egér

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 összegyűjti (billentyűzet, egérmozdulatok, időzítések, ablak bezárása), é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>
#include <stdbool.h>

int main(int argc, char *argv[]) {
    SDL_Event event;
    SDL_Surface *screen;

    /* SDL inicializálása és ablak megnyitása */
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
    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 */
    bool quit = false;
    bool click = false;
    int elozox = 0;
    int elozoy = 0;
    while (!quit) {
        SDL_WaitEvent(&event);

        bool rajzoltam = false;

        switch (event.type) {
            /* eger kattintas */
            case SDL_MOUSEBUTTONDOWN:
                if (event.button.button == SDL_BUTTON_LEFT) {
                    click = true;
                    elozox = event.button.x;
                    elozoy = event.button.y;
                }
                else if (event.button.button == SDL_BUTTON_RIGHT) {
                    boxColor(screen, 0, 0, 359, 359, 0x000000FF);
                    rajzoltam = true;
                }
                break;
            /* egergomb elengedese */
            case SDL_MOUSEBUTTONUP:
                if (event.button.button == SDL_BUTTON_LEFT) {
                    click = false;
                }
                break;
            /* eger mozdulat */
            case SDL_MOUSEMOTION:
                if (click) {
                    aalineColor(screen, elozox, elozoy,
                                event.motion.x, event.motion.y, 0xFFFFFFFF);
                    rajzoltam = true;
                }
                /* a kovetkezo mozdulat esemenyhez */
                elozox = event.motion.x;
                elozoy = event.motion.y;
                break;
            /* ablak bezarasa */
            case SDL_QUIT:
                quit = true;
                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

Pattogó labda

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 <stdbool.h>
#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;
    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");

    g.x = ABLAK/2;
    g.y = ABLAK/3;
    g.vx = 3;
    g.vy = 2;
    /* idozito hozzaadasa: 20 ms; 1000 ms / 20 ms -> 50 fps */
    id = SDL_AddTimer(20, idozit, NULL);
    /* szokasos esemenyhurok */
    bool quit = false;
    while (!quit) {
        SDL_WaitEvent(&event);

        switch (event.type) {
            /* felhasznaloi esemeny: ilyeneket general az idozito fuggveny */
            case SDL_USEREVENT:
                /* kitoroljuk az elozo poziciojabol (nagyjabol) */
                filledCircleColor(screen, g.x, g.y, GOLYO_R, 0x000000E0);
                /* kiszamitjuk az uj helyet */
                g.x += g.vx;
                g.y += g.vy;
                /* visszapattanás */
                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 */
                filledCircleColor(screen, g.x, g.y, GOLYO_R, 0x8080FFFF);
                SDL_Flip(screen);
                break;

            case SDL_QUIT:
                quit = true;
                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 konstanstól (ez egy felsorolt típusú érték) 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

Sakktábla

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

Sakk figurák
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 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 = { (melyik % 6) * 62 + 10, (melyik / 6) * 60 + 10, 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)
        boxColor(celkep, palyapos(x), palyapos(y), palyapos(x + 1) - 1, palyapos(y + 1) - 1, 0xCCAD99FF);
    else
        boxColor(celkep, palyapos(x), palyapos(y), palyapos(x + 1) - 1, palyapos(y + 1) - 1, 0xE6D1C3FF);

    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 */
    boxColor(celkep, 0, 0, celkep->w - 1, celkep->h - 1, 0x90E090FF);
    rectangleColor(celkep, palyapos(0) - 1, palyapos(0) - 1, palyapos(8), palyapos(8), 0x00000080);
    /* 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 | SDL_INIT_TIMER);
    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(1);
    }

    /* 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;
}
Fontos!

Néhány fontos dolog:

  • A betöltött képet, amíg nem dobtuk el az SDL_FreeSurface() függvénnyel, akárhányszor használhatjuk.
  • A képeket ide-oda adogathatjuk a programban: az SDL_Surface* típusú képhivatkozás akár függvény paramétere vagy visszatérési értéke is lehet.
  • Az SDL_BlitSurface() függvény az utolsó paraméterében megadott SDL_Rect téglalapba beírja, hogy a másolt kép mekkora területe látható az ablakban (ami kisebb lehet a kép teljes méreténél, ha kilóg abból). Ha egy SDL_Rect változót többször kell használni, akkor mindig újra w=0 és h=0 értékeket kell tenni a struktúrába.

5. Szövegek megjelenítése

Az SDL_gfx stringRGBA() és stringColor() 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 a hosszú ő é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 | SDL_INIT_TIMER);
    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(1);
    }

    /* felirat megrajzolasa */
    /* ha sajat kodban hasznalod, csinalj belole fuggvenyt! */
    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);

    /* ha sajat kodban hasznalod, csinalj belole fuggvenyt! */
    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);

    /* ha sajat kodban hasznalod, csinalj belole fuggvenyt! */
    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;
}
Fontos!

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. Ha többször is szeretnénk használni a betűtípust, nem szabad mindig törölni és újra betölteni azt, mivel nagyon időigényes az a művelet! A betöltött betűtípusokat hivatkozó TTF_Font* típusú mutatók a képekhez hasonlóan függvényeknek is átadhatjuk.

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 egy SDL_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. Kell neki 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>
#include <stdbool.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. */
bool 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 = 0;
    dest[hossz] = 0x0000;   /* lezaro 0 */
    SDL_EnableUNICODE(1);
    bool enter = false;
    bool kilep = false;
    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 = true;
                        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 = true;
                break;
        }
    }

    /* igaz 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_INIT_TIMER);
    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(1);
    }
    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) {
        boxColor(screen, 0, 0, screen->w, screen->h, 0x000000FF);
        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 pár apróság, amit tudni kell az SDL-es projektek fordításáról és futtatásáról.

Fontos!

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 tárgyon. Használjuk inkább az SDL telepítős írásból letölthető InfoC SDL projekt típust!

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 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 van is ilyen ablak. 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 (#ifdef-fel együtt!), platformfüggetlen marad.

Az SDL igényli azt is, hogy a main() függvénynek meglegyenek a paraméterei: int argc és char *argv[]. Enélkül a program lefordítása nem fog mindenhol sikerülni.

A parancssori argumentumokról és a többmodulos programokról egy külön előadáson beszélünk majd.