Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

09 Dezember, 2009

Tag 10: Open Source nutzen, SDL und Grundgerüst

TL;DR:

Open Source ist deshalb so erfolgreich, weil der Programmcode frei zugänglich ist und von jedem verwendet und erweitert werden darf. Und genau das werden wir jetzt auch machen. Da wir ein 2D-Spiel programmieren wollen, aber nicht bei Null anfangen wollen, greifen wir auf SDL zurück, welches uns den Zugang zur Grafikkarte erleichtert.

SDL steht für Simple Directmedia Layer und gibt uns eine Abstraktionsebene, von der aus wir auf die Hardware wie Grafikkarte, Eingabegeräte und Ausgabegeräte zugreifen können. Wir können also bequem einen Pixel auf den Bildschirm zeichnen, ohne uns darüber Gedanken zu machen, welche Grafikkarte der User gerade verwendet. Dies nimmt uns SDL ab.

Mit SDL können wir auch auf den 2D Framebuffer zugreifen, was unser Ziel sein wird. Hiermit können wir sehr schnell auf einer 2D-Oberfläche zeichnen. Da wir uns als Grundidee überlegt haben, Plattform unabhängig zu sein, ist SDL ideal dafür. Auf der Homepage ist eine vierzeilige Liste von Systemen, die unterstützt werden. (SDL funktioniert nicht nur mit C sondern kann von fast allen Sprachen verwendet wird. Das hier gelernte ist also nicht umsonst!)

Also fangen wir an. Die ersten haben schon das Projekt auf github geforked und schon commits eingereicht, prima! Wer das noch nicht getan hat: Jetzt ist die passende Gelegenheit dazu! Im git-Repository habe ich eine Basis angelegt, auf der wir das Spiel entwickeln können. Das Projekt besteht noch nicht aus all zu vielen Dateien, so dass man sich schnell einen Überblick verschaffen kann. Florian hat zudem das Makefile aus dem vorletzten Artikel überarbeitet, so dass der Programmcode sauber vom Rest getrennt bleibt, danke dafür.

Schauen wir uns den Programmcode etwas näher an. Zuerst die main.c. Dies ist die Startdatei, in der sich auch die main()-Funktion befindet. In dieser Funktion initialisieren wir zuerst SDL mit SDL_init(). Da wir momentan nur die Grafikfunktionen von SDL benötigen, steht hier SDL_INIT_VIDEO als Parameter. Wollen wir weitere Features von SDL nutzen, können wir diese mit einem oder (|, Pipe) verknüpfen. Eine solche Technik wird eine Zeile weiter unten schon angewendet. Dies ist ziemlich gängig bei C-Programmen, da man so viele Optionen in einem einzigen Integer speichern kann. Jedes Bit steht für eine andere Option und durch ein binäres oder werden die einzelnen Bits gesetzt.

Weiter geht es mit SDL_SetVideoMode(), von dem wir einen Pointer auf ein SDL_Surface bekommen. Wir speichern uns diesen Pointer in der Variablen screen, da wir ihn noch oft brauchen werden. Dieser Pointer ist unser Zugang zu unserer Zeichenfläche. Die Option SDL_HWSURFACE steht dafür, dass die Zeichenfläche im Arbeitsspeicher der Grafikkarte abgelegt wird, dies macht die Anwendung schneller. SDL_DOUBLEBUF verhindert hässliches Flimmern, dazu evtl. später mehr.

Dann kommt eine while-Schleife, in der das Programm/Spiel die restliche Zeit verbringen wird, bis es beendet wird. In ihr werden zuerst die Events abgefragt und dann auf den Bildschirm bezeichnet. Danach rufen wir SDL_Flip() auf, um den nächsten Frame zu zeichnen. Diese Schleife stellt unser Spiel dar: Events abfragen, Berechnungen machen (gerne auch als Logik-Teil bezeichnet), aufs Display zeichnen, Frame anzeigen, Events abfangen, berechnen, zeichnen, Frame anzeigen, und immer so weiter ...

Wie vielleicht schon gemerkt, habe ich hier eine config.h und eine draw.h eingebunden. In der config.h bzw. config.c sind globale Variablen und Konstanten definiert die wir in den anderen Dateien benötigen, so wie die Variable screen. Dies war notwendig, da wir jede einzelne c-Datei einzeln compilieren und anschließend linken.

