资料收集站

SDL

Wednesday
Dec 03rd
Text size
  • Increase font size
  • Default font size
  • Decrease font size

SDL-Tutorial - Erste Schritte

一份非常好的SDL教程(我只看了代码,!)。

源网址:resourcecode.de

Installation, Bitmaps zeichnen, bewegen und animieren, Scrolling, Partikel, OpenGL und mehr als Druckversion zum gem?en Offline-lesen.


{mospagebreak}

SDL-Tutorial #1 - Erste Schritte

Was erwartet Euch?


In diesem Tutorial, das sich über mehrere Kapitel erstrecken wird, versuche ich Euch SDL näher zu bringen.
Ihr solltet auf jeden Fall die Sprache C und Eure Programmiertools einigermaßen beherrschen, auch wenn ich mich mit überkomplizierten Konstrukten zurückhalte.

Was ist SDL überhaupt?


SDL steht für Simple DirectMedia Layer und ist eine Opensource-Bibliothek für die Spieleprogrammierung. Die Homepage findet ihr auf
http://www.libsdl.org/.

Man kann SDL vom Standpunkt des Programmierers recht gut mit DirectX vergleichen, obwohl die beiden sehr verschieden sind. SDL hat gegenüber DirectX zwei entscheidende Vorteile:
1. SDL ist Opensource, das heißt man kann sich den Quellcode der Bibliothek selbst ansehen.
2. SDL ist plattformübergreifend. Wenn man einmal ein Spiel unter Windows programmiert hat muß man es lediglich neu kompilieren und es läuft auch unter Linux und anderen Betriebssystemen.

Eigentlich ist SDL aber etwas grundlegend anderes als DirectX: SDL ist eine Bibliothek, die auf verschiedenen Betriebssystemen die zugrundeliegenden APIs so vor dem Programmierer (also Euch) versteckt, daß ein und derselbe Quellcode ohne großen Portierungsaufwand auf verschiedenen Betriebssystemen kompilier- und lauffähig ist.

Unter Windows verwendet SDL zum Beispiel selbst DirectX (weshalb SDL auch nicht wirklich mit DirectX vergleichbar ist), um auf die Hardware zuzugreifen, wie im folgenden Schema zu sehen ist:

Was man an diesem Schema auch sieht: SDL führt einen zusätzlichen Schritt auf dem Weg zwischen dem Spiel und der Hardware ein. Das wirkt sich minimal auf die Performance aus. Alles in allem ist dieser Unterschied allerdings so gering, daß sich die Vorteile von SDL durchaus auszahlen.

Installation von SDL


Bevor man überhaupt mit dem Programmieren anfängt muß man natürlich gewisse Vorbereitungen treffen. Ich will die nötigen Schritte für Windows und für Linux schildern. SDL unterstützt zwar auch andere Betriebssysteme, aber die verwende ich selbst nicht - wenn jemand einen entsprechenden Abschnitt hinzufügen kann wäre ich dankbar.

Installation unter Windows


Unter Windows kann man bei SDL eigentlich gar nicht von einer Installation reden. Holt Euch einfach das Developmentpaket für MSVC++ von der SDL-Webseite (
http://www.libsdl.org/ - Download - Development Libraries) und entpackt es irgendwohin. Dieses Paket enthält übrigens auch eine Onlinedokumentation der SDL-Funktionen und -Strukturen im HTML-Format (auf englisch). Wenn Ihr wollt könnt Ihr jetzt noch die SDL.DLL aus dem LIB-Verzeichnis in das \WINDOWS\SYSTEM-Verzeichnis kopieren. Andernfalls müßt Ihr die SDL.DLL nachher in das Verzeichnis kopieren, in das dann auch die kompilierte .EXE-Datei kommt.

Installation unter Linux


Zunächst solltet Ihr überprüfen, ob SDL vorinstalliert ist - auf manchen Distributionen ist das vielleicht der Fall. Dazu öffnet Ihr eine Shell und führt sdl-config aus. Typischerweise ergibt sich folgendes Bild:

prefect@leprechaun:~ > sdl-config --version
1.2.2


Hier ist also die Version 1.2.2 von SDL installiert. Sollte der Befehl 'sdl-config' nicht gefunden werden ist SDL auch nicht installiert.

Wenn SDL nicht installiert ist oder Eure Version veraltet ist müßt Ihr SDL installieren. Dies geht im Normalfall recht schnell:

1. Ladet den Quellcode von
http://www.libsdl.org/ runter. Es gibt zwar auch eine binäre Distribution, aber bei Linux sind binäre Pakete so eine suspekte Sache...

2. Entpackt das Quellcodearchiv. Dateien im .tar.gz-Format kann man in einer Shell per

tar xzvf dateiname.tar.gz


entpacken.

3. Der Rest der Installation ist ein typischer autoconf/automake-Prozess:
Wechselt in das Verzeichnis, in dem sich der Quellcode befindet.
Führt './configure' aus. Damit wird SDL unter dem Verzeichnis /usr/local/* installiert. Oft empfiehlt es sich, './configure --prefix=/usr' zu verwenden - damit wird SDL später unter /usr/* installiert, und ist damit für Programme leichter zu finden.
Führt 'make && make install' in der Shell aus.

Sobald diese Prozesse abgeschlossen sind ist SDL installiert.

Einrichtung der Kompilierumgebung


Nachdem SDL installiert ist könnt Ihr aber nicht sofort loslegen. Der Compiler weiß ja nichts von SDL, und deswegen muß ihm erstmal beigebracht werden, was es mit SDL auf sich hat. Die notwendigen Schritte sind natürlich wiederum bei jedem Compiler anders.

Projekte unter MSVC++


A. Projekt einrichten
1) Startet MSVC++ und geht auf den Menüpunkt Datei->Neu...
2) Wählt dann aus dem Projekte-Tabulator die Option "Win32-Konsolenanwendung" aus, gebt den Verzeichnis- und Projektnamen ein und klickt auf "Ok".
3) Im nächsten Dialogfenster wählt Ihr "Ein leeres Projekt" aus und klickt auf "Fertigstellen".
4) Bestätigt das nächste Dialogfenster einfach mit "Ok".

Nun habt Ihr bereits ein einfaches, leeres Projekt. Jetzt müßt Ihr erstmal die SDL-Einstellungen vornehmen.

B. SDL-Bibliothek einbinden
1) Wählt im Arbeitsbereich (dieses eingebettete Fenster im linken Teil der IDE) den Tabulator "Dateien" aus. Rechtsklickt auf "<Projektname> Dateien" und wählt "Dateien zu Projekt hinzufügen..." aus.
2) Wechselt in dem jetzt erscheinenden Dialogfenster, in das LIB-Verzeichnis Eurer SDL-Installation. Wählt als Dateityp "Bibliothekdateien (.lib)" aus. Es sollten zwei Dateien (SDL.lib und SDLmain.lib) erscheinen.
3) Markiert "SDL.lib" und klickt auf "Ok".
Die Bibliothek sollte jetzt im Arbeitsbereich erscheinen.

Als nächstes muß dem Compiler gesagt werden, wo die SDL-Headerdateien zu finden sind.

C. Include-Verzeichnis hinzufügen
1) Wählt die Menüoption Extras->Optionen.. aus und wechselt auf den Tabulator "Verzeichnisse".
2) Wählt Verzeichnisse anzeigen für: "Include-Dateien" aus, falls dies nicht schon der Fall ist.
3) Fügt im großen Feld das INCLUDE-Verzeichnis Eurer SDL-Installation hinzu (einfach per Doppelklick auf das leere Rechteck).

Jetzt könnt Ihr das Projekt ganz normal bearbeiten.

Hinweis: Falls Ihr die SDL.DLL nicht ins \WINDOWS\SYSTEM-Verzeichnis kopiert habt müßt Ihr die SDL.DLL später in das Temporärverzeichnis kopieren, indem sich die kompilierte .EXE-Datei befindet (z.B. sdl-tut\tut1\Debug).

Projekte mit GCC (auch MingW32)


Wenn Ihr mit gcc arbeitet, solltet Ihr Makefiles verwenden, da SDL-Programme eventuell mehrere Optionen benötigen, um richtig kompiliert zu werden. Ich zeige hier einmal die Minimalisten-Makefile, die in den ersten Tutorials verwendet wird.
Für größere Projekte müssen natürlich ausgefeiltere Mechanismen geschaffen werden. Ideal wäre die Verwendung von Tools wie autoconf/automake.

Eine sehr einfache Makefile sieht typischerweise so aus:

tut1: tut1.c
    gcc -Wall -o tut1 tut1.c


Diese Makefile sagt dem make-Programm folgendes:
immer, wenn tut1.c verändert wurde muß tut1 neu erstellt werden
um tut1 zu erstellen, soll 'gcc -Wall -o tut1 tut1.c' aufgerufen werden

Update:
In der Zeile zwei des obigen Codes muß sich vor 'gcc' ein Tabulator befinden. Manche Editoren ersetzen Tabulatoren durch Leerzeichen, genauso wie Copy&Paste aus den meisten Browsern heraus. Wenn statt dem Tabulator Leerzeichen stehen beendet sich make mit der etwas seltsam anmutenden Fehlermeldung:

Makefile:7: *** missing separator.  Stop.


Sorgt also dafür, daß vor 'gcc' nur ein Tabulator steht. Des weiteren solltet Ihr Euren Editor so einstellen, daß er Tabulatoren nicht in Leerzeichen umwandelt. Bei allen mir bekannten Editoren, die dieses bescheuerte "Feature" unterstützen ist das möglich. Vielen Dank an TheVoice für diesen Hinweis!

Die Compileroption '-Wall' weißt gcc an, bei jeder Kleinigkeit eine Warnung auszugeben. Das mag zwar nerven, auf Dauer erspart es einem aber manche mühsame Fehlersuche, denn Fehlerquellen werden früher entdeckt.

Nun muß man nur noch in einer Shell 'make' eingeben, und das make-Programm überprüft automatisch, ob irgend etwas neu kompiliert werden muß - und tut dies dann auch falls nötig. Dadurch erspart man sich das lästige Tippen der immer gleichen Befehle.

Leider reicht diese minimalistische Makefile nicht für SDL-Programme - SDL benötigt nämlich zusätzliche Include- bzw. Headerdateien und Bibliotheken. Hier hilft uns aber das nützliche Programm 'sdl-config' weiter. Dieses Programm informiert uns über die zusätzlichen Compileroptionen, die benötigt werden, um ein SDL-Programm zu kompilieren. Führt dazu in einer Shell folgendes aus:

prefect@leprechaun:~ > sdl-config --libs
-L/usr/lib -Wl,-rpath,/usr/lib -lSDL -lpthread
prefect@leprechaun:~ > sdl-config --cflags
-I/usr/include/SDL -D_REENTRANT


Entsprechend erweitern wir unsere minimalistische Makefile - sie sieht nun wie folgt aus:

tut1: tut1.c
    gcc -L/usr/lib -Wl,-rpath,/usr/lib -lSDL -lpthread -I/usr/include/SDL \
        -D_REENTRANT -o tut1 tut1.c


Update:
Natürlich gilt hier das gleiche wie beim obigen Makefile-Beispiel. Vor dem 'gcc' darf nur ein Tabulator, und keine Leerzeichen, stehen!


Hier habe ich einfach die Ausgabe von 'sdl-config' zum Compileraufruf hinzugefügt. Bei Euch gibt 'sdl-config' vielleicht andere Optionen aus - in dem Fall müßt Ihr natürlich auch die Makefile anpassen.

Der Backslash '\' funktioniert in Makefiles übrigens genauso wie in C-Quellcode - er sorgt dafür, daß das make-Programm die darauffolgende Zeile direkt an die vorhergehende anfügt. Nach dem '\' darf kein weiteres Zeichen mehr stehen.

So - die Makefile ist fertig. Jetzt fehlt nur noch das eigentliche Programm ;)

Hallo Welt


Im Folgenden werde ich ein winziges Programm vorstellen, das lediglich SDL initialisiert und dann auf einen Tastendruck wartet, nur um sich selbst zu beenden.

#include <stdlib.h>
#include <stdio.h>

#include <SDL.h>



Hier werden die obligatorischen Header eingebunden. SDL.h selbst lädt alle anderen Header, die Teil von SDL sind. SDL.h ist also die einzige Headerdatei, die Ihr in Eurem Programm direkt einfügen müßt.

#ifdef _WIN32
#undef main
#endif
int main()
{
    SDL_Surface *screen; /* screen und running werden später verwendet */
    int running;

    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "SDL konnte nicht initialisiert werden: %s\n",
            SDL_GetError());
        exit(1);
    }

    atexit(SDL_Quit);



