Inhaltsverzeichnis

SDL-Tutorial #5 - Scrolling die Zweite

Autor: Nicolai 'Prefect' Haehnle

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 Treibern (siehe Linux) nicht unterstützt wird, oder wenn das Programm in einem Fenster ausgeführt wird (zur Erinnerung: für Vollbildmodus muß SDL_SetVideoMode() das Flag SDL_FULLSCREEN übergeben werden). Auf jeden Fall kümmert sich SDL aber darum, einen möglichst flackerfreien Modus zu verwenden.

Hier kommt die Maus!

Zu guter Letzt will ich noch das Scrolling per Maus einführen. Beim Scrolling gibt es generell drei Methoden, die „Zerrmethode“, die „Schiebemethode“ und die „Klickmethode“ (ich habe keine Ahnung, ob es dafür offizielle Bezeichnungen gibt).

Die Zerrmethode ist bei GUI-Anwendungen bevorzugt, z.B. beim Acrobat Reader. Dabei drückt der Anwender auf eine Maustaste und zerrt dann den Bildausschnitt an eine andere Stelle. Die Schiebemethode ist die bei Strategiespielen wohl am weitesten verbreitete. Dabei wird der Bildausschnitt gescrollt, sobald sich die Maus dem Bildschirmrand nähert. Die Klickmethode dürfte aus Civilization bekannt sein. Hierbei klickt man auf eine bestimmte Stelle des Bildschirms, und der Bildausschnitt zentriert sich auf diese Stelle.

Da diese Methoden sehr einfach zu implementieren sind, werde ich alle drei vorstellen.

Die Zerrmethode

Genauso wie SDL Tastaturereignisse schickt schickt es auch Mausereignisse. Es gibt zwei verschiedene Sorten von Mausereignissen: SDL_MOUSEMOTION, was eine Bewegung der Maus anzeigt, und SDL_MOUSEBUTTONDOWN/SDL_MOUSEBUTTONUP, was ausgelöst wird wenn eine Maustaste gedrückt bzw. losgelassen wird.

In unserem Fall ist natürlich die Mausbewegung interessant. Nähere Informationen zu einem SDL_MOUSEMOTION-Ereignis stehen in der Membervariable motion der SDL_Event-Struktur. motion ist vom Typ SDL_MouseMotionEvent, der wie folgt in einer der SDL-Headerdateien deklariert ist:

typedef struct {
        Uint8 type;     /* SDL_MOUSEMOTION */
        Uint8 which;    /* The mouse device index */
        Uint8 state;    /* The current button state */
        Uint16 x, y;    /* The X/Y coordinates of the mouse */
        Sint16 xrel;    /* The relative motion in the X direction */
        Sint16 yrel;    /* The relative motion in the Y direction */
} SDL_MouseMotionEvent;

Uns interessieren natürlich besonders die Membervariablen state und xrel/yrel. state ist ein Bitfeld, in dem für jede gedrückte Maustaste ein Bit gesetzt ist. Die entsprechenden Bitmasken erhält man über das Makro SDL_BUTTON. xrel/yrel geben die Veränderung der Mausposition seit dem letzten Ereignis an. x/y geben noch die absolute Mausposition an, aber die ist in unserem Fall uninteressant.

Wir benötigen also lediglich einen wenige Zeilen langen case-Zweig in der Ereignisabfrage, bei Zeile 135:

                }
                break;
 
            case SDL_MOUSEMOTION:
                if (event.motion.state & SDL_BUTTON(SDL_BUTTON_RIGHT)) {
                    scrollx -= event.motion.xrel;
                    scrolly -= event.motion.yrel;
                }
                break;
 
            case SDL_QUIT:

Natürlich kann man SDL_BUTTON_RIGHT im obigen Code auch durch SDL_BUTTON_LEFT oder SDL_BUTTON_MIDDLE austauschen. Und das war schon alles! Ihr könnt jetzt mit der rechten Maustaste scrollen.

Die Schiebemethode

Man könnte die Schiebemethode natürlich genauso wie die Zerrmethode mit Ereignissen implementieren. Es geht aber auch einfacher: Genauso wie beim Scrolling per Tastatur fragen wir die momentane Position der Maus jeden Frame explizit ab.

Dafür gibt es die Funktion SDL_GetMouseState(). Sie gibt zum einen die Bitmaske der gedrückten Maustasten zurück - diese ist schon vom SDL_MOUSEMOTION-Ereignis bekannt. Wir können diesen Wert getrost ignorieren. Zum anderen nimmt die Funktion aber auch optional zwei Pointer entgegen, in denen die momentane Position der Maus gespeichert wird.

