Tunnus:

Salasana:

Uusi käyttäjä

Haku

Pikalinkit

Kesähaaste 2010

Paranna Morpion-pelin kansainvälisiä ennätyksiä!

Ohjeet | Nettipeli | Tuloslista

Putkaposti

Suunnittele tiedosto, josta tulee suuri ZIP-paketti!

Vastauksia: 32
Paras: 1158

Tehtävään...

Keskustelu

Juu muutkin käy tietysti, mutta olisi tuttua ennestään MFC kirjastot ymm.. Jos niissä samat säännöt voisi (C / C++ ja Delphi / Pascal) lisää...


Oppaat: Peliohjelmointi C++:lla: matopeli

Kirjoittaja: Metabolix

Opassarja: [ 1 2 3 4 ]

Edellinen opasSeuraava opas

Tulostettava versio: tämä opas | koko opassarja

Osa 2 - Ohjelman perustoiminnot ja valikko

Ohjelman alku ja loppuKäyttäjän syöteToiminta ja piirtäminenTekstin piirtoTarvittavat esittelytKuvat ja kuvafunktiotAlku- ja loppufunktiotSyötteenlukufunktiotValikon piirtäminenValikon toimintaEsimerkkipelin tilanne

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.

Ohjelman alku ja loppu

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.

Käyttäjän syöte

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.

Toiminta ja piirtäminen

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.

Tekstin piirto

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.

Tarvittavat esittelyt

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

Kuvat ja kuvafunktiot

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. 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)) {
        // Yritetään optimoida.
        if (SDL_Surface *opti = SDL_DisplayFormat(tmp)) {
            // Tuhotaan alkuperäinen ja palautetaan optimoitu.
            SDL_FreeSurface(tmp);
            tmp = opti;
        }
        // Asetetaan läpinäkyvä väri (magenta eli pinkki).
        if (lapinakyva) {
            SDL_SetColorKey(tmp, SDL_SRCCOLORKEY, SDL_MapRGB(tmp->format, 255, 0, 255));
        }
        // 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);
}

Alku- ja loppufunktiot

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();
}

Syötteenlukufunktiot

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!

Valikon piirtäminen

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ä.
        // Tilan säästämiseksi numerot ovat eri tiedostossa,
        // joka sisältyy oppaan lopusta löytyvään pakettiin.
        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);
}

Valikon toiminta

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;
            }
        }
    }
}

Esimerkkipelin tilanne

(Lataa koodipaketti!)

Tässä vaiheessa ohjelma voisi näyttää tältä:
Kuva pelin valikosta

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::loppu()

Lauri Kenttä, 7.11.2009

Edellinen opasSeuraava opas


Anaatti [15.11.2009 17:43:11]LainaaMuokkaa
On kyllä hyvä opassarja. Opin tämän avulla optimoimaan noita Surfaceja ja johan nousi pelini fps sadasta tuhanteen. :)

ylläpito Antti Laaksonen, ulkoasu Otto Seiskari