Kirjautuminen

Haku

Tehtävät

Koodit: C++: Kuvien lataus ja säilöntä helposti (SDL)

Kirjoittaja: Metabolix

Kirjoitettu: 03.10.2009 – 03.10.2009

Tagit: grafiikka, koodi näytille, vinkki

Kuvien hallinta on etenkin aloittelijalle yksi peliohjelmoinnin pulma: ne pitää ladata ennen piirtämistä ja vapauttaa piirtämisen jälkeen, ja kuitenkaan niitä ei pitäisi latailla ja vapautella toistuvasti.

Seuraava koodilistaus sisältää yleisen cache-malliluokan, joka tukee nimetyn datan lisäämistä ja poistamista ja huolehtii siitä, että lopuksi data vapautetaan. Tästä luokasta kehitetään edelleen SDL:n kuvapinnoille suunniteltu versio, joka lataa puuttuvat kuvat tiedostoista.

Koodia on melkoisesti, eikä se varmasti ole helpompaa ymmärtää kuin yksinkertaiset kuvien lataukset ja vapautukset. Tämän luokan avulla monet asiat kuitenkin käyvät helpommin: Kuvat ladataan ensimmäisellä käyttökerralla ja vapautetaan automaattisesti ohjelman lopussa. Kuvia voi myös ladata etukäteen ja vapauttaa itse, kun tietää, ettei niitä enää tarvita. Varastoluokasta voi vapaasti tehdä useammankin yksilön; vaikkapa pelin valikon kuvat ja pelikentän kuvat voisi tallentaa eri varastoihin, jolloin jokaisen pelikentän lopussa on helppo vapauttaa pelikentän käyttämät kuvat ja antaa tilaa seuraavaa pelikenttää varten. Tällaisen varaston käyttö on myös yksi askel uudelleenkäytettävää koodia kohti: sama varastoluokka toimii helposti monessa ohjelmassa, ja kun kuvien lataukseen vaadittavat asiat on koottu yhteen paikkaan, latausmenetelmän muutokset (esimerkiksi tiedostojen salaus tai uusi tiedostomuoto) on helppo tehdä yhteen kohti.

Samasta pohjasta voi oikein hyvin tehdä luokka muunkinlaiselle datalle, vaikka äänille tai jonkin toisen grafiikkakirjaston kuville. Listauksen puolivälistä alkaa SDL-spesifinen osuus; muutettavia kohtia ei todellakaan ole monta!

Lopullinen luokka sisältää seuraavat funktiot:

metodikuvaus
gethakee kuvan säilöstä, lataa puuttuvan kuvan tiedostosta
findhakee kuvan säilöstä, ei lataa puuttuvaa
erasepoistaa kuvan
cleartyhjentää varaston
setlisää valmiin kuvan
commonpalauttaa staattisen varasto-olion

Luokka on suunniteltu niin, että se olisi toisaalta helppo aloittelijoille mutta toisaalta riittävän joustava vähän hienompaankin käyttöön. Esimerkkiohjelmassa ladataan taustakuva ja hiiri ja esitellään aivan keskeisimmät ominaisuudet: common-funktio valmiin varasto-olion hakuun, get-funktio kuvien hakuun sekä clear- ja erase-funktiot kuvien poistoon. Taitavammat osaavatkin itse lukea koodista näiden ja muiden funktioiden käyttötavat; esimerkiksi common-funktio ei ole tarpeen, jos haluaa luoda varasto-olionsa itse.

Esimerkkiohjelma vaatii kuvat tausta.bmp (640x480) ja hiiri.bmp (pienehkö kuva).

image_cache.hpp

Tässä on siis varsinainen vinkki. Tiedosto tarvitsee vain tallentaa projektiin ja liittää #include-rivillä muuhun koodiin.

// Otsikot pitää sisällyttää vain kerran.
// Tämä varmistetaan esikääntäjällä.
#ifndef IMAGE_CACHE_HPP
#define IMAGE_CACHE_HPP

// Käytetään joitakin standardikirjaston luokkia.
#include <string>
#include <map>
#include <stdexcept>

