Kirjautuminen

Haku

Tehtävät

Oppaat: SDL2: Osa 2 - Syöte ja tapahtumat

  1. Osa 1 - Käyttöönotto ja grafiikka
  2. Osa 2 - Syöte ja tapahtumat

Kirjoittaja: Metabolix. Vuosi: 2013.

Syöte (engl. input) tarkoittaa ohjelman ulkopuolelta tulevaa tietoa. SDL:n tapauksessa syöte tarkoittaa erityisesti sitä, mitä käyttäjä tekee hiirellä ja näppäimistöllä. Syötteen yksittäiset osaset kuten hiiren liikahdukset ja napinpainallukset ovat tapahtumia (engl. event). Lisäksi käyttöympäristöön liittyy sellaisia tapahtumia kuin ikkunan koon muuttuminen ja ohjelman sulkeminen.

Tapahtumat ohjaavat yleensä ohjelman toimintaa, ja siksi SDL-ohjelman keskipisteenä onkin usein silmukka, jossa tehdään kolme asiaa: tutkitaan tapahtumat, suoritetaan toiminnot ja piirretään tilanne ruudulle. Tätä silmukkaa kutsutaan pääsilmukaksi. Monissa peleissä on tärkeää, että toiminnot etenevät tietyllä nopeudella riippumatta käyttäjän ohjeista, ja sitä varten täytyy mitata aikaa. Kaikki nämä asiat käsitellään tässä oppaassa. Lopussa on pieni esimerkkiohjelma, jossa kaikki oppaan asiat esiintyvät käytännössä.

Ohjelman runko

Seuraava runko on helppo lähtökohta SDL-ohjelmalle. Siinä ovat selvästi erillään ohjelman keskeiset osat: alustus, tapahtumat, toiminta, piirto ja lopetus. Tässä oppaassa käytetään tätä runkoa; voit jo täyttää alustuksen, lopetuksen ja piirron edellisen oppaan avulla.

void alustus() {
	// SDL_Init, atexit(SDL_Quit), ikkunan luonti jne.
}
void lopetus() {
	// Ikkunan tuhoaminen jne.
}
void piirto() {
	// Piirto, lopussa SDL_RenderPresent.
}
int tapahtumat() {
	// Täällä käsitellään tapahtumat kuten käyttäjän syöte.
	// 0 = ohjelma loppuu, 1 = ohjelma jatkuu.
	return 0;
}
int toiminta() {
	// Tänne kuuluu ohjelman toiminta, esimerkiksi pelihahmon liikutus.
	// 0 = ohjelma loppuu, 1 = ohjelma jatkuu.
	return 1;
}

// Pääohjelma ja sen sisällä pääsilmukka.
int main(int argc, char** argv) {
	alustus();
	while (tapahtumat() && toiminta()) {
		piirto();
	}
	lopetus();
	return 0;
}

Perinteisten funktioiden sijaan voi hyvin käyttää erilaisia luokka- ja olioratkaisuja, eikä isoja ohjelmia tietenkään kannata kirjoittaa vain muutamaan funktioon.

Ajan mittaaminen

Ajan mittaaminen on peleissä tärkeää. Tätä varten SDL sisältää funktion SDL_GetTicks, joka kertoo SDL:n alustuksesta kuluneen ajan millisekunteina eli sekunnin tuhannesosina. Yleensä hyödyllinen tieto on, paljonko aikaa on kulunut tietyllä välillä, ja se selviää vähennyslaskulla.

Uint32 alku = SDL_GetTicks();
SDL_Delay(100); // Odotetaan jonkin aikaa.
Uint32 loppu = SDL_GetTicks();

Uint32 kulunut = loppu - alku;
printf("Aikaa kului %u millisekuntia.\n", kulunut);

Esimerkkiohjelmassa ensimmäinen aikaleima otetaan muistiin alustuksen lopussa ja erotus lasketaan aina toiminta-funktion alussa. Tämäntapainen menetelmä on peleissä yleinen.

Tapahtumat

Useimmissa ohjelmissa on tärkeää reagoida edes joihinkin tapahtumiin. Esimerkiksi eräs tärkeä tapahtuma on ohjelman sulkeminen (rastista ikkunan kulmassa tai Alt+F4-näppäinyhdistelmällä), jolloin ohjelman on syytä sulkeutua. Myös napinpainallukset ja hiiren toiminta ovat usein kiinnostavia tapahtumia.

Funktio SDL_PollEvent hakee seuraavan tapahtuman, jos sellainen on. Tietue SDL_Event kuvaa yhtä tapahtumaa, ja sen eri kentistä selviävät tapahtuman tyyppi ja erilaisiin tapahtumiin liittyvät lisätiedot kuten hiiren sijainti tai painetun näppäimen tunnus.

Seuraava funktio käsittelee jonossa olevat tapahtumat. Se palauttaa arvon 1, jos mitään erityistä ei tapahdu, tai arvon 0, jos ohjelma pitää sulkea.

int tapahtumat() {
	SDL_Event tapahtuma;
	// Haetaan tapahtumia, kunnes ne on tältä erää kaikki käsitelty.
	while (SDL_PollEvent(&tapahtuma)) {
		// SDL_QUIT: ohjelma yritetään sulkea rastista tai näppäimillä Alt+F4.
		if (tapahtuma.type == SDL_QUIT) {
			// Palautetaan pääsilmukkaan tieto, että ohjelma pitää sulkea (0).
			return 0;
		}
		// SDL_KEYDOWN: käyttäjä painoi jotain näppäintä.
		if (tapahtuma.type == SDL_KEYDOWN) {
			// Jos näppäin on esc, suljetaan ohjelma.
			if (tapahtuma.key.keysym.sym == SDLK_ESCAPE) {
				return 0;
			}
		}
		// SDL_MOUSEBUTTONDOWN: käyttäjä painoi hiiren napin alas.
		if (tapahtuma.type == SDL_MOUSEBUTTONDOWN) {
			// Jos nappi oli vasen, siirretään kuva klikattuun kohtaan.
			if (tapahtuma.button.button == SDL_BUTTON_LEFT) {
				kuvan_paikka.x = tapahtuma.button.x;
				kuvan_paikka.y = tapahtuma.button.y;
			}
		}
	}
	// Tänne päästään, jos lopetuskäskyä ei ole tullut. Ohjelma saa jatkua (1).
	return 1;
}

Esimerkiksi lautapelien ei yleensä tarvitse edetä, kun pelaaja vasta miettii siirtoaan. Funktiolla SDL_WaitEvent voi pysäyttää ohjelman odottamaan, että käyttäjä tekee jotain. Funktiokutsu sopii äskeisen tapahtumat-funktion alkuun.

SDL_WaitEvent(0);

Funktiolla SDL_WaitEventTimeout voi määrätä, että seuraavaa tapahtumaa odotetaan enintään tietyn aikaa, ennen kuin suoritus jatkuu joka tapauksessa. Funktiolle annetaan ensimmäiseksi parametriksi nolla ja toiseksi odotettava aika sekunnin tuhannesosina, ja ajan on oltava nollaa suurempi.

SDL_WaitEventTimeout(0, 100); // Odotetaan 100 millisekuntia eli 0,1 sekuntia.

Tapahtumien käsittely edes jollain tavalla on välttämätöntä, jotta myös seuraavaksi esiteltävät funktiot toimisivat. Jos tapahtumat eivät lainkaan kiinnosta, käsittelysilmukan voi korvata kutsulla funktiolle SDL_PumpEvents, tai koko tapahtumat-funktion voi korvata SDL:n makrolla SDL_QuitRequested, joka lukee tapahtumat ja kertoo, pitäisikö ohjelman sulkeutua (paluuarvo 1) vai ei (paluuarvo 0).

Näppäimen tilan selvittäminen

Yksinkertaisissa peleissä halutaan usein vain tarkistaa, onko tietty näppäin pohjassa. Funktio SDL_GetKeyboardState palauttaa tiedon kaikista näppäimistä scan-koodin mukaan.

// Haetaan tiedot.
const Uint8 *napit = SDL_GetKeyboardState(0);

// Tarkistetaan välilyönti.
if (napit[SDL_SCANCODE_SPACE]) {
	// Välilyönti on pohjassa, pelaaja voisi nyt ampua.
}

// Tarkistetaan sivunuolet. 0 = ei pohjassa, 1 = pohjassa.
int vasen = napit[SDL_SCANCODE_LEFT];
int oikea = napit[SDL_SCANCODE_RIGHT];

Lista näppäinten scan-koodeista on SDL:n wikin sivulla SDL_Scancode.

Jos ohjelma on tulossa laajempaan jakeluun, kannattaa olla varovainen näppäimistön suhteen: eri maissa näppäimet ovat eri paikoilla.

Tekstin syöttämiseen palataan myöhemmin opassarjassa.

Hiiren paikan selvittäminen

Hiiren sijainti ja pohjassa olevat napit selviävät funktiolla SDL_GetMouseState. Parametreiksi annetaan osoittimet int-muuttujiin, joihin funktio tallentaa hiiren sijainnin. Funktio palauttaa kokonaisluvun, jonka bitit ilmaisevat painettuna olevat näppäimet, ja yksittäiset napit saa purettua muuttujasta ja-operaatiolla seuraavan esimerkin mukaisesti.

int x, y;
Uint32 napit;
// Haetaan hiiren sijainti ja tiedot napeista muuttujiin x, y ja napit.
napit = SDL_GetMouseState(&x, &y);
// Tunnistetaan, mitkä napit ovat pohjassa.
// Muuttujiin tulee 1, jos nappi on pohjassa, ja muuten 0.
int vasen = (napit & SDL_BUTTON(SDL_BUTTON_LEFT)) ? 1 : 0;
int keski = (napit & SDL_BUTTON(SDL_BUTTON_MIDDLE)) ? 1 : 0;
int oikea = (napit & SDL_BUTTON(SDL_BUTTON_RIGHT)) ? 1 : 0;

Samankaltainen funktio SDL_GetRelativeMouseState antaakin muuttujiin x ja y sen matkan, jonka hiiri on liikkunut sen jälkeen, kun funktiota viimeksi kutsuttiin.

int x, y;
Uint32 napit;
napit = SDL_GetRelativeMouseState(&x, &y);
printf("Hiiri on liikkunut x-suunnassa %d ja y-suunnassa %d pistettä.\n", x, y);
SDL_GetRelativeMouseState(&x, &y);
printf("Nyt x = 0 ja y = 0, koska edellinen kutsu jo nollasi laskurit.\n");

Kumpikaan näistä funktioista ei skaalaa hiiren sijaintia piirtoresoluution mukaan, vaan funktiot mittaavat todellisia pikseleitä.

Hiiren kaappaus

Hiiri on mahdollista kaapata sillä tavalla, että kursoria ei näy ruudulla eikä suurikaan liike vie sitä ulos ohjelman ikkunasta. Esimerkiksi 3D-räiskintäpelit toimivat usein näin: hiirtä voi liikuttaa vaikka kuinka, tähtäin pysyy ruudun keskellä ja hahmo pyörii. Tämä kaikki tapahtuu funktiolla SDL_SetRelativeMouseMode.

// Kaappaus:
SDL_SetRelativeMouseMode(SDL_TRUE);
// Normaalitila:
SDL_SetRelativeMouseMode(SDL_FALSE);

Kun hiiri on kaapattu, sen absoluuttisella sijainnilla ei ole merkitystä, vaan on parempi seurata suhteellista sijaintia. Hiiren liikettä kuvaavissa tapahtumissa on tätä varten kentät xrel ja yrel, jotka kertovat, paljonko hiiri on liikkunut edellisen tapahtuman jälkeen.

// SDL_MOUSEMOTION: käyttäjä liikutti hiirtä.
if (tapahtuma.type == SDL_MOUSEMOTION) {
	kuvan_paikka.x += tapahtuma.motion.xrel;
	kuvan_paikka.y += tapahtuma.motion.yrel;
}

Suhteellisessa liikkeessä ei huomioida ikkunan piirtoresoluutiota, vaan liike mitataan todellisina pikseleinä.

Liikkeen määrän voisi selvittää myös funktiolla SDL_GetRelativeMouseState, joka jo äsken esiteltiin.

Esimerkkiohjelma

Tämän ja edellisen oppaan koodilistauksia sopivasti yhdistelemällä muodostuu pieni ohjelma, jossa voi liikuttaa kuvaa nuolinäppäimillä. Ohjelma vaatii toimiakseen kuvatiedoston kuva.bmp.

#include <SDL2/SDL.h>
#include <math.h>
#include <stdlib.h>
#include <stdio.h>

SDL_Window *ikkuna;
SDL_Renderer *piirtaja;
SDL_Texture *tekstuuri;
struct piste { double x, y; };
struct piste kuvan_paikka = {320, 240};
double kuvan_kulma = 0;
const double nopeus_px_per_sekunti = 100;
const double nopeus_rad_per_sekunti = 4;
Uint32 SDL_GetTicks_viimeksi;

double rad2deg(double t) {
	return t * 180 / 3.14159265358979323846;
}

int SDL_RenderCopyEx_Keskitetty(SDL_Renderer *piirtaja, SDL_Texture *tekstuuri, SDL_Rect kohde, double kulma) {
	kohde.x -= kohde.w / 2;
	kohde.y -= kohde.h / 2;
	return SDL_RenderCopyEx(piirtaja, tekstuuri, 0, &kohde, kulma, 0, SDL_FLIP_NONE);
}

void alustus() {
	// Alustetaan kirjasto.
	if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
		fprintf(stderr, "Virhe: %s\n", SDL_GetError());
		exit(1);
	}
	// Asetetaan SDL_Quit suoritettavaksi ohjelman lopussa.
	atexit(SDL_Quit);

	// Luodaan koko ruudun täyttävä ikkuna nykyisellä resoluutiolla.
	if (SDL_CreateWindowAndRenderer(0, 0, SDL_WINDOW_FULLSCREEN_DESKTOP, &ikkuna, &piirtaja) != 0) {
		fprintf(stderr, "Virhe: %s\n", SDL_GetError());
		exit(2);
	}

	// Asetetaan ikkunalle otsikko.
	SDL_SetWindowTitle(ikkuna, "SDL2-kokeilu 2");

	// Asetetaan paras skaalaustapa ja näennäisesti 640x480-resoluutio.
	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
	SDL_RenderSetLogicalSize(piirtaja, 640, 480);

	// Ladataan tekstuuri tiedostosta kuva.bmp.
	SDL_Surface *kuvapinta;
	kuvapinta = SDL_LoadBMP("kuva.bmp");
	if (!kuvapinta) {
		fprintf(stderr, "Virhe: %s\n", SDL_GetError());
		exit(3);
	}
	tekstuuri = SDL_CreateTextureFromSurface(piirtaja, kuvapinta);
	SDL_FreeSurface(kuvapinta);

	// Tästä alkaa ajanotto.
	SDL_GetTicks_viimeksi = SDL_GetTicks();
}

void lopetus() {
	SDL_DestroyTexture(tekstuuri);
	SDL_DestroyRenderer(piirtaja);
	SDL_DestroyWindow(ikkuna);
}

void piirto() {
	// Tyhjennetään tausta mustaksi.
	SDL_SetRenderDrawColor(piirtaja, 0, 0, 0, 255);
	SDL_RenderClear(piirtaja);

	// Piirretään koko piirtoalueen täyttävä valkoinen laatikko.
	SDL_SetRenderDrawColor(piirtaja, 255, 255, 255, 255);
	SDL_RenderFillRect(piirtaja, 0);

	// Piirretään kuva niin, että keskipiste on halutussa paikassa.
	// Koordinaatit pitää antaa kokonaislukuina ja kulma asteina.
	SDL_Rect kohde = {(int) round(kuvan_paikka.x), (int) round(kuvan_paikka.y)};
	double kulma = rad2deg(kuvan_kulma);
	SDL_QueryTexture(tekstuuri, 0, 0, &kohde.w, &kohde.h);
	SDL_RenderCopyEx_Keskitetty(piirtaja, tekstuuri, kohde, kulma);

	// Näytetään piirrokset.
	SDL_RenderPresent(piirtaja);
}

int tapahtumat() {
	SDL_Event tapahtuma;
	// Haetaan tapahtumia, kunnes ne on tältä erää kaikki käsitelty.
	while (SDL_PollEvent(&tapahtuma)) {
		// SDL_QUIT: ohjelma yritetään sulkea rastista tai näppäimillä Alt+F4.
		if (tapahtuma.type == SDL_QUIT) {
			// Palautetaan pääsilmukkaan tieto, että ohjelma pitää sulkea (0).
			return 0;
		}
		// SDL_KEYDOWN: käyttäjä painoi jotain näppäintä.
		if (tapahtuma.type == SDL_KEYDOWN) {
			// Jos näppäin on esc, suljetaan ohjelma.
			if (tapahtuma.key.keysym.sym == SDLK_ESCAPE) {
				return 0;
			}
		}
		// SDL_MOUSEBUTTONDOWN: käyttäjä painoi hiiren napin alas.
		if (tapahtuma.type == SDL_MOUSEBUTTONDOWN) {
			// Jos hiiri on kaapattuna, lopetetaan kaappaus.
			if (SDL_GetRelativeMouseMode()) {
				SDL_SetRelativeMouseMode(SDL_FALSE);
				continue;
			}

			// Muussa tapauksessa kaapataan hiiri ja nollataan suhteellisen sijainnin laskuri.
			SDL_SetRelativeMouseMode(SDL_TRUE);
			SDL_GetRelativeMouseState(0, 0);

			// Lisäksi siirretään kuva klikattuun kohtaan, jos painettu nappi oli vasen.
			if (tapahtuma.button.button == SDL_BUTTON_LEFT) {
				kuvan_paikka.x = tapahtuma.button.x;
				kuvan_paikka.y = tapahtuma.button.y;
			}
		}
		// SDL_MOUSEMOTION: käyttäjä painoi hiiren napin alas.
		if (tapahtuma.type == SDL_MOUSEMOTION) {
			// Jos hiiri on kaapattuna, lisätään liike kuvan koordinaatteihin.
			if (SDL_GetRelativeMouseMode()) {
				kuvan_paikka.x += tapahtuma.motion.xrel;
				kuvan_paikka.y += tapahtuma.motion.yrel;
			}
		}
	}
	// Tänne päästään, jos lopetuskäskyä ei ole tullut. Ohjelma saa jatkua (1).
	return 1;
}

int toiminta() {
	// Lasketaan edellisestä kerrasta kulunut aika.
	Uint32 SDL_GetTicks_muutos = SDL_GetTicks() - SDL_GetTicks_viimeksi;
	SDL_GetTicks_viimeksi += SDL_GetTicks_muutos;

	// Lasketaan kulunut aika sekunteina.
	double dt = SDL_GetTicks_muutos * 0.001;

	// Tarkistetaan nuolinäppäimet.
	const Uint8 *napit = SDL_GetKeyboardState(0);
	int vasen = napit[SDL_SCANCODE_LEFT];
	int oikea = napit[SDL_SCANCODE_RIGHT];
	int yla = napit[SDL_SCANCODE_UP];
	int ala = napit[SDL_SCANCODE_DOWN];

	// Muutetaan kulmaa sivunuolten mukaan ja liikutaan oikeaan suuntaan.
	kuvan_kulma += (oikea - vasen) * dt * nopeus_rad_per_sekunti;
	kuvan_paikka.x += cos(kuvan_kulma) * (yla - ala) * dt * nopeus_px_per_sekunti;
	kuvan_paikka.y += sin(kuvan_kulma) * (yla - ala) * dt * nopeus_px_per_sekunti;

	// Ohjelma saa jatkua.
	// Jos ohjelman sisällä olisi jokin lopetusnappi, tilanne voisi olla toinen.
	return 1;
}

int main(int argc, char** argv) {
	alustus();
	while (tapahtumat() && toiminta()) {
		piirto();
	}
	lopetus();
	return 0;
}

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