Kirjautuminen

Haku

Tehtävät

Oppaat: Peliohjelmointi C++:lla: matopeli: Osa 4 - Liike, törmäykset ja viimeistely

  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. Vuosi: 2009.

Peli toimii jo – mitään ei vain tapahdu. Jäljellä on vain muutama pieni asia: liikkuminen, törmäystarkistukset ja omenan arpominen. Nyt peliä voi onneksi testata jokaisen vaiheen jälkeen, jotta näkee, mikä toteutetun funktion lopputulos on. Lopuksi peli täytyy testata ja viimeistellä julkaisukuntoon.

Tässä oppaassa käytetään jonkin verran matematiikkaa, joka ei nykyään kuulu peruskoulun oppimäärään. Laskuissa tarvitaan Pythagoraan lausetta, neliöjuurta ja trigonometrisia funktioita. Jos nämä eivät ole vielä tuttuja tai ovat jo unohtuneet, erinomainen apu löytyy matematiikkaopassarjan 2. osasta, jossa käydään läpi perustiedot kulmista ja kolmioista.

Asetuksia

Kun tutkitaan madon törmäystä seiniin ja omenoihin, täytyy tietää, kuinka suuria nämä kappaleet ovat. Pyöreän kappaleen tapauksessa koko on selkeintä ilmaista säteen avulla.

Liikkeeseen liittyy kaksi asiaa: kääntyminen ja eteneminen. Määritellään kääntymis- ja etenemisnopeudet vakioilla. Nopeus ilmaistaan kahtena arvona: toinen määrää peräkkäisten jaokkeiden välimatkan, toinen kertoo, montako kertaa tämän matkan mato liikkuu sekunnissa.

// peli.cpp; paikallisia vakioita
namespace peli {
	// Jaokkeen ja omenan säteet, puoli yksikköä kummatkin.
	static const float jaokkeen_sade = 0.5, omenan_sade = 0.5;

	// Madon kääntymisnopeus radiaaneina sekunnissa, puoli kierrosta.
	static const float pii = 3.14159265358979323846;
	static const float kaannosnopeus = pii;

	// Madon nopeus: 0,65 madonleveyttä 2,7 kertaa sekunnissa.
	// Nopeus ilmoitetaan kahtena lukuna, koska etenemä kertoo samalla
	// peräkkäisten jaokkeiden etäisyyden toisistaan.
	// (Mato on siistimpi, kun jaokkeet menevät hieman limittäin.)
	static const float vaihenopeus = 2.7;
	static const float etenema = 0.65 * 2 * jaokkeen_sade;

	// Mateluefektin heilahdus suhteessa madon etenemään matkaan.
	static const float matelun_heitto = 0.07;
}

Liikkuminen

Liikkumisfunktion alussa muutetaan madon kulkusuuntaa käännösnopeuden mukaan; nopeus kerrotaan vielä aika-askeleen pituudella, jotta kääntymisnopeudesta saadaan käännytty kulma (X rad / s · Y s = XY rad). Samalla tavalla lasketaan myös madon etenemisvaiheen muutos, mutta tähän eivät vaikuta käyttäjän napinpainallukset. Jos etenemisvaihe ylittää ykkösen, siirretään kaikki madon jaokkeet yhtä edemmäs ja lasketaan nenälle uusi sijainti trigonometristen funktioiden avulla. Jotta liikkeestä tulisi tasaista, siirretään vielä lopuksi jaokkeita jäljelle jääneen vaiheen perusteella hieman tulevaa sijaintiaan kohti. Samalla lisätään matoon hieman aaltoilua, jottei eteneminen olisi liian yksitoikkoista.

// Tarvitaan matematiikkafunktiot esittelevä otsikkotiedosto peli.cpp:n alkuun.
#include <cmath>