In der Datei draw.c habe ich bereits begonnen, ein Minimalset an Funktionen zu implementieren, um auf dem Bildschirm zu zeichnen. Die Funktion drawScreen() wird bei jedem Frame aufgerufen. Die Funktion drawPixel() zeichnet einen einzelnen Pixel an die angegebene Stelle. Bis auf die drawPixel()-Funktion müsste alles weitgehend Selbsterklärend sein, wenn nicht, würde ich mich sehr über einen kurzen Kommentar freuen, so dass ich das dann nachtragen kann. Die drawPixel()-Funktion möchte ich jetzt im Detail erklären, da diese etwas kniffliger ist.

void drawPixel(int x, int y, int color) {
  unsigned int *p = (unsigned int*)screen->pixels;
  int lineOffset = y * (screen->pitch/4);
  p[lineOffset+x] = color;
}

Zuerst einmal holen wir uns aus dem SDL_Surface Pointer screen, den wir zu Beginn in der main()-Funktion gesetzt hatten, einen Pointer auf ein Array, dass die Bildpunkte im Arbeitsspeicher der Grafikkarte repräsentiert. Hier merkt man schon, wie tief man trotz Abstraktionsschicht abtauchen kann. Nun wird aber auf der Grafikkarte nur mit binärdaten gearbeitet. So wird beispielsweise für ein Fenster mit 800x600 Pixeln Platz für 1024x600 Pixel reserviert. Warum das so ist, sieht man an folgendem Bild recht gut:

pitch

1024 sind 2^10, 2^9 wären 512, also nicht ausreichend für 800 Punkte. Somit sind 224*600*4 Bit = ~0.5 Mb Speicher einfach verschenkt, doch das soll uns nicht weiter stören. Wichtig ist für uns jetzt, wie wir die Punkte korrekt auf das Fenster zeichnen. Da in unserem Fall p ein Pointer auf ein eindimensionales Array ist, können wir auf dieses auch wie ein solches darauf zugreifen. Um die Position zu bestimmen, werden wir uns zuerst den Offset berechnen, also wie viele Zeilen wir nach unten gehen müssen. screen->pitch gibt uns die Anzahl Bytes, die eine Zeile (Scanline) hat. Da wir die Farbtiefe auf 32 Bit gestellt haben, braucht jedes Pixel 4 Byte (32 Bit = 2^4 = 4 Byte). Deshalb teilen wir screen->pitch durch 4 und multiplizieren es mit der Anzahl Zeilen (y). Darauf addieren wir x und setzen an genau dieser Position unsere 4 Byte - also unsere Farbe. Wie die Farbe definiert wird sieht man in der drawScreen()-Funktion. Am Besten einfach mit dem Cursor über SDL_MapRGB() gehen und K drücken.

Wie ihr seht, geht es schon recht lowlevel zu, so dass es uns recht gelegen kommt, wenn wir eine simple Funktion haben, deren wir x, y und eine Farbe geben und diese uns ein Pixel auf den Bildschirm malt. Müssten wir uns jetzt noch um all die verschiedenen Grafikkarten und deren Speichermanagement kümmern, würden wir schnell die Lust daran verlieren.

Soweit soll es für heute erst einmal gewesen sein. Macht euch bis übermorgen vertraut mit dem Sourcecode und falls ihr gerne etwas optimieren oder ändern wollt, lasst es mich über Github wissen. Ich freue mich über jede Beteiligung. Auch ein anonymer Kommentar zeigt mir, dass die Artikel gelesen werden :)

Übermorgen geht es dann um das Zeichnen von Linien, Rechtecken und Kreisen. Wir werden uns eine kleine Sammlung von Befehlen zurechtlegen, mit denen wir dann weiterarbeiten können. Wir werden zudem den Debugger gdb verwenden, um einem Segmentation fault mehr als nur einen Programmabsturz zu entlocken. In der Zwischenzeit sind natürlich Code-Optimierungen (mit Sourcecode-Kommentaren vorausgesetzt) und Verbesserungsvorschläge herzlich willkommen. Ich gebe auch sehr gerne Hilfestellungen hier in den Kommentaren, falls etwas nicht so klappen sollte wie hier beschrieben.