Kirjoittaja: Metabolix (2009).
Opassarjan edellisessä osassa tehtiin suunnitelma ohjelman rakenteesta ja ohjelmoitiin pieni runko, joka tulosti tekstinä, mitä pitäisi tapahtua. Seuraava askel on järjestää tulosteiden tilalle jotain konkreettisempaa. Ensin kirjoitetaan funktiot kuvien lataukseen ja piirtoon sekä syötteen lukemiseen, sitten toteutetaan näiden avulla yksinkertainen valikko.
Kun ohjelma käynnistyy, sen pitää yleensä luoda ikkuna. Tässä pienessä matopelissä voidaan lisäksi ladata kaikki pelin tarvitsemat kuvat. Lopussa pitää tehdä samat asiat käänteisesti: vapautetaan kuvat ja suljetaan ikkuna. Nämä asiat kuuluvat funktioihin ohjelma::alku
ja ohjelma::loppu
.
Syötettä voi lukea kahdella tavalla. Joskus on olennaista tietää, mikä on tilanne nyt eli mitkä napit ovat pohjassa ja missä kohti hiiri on. Toisinaan taas on tärkeämpää tietää, mitä on tapahtunut eli onko käyttäjä painanut jotain nappia tai klikannut hiirellä.
Matopelissä on kaksi erilaista tilaa. Valikossa ei tapahdu itsestään mitään, vaan siellä odotetaan aina, että käyttäjä painaisi nappia. Tähän sopii siis funktio, joka osaa odottaa napinpainallusta. Itse pelissä sen sijaan pitää vain tarkistaa, onko nappi pohjassa tietyllä hetkellä, eli tarvitaan toinen funktio, joka ei pysähdy odottamaan.
Kun tapahtumia ei käsitellä vaan luetaan vain napin kulloinenkin tila, voi käydä niin, että varsinaiset tapahtumat jäävät odottamaan ja tulevatkin jonossa heti, kun palataan pelistä valikkoon ja toiseen syötteenlukutapaan. Tätä varten tarvitaan mahdollisesti funktio, joka tyhjentää tapahtumajonon; muuten valikko toimisi pelin jälkeen kummallisesti itsekseen.
Pelin toiminta ja tilanteen piirtäminen kannattaa pitää erillään. Jos piirtotoiminnot olisivat siellä täällä muun koodin seassa, olisi vaikeampi hahmottaa kummankaan osan kokonaisuutta; koodista myös tulee paljon selkeämpää, kun selvästi toisistaan riippumattomat asiat sijoitetaan eri funktioihin. Kun kaikki asiat piirretään kerralla yhdessä paikassa, on myös helppo tutkia ja muuttaa piirtojärjestystä ja jopa ohjelman ulkoasua ilman, että tarvitsee ollenkaan katsoa varsinaista toimintaa.
Esimerkkipelissä valikko piirretään funktiossa ohjelma::piirra_valikko
, jolloin kaikki grafiikkaan liittyvä pysyy siististi samassa nimiavaruudessa. Toinen mahdollisuus olisi tehdä funktio valikko::piirra
.
Esimerkkipelissä on vain muutama teksti, joten ne on tallennettu kokonaisina kuvina. Valmiiden kuvien hyvä puoli on, että jokaisesta tekstistä voi tehdä aivan omanlaisensa. Pistemäärä voi vaihdella, joten sitä varten täytyy keksiä muu tapa. Yksi mahdollisuus olisi tallentaa kuva jokaisesta numerosta ja koota niistä oikea luku. Tällä kertaa käytetään toista tapaa: numerot kootaan omenoista digitaalinäytön tapaan, ja valmiissa taulukossa on lueteltu kuhunkin numeroon tarvittavat omenat.
Seuraavaksi siirretään äskeinen teoria käytäntöön. Aivan ensiksi esitellään juuri keksityt funktiot: syötteen luku kahdella tavalla, syötepuskurin tyhjennys ja funktio valikon piirtoon. Lisäksi määritellään tunnukset pelin olennaisille näppäimille (oikea, vasen, Enter, Escape).
// ohjelma.hpp #ifndef _OHJELMA_HPP #define _OHJELMA_HPP #include "valikko.hpp" #include "peli.hpp" namespace ohjelma { // Funktiot ohjelman aloitukseen ja lopetukseen. void alku(); void loppu(); // Pelissä tarvittavat näppäimet; voitaisiin käyttää myös suoraan // esimerkiksi int-tyyppiä ja SDL.h:n näppäinkoodeja (SDLK_*). enum nappi { NAPPI_VASEN, NAPPI_OIKEA, NAPPI_ENTER, NAPPI_ESCAPE, NAPPI_MUU }; // Funktiot painalluksen odotukseen ja napin nykytilan selvitykseen // sekä vielä erikseen syötepuskurin tyhjennykseen. nappi odota_nappi(); bool lue_nappi(nappi n); void tyhjenna_syote(); // Funktio valikon piirtoon. Tämä voitaisiin toteuttaa aivan hyvin // valikon omassakin nimiavaruudessa, jolloin ohjelma-nimiavaruus // sisältäisi vain keskeiset ikkunan ja kuvien hallintaan tarvittavat // funktiot kuten lataa_kuva, piirra_kuva jne. void piirra_valikko(int pelin_tulos, valikko::valinta valittu); } #endif
Kuvia ja niiden käsittelyyn liittyviä funktioita ei ole tarkoitus käyttää suoraan toisista tiedostoista, joten niitä ei esitellä lainkaan otsikkotiedostossa vaan ne sijoitetaan suoraan tiedostoon ohjelma.cpp
.
// ohjelma.cpp #include <SDL.h> namespace ohjelma { // Staattisia, siis vain tämän tiedoston käyttöön. static SDL_Surface *ruutu; static void piirra_kuva(SDL_Surface *kuva, int x, int y, bool keskikohta = false); namespace kuvat { // Funktio kuvan lataukseen ja virheen heittämiseen. static SDL_Surface *lataa(const char *nimi, bool lapinakyva); // Kuvat. static SDL_Surface *tausta_valikko, *tausta_peli; static SDL_Surface *valikko_peli, *valikko_peli_valittu; static SDL_Surface *valikko_lopetus, *valikko_lopetus_valittu; static SDL_Surface *valikko_pistemaara; static SDL_Surface *omena, *matopallo, *reunapala; } }
Funktioiden toteutuksissa on tavallista SDL-asiaa, jonka pitäisi olla jo ennestään tuttua tai vähintäänkin helppo selvittää itse. Virhetilanteessa heitetään aina poikkeus.
// Lataa kuvan ja optimoi sen piirtoa varten. static SDL_Surface *ohjelma::kuvat::lataa(const char *nimi, bool lapinakyva) { // Jos lataus onnistuu... if (SDL_Surface *tmp = SDL_LoadBMP(nimi)) { // Asetetaan läpinäkyvä väri (magenta eli pinkki). if (lapinakyva) { SDL_SetColorKey(tmp, SDL_SRCCOLORKEY, SDL_MapRGB(tmp->format, 255, 0, 255)); } // Yritetään optimoida. if (SDL_Surface *opti = SDL_DisplayFormat(tmp)) { // Tuhotaan alkuperäinen ja palautetaan optimoitu. SDL_FreeSurface(tmp); tmp = opti; } // Palautetaan kuva. return tmp; } // Muuten heitetään virhe. throw std::runtime_error(SDL_GetError()); } // Piirtää yhden kuvan. static void ohjelma::piirra_kuva(SDL_Surface *kuva, int x, int y, bool keskikohta) { SDL_Rect r = {x, y}; if (keskikohta) { r.x -= kuva->w / 2; r.y -= kuva->h / 2; } SDL_BlitSurface(kuva, 0, ruutu, &r); }
Ohjelman alussa täytyy luoda ikkuna ja ladata kuvat, ja lopussa täytyy vastaavasti vapauttaa kuvat ja sulkea ikkuna. Virhetilanteissa heitetään taas C++:n poikkeuksia.
// Alustusfunktio. void ohjelma::alku() { std::clog << "ohjelma::alku()" << std::endl; // Alustetaan SDL tai heitetään virhe. if (SDL_Init(SDL_INIT_VIDEO) != 0) { throw std::runtime_error(SDL_GetError()); } // Avataan ikkuna tai heitetään virhe. ruutu = SDL_SetVideoMode(640, 480, 32, SDL_DOUBLEBUF); if (!ruutu) { throw std::runtime_error(SDL_GetError()); } // Asetetaan otsikko. SDL_WM_SetCaption("Matopeli", "Matopeli"); // Ladataan kuvat tai heitetään virhe. kuvat::tausta_valikko = kuvat::lataa("kuvat/tausta_valikko.bmp", false); kuvat::tausta_peli = kuvat::lataa("kuvat/tausta_peli.bmp", false); kuvat::valikko_peli = kuvat::lataa("kuvat/valikko_peli.bmp", true); // jne... } // Lopetusfunktio. void ohjelma::loppu() { std::clog << "ohjelma::loppu()" << std::endl; // Vapautetaan kuvat. SDL_FreeSurface(kuvat::tausta_valikko); SDL_FreeSurface(kuvat::tausta_peli); // jne... // Suljetaan SDL. SDL_Quit(); }
Enää yksi tärkeä osa-alue puuttuu, nimittäin syötteen luku. Kuten aiemmin todettiin, tarvitaan kaksi erilaista funktiota: yksi napin odottamiseen ja toinen napin tilan tarkistamiseen. Valikko käyttää näistä ensimmäistä, itse peli myöhemmin toista. Lisäksi tarvitaan funktio, jolla voi pyyhkiä napinpainallukset jonosta, koska pelkkä napin tilan tarkistus jättää painallukset yhä jonoon, jolloin pelin päättyessä valikko saisi vaivoikseen jo pelin aikana tapahtuneita painalluksia.
// Lukee seuraavan napinpainalluksen. ohjelma::nappi ohjelma::odota_nappi() { // Odotellaan, kunnes tulee napinpainallus. SDL_Event e; while (SDL_WaitEvent(&e)) { if (e.type != SDL_KEYDOWN) continue; switch (e.key.keysym.sym) { case SDLK_ESCAPE: return NAPPI_ESCAPE; case SDLK_RETURN: return NAPPI_ENTER; case SDLK_LEFT: return NAPPI_VASEN; case SDLK_RIGHT: return NAPPI_OIKEA; default: return NAPPI_MUU; } } // Jokin meni pieleen! throw std::runtime_error(SDL_GetError()); } // Kertoo napin nykytilan. bool ohjelma::lue_nappi(nappi n) { // Käsketään SDL:n hoitaa viestit, jolloin sen tieto napeista päivittyy. SDL_PumpEvents(); // Tarkistetaan pyydetty nappi. Uint8 *napit = SDL_GetKeyState(0); switch (n) { case NAPPI_VASEN: return napit[SDLK_LEFT]; case NAPPI_OIKEA: return napit[SDLK_RIGHT]; case NAPPI_ENTER: return napit[SDLK_RETURN]; case NAPPI_ESCAPE: return napit[SDLK_ESCAPE]; default: return false; } } // Tyhjentää syötepuskurin. void ohjelma::tyhjenna_syote() { SDL_Event e; while (SDL_PollEvent(&e)); }
Nyt ohjelman perustoiminnot ovat valmiit!
Jotta ohjelmaan saataisiin jotain näkyvää, toteutetaan valikon piirto. Ensiksi piirretään tausta ja tekstit, sitten viritellään pelin tulos paikalleen digitaalinumeroilla, ja lopuksi näytetään piirustus.
// Piirtää valikon. void ohjelma::piirra_valikko(int pelin_tulos, valikko::valinta valittu) { std::clog << "ohjelma::piirra_valikko(tulos, valittu)" << std::endl; // Valitaan oikeat kuvat. SDL_Surface *kuva_peli = kuvat::valikko_peli; SDL_Surface *kuva_lopetus = kuvat::valikko_lopetus; switch (valittu) { case valikko::PELI: kuva_peli = kuvat::valikko_peli_valittu; break; case valikko::LOPETUS: kuva_lopetus = kuvat::valikko_lopetus_valittu; break; } // Piirretään. piirra_kuva(kuvat::tausta_valikko, 0, 0); // Ensimmäisen tekstin vasemman yläkulman sijainti, (16, 16). int x = 16, y = 16; // Päivitetään y-koordinaattia joka tekstin jälkeen // niin, että tekstit asettuvat siististi allekkain. piirra_kuva(kuva_peli, x, y); y += kuva_peli->h; piirra_kuva(kuva_lopetus, x, y); y += kuva_lopetus->h; piirra_kuva(kuvat::valikko_pistemaara, x, y); y += kuvat::valikko_pistemaara->h; // Jaetaan pistemäärä numeroiksi ja käsitellään nolla fiksusti. int numerot[10], maara = 0; for (int i = pelin_tulos; i != 0; i /= 10) { numerot[maara] = i % 10; ++maara; } if (maara == 0) { numerot[0] = 0; ++maara; } // Tulostetaan teksti 5x5 pisteen (3x5 + välit) digitaalinumeroilla. for (int i = 0; i < maara; ++i) { // Taulukko digitaalinumeroiden pisteistä. const bool diginum[10][5][5] = { #include "numerot.inc" }; // Luvun numerot ovat taulukossa käänteisessä järjestyksessä. int n = numerot[maara - i - 1]; // Piirretään diginum-taulukon mukaan 5x5-ruudukkoon palloja. for (int iy = 0; iy < 5; ++iy) { // Oikea y-sijainti lasketaan pallon kohdasta ja alkukohdasta (y). const int y_paikka = y + (int)(iy * kuvat::omena->w); for (int ix = 0; ix < 5; ++ix) { if (!diginum[n][iy][ix]) continue; // Oikea x-sijainti lasketaan pallon kohdasta (ix) // ja merkin indeksistä (i) sekä alkukohdasta (x). const int x_paikka = x + (int) ((0.5 * ix + 4 * i) * kuvat::omena->w); piirra_kuva(kuvat::omena, x_paikka, y_paikka); } } } // Laitetaan piirustukset esille. SDL_Flip(ruutu); }
Valikossa täytyy aina piirtää tilanne ja odottaa sitten napinpainallusta. Kun näille toiminnoille on jo valmiit funktiot, itse valikkoa koskeva koodi ei ole kovin pitkä.
// valikko.cpp #include "valikko.hpp" #include "ohjelma.hpp" #include <iostream> valikko::valinta valikko::aja(int pelin_tulos) { std::clog << "valikko::aja(" << pelin_tulos << ")" << std::endl; // Valikon alkutilanne. valinta valittu = PELI; // Valikon silmukka. while (true) { // Piirretään valikon tilanne, odotetaan valintaa. ohjelma::piirra_valikko(pelin_tulos, valittu); ohjelma::nappi n = ohjelma::odota_nappi(); if (n == ohjelma::NAPPI_ENTER) { // Enter => lopetetaan valikko, palautetaan valittu. return valittu; } else if (n == ohjelma::NAPPI_ESCAPE) { // Escape => lopetetaan valikko, palautetaan LOPETUS. return LOPETUS; } else { // Muu nappi => vaihdetaan valintaa. if (valittu == PELI) { valittu = LOPETUS; } else { valittu = PELI; } } } }
Tässä vaiheessa ohjelma voisi näyttää tältä:
Ohjelma tulostaa yhä tietoja toiminnastaan. Tuloste voi olla vaikkapa tällainen:
ohjelma::alku() valikko::aja(0) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) peli::aja() TULOS = 7125 valikko::aja(7125) ohjelma::piirra_valikko(tulos, valittu) peli::aja() TULOS = 7125 valikko::aja(7125) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::piirra_valikko(tulos, valittu) ohjelma::loppu()
Lauri Kenttä, 7.11.2009
On kyllä hyvä opassarja. Opin tämän avulla optimoimaan noita Surfaceja ja johan nousi pelini fps sadasta tuhanteen. :)
Todella hyvä opassarja. Olen tehnyt jo tällä pelinkin.
oiskos mahdollista saada tästä päivitettyä versiota SDL2:lle? Yritin itse tehdä niin, mutta tuli hieman onkelmia tuon ruudun kanssa kun se toimii erillailla.
Eipä tuo ruudun käsittely hirveästi ohjelmaa muuta. Itse sijoitin SDL_Rendererin tuohon ohjelma-luokkaan ja passasin sen sitten viittauksena funktoihin, joissa sitä tarvitaan (esim tekstuurien lataus, piirto jne.).
juhis1234 kirjoitti:
oiskos mahdollista saada tästä päivitettyä versiota SDL2:lle?
Oppaan tarkoitus ei ole esitellä SDL:n käyttöä vaan peliohjelmoinnin periaatteita. SDL on valittu tähän yksinkertaisuutensa vuoksi. Opasta päivitetään, jos SDL ei ole enää yleisesti saatavilla. Erillistä versiota ei ole tulossa SDL2:lle eikä muillekaan kirjastoille (joita olisi kymmenittäin). Jos SDL2:n käyttö tuntuu hankalalta, sitä pitää opetella joka tapauksessa jostain muusta oppaasta.
Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.