// Tehdään ensin yleinen cache-luokan malli, jossa ovat yleispätevät funktiot.
// T: säilöttävästä tyypistä luotu rakenne, jossa ovat staattiset jäsenet
//    - typedef  ???  type;       // säilöttävä tyyppi,
//    - void free(type& t);       // funktio arvon vapauttamiseen,
//    - type null();              // funktio, joka palauttaa tyhjän arvon,
//    - void null(type const& t); // funktio, joka tarkistaa, onko arvo tyhjä.
// K: avaimen tyyppi; oletuksena std::string
// C: tietorakenne tallennukseen; oletuksena std::map<K, T::type>.
template <typename T, typename K = std::string, typename C = std::map<K, typename T::type> > class cache {
	// Säilöjä ei kuulu kopioida, joten kopiomuodostin on yksityinen.
	cache(const cache& t) {
		throw std::logic_error("cache(const cache& t)");
	}
protected:
	// Tyypit: avain, säilöttävä objekti, tietorakenne,
	// tietorakenteen iteraattori sekä tämä luokka itse.
	typedef K key;
	typedef typename T::type object;
	typedef C container;
	typedef typename container::iterator iterator;
	typedef cache<T, K, C> cache_type;

	// Data aiemmin määrätyssä tietorakenteessa (map)
	container data;

	// Muodostin on tyhjä.
	cache() {}
public:
	// Jäsenen asetus: poistetaan vanha, lisätään uusi, jos se ei ole tyhjä.
	void set(const key& k, object t) {
		erase(k);
		if (T::is_null(t)) {
			data[k] = t;
		}
	}
	// Haku: etsitään ja palautetaan, jos löytyi; muuten palautetaan tyhjä.
	object find(const key& name) {
		iterator i = data.find(name);
		if (i == data.end()) {
			return T::null();
		}
		return i->second;
	}
	// Tyhjennys: vapautetaan ja poistetaan kaikki.
	void clear() {
		for (iterator i = data.begin(); i != data.end(); ++i) {
			T::free(i->second);
		}
		data.clear();
	}
	// Poisto nimen perusteella: etsitään, vapautetaan ja poistetaan.
	// Toisella parametrilla voi kieltää vapauttamisen, jos jostain syystä
	// haluaa vain ottaa arvon pois säilöstä ja huolehtia itse lopusta.
	bool erase(const std::string& key, bool free_data = true) {
		iterator i = data.find(key);
		if (i == data.end()) {
			return false;
		}
		if (free_data) {
			T::free(i->second);
		}
		data.erase(i);
		return true;
	}
	// Poisto datan perusteella: käydään listaa läpi, poistetaan osuma.
	// Toinen parametri sama kuin yllä.
	bool erase(object t, bool free_data = true) {
		for (iterator i = data.begin(); i != data.end(); ++i) {
			// Jos löytyi, vapautetaan ja poistetaan.
			if (i->second == t) {
				if (free_data) {
					T::free(i->second);
				}
				data.erase(i);
				return true;
			}
		}
		// Ei löytynyt.
		return false;
	}
	// Tuhoaja: tyhjennetään sisältö.
	~cache() {
		clear();
	}
};

// Varsinainen SDL-osuus alkaa tästä:

#include <SDL.h>
#ifdef USE_SDL_IMAGE
	// Tuki muillekin kuin BMP-kuville saadaan SDL_image-kirjastosta:
	#include <SDL_image.h>
#endif

// Toteutetaan SDL_Surface*-tyypille cache-mallin tarvitsema rakenne.
struct image_cache_data_type {
	// Tyyppi: SDL_Surface *.
	typedef SDL_Surface* type;

	// Vapautus: SDL_FreeSurface.
	static void free(type& t) {
		SDL_FreeSurface(t);
	}

	// Tyhjä arvo: 0-osoitin (NULL-osoitin).
	static type null() {
		return 0;
	}

	// Tyhjän tarkistus: vertailu nollaan.
	static bool is_null(type const& t) {
		return t == 0;
	}
};

// Tehdään mallista tämän tyypin avulla luokka kuville.
class image_cache: public cache<image_cache_data_type> {
	// Tätäkään ei kuulu kopioida, joten kopiomuodostin on yksityinen.
	image_cache(const image_cache& t): cache_type() {
		throw std::logic_error("image_cache(const image_cache& t)");
	}
public:
	// Muodostin on tyhjä.
	image_cache() {}

	// Latausfunktio: haetaan vanha kuva tai ladataan tiedostosta.
	SDL_Surface *get(const std::string& file) {
		// Etsitään vanhaa kuvaa, ja jos löytyi, palautetaan se.
		SDL_Surface *tmp = find(file);
		if (tmp) {
			return tmp;
		}
		// Ladataan kuva, ja jos epäonnistui, heitetään virhe.
		#ifdef USE_SDL_IMAGE
			// SDL_image-kirjastossa on tuki monelle formaatille.
			tmp = IMG_Load(file.c_str());
		#else
			tmp = SDL_LoadBMP(file.c_str());
		#endif
		if (!tmp) {
			throw std::runtime_error(SDL_GetError());
		}
		// Laitetaan kuva säilöön ja palautetaan se.
		data[file] = tmp;
		return tmp;
	}
	// Staattinen funktio, jossa on staattinen image_cache-olio.
	// Staattinen olio luodaan ohjelman alussa ja tuhotaan lopussa aivan
	// automaattisesti, ja olion tuhoajafunktiossa tuhotaan myös sen
	// sisältämät kuvat (~cache => clear => free => SDL_FreeSurface).
	// Funktio palauttaa viittauksen olioon, joten kaikki pääsevät
	// käyttämään samaa image_cache-oliota.
	static image_cache& common() {
		static image_cache c;
		return c;
	}
};

