Kirjautuminen

Haku

Tehtävät

Oppaat: Peliohjelmointi, C++-matopeli: Osa 1 - Välineet ja suunnitelma

  1. Osa 1 - Välineet ja suunnitelma
  2. Osa 2 - Ohjelman perustoiminnot ja valikko
  3. Osa 3 - Pelin raakaversio
  4. Osa 4 - Liike, törmäykset ja viimeistely

Kirjoittaja: Metabolix (2009).

C++ on suosittu kieli pelien tekemiseen, mutta harva C++-opas kertoo, miten niitä lopulta tehdään. Ei hätää: aivan tavalliset ehtolauseet, silmukat ja funktiot ovat hienoimpienkin pelien takana. Kuvat ladataan ja piirretään funktioilla, äänet soitetaan funktioilla, hahmojen sijainnit tallennetaan muuttujiin ja törmäykset lasketaan tavallisilla laskulausekkeilla. C++:n lisäksi tarvitaan matematiikkaa, logiikkaa ja suunnittelutaitoja.

Tämä opassarja käy läpi pelin ohjelmoinnilliseen suunnitteluun liittyviä perusasioita sekä pienen matopelin vaiheet pelkästä suunnitelmasta toimivaksi peliksi asti. Esimerkki on yksinkertainen, mutta pelinteko on parasta aloittaa yksinkertaisesta! Opassarjassa nähdään vain yksi tapa pelin ohjelmointiin. Hyviä lähestymistapoja on vaikka millä mitalla, ja niistä paras riippuu tekijän omista mieltymyksistä ja taidoista sekä projektin koosta ja muodosta. Nyt tarkoituksena on toteuttaa pieni peli siististi, lyhyesti ja C++-opassarjan alkupuolen tiedoilla. Myöhemmin on tarkoitus julkaista lisää oppaita, joissa tehdään toisenlaisia pelejä jossain määrin toisilla menetelmillä.

Opassarjan kunkin osan lopussa on linkki pelin lähdekoodeihin kyseisessä kehitysvaiheessa. Testauksessa käytettävät kuvat voi jokainen piirtää itse. Kärsimättömät voivat kuitenkin ladata jo nyt esimerkkipelin täyden lähdekoodin sekä valmiin Windows-version.

Valmis peli näyttää tältä:
Valikko Peli

Huomio: Jos levität pelin muokattua versiota, mainitse, mitkä osat ovat alkuperäisiä ja mitkä omiasi, ja anna lisäksi suora linkki tähän opassarjaan.

Ohjelmointikieli ja -tyyli

Pelejä voi ohjelmoida millä tahansa kielellä ja tyylillä, eikä mikään vaihtoehto ole kaikessa paras. Ennen tämän opassarjan lukemista täytyy tuntea ohjelmoinnin perusasiat kuten muuttujat, ehtolauseet, silmukat ja funktiot. Tämän opassarjan peli kirjoitetaan C++:lla, ja kaikki tarvittavat rakenteet käsitellään C++-opassarjan ensimmäisissä seitsemässä osassa. Jos jokin kohta koodissa ei aukea, on syytä palata C++-oppaiden ääreen ja selvittää vaikka kynän ja paperin kanssa, mitä koodissa tapahtuu.

Esimerkissä toiminnot on koottu tavallisiin funktioihin. Koodin pitäisi olla kohtalaisen helppo kääntää muillekin kielille, jotka tukevat vastaavaa ohjelmointityyliä. Olioita ei tässä oppaassa käytetä, mutta esitetyt periaatteet pätevät yhtä lailla myös olio-ohjelmointiin.

Kirjastot

Koska C++ itsessään ei sisällä funktioita esimerkiksi grafiikan piirtämiseen, siihen pitää käyttää erillistä kirjastoa. Ennen tämän opassarjan lukemista täytyy kyetä kääntämään ja ajamaan ohjelma, joka avaa ikkunan, lataa tiedostosta kuvan, piirtää kuvan tiettyyn kohtaan ruudulla ja reagoi jotenkin näppäinten painamiseen. Esimerkkipelissä käytetään SDL:ää, ja sen toimintaa ei paljonkaan selitellä; vaaditut asiat kerrotaan SDL-opassarjan ensimmäisessä ja toisessa osassa.

Esimerkkiohjelma on laadittu niin, että kirjaston vaihtaminen ei vaadi kuin muutaman muutoksen ohjelmassa.