// Liikuttaa matoa.
void peli::liikuta(mato& m, bool oikealle, bool vasemmalle) {
	// Käännetään matoa, jos toinen (ja vain toinen) napeista on pohjassa.
	if (oikealle && !vasemmalle) {
		m.suunta += ajan_muutos * kaannosnopeus;
	} else if (vasemmalle && !oikealle) {
		m.suunta -= ajan_muutos * kaannosnopeus;
	}
	// Muutetaan madon vaihetta ja käsitellään kokonaiset etenemiset.
	m.vaihe += ajan_muutos * vaihenopeus;
	while (m.vaihe >= 1) {
		m.vaihe -= 1;

		// Siirretään jokainen jaoke seuraavan kohdalle,
		// paitsi nenälle lasketaan uusi sijainti suunnan perusteella.
		for (mato::jaoke *j = m.hanta; j != m.nena; j = j->nenampi) {
			j->x0 = j->nenampi->x0;
			j->y0 = j->nenampi->y0;
		}
		m.nena->x0 += std::cos(m.suunta) * etenema;
		m.nena->y0 += std::sin(m.suunta) * etenema;
	}

	// Asetellaan jaokkeiden koordinaatit (x, y) sulavasti.
	// Tasaisessa liikkeessä riittäisi, että asetetaan jaokkeet vaiheen
	// mukaan oman perussijaintinsa ja seuraavan jaokkeen perussijainnin
	// välille. Nenän sijainti lasketaan madon suunnan perusteella.
	m.nena->x = m.nena->x0 + std::cos(m.suunta) * etenema * m.vaihe;
	m.nena->y = m.nena->y0 + std::sin(m.suunta) * etenema * m.vaihe;

	// Jotta mato aaltoilisi hieman, lisätään taaempien jaokkeiden
	// etenemiseen sinifunktion mukaan hieman sivusuuntaista heilahdusta.
	// Indeksin i avulla saadaan joka toinen jaoke liikkumaan eri puolelle.
	int i = 0;
	for (mato::jaoke *j = m.hanta; j != m.nena; j = j->nenampi) {
		++i;
		const float dx = j->nenampi->x0 - j->x0;
		const float dy = j->nenampi->y0 - j->y0;
		const float heitto = matelun_heitto * std::sin((2 * m.vaihe + i) * pii);
		j->x = j->x0 + m.vaihe * dx - heitto * dy;
		j->y = j->y0 + m.vaihe * dy + heitto * dx;
	}
}

Nyt mato liikkuu! Ruudulta pääsee ulos, koska törmäystarkistus puuttuu, ja omenoitakaan ei voi vielä syödä. Jatketaan siis näistä asioista.

Syöminen

Mato syö etupäällään. Koska madon jaoke on pyöreä ja omena on pyöreä, törmäys on helppo tarkistaa laskemalla ympyröiden keskipisteiden välinen etäisyys ja vertaamalla sitä säteiden summaan. Etäisyys selviää Pythagoraan lauseen avulla. Lisätään ensin apufunktio, joka laskee etäisyyden, ja käytetään sitten tätä tarkistuksessa.

namespace peli {
	// Apufunktio etäisyyden laskemiseen Pythagoraan lauseen avulla.
	static inline float etaisyys(float x0, float y0, float x1, float y1) {
		float dx = x1 - x0, dy = y1 - y0;
		return std::sqrt(dx * dx + dy * dy);
	}
}
// Tarkistaa osumisen omenaan.
bool peli::osuu_omenaan(mato const& m, omena const& o) {
	// Tarkistetaan nenän etäisyys omenasta.
	if (etaisyys(m.nena->x, m.nena->y, o.x, o.y) < jaokkeen_sade + omenan_sade) {
		return true;
	}
	return false;
}

Nyt törmäykset omenoihin havaitaan oikein, joten mato pystyy syömään. Kasvaminen on jo toteutettu, joten pelihän on melkein valmis! Yksi pieni pulma tässä omenansyönnissä kuitenkin on: koska omenat ilmestyvät aina samaan kohti, mato onnistuu syömään omenaa yhdellä kertaa melkoisesti; uusia jaokkeita voi tulla useita kymmeniä! Seuraavaksi voidaankin jo korjata tämä asia.

