Peliohjelmoinnin peruskäsitteet - Osa 2
Tämä toinen osa peliohjelmoinnin peruskäsitteet -kurssista jatkaa siihen mihin ensimmäisessä jäätiin. Siinä käsitellään tietorakenteita ja tiedon käsittelyä pelien kannalta. Tärkeitä perustietoja siis luvassa.
2.6.2002 julkaistun artikkelin on kirjoittanut jalaine.
Mitäs meillä nyt jo on kasassa? #
Niinpä. Tämä kakkososa on jo käytännössä loppu, mutta otetaanpa vielä lyhyt kertaus ennen seuraavaan lukuun jatkamista. Pohdimme tässä osassa siis tietorakenteita, joita ristinollapelissämme tulemme tarvitsemaan, ja päädyimme siihen, että pelilauta on taulukko ja pelaaja tietue. Lisäksi kokeilimme pientä tietueenkäsittelyohjelmaa. Seuraavassa osassa jatkamme pelin rakenteeseen, eli ruvetaan oikeasti ohjelmoimaan peliä, tai paremminkin pelin runkoa, joka kuitenkin jo toimii - jotenkin :) Kiitoksia seurasta ja kärsivällisyydestä, moi!
Jees. Olet nyt varmaankin lukenut ensimmäisen osan tästä peliohjelmointikurssista ja valmis ottamaan askelen eteenpäin. Jos et edellistä osaa ole lukenut, suosittelen siihen palaamista vielä ennen tämän tekstin lukemista. Nimittäin, vaikka se saattaakin tuntua paikoin tylsältä ja epäoleelliselta, on ykkösosan asioiden ymmärtäminen hyvän lopputuloksen kannalta tärkeää. Joka tapauksessa oletan, että olet sen lukenut tai muuten vaan ymmärrät suunnittelun perusasiat.
Tässä osassa mennään siis eteenpäin erittäin tärkeään asiaan. Vaikka vielä ei ainakaan ihan heti ruvetakaan koodaamaan, puhutaan paljon C-kielestä, ja ajatellaan jo toteutuksen kannalta äärimmäisen tärkeää asiaa: tietoa, ja sen tallennusta. Osasta ei luultavasti tule kovin pitkä, koska ristinollassa emme mitään kovin ihmeellisiä tietorakenteita käytä. Mutta mitä tässä enää jaarittelemaan, aloitetaan!
Tietoa ja tietorakenteita #
Eiköhän aluksi tehdä selväksi mitä tiedolla tässä yhteydessä tarkoitetaan. Tieto kun voi tarkoittaa niin montaa asiaa. Mietipä hetki itse, ja kirjaa vaikka paperille mieleesi tulevia ristinollassa käsiteltyjä tietoja! Tuliko listastasi jotain tämän näköistä?
- Pelilauta / -tilanne
- Pelaajat
- Pelimerkit
- Pisteet
Luultavasti alkuperäinen listasikin muistutti enemmän tätä toista listaa, vai kuinka? Mennään kohta tarkemmin siihen, että minkälaisia tietorakenteita voidaan käyttää näiden tietojen säilyttämiseen, mutta pohditaan ensin mitä nämä tiedot oikeastaan ovat.
Pelilautahan on ruudukko, jossa on joko 3x3 ruutua tai sitten enemmän. Koskapa ekassa osassa utelemiani ajatuksia pelilaudan koosta ei tullut, päätin sitten itse, että kyllä meille tässä vaiheessa riittää 3x3 -ruudukko. Miksi päädyin tähän tulokseen selviää seuraavassa luvussa. (Vähän jännitystä ;). Miksi laitoin pelilaudan ja pelitilanteen samalle riville? Tähän on ihan hyvä syy; pelilautahan ristinollassa kertoo pelitilanteen, koska pelilautaan merkitään missä kohti on kenenkin pelimerkki.
Sitten pelaajat. Jos pelaajien pelimerkkien paikat määritetään pelilaudassa, niin mitä hyötyä on pelaaja-tietotyypistä. Ei paljonkaan, mutta kuitenkin jotain. Onhan johonkin tallennettava pelaajan nimi, pelaajan pelimerkki (onko se x vai 0 vaiko joku muu?) ja muuta hauskaa tietoa pelaajasta.
Pelimerkit ja pisteet voidaan hylätä, koska ne sisältyvät jo edellisiin tietotyyppeihin eikä niitä sen vuoksi tarvitse erikseen olla - eikä ristinollassa muutenkaan lasketa pisteitä!
Pelilauta taulukkoon #
Kaikkein yleisin tietotyyppi heti muuttujan jälkeen on taulukko. Hmm.. Tiedät varmaan mikä taulukko on, mutta otetaanpa esimerkki:
+-+-+-+ | | | | +-+-+-+ | | | | +-+-+-+ | | | | +-+-+-+
Tuohan siis on 3x3 -taulukko, juuri sellainen mitä ristinollassa tulemme tarvitsemaan. Sen luominen C-kielellä tapahtuu seuraavasti:
int pelilauta[3][3];
Näin. Miksikö tein tuosta taulukosta int-tyyppisen? Okei.. Varsinkin, kun pelimerkit ovat kirjaimia, olisi voinut olla helppoa tehdä merkkitaulukko, jolloin taulukkoon olisi voinut tallentaa suoraan pelimerkin. Ajattelin kuitenkin, että voisi olla mielekkäämpää tallentaa taulukkoon aina sen pelaajan numero, kuka on pelimerkkinsä laittanut, ja sitten hakea pelimerkki pelaajatiedoista. Eli tähän tapaan:
+-+-+-+ |1|0|2| +-+-+-+ |2|2|0| +-+-+-+ |1|0|1| +-+-+-+
Kaikki tyhjät ruudut ovat siis nollia. Aika järkevää, eikö vain? Taulukoista ei nyt tämän enempää. Kunhan päästään toteutukseen, kerron tarkemmin kuinka taulukoita luetaan ja niihin sijoitetaan. Nyt riittää, kun ymmärrät miksi käytämme taulukkoa ja mikä taulukko oikeastaan edes on.
Monimutkaisempia tietorakenteita #
Okei. Pelilauta on siis taulukko, johon tallennetaan pelitilanne sitä mukaa, kun peli etenee. Mutta mikä on sitten pelaajan tietotyyppi? Mitenkäs olisi tietue? Jos tietue kuulostaa vieraalta, selitän sen nyt. Tietue (struct) on eräänlainen tietokokoelma, jonka sisään voidaan koota kaikenlaista yhteenkuuluvaa tietoa, tietotyypistä riippumatta. Mietitäänpä mitä tietoa pelaajasta haluamme tallentaa.
- Nimi
- Ikä :)
- Pelimerkki
- Pisteet
Esimerkiksi tuollaisia.. Tähän asti luultavasti olet tuollaisia tietoja käsitellyt ihan yksittäisinä muuttujina tai taulukoina, nimihän olisi tietysti merkkijonotaulukko:
char nimi[50];
Eikös vain? Ja ikä ihan tavallinen int-muuttuja. Tai jos haluttaisiin enemmän tarkkuutta, kävisi myös floatti tai vaikka double :). Samoin pisteet, mutta pisteitähän emme halunneet tallentaa, joten -- mitenkäs se tuonne taas pääsikään livahtamaan? ;-)
Entäs sitten Pelimerkki? Se voisi olla vaikka char-muuttuja johon tallennetaan x tai 0. Mutta koska emme kuitenkaan aio pelistä tekstipohjaista tehdä, olisi x:ien ja nollien kanssa temppuilu turhaa. Mitä merkkejä ristinollassa siis voi olla? Ristejä ja nollia! Vain kaksi vaihtoehtoa. Niinpä tehdään oma luettelotietotyyppi, kas näin:
typedef enum { RISTI, NOLLA } PELIMERKKI;Nyt voimme luoda PELIMERKKI-tyyppisiä muuttujia jotka voivat sisältää jommankumman noista arvoista RISTI tai NOLLA, esimerkiksi tällä tavoin:
PELIMERKKI nappula; nappula=RISTI;
Huomaatko mitä hyötyä tästä on? Jos et, älä huoli, se selviää myöhemmin, ja onhan toki muitakin yhtä hyviä tapoja, esimerkiksi yksinkertaista int-muuttujaa voi käyttää.
Mutta nyt siihen pääasiaan eli tietueihin. Meillä on tuossa kaikenlaista tietoa, mutta sitä on aika hankala käsitellä tuollaisina palasina, joten kootaanpa muuttujat yhteen, tietueeksi:
typedef enum { RISTI, NOLLA } PELIMERKKI;
typedef struct _PELAAJA
{
char nimi[50];
PELIMERKKI nappula;
} PELAAJA;
Tuossa on nyt pelaajan tiedot yhdessä nätissä kasassa. Samoin kuin PELIMERKKEJä voidaan nyt myös PELAAJia luoda:
PELAAJA pelaaja_1; strcpy(pelaaja_1.nimi,"Jarkko"); pelaaja_1.nappula = RISTI;
Näin yksinkertaisesti se toimii. Kun PELAAJA-tyyppinen tietue, pelaaja_1 (esim), on luotu, voidaan sen sisältämiin kohteisiin (muuttujiin yms.) viitata kirjoittamalla tietueen nimi, piste ja tietueen sisältämän muuttujan nimi. Niinkuin tuossa esimerkissäkin näkyy: pelaaja_1.nappula = RISTI;. Se on näin helppoa. Jos strcpy-funktio hämää, kerron siitä sen verran, että sehän on yksi C:n standardifunktioista, oma suosikkini ;). Se kopioi ensimmäisenä parametrina annettuun merkkijonoon, toisena parametrina annetun merkkijonon, eli tässä tapauksessa nimi-muuttujaan nimen "Jarkko"..
Kootaan palaset #
Jotta tämä ei kaikki jäisi ihan teorian tasolle, tehdäänpä nyt pieni ohjelma tietueita käyttäen. Ei vielä itse ristinollapeliä, mutta jotain mukavaa kuitenkin. Niin, no voihan tätäkin siinä pelissä sitten käyttää, eli sellainen systeemi, joka kyselee pelaajan nimeä ja tallentaa sen sitten pelaaja-tietueeseen. Niinkuin yleensäkin, tee ohjelma ensin itse ja vasta, kun se on valmis tai ei toimi, katso tästä. En ole jaksanut koodia sen kummemmin piilottaa, mutta älä silti kurki ;)
/* Esimerkkiohjelma 1
------------------
(C) Jarkko Laine 2000
*/
#include <stdio.h>
/* Määritellään PELIMERKKI-tietotyyppi */
typedef enum { RISTI, NOLLA } PELIMERKKI;
/* Määritellään PELAAJA-tietue */
typedef struct _PELAAJA
{
char nimi[40];
PELIMERKKI nappula;
} PELAAJA;
int main(void)
{
PELAAJA pelaaja1; /* Ykköspelaajan tiedot */
char merkkijono[100]; /* Merkkijono syötteiden lukemista varten */
/* Kysytään käyttäjältä nimeä */
printf("Mikä on nimesi? ");
/* Luetaan vastaus fgets() -funktiolla. Se toimii siten, että se lukee
ensimmäisenä parametrina annettuun merkkijonoon syötettä viimeisenä
annetusta tietovirrasta kunnes painetaan enteriä. Kuitenkin enintään
kakkosparametrin verran merkkejä. stdin on standardisyötevirta eli
normaalisti näppäimistö */
fgets(merkkijono,100,stdin);
/* Kopioidaan syötetty nimi pelaajan nimeksi */
strcpy(pelaaja1.nimi, merkkijono);
/* Ja tulostetaan kokeeksi käyttäjälle */
printf("Jaa-a. Nimesi on siis %s\n",pelaaja1.nimi);
/* Kysellään vielä pelimerkkiä */
printf("Haluatko käyttää (x) ristiä vai (0) nollaa? ");
/* Luetaan koko rivi, koska saattaahan joku hyvinkin kirjoittaa
pitkänkin rimpsun tekstiä :) */
fgets(merkkijono,100,stdin);
/* Tutkitaan kuitenkin vain ekaa merkkiä.. Tätä voisi hyvinkin laajentaa
paljon paremmaksikin, mutta eiköhän tämä tähän hätään riitä.. */
if(merkkijono[0] == \'x\') pelaaja1.nappula = RISTI;
else if(merkkijono[0] == \'0\') pelaaja1.nappula = NOLLA;
/* Näin, ja sitten vielä tarkistetaan että mikäs se oikein oli tuo
pelaajan valitsema nappula.. Switch-lauseella, ihan huvin vuoksi :) */
switch(pelaaja1.nappula)
{
case RISTI:
{
printf("Valitsit siis RISTIN.\n");
} break;
case NOLLA:
{
printf("Valitsit siis NOLLAN.\n");
break;
}
default: break;
}
return 0;
}
Semmoisen ohjelman kyhäsin minä.. Sinun ohjelmasi ei tarvitse olla samanlainen, tietoa kun voi hyvinkin kysellä myös muilla funktioilla kuin fgets():illä. Itse vain olen siihen mieltynyt :).