Suunnittelu

Tärkeä asia peliä tehdessä on suunnitella etukäteen, mitä peliin kuuluu. Suunnitelmaan kuuluvat pelin käyttöliittymä (valikot, säädettävät asetukset), erikoisuudet (tuloslista), keskeiset ominaisuudet (moninpeli) sekä tietenkin itse pelin sisältö. Kaikkea ei varmasti voi tulla ajatelleeksi etukäteen, mutta mitä tarkempi suunnitelma on, sitä helpompi peli on lopulta toteuttaa, kun pysyy hyvin perillä siitä, mitä pelissä on ja mitä puuttuu sekä miten jokin päätös rajoittaa tai edistää puuttuvien asioiden toteutusta.

Suunnitelma on hyvä kirjoittaa muistiin, ja lisäksi kannattaa heti ohjelmoida jonkinlainen runko, johon ohjelman eri osat on myöhemmin helppo liittää. Tämän oppaan lopussa on yksinkertainen runko matopelille kokonaisuutena, ja kolmannessa osassa kootaan vastaava mutta vielä mutkikkaampi runko varsinaisen pelin kululle.

Toteutus

Toteutuksessa on hyvä edetä edes jossain määrin järjestelmällisesti pala kerrallaan. Asioille ei ole yhtä oikeaa järjestystä: voi aloittaa yhtä hyvin logiikasta kuin grafiikkafunktioista tai valikostakin. Mitään yksittäistä asiaa ei kannata hioa liian huolella loppuun asti, jos on vielä epävarmaa, onko se alkuunkaan sopiva pelin tarpeisiin. Jos nimittäin jokin tärkeä ominaisuus jääkin puuttumaan, se voi olla vaikeampi lisätä valmiiseen ja viimeisteltyyn koodiin kuin keskeneräiseen. Mutkikkaassa pelissä on varminta toteuttaa joka osasta jonkinlainen raakaversio, jotta näkee, että osat on mahdollista sovittaa järkevästi yhteen.

Kun pelin ohjelmointi etenee, tulee usein vastaan tilanteita, joissa jokin aiempi päätös osoittautuu huonoksi. Suurenkaan koodimäärän pyyhkimistä ei pidä pelätä, mutta toisaalta koko projektin tuhoaminen vie harvoin tehokkaasti eteenpäin. Joka tilanteessa täytyykin erikseen harkita, kuinka paljon työtä menee hukkaan koodin poistamisen myötä, kuinka paljon ylimääräistä työtä pitäisi tehdä, jotta aiemmat puutteet saisi paikattua muuten, ja kuinka paljon lopputulosta haittaa, jos huonon koodin jättää paikalleen ja tekeekin jonkin asian suunniteltua yksinkertaisemmin tai rumemmin.

Tämän opassarjan esimerkkipeli on yksinkertainen ja harrastelijaprojektiksi harvinaisen hyvin suunniteltu, ja lisäksi umpikujat ja muutokset on jätetty oppaasta pois, jotteivät lopputuloksen kannalta epäolennaiset asiat paisuttaisi opasta. Harva todellinen ohjelma päätyy ensimmäisellä yrittämällä lopulliseen muotoonsa!

Virheiden varalta

Virheet ovat ohjelmoijalle arkipäivää. Hyödyllisiä virheenetsintämenetelmiä ovat tavalliset tulostusmenetelmät kuten tulostus cout-virtaan sekä C++:n poikkeukset. Alempana esimerkkipelin koodissa on jo yksi hyvä niksi: main-funktio ei sisällä juuri mitään toiminnallisuutta, mutta siellä siepataan ja tulostetaan kaikki tavalliset C++:n virheilmoitukset. Usein odottamattomassa virhetilanteessa paras ratkaisu on heittää poikkeus, kuten esimerkissäkin tehdään. Tällöin ohjelman suoritus saadaan keskeytettyä, ja selkeä virheilmoitus on ongelman etsimisen kannalta paljon parempi kuin ohjelman yllättävä kaatuminen.

Jos käy niin, että ohjelmassa on kummallinen bugi, kaksi ratkaisua on ylitse muiden. Kaatumistilanteessa debuggerit osaavat usein kertoa, missä kohti ohjelma kaatuu, ja niillä voi tutkia myös muuten ohjelman toimintaa. Ohjelman kulusta ja muuttujien arvoista voi myös tulostaa tietoja ruudulle tai tiedostoon. Keskustelufoorumeiltakin saa helpommin apua, jos osaa valmiiksi kertoa, missä kohti koodia virhe tulee ja millaisia arvoja muuttujilla on virheen tapahtuessa.