Zunächst brauchen wir also zwei Variablen mousex und mousey, in denen die Mausposition gespeichert wird. Ich definiere sie zu Beginn der Hauptschleife, also etwa bei Zeile 118:

    while(running) {
        SDL_Event event;
        Uint8 *keystate;
        int mousex, mousey;
 
        while(SDL_PollEvent(&event)) {

Um Code zu sparen kombiniere ich das Mausscrolling nun mit dem Tastaturscrolling, so daß der entsprechende Codeteil ab Zeile 157 wie folgt aussieht:

        keystate = SDL_GetKeyState(0);
        SDL_GetMouseState(&mousex, &mousey);
 
        if (keystate[SDLK_LEFT] || mousex < 16)
            scrollx -= (400 * frametime)/1000;
        if (keystate[SDLK_RIGHT] || mousex > 640-16)
            scrollx += (400 * frametime)/1000;
        if (keystate[SDLK_UP] || mousey < 16)
            scrolly -= (400 * frametime)/1000;
        if (keystate[SDLK_DOWN] || mousey > 480-16)
            scrolly += (400 * frametime)/1000;

So ist das Programm sogar schon verwendbar. Es hat nur noch einen Haken: Wenn das Programm im Fenster abläuft kann man die Maus aus dem Fenster herausbewegen. Das ist natürlich nicht optimal.
Genau für diesen Fall gibt es die Funktion SDL_WM_GrabInput(). Sie sorgt dafür, daß sich die Maus nicht aus dem Fenster herausbewegen kann. Wir können also einen entsprechenden Funktionsaufruf in den Initialisierungscode einfügen (nahe Zeile 96):

    if (!g_pSurfScreen) {
        fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n",
            SDL_GetError());
        exit(1);
    }
 
    SDL_WM_GrabInput(SDL_GRAB_ON);
 
    g_pSurfGalaxies = SDL_LoadBMP("galaxien.bmp");

Um die Maussperre wieder aufzuheben ruft man einfach SDL_WM_GrabInput() mit dem Parameter SDL_GRAB_OFF auf.

Achtung: Je nach zugrundeliegendem System werden auch Tastaturereignisse wie Alt+Tab abgefangen, so daß man eventuell nicht mehr von der Anwendung wegwechseln kann. Um dem Spieler dennoch das Wegwechseln zu ermöglichen kann man z.B. als Reaktion auf einen Tastendruck die Funktion SDL_WM_IconifyWindow() aufrufen, die das Programm in den Hintergrund versetzt.

Damit bleibt noch eine Methode übrig…

Die Klickmethode

Für die Klickmethode sind natürlich wieder Ereignisse wichtig. Ich habe die Ereignisse SDL_MOUSEBUTTONDOWN und SDL_MOUSEBUTTONUP ja bereits erwähnt. Jetzt werden wir das Ereignis SDL_MOUSEBUTTONDOWN, das beim Druck auf einen Mausknopf ausgelöst wird, verwenden. Das Ereignis SDL_MOUSEBUTTONUP funktioniert genauso, ist für uns aber uninteressant.

Die Zusatzinformationen zu diesen Ereignissen befinden sich in der Membervariable button von SDL_Event. Diese ist vom Typ SDL_MouseButtonEvent, der in einer der SDL-Headerdateien wie folgt definiert ist:

typedef struct {
        Uint8 type;     /* SDL_MOUSEBUTTONDOWN or SDL_MOUSEBUTTONUP */
        Uint8 which;    /* The mouse device index */
        Uint8 button;   /* The mouse button index */
        Uint8 state;    /* SDL_PRESSED or SDL_RELEASED */
        Uint16 x, y;    /* The X/Y coordinates of the mouse at press time */
} SDL_MouseButtonEvent;

Die Membervariable button gibt an, welche Maustastegedrückt wurde. Dafür werden die Konstanten SDL_BUTTON_LEFT, SDL_BUTTON_RIGHT und SDL_BUTTON_MIDDLE verwendet.
Die Membervariablen x und y enthalten die Mausposition zum Zeitpunkt des Tastendrucks.

Wir müssen also Ereignisse vom Typ SDL_MOUSEBUTTONDOWN abfangen und überprüfen, welche Maustaste gedrückt wurde. Wenn es die richtige ist - ich verwende in meinem Beispiel die linke Maustaste - wird der Abstand des Mauszeigers zum Bildschirmmittelpunkt berechnet und die Scrollposition entsprechend verändert.

Dazu fügen wir in der Ereignisabfrage folgenden case-Zweig ein (nahe Zeile 148):

            case SDL_MOUSEBUTTONDOWN:
                if (event.button.button == SDL_BUTTON_LEFT) {
                    scrollx += event.button.x - 320;
                    scrolly += event.button.y - 240;
                }
                break;

Damit bin ich mit dem Thema Scrolling zunächst einmal fertig. Das heißt natürlich nicht, daß das Scrolling nicht noch verbessert werden könnte. Bei der Schiebemethode könnte man die Scrollgeschwindigkeit zum Beispiel davon abhängig machen, wie nahe die Maus am Bildschirmrand ist. Oder man könnte die Scrollgeschwindigkeit erhöhen, je länger der Spieler scrollt. Aber solche Verbesserung überlasse ich euch.

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

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.