Die drei Präprozessorbefehle sind zumindest bei mir nötig, um SDL-Programme auf Windows erfolgreich zu kompilieren, da der Windows-Build Probleme aufweist.
Solltet Ihr eine neuere Version von SDL haben, könntet Ihr probehalber die Präprozessorbefehle entfernen und dafür die Bibliothek SDLmain.lib zum Projekt hinzufügen.
Da Windows für mich lediglich eine Nebenplattform ist kann ich das Problem nur schlecht weiter analysieren, und ich wäre für Anregungen dankbar.

Der Befehl SDL_Init() muß in jedem SDL-Programm vor allen anderen SDL-Funktionen aufgerufen werden.

Der einzige Parameter gibt an, welche Teile von SDL verwendet werden. Für dieses Beispiel brauchen wir nur den Graphikteil - der sogenannte Ereignisteil der sich um Tastatur und Maus kümmert wird immer automatisch mitinitialisiert.

Andere Werte, die man per ODER-Verknüpfung angeben kann, sind: SDL_INIT_TIMER, SDL_INIT_AUDIO, SDL_INIT_CDROM, SDL_INIT_JOYSTICK

Das Gegenstück zu SDL_Init() ist SDL_Quit(). SDL_Quit() muß immer am Ende eines Programms aufgerufen werden. Ansonsten kann es z.B. vorkommen, daß der Bildschirmmodus nicht richtig zurückgesetzt wird. Es könnten auch Memoryleaks entstehen, da Speicher nicht ordnungsgemäß freigegeben wird.

Natürlich könntet Ihr SDL_Quit() auch immer manuell aufrufen wenn sich das Programm beendet. Dann wäre es aber notwendig, bei jedem Aufruf von exit() auch SDL_Quit() aufzurufen, und das wird mit der Zeit unübersichtlich.

Deshalb empfiehlt es sich, SDL_Quit mit atexit() zu registrieren. atexit() ist eine simple Funktion der Standard-C-Bibliothek und sorgt dafür, daß die übergebene Funktion beim Programmende aufgerufen wird - ganz egal, ob sich das Programm ordnungsgemäß am Ende von main() oder über exit() verabschiedet. Man kann mit atexit() beliebig viele solcher "Aufräumfunktionen" registrieren.

     screen = SDL_SetVideoMode(640, 480, 0, 0);
    if (!screen) {
        fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n",
            SDL_GetError());
        exit(1);
    }




Hier wird mit SDL_SetVideoMode() ein einfacher Bildschirmmodus gesetzt. Die ersten beiden Parameter geben die Bildschirmauflösung (640x480) an. Der dritte Parameter steht für die Farbtiefe. Wenn hier eine 0 übergeben wird verwendet SDL die momentane oder beste Farbtiefe. Der letzte Parameter ist der flags-Parameter, mit dem eine Menge Optionen verändert werden können.

In diesem Beispiel wechselt SDL nicht in den Vollbildmodus. Dazu müßte das Flag SDL_FULLSCREEN im vierten Parameter übergeben werden. Andere Flags können z.B. verwendet werden, um Double-Buffering anzuschalten. Auf die wichtigsten dieser Flags werde ich in späteren Kapiteln auf jeden Fall eingehen.

Nun folgt das, was sich später einmal zum wichtigsten Teil des Programms mausern wird:

    running = 1;
    while(running) {
        SDL_Event event;

        while(SDL_PollEvent(&event)) {
            switch(event.type) {
            case SDL_KEYDOWN:
                running = 0;
                break;
            case SDL_QUIT:
                running = 0;
                break;
            }
        }
    }




Das ist die Programmschleife. Diese Schleife ruft SDL_PollEvent() auf, um verschiedene Ereignisse abzufragen. Alle möglichen Dinge können Ereignisse hervorrufen: Mausbewegungen und -klicks, Tastatureingaben, etc...

Dieses Programm reagiert - wie bereits gesagt - nur auf Tastendrücke (SDL_KEYDOWN) und auf die Aufforderung, sich zu beenden (SDL_QUIT). Ein SDL_QUIT-Ereignis wird z.B. erzeugt, wenn der Nutzer auf das Schließen-Icon des Fensters klickt. In beiden Fällen beendet sich das Programm.

Es gibt noch viel mehr Ereignistypen, und auf die wichtigsten werde ich noch zu sprechen kommen.

SDL_PollEvent() kehrt immer sofort zum Aufrufer zurück, auch wenn gar kein Ereignis vorhanden ist. Deshalb ist diese Programmschleife eine Busy-Loop, die den Prozessor immer voll auslastet, es sei denn es laufen noch andere Programme. Manchmal ist das nicht wünschenswert, und für solche Fälle gibt es die Funktion SDL_WaitEvent(). Mehr Informationen dazu gibt's in der SDL-Dokumentation. Bei Spielen ist aber die Prozessorauslastung unwichtig, und wir wollen ja möglichst hohe Framerates erreichen. Deswegen habe ich auch hier SDL_PollEvent() verwendet.

Es mag auf den ersten Blick auch nicht ersichtlich sein, warum zwei ineinander verschachtelte while()-Schleifen verwendet werden. Das liegt daran, daß die äußere Schleife später genau einmal pro Frame durchlaufen wird. Es ist aber durchaus möglich, daß mehrere Ereignisse innerhalb eines Frames auftreten. Würde man SDL_PollEvent() nicht in eine Schleife stecken könnte die Ereigniswarteschlange irgendwann voll werden und überlaufen.

        return 0;
}


So, das war's. Den Quellcode zu diesem kleinen Programm gibt's unten auch zum Download.

Ein Wort zu Bildschirmmodi


Leider haben einige Plattformen des öfteren Probleme, wenn es um Bildschirmmodi geht. Besonders X-Windows wechselt die Auflösung und Farbtiefe nur recht ungern. Es kann durchaus passieren, daß SDL z.B. nicht in einen 256-Farben-Vollbildmodus wechseln kann, obwohl die Hardware selbst damit eigentlich gar keine Probleme haben müßte.

Versucht also möglichst immer, für den Spieler einen Rückzugsweg bereitzuhalten (und wenn's nur Fenstermodus ist). In diesem Tutorial werde ich - sofern möglich - immer die Standardfarbtiefe verwenden. Manchmal - z.B. wenn es um Palette- Funktionen geht - ist das natürlich nicht möglich. SDL_ListModes() kann verwendet werden, um die vorhandenen Videomodi im Voraus aufzulisten.

Ende gut, alles gut!


Damit ist das erste Kapitel zu Ende. In den nächsten Kapiteln werde ich Schritt für Schritt neue Funktionen von SDL vorstellen, bis irgendwann einmal alle Teile von SDL ausführlich erklärt sind.
Vielleicht wird ja auch ein kleines Spiel daraus.

Dann war da noch


Der Quellcode zu diesem Tutorial sowie die Makefile und MSVC++-Projektdateien sind zum Download verfügbar (
tar gzipped, 2.1kB)

Dieses Tutorial ist Copyright (c) 2001 Nicolai 'Prefect' Hähnle. Es darf zu nicht-kommerziellen Zwecken beliebig in ungekürzter und unmodifizierter Form vervielfältigt werden, sofern alle dazugehörigen Dateien, ebenfalls unmodifiziert, mitgeliefert werden.
Der mitgelieferte Sourcecode ist Public Domain.

Ihr könnt mich unter
This e-mail address is being protected from spambots. You need JavaScript enabled to view it erreichen. Außerdem lese ich die Coding-Foren von www.thewall.de und hänge zuviel in #thewall.de auf irc.gamesnet.net rum...


{mospagebreak}

SDL-Tutorial #2 - Bitmaps

Im letzten Kapitel haben wir SDL eingerichtet und ein erstes kleines Programm geschrieben. Dieses hat aber nur ein schwarzes Fenster gezeigt und sich dann bei Tastendruck beendet.

In diesem Kapitel werden wir ein Bitmap einladen und auf dem Bildschirm darstellen.

Glücklicherweise hat SDL eine Funktion, die .bmp-Dateien einlädt. Diese Funktion, SDL_LoadBMP() speichert das Bild der Datei dann in einer sogenannten Surface (zu deutsch: Oberfläche).
Eine Surface ist im Prinzip einfach nur ein rechteckiger Bereich, indem sich irgendwelche Grafiken befinden. Sie kann also das Bild aus einer .bmp-Datei enthalten. Genauso könnte sich in einer Surface aber auch ein automatisch vom Programm erzeugtes Muster befinden.
Selbst der Bildschirm, d.h. das, was der Spieler sehen kann, wird im Programm als Surface repräsentiert - diese Bildschirmsurface war schon im letzten Kapitel zu sehen, aber wir haben uns nicht weiter darum gekümmert.

So... jetzt wird es Zeit, das Programm vorzustellen:


#include <stdlib.h>
#include <stdio.h>

#include <SDL.h>

#ifdef _WIN32
#undef main
#endif
int main()
{
    SDL_Surface *screen, *bitmap;
    int running;



Wie Ihr seht brauchen wir einen zweiten Zeiger auf SDL_Surface, bitmap.


    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "SDL konnte nicht initialisiert werden: %s\n", SDL_GetError());
        exit(1);
    }
    atexit(SDL_Quit);

    screen = SDL_SetVideoMode(640, 480, 0, 0);
    if (!screen) {
        fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n", SDL_GetError());
    exit(1);
    }



An der Initialisierungssequenz hat sich nichts geändert.


    bitmap = SDL_LoadBMP("galaxien.bmp");
    if (!bitmap) {
        fprintf(stderr, "Bitmap konnte nicht geladen werden: %s\n",
            SDL_GetError());
        exit(1);
    }




Jetzt laden wir das Bitmap, 'galaxien.bmp'. Die Funktion SDL_LoadBMP() macht glücklicherweise alle Arbeit für uns. Bei Erfolg wird ein Zeiger auf die neue Surface zurückgegeben, bei Fehlern wird 0 zurückgegeben.
SDL_LoadBMP() kann alle unkomprimierten Windows BMP-Dateien laden. Komprimierte Bitmaps werden im Moment nicht unterstützt, und solange sich nicht einer von Euch dazu aufrafft, eine entsprechende Funktion zu schreiben, werden komprimierte Bitmaps wahrscheinlich auch nie unterstützt werden. Hier sieht man recht deutlich die Nachteile (faule Programmierer) und Vorteile (jeder kann Features hinzufügen) von Opensource ;)

Ihr solltet übrigens keine DOS-artigen Pfade, sondern Unix-artige Pfade im Zusammenhang mit SDL_LoadBMP() verwenden. Mit anderen Worten, verwendet: ‘SDL_LoadBMP("pics/dasbild.bmp");’ anstatt ‘SDL_LoadBMP("pics\\dasbild.bmp");’.


    SDL_BlitSurface(bitmap, 0, screen, 0);



Die Funktion SDL_BlitSurface() kopiert Bildausschnitte von einer Surface in eine andere. In unserem Fall wird als zweiter und vierter Parameter 0 übergeben, weil wir das ganze Bitmap kopieren wollen. Normalerweise übergibt man aber einen Zeiger auf zwei SDL_Rect, die die Position und Größe der zu kopierenden Bildausschnitte angeben (wir werden das erst im nächsten Kapitel machen).

SDL_BlitSurface() wandelt verschiedene Bildformate automatisch ineinander um, falls dies notwendig sein sollte. SDL_BlitSurface() kann Bildausschnitte allerdings nicht strecken oder stauchen.


    SDL_UpdateRect(screen, 0, 0, 0, 0);



SDL_UpdateRect() sorgt dafür, daß alle Änderungen, die wir an der Surface 'screen' vorgenommen haben, auch garantiert auf dem Bildschirm erscheinen. Die vier zusätzlichen Parameter geben an, welche Teile der Surface geändert wurden. Wir haben aber die komplette Surface geändert. Dies geben wir an, indem wir alle Parameter auf 0 setzen.


    running = 1;
    while(running) {
        SDL_Event event;

        while(SDL_PollEvent(&event)) {
            switch(event.type) {
            case SDL_KEYDOWN:
                running = 0;
                break;
            case SDL_QUIT:
                running = 0;
                break;
            }
        }
    }

    SDL_FreeSurface(bitmap);

  return 0;
}



An der Programmschleife hat sich nichts geändert. Wir müssen aber, bevor das Programm beendet wird, die Surface 'bitmap' löschen, da ansonsten nicht aller Speicher freigegeben wird. Die screen-Surface sollte man übrigens nicht löschen, da SDL dies automatisch tut.

Ein Hinweis für die MSVC++-Leute


Ihr könnt meine Projektdateien natürlich verwenden, andererseits enthalten sie womöglich absolute Pfadangaben, die mit Eurem System nicht übereinstimmen. Ich selbst habe die Tutorialdateien immer im Verzeichnis c:\develop\sdl-tut\tut2\ usw. SDL ist in c:\develop\sdl\sdl-1.2.2\ installiert.

Da MSVC++ die kompilierte .exe-Datei in einem Unterverzeichnis des Projektverzeichnisses speichert, empfiehlt es sich eigentlich, das Programm beim Testen aus dem eigentlichen Projektverzeichnis heraus zu starten. Ansonsten müßtet Ihr evtl. DLLs und Bitmaps immer in das Verzeichnis kopieren, in dem sich die .exe befindet.
Um das Startverzeichnis beim Debuggen von MSVC++ aus zu ändern geht Ihr ins Menü Projekt -> Einstellungen... und wählt Debug-Einstellungen. Dort gibt es ein entsprechendes Eingabefeld.

Dann war da noch


Der Quellcode zu diesem Tutorial sowie die Makefile und MSVC++-Projektdateien und das Beispielbild galaxien.bmp sind zum Download verfügbar (
tar gzipped, 717kB)

Das von mir verwendete Bild "galaxien.bmp" stammt übrigens vom Hubble Space Telescope, und kann über die Homepage der NASA
NASA heruntergeladen werden. Dort findet Ihr auch die Bestimmungen zur Nutzung des Bildes.

Dieses Tutorial ist Copyright (c) 2001 Nicolai 'Prefect' Hähnle.
Es darf zu nicht-kommerziellen Zwecken beliebig in ungekürzter und unmodifizierter Form vervielfältigt werden, sofern alle dazugehörigen Dateien, ebenfalls unmodifiziert, mitgeliefert werden.
Der mitgelieferte Sourcecode ist Public Domain.

Ihr könnt mich unter
This e-mail address is being protected from spambots. You need JavaScript enabled to view it erreichen. Außerdem lese ich die Coding-Foren von http://www.thewall.de/ und hänge zuviel in #thewall.de auf irc.gamesnet.net rum...


{mospagebreak}

SDL-Tutorial #3 – Mehr Bitmaps

Im letzten Kapitel des Tutorials haben wir ein simples Bitmap auf den Bildschirm gebracht. In diesem Kapitel werden wir nun das Bild aus mehreren kleineren Bitmaps zusammensetzen.

Die Vision


Ich will in diesem Tutorial Schritt für Schritt ein kleines Weltraumspiel aufbauen, in dem verschiedene Spieler (vielleicht ja auch übers Netzwerk, wer weiß...) gegeneinander antreten.

Der Weltraum ist, wie wir ja wissen, nicht einfach schwarz. Man sieht viel mehr im Hintergrund verschiedene Sterne und Galaxien, die natürlich auch entsprechend hin- und herscrollen, wenn man sich bewegt. Dieses Scrollen ist zwar völlig unsinnig - vom physikalischen Standpunkt betrachtet - aber es gehört zum richtigen Feeling einfach dazu.

Natürlich könnte man nun versuchen, einfach ein Bild wie das, was ich im letzten Kapitel verwendet habe, als Hintergrund einzusetzen. Allerdings wird man sehr schnell auf Probleme stoßen, denn dieses Bild eignet sich nicht zum "Kacheln": Wenn man das Bild zweimal direkt nebeneinander sieht ergibt sich ein ziemlich deutlicher Einschnitt und damit kein fließender Hintergrund. Das Kacheln ist aber notwendig, wenn sich der Hintergrund bewegen soll.

Natürlich könnte man mit entsprechend viel Mühe und einem entsprechenden Graphikprogramm ein Bild erstellen, das auch gekachelt noch gut aussieht. Meine Lösung (und nicht nur meine - andere Leute haben sie vor mir verwendet...) zu diesem Problem hat aber drei Vorteile: Erstens paßt sie besser in dieses Tutorial. Zweitens ist sie abwechslungsreicher. Und zu guter letzt erfordert sie weniger zeichnerisches Talent.

Diese Lösung sieht so aus: Wir bauen uns einen Satz Bilder verschiedener Sterne und Galaxien. Diese stellt dann das Programm rein zufällig zur Laufzeit zusammen.

Der Plan


Es gibt natürlich mehrere Möglichkeiten, diese Idee umzusetzen. Man kann zum Beispiel für jeden Galaxientyp ein eigenes Bitmap erstellen, und diese Bitmaps dann alle zur Laufzeit laden. Alternativ kann man alle Galaxientypen in ein einziges, großes Bitmap stecken. Ich habe mich für die zweite Option entschieden.

Als erstes muß unser neues Bitmap "galaxien.bmp" erstellt werden. Der Einfachkeit halber werden die Galaxientypen in einer horizontalen Reihe angeordnet, und jeder Galaxientyp nimmt genau 16x16 Pixel des Bitmaps ein. Die überflüssigen Randflächen sollen einfach schwarz sein. Das Ganze sieht dann so aus:



Übrigens hat das noch einen zusätzlichen Vorteil: Der Code wird so aufgebaut sein, daß er automatisch die Größe der "galaxien.bmp" ermittelt, und daraus die Anzahl der Galaxientypen ermittelt. Ihr könnt dann also neue Typen hinzufügen, ohne irgendetwas am Sourcecode zu ändern.

In diesem Kapitel werde ich zunächst den Code vorstellen, der zufällig die Positionen der Galaxien einstellt und diese dann darstellt. Das Scrollen muß bis zum nächsten Kapitel warten.

Da wir ab dem nächsten Kapitel - in dem Scrolling eingeführt wird - den Hintergrund immer wieder neu zeichnen müssen, müssen wir die Koordinaten am Anfang des Programms berechnen und dann in einem Array abspeichern. Das Berechnen wird in einer getrennten Funktion ablaufen. Dadurch wird der Code übersichtlicher. Zudem benötigen wir noch eine Funktion, die dann den Hintergrund auch tatsächlich darstellt.

Die Umsetzung


So, genug geredet. Jetzt wird es Zeit für ein bißchen Quellcode:


#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include <SDL.h>

SDL_Surface *g_pSurfScreen; /* Der Bildschirminhalt */
SDL_Surface *g_pSurfGalaxies; /* galaxien.bmp */

Uint32 g_Black; /* Farbwert schwarz */



Auch wenn mich Verfechter von OOP dafür lynchen werden - ich definiere die Surfaces ab jetzt als Globals. Da machen sie in unserem Fall auch wirklich mehr Sinn.

Wir benötigen auch eine zusätzliche Headerdatei "time.h" und eine neue Variable g_Black. Dazu komme ich aber später noch.

Als nächstes kommt die Struktur, in der wir die Positionen der Galaxien speichern werden.


/* Wie viele Galaxien sind auf dem Bildschirm? */
#define NUM_GALAXIES        50

struct galaxy {
    int x, y; /* Koordinaten der Galaxie */
    int type; /* Welches Bild wird für die Galaxien verwendet? */
};

struct galaxy g_Galaxies[NUM_GALAXIES];




Dies ist hoffentlich selbsterklärlich. Für jede Galaxie speichern wir zunächst ihre Koordinaten auf dem Bildschirm und den "Typ". Der Typ ist einfach der Index für das Bild der Galaxie. Der Galaxie ganz links im Bitmap "galaxien.bmp" wird der Typ 0 zugeordnet, der rechts daneben der Typ 1 und so weiter.

Ich habe die Anzahl der Galaxien auf dem Bildschirm konstant gesetzt um den Code einigermaßen einfach zu halten. Wer eine Herausforderung sucht, kann diese Zahl ja dynamisch machen (per Zufallszahl gewählt und unter Verwendung von Heapfunktionen wie malloc() und free()).


void RandBackground()
{
    int i;
    int num_types;

    num_types = g_pSurfGalaxies->w / 16;

    for(i = 0; i < NUM_GALAXIES; i++) {
        g_Galaxies[i].x = rand() % 640;
        g_Galaxies[i].y = rand() % 480;
        g_Galaxies[i].type = rand() % num_types;
    }
}



Die Funktion RandBackground() füllt das Galaxien-Array mit den benötigten Zufallswerten.

Zuerst bestimmen wir aber die Anzahl an verschiedenen Galaxientypen. Das geht eigentlich recht einfach: g_pSurfGalaxies (also die Surface, die das Bitmap "galaxien.bmp" enthält) ist natürlich vom Typ SDL_Surface, und SDL_Surface ist eine einfache C-Struktur, die offen zugänglich ist. Sie ist in einer der Headerdateien von SDL (SDL_video.h) so definiert:


typedef struct SDL_Surface {
        Uint32 flags;                           /* Read-only */
        SDL_PixelFormat *format;                /* Read-only */
        int w, h;                               /* Read-only */
        Uint16 pitch;                           /* Read-only */
        void *pixels;                           /* Read-write */
        int offset;                             /* Private */

        /* Hardware-specific surface info */
        struct private_hwdata *hwdata;

        /* clipping information */
        SDL_Rect clip_rect;                     /* Read-only */
        Uint32 unused1;                         /* for binary compatibility */

        /* Allow recursive locks */
        Uint32 locked;                          /* Private */

        /* info for fast blit mapping to other surfaces */
        struct SDL_BlitMap *map;                /* Private */

        /* format version, bumped at every change to invalidate blit maps */
        unsigned int format_version;            /* Private */

        /* Reference count -- used when freeing surface */
        int refcount;                           /* Read-mostly */
} SDL_Surface;



Die wichtigsten Membervariablen dieser Struktur sind w und h - Breite und Höhe der Surface. Ansonsten sind auch die Membervariablen format, die Informationen über den verwendeten Farbmodus enthält, und pixels, ein Zeiger auf die Graphikdaten interessant. Auf diese beiden werde ich später noch zu sprechen kommen.

Die Anzahl der Galaxientypen ist also ganz einfach die Breite des Bitmaps geteilt durch 16, da ja jeder Galaxientyp genau 16 Pixel breit ist, und die Galaxientypen in einer waagrechten Reihe angeordnet sind.

Als nächstes werden die Positionen der Galaxien vom Zufallsgenerator erzeugt. Für den x-Anteil wird ein Wert von 0 bis 639 erzeugt, für den y-Anteil ein Wert von 0 bis 479.

Auch der Typ der Galaxie wird zufällig ausgewählt. rand() gibt eine Zahl von 0 bis RAND_MAX zurück. RAND_MAX ist eine Präprozessorkonstante, die auf jedem System unterschiedlich sein kann. Typischerweise wird RAND_MAX auf die größte positive Zahl gesetzt, die ein (int) annehmen kann (2^31-1). Um auf eine Zufallszahl zwischen 0 und num_types-1 zu kommen, wird der Modulooperator '%' verwendet. Der Modulooperator gibt den Rest einer ganzzahligen Divison zurück.


void DrawBackground()
{
    int i;

    for(i = 0; i < NUM_GALAXIES; i++) {
        SDL_Rect src;
        SDL_Rect dest;

        src.x = g_Galaxies[i].type * 16;
        src.y = 0;
        src.w = 16;
        src.h = 16;

        dest.x = g_Galaxies[i].x;
        dest.y = g_Galaxies[i].y;

        SDL_BlitSurface(g_pSurfGalaxies, &src, g_pSurfScreen, &dest);
    }
}



Die Funktion DrawBackground() ist dafür zuständig, die Galaxien auf dem Bildschirm darzustellen. Logischerweise ist die Funktion als eine Schleife aufgebaut, die nacheinander alle Galaxien auf den Bildschirm bringt.

Die Funktion SDL_BlitSurface() erwartet neben der Quell- und Zielsurface auch noch die Position und Größe des Quellrechtecks (src) sowie die Position des Zielrechtecks (dest). Da die Funktion SDL_BlitSurface() keine Streckung oder Dehnung vornimmt, werden die Größenangaben des Zielrechtecks ignoriert.

Da jeder Galaxientyp im Quellbitmap genau 16x16 Pixel groß ist und kein Zwischenraum vorhanden ist lassen sich die Quellkoordinaten durch eine einfache Multiplikation berechnen. Die Zielkoordinaten werden einfach aus dem zuvor in RandBackground() gefüllten Array genommen.


#ifdef _WIN32
#undef main
#endif
int main()
{
    int running;

    srand(time(0));



Wir verwenden in unserem Programm den Pseudo-Zufallszahlengenerator der Standard-C-Bibliothek. Dieser Zufallszahlengenerator basiert auf recht einfachen Berechnungen, die lediglich scheinbar zufällige Zahlen erzeugt. Nachdem das Programm geladen ist ist dieser Zufallszahlengenerator immer mit der gleichen Zahl initialisiert. Das heißt auch, daß die Funktion rand() immer die gleiche Zahlenfolge zurückgibt.

Um dies zu vermeiden müssen wir den Zufallsgenerator mit einem Wert initialisieren, der bei jedem Programmstart unterschiedlich ist. Dazu eignet sich natürlich die momentane Systemzeit, die von der Funktion time() zurückgegeben wird. Die Funktion time() gibt die Anzahl der Sekunden, die seit der "Epoch" (d.h. seit dem 1.1.1970 um 00:00 UTC) verstrichen sind, zurück. Da die Funktion time() in der Headerdatei time.h deklariert ist, habe ich schon oben die entsprechende #include-Direktive eingefügt.


    if (SDL_Init(SDL_INIT_VIDEO) g_pSurfScreen = SDL_SetVideoMode(640, 480, 0, 0);
    if (!g_pSurfScreen) {
        fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n",
            SDL_GetError());
        exit(1);
    }

    g_pSurfGalaxies = SDL_LoadBMP("galaxien.bmp");
    if (!g_pSurfGalaxies) {
        fprintf(stderr, "galaxien.bmp konnte nicht geladen werden: %s\n",
            SDL_GetError());
        exit(1);
    }

    RandBackground();



Die Initialisierung sollte inzwischen ja geläufig sein. Nachdem das Galaxienbitmap geladen wurde, wird die Funktion RandBackground() aufgerufen, die die Galaxien im Hintergrund initialisiert.


    g_Black = SDL_MapRGB(g_pSurfScreen->format, 0, 0, 0);
    SDL_FillRect(g_pSurfScreen, 0, g_Black);




Dieser Code löscht den Bildschirm, indem er ein schwarzes Rechteck über die gesamte Bildschirmfläche zieht. Bis jetzt war der Bildschirm nach einem Aufruf von SDL_SetVideoMode() zwar sowieso immer schwarz, allerdings wird dies von SDL nicht garantiert. Es ist also eine gute Idee, den Bildschirm zu löschen, und in späteren Kapiteln werden wir sowieso nicht mehr darum herumkommen.

Die Funktion SDL_FillRect() hat aber eine Eigenheit: Sie erwartet die Farbe nicht in der Form eines RGB-Triplets, sondern im Pixelformat der Zielsurface. Und dieses Pixelformat ist natürlich je nach Farbtiefe, Palette und sogar Grafikkarte unterschiedlich. Wir müssen also irgendwie unser RGB-Triplet (0,0,0) in das momentane Pixelformat bringen.

Glücklicherweise stellt SDL dafür die Funktion SDL_MapRGB() zur Verfügung. Diese Funktion erwartet einerseits das RGB-Triplet selbst, anderseits aber natürlich auch das Pixelformat, in das dieses RGB-Triplet umgewandelt werden soll. In SDL gibt es die Struktur SDL_PixelFormat, die alle notwendigen Daten enthält. Diese Struktur ist offen zugänglich, ich werde mich aber erst später mit den Details befassen. Wichtig ist im Moment nur, daß jede Surface eine Membervariable namens 'format' hat, die auf so eine SDL_PixelFormat-Struktur zeigt. Und natürlich übergeben wir das Pixelformat des Bildschirms an die Funktion SDL_MapRGB().

Übrigens gibt es auch die Funktion SDL_GetRGB(), die eine Farbe im Pixelformat wieder in ein RGB-Triplet zurückwandelt. Sie wird aber nur selten verwendet.

Ich werde mich in einem späteren Kapitel noch detailliert mit den verschiedenen Pixelformaten auseinandersetzen, wenn es darum geht, per Hand Pixel auf eine Surface zu schreiben.


    DrawBackground();

    SDL_UpdateRect(g_pSurfScreen, 0, 0, 0, 0);

    running = 1;
    while(running) {
        SDL_Event event;

        while(SDL_PollEvent(&event)) {
            switch(event.type) {
            case SDL_KEYDOWN:
                running = 0;
                break;
            case SDL_QUIT:
                running = 0;
                break;
            }
        }
    }

    SDL_FreeSurface(g_pSurfGalaxies);

    return 0;
}



Der Rest ist schnell erklärt: Das Programm ruft die Funktion DrawBackground() auf, die den Hintergrund zeichnet und bereits besprochen wurde. Nach wie vor ist ein Aufruf von SDL_UpdateRect() notwendig, um sicherzustellen, daß die Änderungen auch tatsächlich auf den Bildschirm übertragen werden.

Dann kommt noch die bereits bekannte Hauptschleife des Programms, und zum Schluß wird die Surface, die das Bitmap "galaxien.bmp" enthält, freigegeben.

So.. dieses Kapitel ist wieder ein bißchen länger geworden. Im nächsten Kapitel werde wir den Hintergrund dann zum Scrollen bewegen.

Dann war da noch


Der Quellcode zu diesem Tutorial sowie die Makefile, MSVC++-Projektdateien und das Galaxienbitmap galaxien.bmp sind zum Download verfügbar (
tar-gzipped, 3.53kB).

Das von mir verwendete Galaxienbitmap entstammt übrigens einzelnen Galaxien aus dem Bild vom letzten Kapitel.

Dieses Tutorial ist Copyright (c) 2001 Nicolai 'Prefect' Hähnle.
Es darf zu nicht-kommerziellen Zwecken beliebig in ungekürzter und unmodifizierter Form vervielfältigt werden, sofern alle dazugehörigen Dateien, ebenfalls unmodifiziert, mitgeliefert werden.
Der mitgelieferte Sourcecode ist Public Domain.

Ihr könnt mich unter
This e-mail address is being protected from spambots. You need JavaScript enabled to view it erreichen. Außerdem lese ich die Coding-Foren von http://www.thewall.de/ und hänge zuviel in #thewall.de auf irc.gamesnet.net rum...


{mospagebreak}

SDL-Tutorial #4 - Und es bewegt sich doch!

Bis jetzt haben wir zwar schon einiges über Bitmaps gelernt, aber es hat sich noch nie etwas auf dem Bildschirm bewegt. Und ohne Bewegung sind Spiele ziemlich witzlos, oder?

In diesem Kapitel zeige ich Euch, wie Ihr das Programm aus dem letzten Kapitel zum Scrollen bringt. Übrigens werde ich hier kein komplettes Programmlisting mehr hereinstellen, sondern beziehe mich nur noch auf Zeilennummern und Funktionen aus dem Programm des letzten Kapitels.

Der erste Versuch


Zuerst wollen wir per Tastatur scrollen.
Schon die letzten Kapitel haben ja eigentlich auf die Tastatur reagiert: sie haben sich bei Tastendruck einfach beendet. Der zugehörige Programmteil befindet sich in dem switch(event.type)-Statement. Wir könnten da ja eigentlich Code reinschreiben, der überprüft, welche Taste eigentlich gedrückt wurde, und dann entsprechend scrollen. Genau das tun wir jetzt auch erstmal.
Als erstes aber muß eine, oder eigentlich zwei, Variablen her, in denen die momentane Position gespeichert wird. Fügen wir also zwei Integervariablen scrollx und scrolly in main() ein, in denen jeweils die X- und Y-Position gespeichert wird. Ich habe die Variablen um Zeile 74 herum eingefügt:


{
    int running;
    int scrollx, scrolly;




Natürlich müssen diese Variablen vor der Programmschleife noch mit vernünftigen Werten initialisiert werden, deshalb habe ich den Code um Zeile 120 entsprechend modifiziert:


    running = 1;
    scrollx = scrolly = 0;
    while(running) {



Zu Beginn befindet sich der Spieler an der Position (0/0). Ich verwende nun für diese Position ein Koordinatensystem, wie es beim Computer seit Urzeiten verwendet wird - wie es aber für Mathematiker unverständlich ist.

In der Mathematik gibt es das sogenannte kartesische Koordinatensystem. Das ist kein Zauberwort, sondern einfach das KOSY mit den beiden Achsen (x und y), wie es im Mathematikunterricht andauernd verwendet wird. Bei diesem KOSY befinden sich positive x-Werte rechts vom Ursprung, und positive y-Werte befinden sich oberhalb des Ursprungs.

Am Computer wird üblicherweise ein anderes KOSY verwendet, daß aber zumindest aus Grafikprogrammen allen geläufig sein müßte: Die obere linke Ecke des Bildschirms wird mit (0/0) bezeichnet. Nach rechts nehmen die x-Werte zu - genauso wie beim kartesischen System. Allerdings nehmen die y-Werte nach unten hin zu - das heißt, auf dem sichtbaren Bereich des Bildschirms sind die Koordinaten immer positiv.

Ich habe die beiden Koordinatensysteme hier in einem kleinen Schema noch mal illustriert:



Wie ich schon gesagt habe, lehnen sich die Zahlenwerte der Scrollposition an das beim Computer übliche KOSY an. Wenn man nach unten scrollt nehmen die y-Werte also zu, und wenn man nach rechts scrollt nehmen die x-Werte zu.

Als nächstes müssen wir Tastendrücke abfangen und die Scrollposition entsprechend abändern. Dazu erweitern wir, wie schon angesprochen, das switch()-Statement, das die Ereignisse verarbeitet. Es sollte nun wie folgt aussehen:


            switch(event.type) {
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    running = 0;
                    break;

                case SDLK_LEFT:
                    scrollx -= 10;
                    break;
                case SDLK_RIGHT:
                    scrollx += 10;
                    break;
                case SDLK_UP:
                    scrolly -= 10;
                    break;
                case SDLK_DOWN:
                    scrolly += 10;
                    break;

                default:
                    break;
                }
                break;

            case SDL_QUIT:
                running = 0;
                break;
            }



Wir ändern nichts an der Bearbeitung von SDL_QUIT. Wenn aber ein Ereignis vom Typ SDL_KEYDOWN eintrifft, überprüfen wir, was denn überhaupt für eine Taste gedrückt wurde. Glücklicherweise wird diese Information in der Struktur SDL_Event mitgeliefert. event.key enthält mehr Informationen zu Tastaturereignissen (es gibt auch noch SDL_KEYUP, wenn eine Taste wieder losgelassen wurde). event.key.keysym beschreibt die Taste, die gedrückt wurde.

keysym selbst hat wieder mehrere Membervariablen.
Die wichtigste ist 'sym'. Sie enthält eine (von SDL definierte) Konstante, die angibt, welche Taste gedrückt wurde. Eine vollständige Liste dieser Konstanten könnt Ihr in der SDL-Dokumentation unter Events -> SDL Event Structures -> SDLKey finden. 'sym' gibt aber wirklich nur die gedrückte Taste zurück. Das heißt, 'sym' unterscheidet nicht, ob der Benutzer einfach nur 'a' gedrückt hat, oder ob er auch noch die Shift-Taste gedrückt hält. Laßt Euch nicht davon aus dem Konzept bringen, daß es auch Konstanten z.B. für das Dollarzeichen gibt. Auf einer deutschen Tastatur ist das '$' die Tastenkombination Shift-4, und dementsprechend gibt SDL in 'sym' ein SDLK_4, und kein SDLK_DOLLAR zurück.

Die Membervariable 'mod' ist ein Bitfeld und gibt an, welche Modifiertasten (also Shift, Strg, etc...) gerade gedrückt sind. Für diese Modifiertasten erzeugt SDL nämlich nicht unbedingt Ereignisse (auf X-Windows werden auf jeden Fall keine erzeugt - auf anderen Plattformen verhält sich SDL vielleicht anders).

Ein anderes praktisches Feature ist die Membervariable 'unicode'. Um dieses Feature zu nutzen, muß zuerst die Funktion SDL_EnableUNICODE() aufgerufen werden. Danach liefert die Membervariable 'unicode' den Zeichenwert der gedrückten Taste, so wie er auf dem Bildschirm ausgegeben werden sollte. Aus Shift+A wird dann zum Beispiel 'A', währen aus A alleine 'a' wird. Das ist recht nützlich wenn Ihr in Euer Spiel auch eine Art Konsole oder graphische Oberfläche einbauen wollt.

In unserem Fall verwenden wir also die Membervariable sym, um die Taste zu ermitteln. Beim Druck von Escape beendet sich das Programm, beim Druck der Pfeiltasten ändert sich die Scrollposition.

Natürlich nützt es nicht viel, wenn sich die zwei Variablen scrollx und scrolly ändern, der Bildschirminhalt aber nicht verändert wird. Deshalb verschieben wir jetzt die Funktionsaufrufe, die zum Bildschirmaufbau verwendet werden, in die Hauptschleife.

Der Programmteil oberhalb der Hauptschleife sollte nun so aussehen:


    g_Black = SDL_MapRGB(g_pSurfScreen->format, 0, 0, 0);

    running = 1;
    scrollx = scrolly = 0;



Dafür sieht das Ende der Hauptschleife wie folgt aus:


        }

        SDL_FillRect(g_pSurfScreen, 0, g_Black);

        DrawBackground(scrollx, scrolly);

        SDL_UpdateRect(g_pSurfScreen, 0, 0, 0, 0);
    }

    SDL_FreeSurface(g_pSurfGalaxies);
        return 0;
}



Beachtet bitte, daß sich diese drei Funktionsaufrufe in der äußeren Schleife und nicht in der Ereignisschleife befinden müssen. Jetzt kann man auch schon den äußeren Anblick eines jeden SDL-Spiels ganz gut erkennen:

Da ist zunächst mal eine große Schleife, die genau einmal pro Frame ausgeführt wird. Diese Schleife enthält zunächst einmal eine zweite, kleinere Schleife, in der Ereignisse bearbeitet werden. Darauf folgt dann der Code, um den Bildschirm zu füllen.

Beim Darstellen selbst löschen wir den Bildschirm zunächst mit Schwarz, da ja nicht alle Bereiche des Bildschirms überschrieben werden. Wer das nicht macht erhält zwar vielleicht einen netten Matrix-Effekt, aber kein akzeptables Spiel.

Euch wird sicher aufgefallen sein, daß ich den Aufruf zu DrawBrackground() leicht verändert habe: die momentane Scrollposition wird übergeben. Es ist ja ganz logisch, daß DrawBackground() angepaßt werden muß. Dazu erstmal ein kleines bißchen Theorie vorweg:

Stellt Euch den Galaxienhintergrund einmal wie eine gekachelte Wand vor, bei der auf jeder Kachel genau das gleiche Bild ist. Der Bildschirm wäre ein Rahmen, der kleiner ist als die Kacheln. Die Scrollposition gibt an, an welcher Position sich dieser Rahmen befindet.


Wenn man nun die Koordinaten in diesem Bild in etwa abschätzt, so befindet sich der erste (dunkelgrüne) Rahmen in diesem Bild an Scrollposition (0/0), und die Galaxie befindet sich an der Position (500/400) innerhalb der Kacheln. Der zweite (hellgrüne) Rahmen befindet sich an der Scrollposition (450/200), und wie Ihr sicher erkennen könnt, ist die Galaxie relativ zu diesem Rahmen dann an der Position (50/200). Man erkennt leicht, daß man die Scrollposition von den Koordinaten der Galaxie abziehen muß, wenn man die Position der Galaxie relativ zum Bildschirm erhalten will.

Wir müssen nun also zunächst den Funktionskopf von DrawBackground() anpassen, so daß er wie folgt aussieht:


void DrawBackground(int scrollx, int scrolly)
{



Ich habe lediglich die beiden Parameter scrollx und scrolly hinzugefügt.

Jetzt müssen wir unsere Erkenntnisse über die Galaxienposition noch anwenden, das heißt wir müssen den Code ändern, der die Position der Galaxie auf dem Bildschirm bestimmt. Dieser ist gerade mal zwei Zeilen lang - er füllt das Zielrechteck mit Werten. Der Code um Zeile 63 herum wird also so geändert:


        dest.x = g_Galaxies[i].x - scrollx;
        dest.y = g_Galaxies[i].y - scrolly;

        SDL_BlitSurface(g_pSurfGalaxies, &src, g_pSurfScreen, &dest);



An dieser Stelle könnt Ihr das Programm kompilieren und ausprobieren.

Ihr werdet ziemlich schnell auf zwei üble Probleme stoßen:
Das Programm scrollt nur wenn man eine Pfeiltaste drückt. Läßt man die Taste länger gedrückt wird nicht weitergescrollt.
Sobald man ein bißchen gescrollt hat, erscheinen keine Galaxien mehr im Hintergrund.
Natürlich kann das so nicht bleiben...

Gedrückt oder nicht gedrückt?


Eigentlich ist der Grund für das erste Problem recht naheliegend. SDL erzeugt ja nur ein Tastendruck-Ereignis, wenn die Taste tatsächlich gedrückt wurde.

Anmerkung:
Über die Funktion SDL_EnableKeyRepeat() kann man SDL auch dazu bringen, SDL_KEYDOWN-Ereignisse erneut zu erstellen, wenn eine Taste gedrückt bleibt - genauso wie bei Terminals und Texteditoren. In unserem Fall gibt es aber eine bessere Lösung.

SDL erzeugt aber nicht nur ein SDL_KEYDOWN-Ereignis, es erzeugt auch ein SDL_KEYUP-Ereignis wenn eine Taste wieder losgelassen wird. Man könnte also bei einem SDL_KEYDOWN-Ereignis eine Variable setzen, und diese bei SDL_KEYUP wieder löschen. Dann müßte man nur noch innerhalb der Hauptschleife auf diese Variable prüfen.

Es geht aber noch einfacher: SDL merkt sich selbst, welche Tasten zur Zeit gedrückt sind und welche nicht. Diese Information kann, in Form eines Arrays vom Typ Uint8, über die Funktion SDL_GetKeyState() abgerufen werden. Das zurückgegebene Array ist in einem globalen Speicherbereich und wird über die SDLK-Konstanten, die wir schon im Zusammenhang mit Ereignissen kennengelernt haben, indiziert. Dadurch ist die Funktion SDL_GetKeyState() sehr einfach und komfortabel zu verwenden.
Übrigens behält SDL diese Informationen auch immer im Speicher, und ein Aufruf von SDL_GetKeyState() kostet praktisch keine Rechenzeit.

Genug der Vorrede. Entfernen wir also die vier case-Zweige, die bis jetzt fürs Scrolling gesorgt haben. Das switch(event.key.keysym.sym)-Statement bei Zeile 120 sollte nun so aussehen:


                switch(event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    running = 0;
                    break;

                default:
                    break;
                }



Wir brauchen noch einen Zeiger, in dem wir den Zeiger auf das Array, der von SDL_GetKeyState() zurückgegeben wird, speichern. Diesen Zeiger - ich nennen ihn sinnigerweise 'keystate' - müssen wir natürlich am Anfang der Hauptschleife (bei Zeile 114) definieren:


    while(running) {
        SDL_Event event;
        Uint8 *keystate;



Folgt nur noch der Code, der dann letztendlich SDL_GetKeyState aufruft und scrollt. Ich habe ihn nach der Ereignisschleife, also bei Zeile 138 eingefügt:


        }

        keystate = SDL_GetKeyState(0);
        if (keystate[SDLK_LEFT])
            scrollx -= 10;
        if (keystate[SDLK_RIGHT])
            scrollx += 10;
        if (keystate[SDLK_UP])
            scrolly -= 10;
        if (keystate[SDLK_DOWN])
            scrolly += 10;



Man kann SDL_GetKeyState() übrigens einen Zeiger auf einen Integer übergeben. In diesem Integer speichert SDL dann die Länge des zurückgegebenen Arrays. Andererseits erstreckt sich dieses Array immer mindestens über alle SDLK-Konstanten. Wir können ihn also ignorieren.

Danach folgen einfache if()-Statements, die die verschiedenen Tasten überprüfen. Die Elemente des keystate-Arrays sind immer 0, wenn die Taste nicht gedrückt ist. Wenn sie aber ungleich 0 ist, ist die Taste gedrückt, und wir verändern die Scrollposition entsprechend.

So.. das erste Problem wäre behoben.

Unendliche Weiten


Leider erstreckt sich der Sternenhintergrund immer noch nicht weiter als über einen Bildschirm. Der Grund dafür ist ja eigentlich ganz logisch:

Wenn der Spieler 640 Pixel nach links gescrollt ist ist die Scrollposition bei (-640/0). Die Galaxienkoordinaten liegen aber im Bereich von 0 bis 640. Wenn jetzt gescrollt wird, liegen die neu berechneten Galaxienkoordinaten zwischen 640 und 1280 - also außerhalb des Bildschirms.

Eine Möglichkeit, dieses Problem zu umgehen, wäre, die Koordinaten in einer while()-Schleife so lange anzupassen, bis sie wieder im richtigen Bereich sind. Eine Schleife ist notwendig, da der Spieler ja beliebig weit herausscrollen kann.

Die while()-Schleife wäre aber eine sehr unschöne Lösung, und es geht in der Tat besser. Die nächstbessere Lösung ist die Verwendung des Modulooperators %. Dieser gibt ja den Rest einer Division zurück. Wenn wir nun einfach die Koordinaten, die SDL_BlitSurface() übergeben werden, modulo 640 bzw. 480 berechnen, sollten ja eigentlich nur Werte im Rahmen herauskommen.

Aber auch diese Lösung hat einen Haken: der Rest der Division einer negativen Zahl ergibt wiederum eine negative Zahl. Wenn wir also nach rechts scrollen wäre die SDL_BlitSurface() übergebene Koordinate immer noch negativ.

Man könnte nun versuchen, negative Werte auf positive zu spiegeln, aber das ergibt ein auffallend symmetrisches Bild am Punkt (0/0). Besser wäre es, die negativen Werte ganz los zu werden. Und genau das werden wir tun. Diese Lösung mag etwas esoterisch erscheinen und auf den ersten Blick nicht ganz einleuchtend. Dennoch solltet Ihr wirklich versuchen, sie im Innersten zu verstehen.

Dazu muß man wissen, wie der Computer Zahlen im Binärformat darstellt. Wie eigentlich bekannt sein sollte, wird ja aus einer Zahl wie z.B. 9 die Binärzahl 0000000000001001. Ich verwende bewußt 32 Binärziffern, denn wir verwenden ja 32bit-Zahlen (Anm. von TTT: Nunja, da kann wohl einer nicht zählen =)). Der Clou ist nun, wie der Prozessor negative Zahlen darstellt. Um eine lange Geschichte sehr stark abzukürzen, -9 wird als 1111111111110111 dargestellt. Man stellt fest, daß


  10000000000000000 (das heißt 2^32)
- 00000000000001001 (dezimal: 9)
------------------
   1111111111110111 (dezimal: -9)



Würden wir die binäre Repräsentation der Zahl -9 als positive Zahl verstehen, so würde sich 2^32 - 9 ergeben. Wenn Ihr Euch das nochmal im Kopf herumgehen laßt, so sollte Euch auch klar werden, warum ein 'unsigned int' Zahlen von 0 bis 2^32 - 1, ein 'signed int' dagegen aber Zahlen von -2^31 bis 2^31 - 1 speichern kann.

Der Trick ist also, daß wir die Koordinate der Galaxie zunächst in ein 'unsigned int' casten, und erst dann die Modulooperation durchführen. Wenn wir jetzt aber einfach Modulo 640 bzw. 480 anwenden ergibt sich leider wieder ein neues Problem: (2^32 - 9) % 640 ist nämlich 247. Allerdings sollte sich -9 ja nur 9 Pixel rechts vom rechten Rand des Bildschirms befinden. Eigentlich müßte also 631 herauskommen. Daß dies nicht der Fall ist liegt daran, daß 640 kein Teiler von 2^32 ist.

Bisher haben wir den Galaxienhintergrund ja immer auf einen Bereich von 640x480 verteilt. Wir werden nun die Grenzen dieses Bereichs auf die nächsten 2er-Potenzen, also 1024x512 aufrunden müssen. Das ist aber eigentlich kein Problem, im Gegenteil, es hat sogar noch zwei Vorteile: erstens wiederholt sich der Hintergrund nicht ganz so häufig. Den zweiten Grund werdet Ihr erst später zu würdigen wissen ;)

Nach so viel trockenem Text, der aber leider notwendig war, wird es Zeit, das gesammelte Wissen umzusetzen. Zunächst muß RandBackground() so verändert werden, daß sich der Hintergrund auf einen Bereich von 1024x512 erstreckt. Wir verändern den Code von RandBackground() also so, daß die innere Schleife wie folgt aussieht:


    for(i = 0; i g_Galaxies[i].x = rand() % 1024;
        g_Galaxies[i].y = rand() % 512;
        g_Galaxies[i].type = rand() % num_types;
    }



Entsprechend muß dann die Berechnung der Zielkoordinaten für SDL_BlitSurface() in DrawBackground() geändert werden. Dies sieht dann so aus:


        dest.x = (unsigned int)(g_Galaxies[i].x - scrollx) % 1024;
        dest.y = (unsigned int)(g_Galaxies[i].y - scrolly) % 512;

        SDL_BlitSurface(g_pSurfGalaxies, &src, g_pSurfScreen, &dest);




Durch die Erweiterung des Hintergrundsbereiches hat sich natürlich die Dichte des Hintergrunds geändert. Je nach Geschmack ist es also angebracht, die Präprozessorkonstante NUM_GALAXIES in Zeile 20 anzupassen.

Das Scrolling sieht nun so weit schon ganz gut aus, hat aber leider immer noch einen kleinen Schönheitsfehler. Wenn Ihr langsam genug scrollt (evtl. müßt Ihr den Code in der Hauptschleife so verändern, daß das Scrolling langsamer läuft) werdet Ihr feststellen, daß eine Galaxie am linken bzw. oberen Rand des Bildschirms ganz plötzlich verschwindet bevor sie ganz herausscrollt.

Auch das hat seinen logischen Grund: Angenommen, eine Galaxie befindet sich an der Bildschirmposition (0/100). Wenn jetzt weiter nach rechts gescrollt wird, müßte sich die Galaxie z.B. an der Bildschirmposition (-8/100) befinden. Durch unsere Modulooperation wird sie aber an die Stelle (1016/100) verschoben und ist damit nicht mehr sichtbar.

Hier zeigt sich ein weiterer Vorteil davon, daß der Hintergrundbereich nun größer ist als der Bildschirm. Wäre dies nicht der Fall, so müßten wir jetzt die Galaxie zweimal darstellen (einmal bei (-8/100) und einmal bei (1016/100)), damit die Anzeige vollständig korrekt ist. Stattdessen können wir uns mit einem ebenso simplen wie effektiven Trick (manch einer mag Hack sagen) behelfen: Wir reduzieren die Zielkoordinate, die SDL_BlitSurface() übergeben wird, einfach um die maximale Breite bzw. Höhe der Galaxie.

Damit sieht der entsprechende Codeausschnitt in DrawBackground() so aus:


        dest.x = ((unsigned int)(g_Galaxies[i].x - scrollx) % 1024) - 16;
        dest.y = ((unsigned int)(g_Galaxies[i].y - scrolly) % 512) - 16;

        SDL_BlitSurface(g_pSurfGalaxies, &src, g_pSurfScreen, &dest);




Ach übrigens: Anstatt Modulo 1024 könnt Ihr auch ein bitweises AND verwenden, also z.B. '(unsigned int)(g_Galaxies[i].x - scrollx) & 1023)' im obigen Codeausschnitt. Warum das so ist, dürft Ihr selbst herausfinden. Für den Prozessor ist ein bitweises AND immer effizienter als eine Division, daher ist das bitweise AND eigentlich vorzuziehen. Andererseits erkennt zumindest gcc dies automatisch und optimiert den Code entsprechend.

Natürlich war das noch längst nicht alles, was ich zum Thema Scrolling zu sagen habe. Andererseits ist dieses Kapitel bereits jetzt mit Abstand länger als alle vorigen. Deswegen hebe ich mir ein paar Verbesserungen sowie Mausunterstützung für das nächste Kapitel auf.

Dann war da noch


Der Quellcode zu diesem Tutorial sowie die Makefile, MSVC++-Projektdateien und das Galaxienbitmap galaxien.bmp sind zum Download verfügbar (
tar-gzipped, 3.75kB).

Das von mir verwendete Galaxienbitmap entstammt übrigens einzelnen Galaxien aus dem Bild vom letzten Kapitel.

Dieses Tutorial ist Copyright (c) 2001 Nicolai 'Prefect' Hähnle.
Es darf zu nicht-kommerziellen Zwecken beliebig in ungekürzter und unmodifizierter Form vervielfältigt werden, sofern alle dazugehörigen Dateien, ebenfalls unmodifiziert, mitgeliefert werden.
Der mitgelieferte Sourcecode ist Public Domain.

Ihr könnt mich unter
This e-mail address is being protected from spambots. You need JavaScript enabled to view it erreichen. Außerdem lese ich die Coding-Foren von http://www.thewall.de/ und hänge zuviel in #thewall.de auf irc.gamesnet.net rum...


{mospagebreak}

SDL-Tutorial #5 - Scrolling die Zweite

Im letzten Kapitel habe ich lang und breit das Scrolling eingeführt. Allerdings gibt es immer noch ein paar Dinge, die verbesserungswürdig sind, ganz zu schweigen von der fehlenden Mausunterstützung.

Konstante Scrollgeschwindigkeit


Bis jetzt ist die effektive Geschwindigkeit, mit der gescrollt wurde, von der Framerate abhängig. Wenn die Framerate niedrig ist, dann werden die if()-Anweisungen zur Überprüfung von Tastendrücken seltener aufgerufen, und damit ändert sich auch die Scrollposition nicht so schnell.

Das ist natürlich sehr schlecht. Eigentlich dürfte jedem das Phänomen bekannt sein, daß man ein altes DOS-Spiel startet nur um festzustellen, daß es viel zu schnell läuft.

Das liegt ganz einfach daran, daß in diesen Spielen keinerlei zuverlässige Zeitmessungen verwendet wurde, und moderne Prozessoren einfach zu schnell sind.

Wir müssen also die Entfernung, die wir innerhalb eines Frames scrollen, davon abhängig machen, wie lang ein Frame dauert. Dazu müssen wir aber zunächst die momentane Zeit ermitteln können. SDL macht uns das Leben hier wieder einmal leichter: Wir können einfach die Funktion SDL_GetTicks() verwenden. Sie gibt die Anzahl Millisekunden seit dem Start von SDL zurück.

Erst einmal benötigen wir drei neue Variablen: in einer speichern wir die Startzeit des Frames, der gerade ausgeführt wird, in einer die Startzeit des vorherigen Frames, und in der dritten speichern wir die Zeit, die der letzte Frame gedauert hat. Alle diese Variablen speichern jeweils die Zeit in Millisekunden.

Der Kopf von main() muß also umgeschrieben werden:


int main()
{
    int running;
    int scrollx, scrolly;
    Uint32 lastframe, curframe, frametime;



An den Initialisierungsroutinen hat sich nichts geändert. Wir müssen aber, bevor die Hauptschleife beginnt, der Variable curframe einen vernünftigen Wert zuweisen. Ansonsten könnte frametime im ersten Frame verquere Werte enthalten und dadurch für Verwirrung sorgen.

Wir fügen also um Zeile 110 folgendes ein:


    g_Black = SDL_MapRGB(g_pSurfScreen->format, 0, 0, 0);

    curframe = SDL_GetTicks();

    running = 1;
    scrollx = scrolly = 0;



Nun folgt die eigentliche Berechnung der Zeit, die ein Frame zur Ausführung benötigt (bei Zeile 140):


        }

        lastframe = curframe;
        curframe = SDL_GetTicks();
        frametime = curframe - lastframe;

        keystate = SDL_GetKeyState(0);




Zunächst wird der Wert von curframe aus dem Weg kopiert, denn was vorher der momentane Frame war ist nun der vorherige. Danach holen wir uns die momentane Zeit von SDL_GetTicks() und berechnen die Länge des vorherigen Frames.

Denkt daran, daß alle diese Werte in Millisekunden angegeben sind.

Jetzt müssen lediglich die 4 if()-Statements, die die Scrollposition verändern, so umgeschrieben werden, daß sie statt einer konstanten Veränderung die Länge des Frames in die Veränderung mit einberechnen. Das sieht dann so aus:


        if (keystate[SDLK_LEFT])
            scrollx -= (400 * frametime)/1000;
        if (keystate[SDLK_RIGHT])
            scrollx += (400 * frametime)/1000;
        if (keystate[SDLK_UP])
            scrolly -= (400 * frametime)/1000;
        if (keystate[SDLK_DOWN])
            scrolly += (400 * frametime)/1000;



Damit scrollen wir nun mit genau 400 Pixel pro Sekunde. Beachtet bitte, daß ich hier zuerst mit 400 multipliziere und dann erst durch 1000 dividieren. Das ist notwendig, denn ich verwende hier Ganzzahlen. Wenn ich zuerst durch 1000 teilen würde, so käme bei dieser Division immer 0 heraus und das Programm würde gar nicht erst scrollen.

Double-Buffering


Wahrscheinlich weiß jeder, daß es sowas wie Double-Buffering gibt. Was aber vielleicht einigen nicht wirklich klar ist: Was bedeutet Double-Buffering denn nun eigentlich?

Bis jetzt hatten wir immer genau eine Surface, oder eine Zeichenebene (eben einen "Buffer"), in der sich der momentane Bildschirminhalt befindet. Nun ergibt sich aber ein Problem. Wir löschen ja in jedem Frame zunächst den Bildschirm, und dann zeichnen wir nach und nach alle Galaxien. Nun kann es deswegen dazu kommen, daß das Bild flackert. Ob es dazu kommt hängt von vielen Faktoren ab, z.B. der von SDL verwendeten API, ob man sich im Vollbildmodus befindet oder nicht, und wie schnell der Computer ist. Fakt ist aber, daß es vorkommt, und daß man es verhindern muß. Und genau hier kommt Double-Buffering ins Spiel.

Beim Double-Buffering werden zwei Zeichenebenen ("Buffer") verwendet: eine von ihnen enthält das Bild, das gerade auf dem Bildschirm sichtbar ist, während wir in der anderen Zeichenebene den nächsten Frame aufbauen. Wenn wir mit dem Bildaufbau fertig sind, werden die Zeichenebenen "geflipt", das heißt sie werden vertauscht. Die Grafiktreiber kümmern sich darum, daß dieses Flipping so schnell wie möglich geschieht ohne daß es zu Bildstörungen kommt.

Unter SDL kann man Double-Buffering sehr einfach aktivieren. Man muß lediglich bein Wechseln des Bildschirmmodus ein Flag (SDL_DOUBLEBUF) übergeben, und wir müssen die Funktion SDL_Flip() aufrufen, um die Zeichenebenen zu vertauschen. Wir können weiterhin die SDL_Surface g_pSurfScreen zum Zeichnen verwenden, da SDL die notwendigen Verwaltungsaufgaben alle transparent im Hintergrund durchführt.

Zunächst muß also der Aufruf von SDL_SetVideoMode() (um Zeile 89) verändert werden:


    g_pSurfScreen = SDL_SetVideoMode(640, 480, 0, SDL_DOUBLEBUF);
    if (!g_pSurfScreen) {
        fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n",
            SDL_GetError());
        exit(1);
    }




Lediglich das Flag SDL_DOUBLEBUF ist neu. Dann muß noch der Aufruf von SDL_UpdateRect() am Ende der Hauptschleife nahe Zeile 165 in einen Aufruf von SDL_Flip() umgeschrieben werden:


        DrawBackground(scrollx, scrolly);

        SDL_Flip(g_pSurfScreen);
    }




Und schon ist Double-Buffering implementiert! Übrigens kann es sein, daß SDL in Wirklichkeit kein Double-Buffering verwendet, z.B. wenn es wegen mangelhaften T