Testidata

Pelissä mutkikkainta on ohjelmointi. Kuvat, äänet ja esimerkiksi pelitasot ovat toki tärkeitä, mutta kun mitään peliä ei ole vielä olemassa, ei kannata liikaa keskittyä taiteelliseen puoleen. Alkuvaiheissa ääninä voi käyttää vaikka käyttöjärjestelmän merkkiääniä ja kuvina palloja, laatikoita, rinkuloita tai tikku-ukkopiirroksia – mitä vain saa pikaisesti piirrettyä. Jos pelissä on erilaisia tasoja, testitason ei tarvitse olla hauska pelattava vaan hyvä testaukseen: sen pitää sisältää kaikki pelin erikoisuudet.

Matopelin suunnitelma

Opassarjassa ohjelmoidaan yksinkertainen matopeli yhdelle pelaajalle. Pelissä on varsinaisen pelitilan lisäksi yksinkertainen alkuvalikko.

Peli: Matoa ohjataan kahdella napilla: oikealle ja vasemmalle. Liikesuunnalle ei aseteta rajoituksia, vaan mato voi kiemurrella vapaasti. Madon pitäisi edetä jouhevasti eikä töksähdellen, ja lisäksi etenemiseen voisi lisätä aaltoilevan efektin, jottei madon liike olisi vain tylsää, tasaista liukumista. Jos mato törmää pelialueen reunaan tai omaan häntäänsä, peli päättyy. Pelikentällä on aina yksi omena. Kun mato syö omenan, omena katoaa ja jonnekin ilmestyy uusi. Aluksi mato on vain muutaman yksikön mittainen, ja jokainen omena kasvattaa matoa yksiköllä. Pelaajan pistemäärä on syötyjen omenoiden määrä.

Valikko: Peli käynnistyy suoraan valikkoon. Valikossa on kaksi valintaa: uusi peli ja lopetus. Lisäksi valikossa näytetään edellisen pelin loppupistemäärä. Valintaa vaihdetaan näppäimillä, Enter vahvistaa valinnan ja Escape sulkee pelin.

Grafiikat: Mato muodostuu peräkkäisistä palloista, siihen tarvitaan yksi kuva. Tarvitaan myös omenan kuva sekä laatta, josta kootaan pelialueen reunat. Valikkoon ja peliin tarvitaan taustakuvat. Valikon valinnat vaativat molemmat kaksi kuvaa, tavallisen ja valitun. Lisäksi tarvitaan teksti "pistemäärä". Itse pistemäärän voi tulostaa digitaalinumeroilla, jotka muodostetaan omenoista.

Ohjelman runko

Suunnitelman mukaan voidaan toteuttaa yksinkertainen runko pelille. Tässä vaiheessa funktiot eivät tee juuri mitään: valikosta "valitaan" automaattisesti ensin peli ja toisella kertaa lopetus, ja peli vain palauttaa ennalta määrätyn pistemäärän. Pelin runko (main.cpp) sen sijaan saadaan aika hyvään vaiheeseen: alussa luodaan ikkuna ja ladataan kuvat (ohjelma::alku), sitten ajetaan valikkoa ja peliä, ja lopussa suljetaan ikkuna ja vapautetaan kuvat (ohjelma::loppu).

Ohjelma tulostaa toiminnastaan viestejä std::clog-virtaan, joka nimensä mukaan on tarkoitettu lokitulosteille. Virhetilanteissa ohjelma tuottaa standardikirjaston poikkeuksia, jotka otetaan kiinni main-funktiossa.

(Lataa koodipaketti!)

// ohjelma.hpp
#ifndef _OHJELMA_HPP
#define _OHJELMA_HPP

namespace ohjelma {
	// Funktiot ohjelman aloitukseen ja lopetukseen.
	void alku();
	void loppu();
}

#endif
// ohjelma.cpp
#include "ohjelma.hpp"
#include <iostream>

void ohjelma::alku() {
	std::clog << "ohjelma::alku()" << std::endl;
}

void ohjelma::loppu() {
	std::clog << "ohjelma::loppu()" << std::endl;
}
// valikko.hpp
#ifndef _VALIKKO_HPP
#define _VALIKKO_HPP