Omenan arpominen

Omenan sijoittelu on matematiikkaa: arvotaan std::rand-funktiolla kaksi lukua ja skaalataan ne oikealle lukuvälille, jotta omena on mukavasti pelialueella. Äskeisessä vaiheessa ilmestyneestä bugista voi kuitenkin ottaa hieman opikseen: lisätään arvontaan pieni tarkistus, ettei omena ilmestyisi madon pään kohdalle. Jos näin uhkaa käydä, siirretään omenaa hieman kohti pelialueen vasenta yläkulmaa tai oikeaa alakulmaa sen mukaan, kumpi on kauempana.

// Arpoo omenan.
peli::omena peli::arvo_omena(const mato& m) {
	peli::omena o;
	// RAND_MAX on rand-funktion suurin mahdollinen arvo, joten sen avulla
	// voidaan skaalata arvonnan tulos välille [0, 1]. Tästä arvottu luku
	// on sitten helppo muuttaa muulle lukualueelle. Nyt sopiva lukualue
	// on sellainen, että omena jää ainakin hieman irti reunoista, jottei
	// sen syöminen olisi nurkastakaan liian vaikeaa.
	// Luku [0, 1] täytyy siis kertoa alueen koolla, ja siihen täytyy
	// lisätä alueen pienin koordinaatti.
	o.x = (float) std::rand() / RAND_MAX * (pelialue.x1 - pelialue.x0 - omenan_sade * 2) + pelialue.x0 + omenan_sade;
	o.y = (float) std::rand() / RAND_MAX * (pelialue.y1 - pelialue.y0 - omenan_sade * 2) + pelialue.y0 + omenan_sade;

	// Jos omena on liian lähellä madon päätä, siirretään se kauemmas.
	if (etaisyys(m.nena->x, m.nena->y, o.x, o.y) < 2 * (jaokkeen_sade + omenan_sade)) {
		// Valitaan kahdesta pelialueen kulmasta kauempi ja siirretään
		// omena puolet matkasta sinne päin.
		if (etaisyys(o.x, o.y, pelialue.x0, pelialue.y0) > etaisyys(o.x, o.y, pelialue.x1, pelialue.y1)) {
			o.x = (o.x + pelialue.x0) / 2;
			o.y = (o.y + pelialue.y0) / 2;
		} else {
			o.x = (o.x + pelialue.x1) / 2;
			o.y = (o.y + pelialue.y1) / 2;
		}
	}
	return o;
}

Törmääminen

Mato voi törmätä seinään tai itseensä. Riittää, että tarkistetaan nenän törmääminen, koska sehän on madon liikkuva osa. Seinän suhteen tarkistetaan yksinkertaisesti x- ja y-koordinaattien pysyminen rajoissa. Madon itsensä suhteen asia on hieman mutkikkaampi: itse tarkistus onnistuu kyllä samalla tavalla kuin omenan kanssa, mutta mitkä jaokkeet pitää tarkistaa? Nenästä seuraava jaoke voi olla liian lähellä nenää ja aiheuttaa valetörmäyksen, ja jos asetuksia (madon etenemistä) säädettäisiin, seuraavakin jaoke voisi tulla liian lähelle. Onneksi ongelmaan on melko yksinkertainen ratkaisu: käydään jaokkeita läpi nenästä lähtien, kunnes löytyy jaoke, joka on tarpeeksi kaukana, ja tarkistetaan törmäys vasta tästä häntään päin olevista jaokkeista.

