SDL-Tutorial #3 - Mehr Bitmaps

Autor: Nicolai 'Prefect' Haehnle

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 16×16 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 16×16 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.

    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.

Der Quellcode zu diesem Tutorial sowie die Makefile und MSVC++-Projektdateien sind zum Download verfügbar: (sdl_tut3.tgz sdl_tut3.zip)

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

Anmerkung

Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.

Die Verwendung aller Dokumente einschließlich der Abbildungen ausschließlich zu nichtkommerziellen Zwecken. Verbreitung des Dokuments auf Speichermedien, (insbesondere auf CD-ROMs als Beilage zu Zeitschriften und Magazinen oder sog. "Mission-Packs" etc.) ist untersagt.
 
Angemeldet als: ()
coding/grafik_api/sdl_tut_3.txt · Zuletzt geändert: 2010/07/29 21:10 von Adrian_Broher