namespace valikko {
	enum valinta {
		LOPETUS, PELI
	};
	// Valikon pääfunktio; tarvitsee pelin tuloksen, palauttaa valinnan.
	valinta aja(int pelin_tulos);
}

#endif
// 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;
	static bool pelattu;
	if (!pelattu) {
		pelattu = true;
		std::clog << ">> PELI" << std::endl;
		return PELI;
	}
	std::clog << ">> LOPETUS" << std::endl;
	return LOPETUS;
}
// peli.hpp
#ifndef _PELI_HPP
#define _PELI_HPP

namespace peli {
	// Pelin pääfunktio; palauttaa pistemäärän.
	int aja();
}

#endif
// peli.cpp
#include "peli.hpp"
#include "ohjelma.hpp"
#include <iostream>

int peli::aja() {
	std::clog << "peli::aja()" << std::endl;
	std::clog << "TULOS = 7125" << std::endl;
	return 7125;
}
// main.cpp
#include "ohjelma.hpp"
#include "valikko.hpp"
#include "peli.hpp"
#include <iostream>
#include <stdexcept>

static bool silmukka() {
	static int pelin_tulos;
	switch (valikko::aja(pelin_tulos)) {
		case valikko::PELI:
			pelin_tulos = peli::aja();
			return true;
		case valikko::LOPETUS:
			return false;
	}
	throw std::logic_error("Virheellinen tilanne valikossa!");
}

int main()
try {
	ohjelma::alku();
	while (silmukka());
	ohjelma::loppu();
	return 0;

} catch (std::exception& e) {
	std::cout << "Pieleen meni!" << std::endl;
	std::cout << e.what() << std::endl;
	return 1;
}

Pelin voi jo kääntää ja ajaa, ja tulosteesta nähdään, että liikkuminen pelin ja valikon välillä toimii suunnitelman mukaan:

ohjelma::alku()
valikko::aja(0)
>> PELI
peli::aja()
TULOS = 7125
valikko::aja(7125)
>> LOPETUS
ohjelma::loppu()

Lauri Kenttä, 7.11.2009


Kommentit

Hengilö [16.03.2013 18:06:35]

#

Mikä tuo "std::clog" on?

Metabolix [16.03.2013 18:48:47]

#

Hengilö: Kuten oppaassa lukee, std::clog-virta on tarkoitettu lokitulosteille. Se on samanlainen virta kuin std::cout (virta tavallisille tulosteille) ja std::cerr (virta virheilmoituksille).

Pascalpoika [05.01.2016 12:02:41]

#

Paras!!!! Mun ennätys on 47.

juhis1234 [18.02.2017 12:30:58]

#

en saa käännettyä tuota koodia. kokeilin ladattuakin versiota, sama juttu:

$ g++ main.cpp

/tmp/ccciANHE.o: In function `silmukka()':
main.cpp:(.text+0x10): undefined reference to `valikko::aja(int)'
main.cpp:(.text+0x1e): undefined reference to `peli::aja()'
/tmp/ccciANHE.o: In function `main':
main.cpp:(.text+0x87): undefined reference to `ohjelma::alku()'
main.cpp:(.text+0x97): undefined reference to `ohjelma::loppu()'
collect2: error: ld returned 1 exit status

Metabolix [19.02.2017 10:18:26]

#

juhis1234, sinun pitää kääntää kaikki cpp-tiedostot. Voit kääntää yhdellä komennolla (g++ *.cpp), tai voit kääntää yksitellen ja linkittää jälkikäteen. Lue koodivinkki usean tiedoston käytöstä.

juhis1234 [19.02.2017 12:52:33]

#

Kiitos Metabolix. Kas kun en tuota hoksannut. Visual Studio tekee hommat ilmeisesti automaattisesti, mutta jos konsolista (etenkin linuxin puolella tulee käytettyä) haluaa ajaa niin pitää jokainen kääntää itse. Pythonilla olin jo tottunut siihen että tekee kaiken tarvittavan taustalla kunhan on importattu paketit koodiin.

Onko tutoriaalille tehty omaa foorumi sivua? En haluaisi turhaan tehdä uutta. Nyt ei tosin ole tarvetta enää, mutta ehkä jatkossa kysymyksille.

Kirjoita kommentti

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.

Muista lukea kirjoitusohjeet.
Tietoa sivustosta