#endif

Esimerkkiohjelma

// Otetaan mukaan tämä image_cache-luokka.
#include "image_cache.hpp"

// Tarvitaan myös SDL:n otsikkotiedosto sekä pari standardikirjaston otsikkoa.
#include <SDL.h>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <iostream>

// SDL:n ruutu.
SDL_Surface *ruutu;

// Ohjelman toistuva toiminta.
bool ohjelma();

// Pääohjelma. Käytetään try-catch-rakennetta virheenkäsittelyyn.
int main(int argc, char **argv)
try {
	// SDL:n alustus ja tarvittaessa virheilmoitus.
	if (SDL_Init(SDL_INIT_VIDEO) != 0) {
		throw std::runtime_error(std::string("SDL_Init: ") + SDL_GetError());
	}

	// Automatisoidaan SDL_Quit-funktion kutsuminen.
	std::atexit(SDL_Quit);

	// Luodaan kaksoispuskuroitu ikkuna.
	if ((ruutu = SDL_SetVideoMode(640, 480, 32, SDL_DOUBLEBUF)) == 0) {
		throw std::runtime_error(std::string("SDL_SetVideoMode: ") + SDL_GetError());
	}

	// Piilotetaan kursori.
	SDL_ShowCursor(SDL_DISABLE);

	// Ladataan kuvat. Täältä lentää automaattisesti virheilmoitus, jos
	// kuvia ei jostain syystä saada auki. Lataus ei välttämättä olisi
	// tarpeen, koska kuvat haetaan myöhemminkin samalla get-metodilla
	// ja siis myös ladataan tarvittaessa tiedostosta.
	image_cache::common().get("tausta.bmp");
	image_cache::common().get("hiiri.bmp");

	// Ajetaan ohjelmaa, kunnes se palauttaa falsen.
	while (ohjelma() == true);

	// Palautetaan 0.
	// SDL_Quit kutsutaan automaattisesti ohjelman lopussa.
	return 0;

} catch (std::exception& e) {
	// Virhetilanteessa tulostetaan virhe ja palautetaan virhekoodi.
	std::cerr << e.what() << std::endl;
	return 1;
}

// Ohjelman toistuva toiminta.
bool ohjelma() {
	// Käsitellään viestit jonosta.
	SDL_Event event;
	while (SDL_PollEvent(&event)) {
		// Painettiinko ruksia?
		if (event.type == SDL_QUIT) {
			return false;
		}
		if (event.type == SDL_KEYDOWN) switch (event.key.keysym.sym) {
			// Escape ja Q lopettavat.
			case SDLK_ESCAPE:
			case 'q':
				return false;

			// H poistaa hiiren muistista, T taustan ja K kaikki.
			// Seuraavalla piirtokerralla ne siis ladataan taas.
			// Jos kuvia on levyllä muokattu, esiin tulevat uudet.
			case 'h':
				image_cache::common().erase("hiiri.bmp");
				break;
			case 't':
				image_cache::common().erase("tausta.bmp");
				break;
			case 'k':
				image_cache::common().clear();
				break;
			default: break;
		}
	}
	// Piirretään ruudulle tausta ja hiiri.
	// Kuvat on ladattu aluksi main-funktiossa, joten nyt ne vain haetaan
	// ohjelman yhteisestä image_cache-oliosta (image_cache::common()),
	// ellei muistia ole tyhjennetty (ks. yllä).
	SDL_BlitSurface(image_cache::common().get("tausta.bmp"), 0, ruutu, 0);

	if (SDL_Surface *hiiri = image_cache::common().get("hiiri.bmp")) {
		SDL_SetColorKey(hiiri, SDL_SRCCOLORKEY, SDL_MapRGB(hiiri->format, 0xff, 0xff, 0xff));
		int x, y;
		SDL_GetMouseState(&x, &y);
		// Piirretään kuvan keskikohta kursorin paikalle.
		SDL_Rect r = {x - hiiri->w / 2, y - hiiri->h / 2};
		SDL_BlitSurface(hiiri, 0, ruutu, &r);
	}

	SDL_Flip(ruutu);
	return true;
}

Kommentit

Teuro [17.10.2009 08:33:35]

#

Hyvä ja tarpeellinen vinkki. Riittävän monipuolisesti kommentoitu, jotta koodin tomintaa voi seurata hieman vähemmälläkin osaamisella.

Koodi123 [03.02.2019 13:42:17]

#

Hyvä vinkki.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta