Game Loops mit SDL
Posted by fkrauthan | Posted in Artikel, Linux, SDL, Windows | Posted on 10-01-2009
Tags: c++, gameloop, gameschleife, SDL, spielschleife, time, tutorial, zeit
4
Das Grundgerüst für unsere Game Loops
Zuerst werde ich euch hier mal ein wenig Code hinklatschen, mit dem wir dann unsere Game Loops testen können.
- //Der SDL Include-Block
- #ifdef WIN32
- #include <SDL.h>
- #include <SDL_image.h>
- #else
- #include <SDL/SDL.h>
- #include <SDL/SDL_image.h>
- #endif
- //Der STD iostream-Include, damit wir mit cout etwas auf der Konsole ausgeben können
- #include <iostream>
- int main(int argc, char **argv) {
- //Starten von SDL
- if(SDL_Init(SDL_INIT_VIDEO) < 0 ) {
- return -1;
- }
- //Dafür sorgen, dass SDL_Quit beim Beenden aufgerufen wird
- atexit(SDL_Quit);
- //Hier erzeugen wir ein Fenster mit 800x600x32 und DoubleBuf-Support. Wollen wir noch Fullscreen haben, hängen wir einfach an SDL_DOUBLEBUF ein " | SDL_FULLSCREEN" an
- SDL_Surface *screen = SDL_SetVideoMode(800, 600, 32, SDL_DOUBLEBUF);
- if(screen == NULL) {
- //Sollte ein Fehler beim Erstellen des SDL-Fensters aufgetreten sein, geben wir die SDL-Fehlermeldung zurück und beenden das Programm
- std::cout << "Konnte-SDL Fenster nicht erzeugen: " << std::endl << SDL_GetError() << std::endl;
- return -1;
- }
- //Nun setzen wir den Anwendungs-Titel
- SDL_WM_SetCaption("SDL - Gameloop","SDL - Gameloop");
- //Hier laden wir unser Bild mit SDL_Image
- SDL_Surface *image = IMG_Load("sdl_example2_tux.jpg");
- if(image == NULL) {
- //Sollte er das Bild nicht laden können
- std::cout << "Konnte das Bild nicht laden: " << std::endl << SDL_GetError() << std::endl;
- return -1;
- }
- //Hier speichern wir die Abmessungen für unser Image in einem SDL_Rect
- SDL_Rect rImage;
- rImage.x = 0;
- rImage.y = 0;
- rImage.w = image->w;
- rImage.h = image->h;
- //Hier speichern wir die Position von unserem Image
- SDL_Rect rPosition = rImage;
- //Die Event-Struktur, um SDL_Events auszuwerten
- SDL_Event event;
- //Unsere Variable die dafür sorgt, dass unser Fenster erstmal geöffnet bleibt
- bool bRun = true;
- //Speichern, wohin es sich bewegen soll
- bool bLeft = false;
- bool bRight = false;
- bool bUp = false;
- bool bDown = false;
- /**
- * Hier kommt unser Game Loop später hin
- */
- //Hier müssen wir noch unser Bild nach der Schleife wieder freigeben
- SDL_FreeSurface(image);
- return 0;
Wie ihr sehen könnt, werden wir wieder ein Bild laden und dieses dann bewegen. Als Bild wird uns wieder ein Pinguin dienen.

Unser Tux
Was ist an einer einfachen while-Schleife schlecht?
Zunächst müssen wir klären, was an unserem ersten Ansatz falsch ist: Eine einfache while-Schleife, in der wir die Spiellogik Frame für Frame erneut ausführen. Das Problem ist, dass wir die Zeit außer achtlassen. Ohne Berücksichtigung der verstrichenen Zeit wird unsere while-Schleife auf jedem PC mit anderer Geschwindigkeit ablaufen, je nach Leistung der Hardware sowie Auslastung durch andere Prozesse, die auf einem Anwendungsrechner in der Regel nebenher laufen (Virenscanner, Network Guards, …).
Game Loop mit dynamischer Framerate
Wir werden als erstes mal einen Game Loop mit dynamischer Framerate entwickeln. Dazu brauchen wir die Funktion SDL_GetTicks(); Diese gibt uns die Zeit seitdem unsere Anwendung SDL gestartet hat in Millisekunden (ms) zurück. Also perfekt, um die Zeit zu messen.
Also definieren wir zuerst ein paar benötigte Variablen:
- //Game Loop mit dynamischer Framerate
- long m_lLastTick = SDL_GetTicks();
- int m_iFPSTickCounter = 0;
- int m_iFPSCounter = 0;
- int m_iCurrentFPS = 1;
- int m_iElapsedTicks = 0;
- while(m_bRun) {
Später wird dann m_iCurrentFPS unsere aktuelle Framerate enthalten. Dieser Code wird einfach nach dem Kommentar, dass hier der Game Loop kommt, eingefügt. Wozu wir die einzelnen Parameter benötigen gehe ich später noch genauer ein.
Unsere while-Schleife mit der Verarbeitung der SDL-Events
- while(bRun) {
- //Hier fragen wir alle anstehenden SDL_Events ab
- while(SDL_PollEvent(&event)) {
- //Und gehen diese durch
- switch(event.type){
- case SDL_KEYDOWN:
- //Eine Taste wurde gedrückt. Nun müssen wir feststellen, welche Taste
- if(event.key.keysym.sym==SDLK_LEFT) {
- //Pfeiltaste Links wurde gedrückt
- bLeft = true;
- }
- else if(event.key.keysym.sym==SDLK_RIGHT) {
- //Pfeiltaste Rechts wurde gedrückt
- bRight = true;
- }
- else if(event.key.keysym.sym==SDLK_DOWN) {
- //Pfeiltaste Oben wurde gedrückt
- bDown = true;
- }
- else if(event.key.keysym.sym==SDLK_UP) {
- //Pfeiltaste Unten wurde gedrückt
- bUp = true;
- }
- if(event.key.keysym.sym==SDLK_ESCAPE) {
- //ESCAPE wurde gedrückt. Also beenden wir alles
- bRun = false;
- }
- break;
- case SDL_KEYUP:
- //Eine Taste wurde losgelassen. Nun müssen wir feststellen, welche Taste
- if(event.key.keysym.sym==SDLK_LEFT) {
- //Pfeiltaste Links wurde losgelassen
- bLeft = false;
- }
- else if(event.key.keysym.sym==SDLK_RIGHT) {
- //Pfeiltaste Rechts wurde losgelassen
- bRight = false;
- }
- else if(event.key.keysym.sym==SDLK_DOWN) {
- //Pfeiltaste Oben wurde losgelassen
- bDown = false;
- }
- else if(event.key.keysym.sym==SDLK_UP) {
- //Pfeiltaste Unten wurde losgelassen
- bUp = false;
- }
- break;
- case SDL_QUIT:
- //Sollte das Fenster geschlossen werden, soll auch der Loop beendet werden
- bRun = false;
- break;
- default:
- break;
- }
- }
- //Hier werden wir die Zeitabfrage einbauen
- }
Gut, das war wieder jede Menge Code. Aber was solls. Nun müssen wir uns um den wichtigen Teil in unserer Game Loop kümmern. Wir müssen die FPS ausrechnen, um anhand der FPS dann unseren Pinguin zu bewegen.
Die FPS ausrechnen
So, erstmal der Code (die Erklärung folgt später):
- //Hier werden wir die Zeitabfrage einbauen
- m_iElapsedTicks = SDL_GetTicks() - m_lLastTick;
- m_lLastTick = SDL_GetTicks();
- m_iFPSTickCounter += m_iElapsedTicks;
- m_iFPSCounter++;
- if(m_iFPSTickCounter >= 1000) {
- m_iCurrentFPS = m_iFPSCounter;
- m_iFPSCounter = 0;
- m_iFPSTickCounter = 0;
- }
Was passiert da nun genau? Zuerst ziehen wir die neue Zeit von der alten Zeit ab, um eine Differenz zu erhalten. Danach speichern wir die neue Zeit in der Variablen m_lLastTick. Nun zählen wir das Ganze zu unserem FPS-Tickcounter hinzu, und erhöhen unseren FPS-Rechner. Sollte nun dieser FPS-Tickcounter mehr als 1000 ms haben, also eine Sekeunde verstrichen sein, speichern wir den FPS-Counter als unsere aktuelle Framerate und setzen den FPS-Counter und den FPS-Tickcounter wieder auf 0. Das wars schon. Nun haben wir immer eine Framerate in iCurrentFPS.
Wie bewegen wir nun unseren Tux?
Dazu werden wir einfach zur aktuellen Position entsprechend der Bewegungsrichtung die Geschwindigkeit dividiert durch die FPS hinzuaddieren. Das Ganze sieht dann so aus:
- //Hier bewegen wir den Spieler um jeweils 1px in die gedrückte Richtung
- if(bLeft) {
- rPosition.x -= 100/m_iCurrentFPS;
- }
- else if(bRight) {
- rPosition.x += 100/m_iCurrentFPS;
- }
- if(bUp) {
- rPosition.y -= 100/m_iCurrentFPS;
- }
- else if(bDown) {
- rPosition.y += 100/m_iCurrentFPS;
- }
- //Zuerst übermalen wir den Bildschirm mit Schwarz
- SDL_FillRect(screen, 0, SDL_MapRGB(screen->format, 0, 0, 0));
- //Hier blitten wir unser Bild an die Position, die wir vorher definiert haben
- SDL_BlitSurface(image, &rImage, screen, &rPosition);
- //Dann aktualisieren wir den Screen, damit wir auch was sehen
- SDL_Flip(screen);
So, wenn wir das nun alles in unserer while-Sschleife haben, sollte das Ganze auf jedem PC gleich schnell laufen.
Der gesamte Code
- /**
- * @FILE : sdl_gameloop_dyn.cpp
- * @AUTOR: Florian Krauthan
- * @DATE : 13.08.2008
- */
- //Der SDL Include Block
- #ifdef WIN32
- #include <SDL.h>
- #include <SDL_image.h>
- #else
- #include <SDL/SDL.h>
- #include <SDL/SDL_image.h>
- #endif
- //Der STD iostream include damit wir mit cout was auf der Console ausgeben können
- #include <iostream>
- int main(int argc, char **argv) {
- //Starten von SDL
- if(SDL_Init(SDL_INIT_VIDEO) < 0 ) {
- return -1;
- }
- //Dafür sagen das SDL_Quit beim Beenden aufgerufen wird
- atexit(SDL_Quit);
- //Hier erzeugen wir ein Fenster mit 800x600x32 mit DoubleBuf Support. Wollen wir noch Fullscreen haben hängen wir einfach an SDL_DOUBLEBUF ein " | SDL_FULLSCREEN" an
- SDL_Surface *screen = SDL_SetVideoMode(800, 600, 32, SDL_DOUBLEBUF);
- if(screen == NULL) {
- //Sollte ein Fehler beim erstellen des SDL Fensters aufgetreten sein geben wir die SDL Fehler Meldung zurück und beenden das Programm
- std::cout << "Konnte SDL Fenster nicht erzeugen: " << std::endl << SDL_GetError() << std::endl;
- return -1;
- }
- //Nun setzen wir den Applikation Titel
- SDL_WM_SetCaption("SDL - Gameloop","SDL - Gameloop");
- //Hier laden wir unser Bild mit SDL_Image
- SDL_Surface *image = IMG_Load("sdl_example2_tux.jpg");
- if(image == NULL) {
- //Sollte er das Bild nicht laden können
- std::cout << "Konnte das Bild nicht laden: " << std::endl << SDL_GetError() << std::endl;
- return -1;
- }
- //Hier speichern wie die Abmessung für unser Image in einem SDL_Rect
- SDL_Rect rImage;
- rImage.x = 0;
- rImage.y = 0;
- rImage.w = image->w;
- rImage.h = image->h;
- //So und hier drinnen Speichern wir die Position von Unserem Image
- SDL_Rect rPosition = rImage;
- //Die Event struktur um SDL_Events auszuwerten
- SDL_Event event;
- //Unser Variable die sorgt das unser Fenster erstmal offen bleibt
- bool bRun = true;
- //Speichern wohin er sich bewgen soll
- bool bLeft = false;
- bool bRight = false;
- bool bUp = false;
- bool bDown = false;
- //Gameloop mit dynamischer Framerate
- long m_lLastTick = SDL_GetTicks();
- int m_iFPSTickCounter = 0;
- int m_iFPSCounter = 0;
- int m_iCurrentFPS = 0;
- int m_iElapsedTicks = 0;
- while(bRun) {
- //Hier fragen wir alle anstehendn SDL_Events ab
- while(SDL_PollEvent(&event)) {
- //Und gehen diese durch
- switch(event.type){
- case SDL_KEYDOWN:
- //Eine Taste wurde gedrückt nun müssen wir feststellen welche taste
- if(event.key.keysym.sym==SDLK_LEFT) {
- //Pfeiltaste Links wurde gedrückt
- bLeft = true;
- }
- else if(event.key.keysym.sym==SDLK_RIGHT) {
- //Pfeiltaste Rechts wurde gedrückt
- bRight = true;
- }
- else if(event.key.keysym.sym==SDLK_DOWN) {
- //Pfeiltaste Oben wurde gedrückt
- bDown = true;
- }
- else if(event.key.keysym.sym==SDLK_UP) {
- //Pfeiltaste Unten wurde gedrückt
- bUp = true;
- }
- if(event.key.keysym.sym==SDLK_ESCAPE) {
- //ESCAPE wurde gedrückt. Also Beenden wir alles
- bRun = false;
- }
- break;
- case SDL_KEYUP:
- //Eine Taste wurde losgelassen nun müssen wir feststellen welche taste
- if(event.key.keysym.sym==SDLK_LEFT) {
- //Pfeiltaste Links wurde losgelassen
- bLeft = false;
- }
- else if(event.key.keysym.sym==SDLK_RIGHT) {
- //Pfeiltaste Rechts wurde losgelassen
- bRight = false;
- }
- else if(event.key.keysym.sym==SDLK_DOWN) {
- //Pfeiltaste Oben wurde losgelassen
- bDown = false;
- }
- else if(event.key.keysym.sym==SDLK_UP) {
- //Pfeiltaste Unten wurde losgelassen
- bUp = false;
- }
- break;
- case SDL_QUIT:
- //Sollte das Fenster geschlosen werden, soll er auch die Loop beenden
- bRun = false;
- break;
- default:
- break;
- }
- }
- //Hier werden wir die Zeitabfrage einbauen
- m_iElapsedTicks = SDL_GetTicks() - m_lLastTick;
- m_lLastTick = SDL_GetTicks();
- m_iFPSTickCounter += m_iElapsedTicks;
- m_iFPSCounter++;
- if(m_iFPSTickCounter >= 1000) {
- m_iCurrentFPS = m_iFPSCounter;
- m_iFPSCounter = 0;
- m_iFPSTickCounter = 0;
- }
- //Hier bewegen wir den Spieler um jewiel 1 px in die gedrückte richtung
- if(bLeft) {
- rPosition.x -= 100/m_iCurrentFPS;
- }
- else if(bRight) {
- rPosition.x += 100/m_iCurrentFPS;
- }
- if(bUp) {
- rPosition.y -= 100/m_iCurrentFPS;
- }
- else if(bDown) {
- rPosition.y += 100/m_iCurrentFPS;
- }
- //Zuerst übermalen wir den Bildschirm mit Schwarz
- SDL_FillRect(screen, 0, SDL_MapRGB(screen->format, 0, 0, 0));
- //Hier Blitten wir unser Bild an die Position die wir vorher definirt haben
- SDL_BlitSurface(image, &rImage, screen, &rPosition);
- //Dann Upadaten wir den Screen, das wir auch was sehen.
- SDL_Flip(screen);
- }
- //Hier müssen wir noch unser Bild nach der Schleife wieder freigeben
- SDL_FreeSurface(image);
- return 0;
- }


(2 votes, average: 4.50 out of 5)
hi, warum steht c++ bei den tags wenn du komplett in c schreibst?
gibts nen grund dafür die fps zu begrenzen?
mfg nacho
Naja im Grunde kannst du auch C++ nehmen daher im Tag c++. SDL ist halt einfach eine C Lib. Der einzige Grund ist das du bei einer begrenzten FPS keine FPS Schwankungen hast. Das heißt wenn du sie unbegrenzt hast und der immer mal wieder länger und kürzer braucht für ein Frame dann merkt man das teilweise. Wenn man sie nun fest beschränkt dann ist die Gefahr solcher Schwankungen geringer, da man die Grenze meist relativ tief setzt.
Wo genau ist der Unterschied zwischen der Ersten und der Dritten Variante? Im Grunde arbeiten beide Verfahren mit einem Faktor, der einfach von der Zeit abhängig ist. Wenn ein Frame länger braucht, ist die Entfernung größer. Diese Abhängigkeit ist bei beiden vorhanden.
Ich hatte es bei meinen Spielen auch wie in der dritten Lösung gemacht. Es gibt einen Zeitfaktor, der bei gewünschter Framerate 1 ist, bei langsamen rechnern >1 und bei schnellen Rechnern <1. Alle Bewegung wird damit multipliziert. So sieht es dann flüssig aus.
Einer der größten unterschied zwischen Version1 und Version3 ist, dass man bei der Version1 eine Division braucht für die Bewegung während Version3 mit Multiplizieren arbeitet. Das macht einen gewaltigen Geschwindigkeitsunterschied aus.