// Tarkistaa madon törmäyksen itseensä ja reunoihin.
bool peli::tormaa(mato const& m) {
	// Tarkistetaan nenän etäisyys kustakin reunasta.
	if (m.nena->x < pelialue.x0 + jaokkeen_sade) return true;
	if (m.nena->y < pelialue.y0 + jaokkeen_sade) return true;
	if (m.nena->x > pelialue.x1 - jaokkeen_sade) return true;
	if (m.nena->y > pelialue.y1 - jaokkeen_sade) return true;

	// Ei lasketa näennäistä "törmäystä" omiin jaokkeisiin, jotka ovat
	// lähellä heti nenän takana. Pidetään muuttujassa tieto, onko jo
	// löydetty jaoke, joka on tarpeeksi kaukana; törmäys lasketaan vain,
	// jos on jo löytynyt tarpeeksi kaukana oleva jaoke.
	bool lahella_nenaa = true;
	for (mato::jaoke *j = m.nena->hannampi; j != 0; j = j->hannampi) {
		// Jos jaoke on kaukana, muutetaan muuttujan arvoa, muuten
		// tarkistetaan muuttujasta, kuuluuko törmäys huomioida.
		if (etaisyys(j->x, j->y, m.nena->x, m.nena->y) > 2 * jaokkeen_sade) {
			// Ei törmätty tähän jaokkeeseen, joten tämä on tarpeeksi kaukana.
			lahella_nenaa = false;
		} else if (!lahella_nenaa) {
			// Törmätään oikeasti vain, jos on jo löytynyt kauempi jaoke.
			return true;
		}
	}
	return false;
}

Viimeistely

Peli toimii jo hienosti. Nyt on viime hetki testata tarkkaan, onko siinä bugeja. Pitää siis kokeilla törmäämistä jokaiseen seinään ja omaan häntään, pitää kierrellä omenoita, pitää painella nappeja valikossa ja pelin jälkeen ja tarkistaa, että kaikki toimii järkevästi. Samalla voi säätää vielä madon etenemis- ja kääntymisnopeutta ja muita pelin asetuksia. Lisäksi täytyy piirtää lopulliset versiot kuvista.

Ohjelmasta voi halutessaan myös poistaa ylimääräiset tulostukset; nythän se ilmoittelee std::clog-virtaan valikon piirrosta, pelin tuloksesta ja muista kohdista. Tulosteita on kuitenkin sen verran vähän, etteivät ne haittaa, ja moni käyttäjä ei edes käynnistä ohjelmaa komentoriviltä eikä siis näe ylimääräisiä tekstejä.

Lopputulos

(Lataa koodipaketti!)

Viimeistelty peli voisi näyttää tältä:
Valikko Peli

Pelistä on tarjolla valmis, kokonainen Windows-versio.

Kokonainen peli C++:lla on nyt valmis! Jos tuntuu, että jokin ominaisuus vielä puuttuu tai voisi toimia toisin, on oikein hyvää harjoitusta kehittää peliä eteenpäin. Mahdollisia parannuksia ovat mm. ennätyksen tallentaminen tiedostoon ja taustamusiikin ja ääniefektien soittaminen. Linkkejä paranneltuihin versioihin voi jättää vaikka tämän oppaan kommentteihin.

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.

Lauri Kenttä, 7.11.2009
Grafiikanteossa mukana Laura Leinonen


Kommentit

ErroR++ [14.06.2011 15:25:06]

Lainaa #

Jaa, oli hyvä opassarja.

python61 [10.09.2013 13:48:29]

Lainaa #

ErroR++ kirjoitti:

Jaa, oli hyvä opassarja.

Niin.

Karhukoodari [23.12.2017 13:46:01]

Lainaa #

Hyvä opas oli, mutta en oikeastaan älynnyt millä funktioilla kuvat oikeastaan piirretään ja miten ne toimivat?

Metabolix [24.12.2017 00:11:25]

Lainaa #

Karhukoodari: Tässä oppaassa esitellään pelin suunnittelemista. Oppaan ei ole tarkoitus opettaa piirtämistä, vaan se pitäisi jo osata. Projektin piirtofunktiot on esitelty lyhyesti 2. oppaassa, ja jos niissä on jotain epäselvää, kannattaa siirtyä lukemaan SDL-oppaasta noita perusasioita.

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 keskustelun ohjeet.
Tietoa sivustosta