Multimediaentwicklung mit SDL


von Marco Kraus (marco@libsdl.de)



Über dieses Tutorial

Beim offiziellen Start von libSDL.de, der offiziellen deutschen libSDL-Seite, im Juni 2002 habe ich mich dazu entschieden, in Zusammenarbeit mit Pro-Linux.de ein deutschsprachiges SDL-Tutorial zu schreiben.

Warum ein deutschsprachiges Tutorial ? Zum einen gibt es leider nur 2 deutschsprachige SDL-Programmier-Tutorials; eines davon basiert auf libSDL 0.10.0 und ist aus dem Jahr 1999, das andere ist sehr rudimentär. Zum anderen gibt es verhätnismässig wenig deutsche SDL-Developer, im Vergleich zu anderen wichtigen OpenSource-Projekten. Vielleicht gibt dieser Artikel ja dem Ein oder Anderen den Anstoß, sich mit einer der wichtigsten freien und plattformübergreifenden Multimediabibliotheken zu befassen.

Programmierkenntnisse, speziell in C, und Entwicklungserfahrung unter dem jeweiligen Betriebssystem (Umgang mit der IDE und/oder dem Compiler und Linker) sind sehr von Vorteil. SDL wird zwar nativ in C programmiert, aber es sind doch einige andere Language-Bindings für SDL vorhanden (dazu am Ende des Tutorials noch ein bisschen mehr). Auch wer bereits Erfahrung mit DirectX-Programmierung hat, dem wird der Umstieg leicht fallen.

Eine aktuelle Version dieses Tutorials ist (nach Veröffentlichung aller Teile bei Pro-Linux.de) unter http://www.libsdl.de/docs.htm zu finden.


[1] Was ist SDL ?

SDL steht für "Simple DirectMedia Layer" und ist eine Programmierschnittstelle für Spiele, Demos und Multimediaanwendungen.

Konkret heisst das, dass SDL eine Schnittstelle zur Verfügung stellt, über die man einfach die Multimediaelemente wie Grafikkarte, Sound, Joystick oder CD-Rom programmieren kann. Man muss sich keine Gedanken mehr darüber machen wie man z.B. ein Bild in den Grafikspeicher ablegt, wie man es wieder ausgibst oder wie man die Soundkarte ansteuert, da die komplette Geräteansteurung über die SDL Schnittstelle erfolgt und dann von SDL erledigt wird.

Bei Erwähnung von SDL kommt direkt immer der Vergleich zu DirectX auf. Aus technischer Sich haben Beide zwar nicht viel gemeinsam, aber um die Funktion von SDL zu verstehen kann man sagen, dass sie vom Prinzip her beide eine Multimediaschnittstelle darstellen.
SDL hat aber zwei entscheidenden Vorsteile: Portabilität und Open Source. SDL gibt es für viele Plattformen, darunter Linux, Windows, die diversen BSD-Derivate, Mac- und BeOS und auch Portierungen für exotischere Hardware wie den Sharp Zaurus, die Sega Dreamcast oder die Sony Playstation II. Die Schnittstelle ist immer gleich. Programme, die beispielweise unter Linux entwickelt wurden, müssen einfach nur mit den SDL-Libs unter Windows übersetzt werden und laufen (mal von unterschiedlichen Compilerinterpretationen abgesehen) auch dort direkt problemlos und ohne Änderungen im Code:



[2] Entstehungsgeschichte

SDL wurde von Sam Lantinga (1) während seiner Zeit als leitender Programmierer von 1999 bis 2001 bei Loki Games entwickelt. Loki (inzwischen ist die Firma leider bankrott) war darauf spezialisiert Windowsspiele nach Linux zu portieren, und der Simple DirectMedia Layer war ihre Grundlage. Mit SDL wurden hochkarätige Spiele wie unter anderem Civilization CTP, Descent 3 und Alpha Centauri nun auch der Linuxgemeinde ermöglicht.

Schon während dieser Zeit wurde SDL unter die LGPL gestellt. Somit können damit freie und auch kommerzielle Anwendungen programmiert werden. Dies war auch ein Grund dafür, dass die SDL schnell im Multimediabereich, speziell unter Linux, an grosser Bedeutung gewann. Es entstanden viele neue Anwendungen wie SMPEG, ein MPEG-Bibliothek die auch z.B. der mplayer nutzt, diverse Emulatoren wie DGen (Sega Genesis) oder Executor (Mac) und auch eine riesige Menge an freien Spielen.

Trotz seines Wechsels im Jahr 2001 von Loki zu Blizzard Software verwaltet Sam die libSDL heute noch immer und ist auch immer noch der wichtigste SDL-Entwickler. Auf der internationalen Developermailingliste (2) kann man selbst aktiv an der Weiterentwicklung teil haben. Für deutschsprache Anwender steht die deutsche libSDL-Mailingliste bereit (3).


[3] Erste Schritte

Natürlich steht vor dem ersten funktionierenden Programm die Installation der SDL. Bei allen gängigen Linuxdistributionen liegen fertige SDL-Pakete (die eigentliche libsdl und normalerweise auch die Standardbibliotheken sdl_image für erweiterte Grafikfunktionen, sdl_ttf für die Darstellung von TrueType Fonts, sdl_mixer für erweiterte Soundfunktionalität und sdl_net für Netzwerkfunktionen) bei. Zu installieren sind hier neben den Bibliothekspaketen natürlich auch die Developer-Pakete, die die benötigten Headerdateien enthalten.Auch diese sind bei den gängigen Distributionen bei.
Wer es dennoch selbst übersetzen will, der kann sich die aktuellen Quellen von der offiziellen libSDL Website (4) ziehen. Configure- und Makefiles liegen dem Archiv bei. Unter Windows kann man sich einfach die SDL Bibliotheken als DLLs und die Header-Files downloaden und dann wie gewohnt in sein Projekt einbinden.
Für KDevelop-User empfiehlt sich noch das Mini-HowTo von Sam Hart (5) zu lesen, da es doch einige Tricks und Kniffe zu beachten gibt; speziell wenn man plattformübergreifende Projekte anlegen möchte. Grundsätzlich gilt hier (wie auch bei andere IDEs mit automake- und autoconf-Unterstützung), dass die Includes sowie die Libpfade für den Linker nicht statisch angegeben werden sollten, sondern über den Aufruf von sdl-config:

  • "sdl-config --cflags" liefert die Include-pfade
  • "sdl-config --libs" zeigt den libSDL-Pfad



  • Somit werden Makefiles erzeugt, die auch bei einem System funktionieren, auf dem die Verzeichnisstruktur anders ist als bei dem Rechner, auf dem die Applikation ursprüglich übersetzt wurde.

    Nachdem nun die Bibliotheken und Includes installiert sind, kommen wir zur ersten Applikation:

    #include <stdlib.h>
    #include "SDL.h"
    
    int main()
    {
        if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) 
        {
           fprintf(stderr, "SDL konnte nicht initialisiert werden:  %s\n", SDL_GetError());
           exit(1);
         }
    
        atexit(SDL_Quit);
    }
    
    

    Hier passiert nun nicht mehr, als dass SDL initialisiert wird und sich das Programm wieder beendet.

    Detail:

    SDL wird in 6 Funktionsbereiche eingeordnet:

  • SDL_INIT_AUDIO
  • SDL_INIT_VIDEO
  • SDL_INIT_CDROM
  • SDL_INIT_TIMER
  • SDL_INIT_JOYSTICK
  • SDL_INIT_EVENTTHREAD

  • Die wichtigsten und funktionsreichsten sind natürlich Video und Audio. SDL_INIT_EVERYTHING initialisiert alle sechs Teilbereiche.

    Wenn man also in seiner Applikation eine Funktionalität des entsprechenden Bereiches nutzen möchte, dann muss dieser zu Beginn des Programms über SDL_Init() initialisiert werden. SDL_Init() kann mehrere Bereiche übergeben werden, wenn sie durch ein Pipe getrennt sind, also z.B.: "SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO)".

    Die Funktion SDL_Quit() sollte immer beim Beenden der Anwendung aufgerufen werden, da sie die zuvor geladenen SDL-Überreste im Speicher aufräumt und diesen wieder frei gibt.

    SDL_GetError() ist eine einfache Routine, die Fehlermeldungen als String zurück gibt. Sie sollte immer bei Fehlerausgaben zu Debuggingzwecken mit ausgegeben werden.


    [4] Videomodus

    Wir haben in unserem kleinen Beispiel bereits die Videosektion initialisiert. Dieser Bibliotheksteil wird beim Schreiben in den Grafikspeicher benötigt. Dies geschieht zum Beispiel wenn man direkt einen Pixel auf den Bildschirm zeichenn möchte oder über entsprechende Funktionen ein Bild laden will. Bevor wir dies jedoch tun können, muessen wir nach dem initialisieren der SDL noch den Videomodus, also die Auflösung und die Farbtiefe des Ausgabebildschirms, einstellen.

    SDL_GetVideoInfo() gibt die am besten Unterstützte Farbtiefe zurück. SDL_ListModes() zeigt alle unterstützten Auflösungen bei einer entsprechenden Farbtiefe.

    Anmerkung:
    Bei 8 Bit wird der Farbwert für jeden Pixel in einer sogenannten Farbpalette gespeichert. Diese muss gewährleistet sein, da es sonst zu Farbverfälschungen kommt. Ab 16 Bit werden die Farbwerte dann in den jeweiligen Pixeln gespeichert (Farbwerte werden noch genauer besprochen, wenn wir einzelne Pixel zeichnen).

    Wir nutzen aber bei unseren Beispielen einfach immer 800x600 Pixel bei 16 Bit Farbiefe (also 32K Farben).

    Der entsprechende Codeteil dafür sieht wie folgt aus:

    
    [...]
    
      SDL_Surface *display;
        
      display = SDL_SetVideoMode( 800, 600, 16, SDL_SWSURFACE );
      if ( display == NULL )
      {
        fprintf(stderr, "Konnte kein Fenster 800x600px oeffnen: %s\n",SDL_GetError());
        exit(1);
      }
    
    [...]
    
    

    Detail:

    Gleich die erste Zeile, das Anlegen eines SDL_Surface, gehört zum elementaren Grundverständnis des Grafikbereichs der libSDL. Bei SDL wird mit sogenannten "Surfaces" gearbeitet. Ein Surface ist eigentlich nur ein reservierter Speicherbereich, dem Bildschirmeigenschaften wie Breite, Höhe, Farbtiefe,etc zugewiesen sind.
    Man muss sich das so vorstellen: man erstellt ein Basis-Surface, in unserem Fall mit dem Name "display", und initialisiert dafür einen Videomode mit SetVideoMode(). "display" wird somit zu unserem Bildschirm; nach offizieller SDL-Definition auch das "display-surface" genannt. Die Parameter sind:

    SDL_SetVideoMode( Screen-Weite, Screen-Höhe, Farbtiefe, Flags)

    Screen-Höhe und -Weite versteht sich von selbst und wird auch beachtet, solange nicht als Flag der Fullscreen-Mode erzwungen wird. Die Farbtiefe sollte mit Sorgfalt gewählt werden, da nicht jedes System mit jeder Farbtiefe umgehen kann. SDL versucht zwar nicht vorhandene Farbtiefen auf dem System zu emulieren, doch schlägt auch dies fehl, so bricht das Programm mit einer Fehlermeldung ab.

    Durch den vierten Parameter, die Video-Flags, kann man die eigentlichen Videoeigenschaften beeinflussen. Die Flags sind wie folgt definiert:

  • SDL_SWSURFACE - Das Surface wird im Hauptspeicher abgelegt (default)
  • SDL_HWSURFACE - Das Surface wird im Grafikspeicher abgelegt
  • SDL_ASYNCBLIT - Surface benutzt asynchrone Blits, wenn möglich
  • SDL_ANYFORMAT - Erlaubt jedes Pixel-Format (nur beim display-surface)
  • SDL_HWPALETTE - Surface nutzt exclusive Farbpalette
  • SDL_DOUBLEBUF - Surface ist "double buffered" (nur display-surface)
  • SDL_FULLSCREEN - Surface im Full-Screen-Mode initialisieren (nur display-surface)
  • SDL_OPENGL - Surface nutzt OpenGL (nur display-surface)
  • SDL_OPENGLBLIT - Surface unterstützt OpenGL blitting (nur display-surface)
  • SDL_RESIZABLE - Surfacefenster ist veränderbar (nur display-Surface)
  • SDL_HWACCEL- Surface blit nutzt Hardwarebeschleunigung
  • SDL_SRCCOLORKEY - Surface nutzt colorkey blitting
  • SDL_RLEACCEL - Colorkey blitting ist durch RLE beschleunigt
  • SDL_SRCALPHA - Surface blit nutzt alpha blending
  • SDL_PREALLOC - Surface nutzt vorher allokierten Speicher

  • Wie man zweifelsfrei erkennen kann, hat man bei der Videoinitialisierung viele Möglichkeiten, die man, wie auch beim SDL_Init() durch ein Pipe getrennt, kombinieren kann.

    Für uns ist am Anfang nur wichtig zu wissen, dass wir durch SDL_SWSURFACE und SDL_HWSURFACE den Speicherbereich der Surfaces beeinflussen und durch SDL_FULLSCREEN einen Vollbildschirm erzwingen könen. Alle weiteren wichtigen Parameter werden uns im Laufe des Tutorials noch begegnen.

    Man hat nun das display-surface, also im Prinzip den Bildschirm. Jetzt erstellt man weiter solche Speicherbereiche mit SDL_Surface() für z.B. ein Bild. Dieses Bild-Surface kann man manipulieren und verändern (siehe nachfolgendes Kapitel).

    Ist die Arbeit getan, kann man einen Bereich oder auch die gesamte Bildfläche auf den Display-Surface kopieren. Dieser Vorgang wird "Blit" genannt. Durch ein einfaches Refresh des entsprechend geänderten Bereichs (oder alles) des Bildschirmdisplays wird dann eine Anzeige erreicht. Dazu aber nun im nächsten Kapitel mehr.


    [5] Bilder anzeigen

    SDL kann ohne zusätzliche Bibliotheken nur Bitmapbilder anzeigen. Dies reicht im Normalfall nicht aus. Deshalb weise ich bereits jetzt auf die Standardbibliothek SDL_image hin, die wir auch gleich verwenden werden:

    
    #include "SDL_image.h"
    
    [...]
    
    SDL_Surface *image;
    
    [...]
    
    image = IMG_Load("tux.jpg");
    
    if (image == NULL)
    {
       fprintf(stderr, "Das Bild konnte nicht geladen werden:%s\n", SDL_GetError());
       exit(-1);
    }
    
    // kopiere das Bild-Surface auf das display-surface
    SDL_BlitSurface(image, NULL, display, NULL);
    
    // den veraenderten Bereich des display-surface auffrischen
    SDL_Flip(display);
    
    SDL_Delay(3000);
    
    // Das Bitmap-Surface löschen
    SDL_FreeSurface(image);
    
    [...]
    
    

    Die Funktion IMG_Load() wird von sdl_image bereit gestellt. Die erfordert dass man SDL_image.h includiert und dem Linker ein "-lSDL_image" als Flag mitgibt, wie bereits bei der libsdl oben bereits beschrieben.

    Wir deklarieren ein neues Surface mit dem Namen "image". Mittels IMG_Load() weisen wir diesem neuen Surface dann das gewünschte Bild zu. Diese Surface hat nun die Grösse der geladenen Grafik.

    Da wir meist mehr über die Surfaceformatierung erfahren wollen, wie zum Beispiel die Grösse und Farbtiefe, hier die interne Struktur des Surfaces:

    typedef struct SDL_Surface {
    
    // allgemeine Surface-Informationen
     
       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   
    
    // hardwarespezifisches Surface-Informationen
    
       struct private_hwdata *hwdata;
    
    // Clipping-Informationen
    
       int clip_minx;                  // Read-only
       int clip_maxx;                  // Read-only
       int clip_miny;                  // Read-only
       int clip_maxy;                  // Read-only
    
    // surface-interne Kopierinformationen
    
       struct SDL_BlitMap *map;        // Private
    
    // Liste der surfaces
    
       struct map_list *mapped;        // Private
    
    // Reference-Counter
    
        int refcount;                  // Read-mostly
    
    } SDL_Surface;
    
    

    In der Struktur des Surfaces wird über die Integer-Werte w und h die Weite und die Höhe gespeichert. Zu Kontrollzwecken wird man daher oft z.B. "image->w" und "image->h" mit ausgeben, um über die zu ändernde Bildgrösse informiert zu sein.

    Nachdem das Bild nun in sein eigenes Surface geladen wurde, und wir vorerst keine weiteren Veränderungen mehr daran vornehmen wollen, müssen wir es auf das display-surface kopieren. Dies geschieht über SDL_BlitSurface(). BlitSurface nimmt als Parameter:
    SDL_BlitSurface(QuellSurface, Quellbereich, ZielSurface, Zielbereich)

    Die Parameter Quell- und Zielbereich wird später genauer erklärt, wenn wir die Grafikroutinen intensiver nutzen. Wir gebrauchen SDL_BlitSurface() im moment so, dass wir als Parameter für die Bereiche einfach 0 übergeben und somit das gesamte Surface kopiert wird.

    Als letzten Schritt wollen wir nun das durch das blitting veränderte display-surface auch zur Anzeige bringen. Die einfachste Möglichkeit ist dies durch:
    int SDL_Flip(SDL_Surface *display)
    zu machen. Hierbei wird (wie bereits schon beim Blit) einfach das gesamte display-surface neu geladen.

    Um das Programm speichertechnisch sauber beenden zu lassen, rufen wir für alle Surfaces (ausser dem display) die Funktion SDL_FreeSurface() auf, um den allokierten Speicher dafür wieder frei zu geben.


    [6] Bildbereiche

    Wie bereits im letzten Kapitel kurz angeschnitten, sind im SDL-Grafikbereich sogenannte "rectangular areas" (dt.: rechteckige Bereiche) von wichtiger Bedeutung. Diese (kurz) SDL_Rects sind einfache Strukturen, die Positions und Grössenangaben gespeichert haben:

    
    typedef struct{
    
      Sint16 x, y;
      Uint16 w, h;
    
    } SDL_Rect;
    
    

    Zu Erklärung der Struktur:

    Die signed Integers x und y sind Positionsangaben auf dem Bildschirm, beginnend in der linken oberen Ecke mit 0,0.
    x kann den Maximalwert display->w und y display->h annehmen, also die maximale Grösse des display-surfaces.
    w und h sind wie bereits gehabt die Ausschnittsgrössen.

    Für was benötigt man also die SDL_Rects ? Die Antwort ist eigentlich klar. Wir wollen nicht immer (wie im letzten Beispiel) das ganze Surface kopieren oder updaten. Schon aus Performancegründen ist es unsinnig das komplette display zu refreshen oder ein komplettes Surface x-mal zu kopieren, wenn es sich nur um ein paar Pixel grosses Bild handelt, was dargestellt werden soll.

    Wir nehmen wieder unser Beispiel zu Hand, und kopieren das halbeBild mit Hilfe von SDL_Rects etwas eingerückt (200x100 Pixel) auf das display-surface. Danach updaten wir nur den auf dem display wirklich durch den Blit geänderten Bereich: wieder durch SDL_Rects.

    #include <stdlib.h>
    #include "SDL.h"
    #include "SDL_image.h"
    
    int main()
    {
      // Die 2 bereits bekannten Surfaces deklarieren
      SDL_Surface *display;
      SDL_Surface *image;
    
      // Deklaration des destination-rect und des source-rect
      SDL_Rect drect;
      SDL_Rect srect;
    
      // init video stuff
      if ( SDL_Init( SDL_INIT_VIDEO) < 0 )
      {
         fprintf(stderr, "SDL konnte nicht initialisiert werden:  %s\n", SDL_GetError());
         exit(1);
       }
    
       atexit(SDL_Quit);
    
       // init screen
       display = SDL_SetVideoMode( 640,480, 8, SDL_SWSURFACE);
       if ( display == NULL )
       {
         fprintf(stderr, "Konnte kein Fenster 640x480 px oeffnen: %s\n", SDL_GetError());
         exit(1);
       }
    
      // Bild laden
      image = IMG_Load("tux.jpg");
      if (image == NULL)
      {
         fprintf(stderr, "Das Bild konnte nicht geladen werden: %s\n", SDL_GetError());
         exit(-1);
      }
    
      // Bildgroesse zu Debuggingzwecke ausgeben
      fprintf(stdout,"Debug: Bild-Surface-Groesse: %i x %i Pixel\n", image->w,image->h);
    
      // Setzen des Quellbereichs
      srect.x = 0;
      srect.y = 0;
      srect.w = (image->w)/2;  // das halbe Bild
      srect.h = image->h;   // das gesamte Bild
    
      // Setzen des Zielbereichs
      drect.x = 200;
      drect.y = 100;
      drect.w = (image->w)/2;
      drect.h = image->h;
              
      // kopiere surface auf display-surface
      SDL_BlitSurface(image, &srect, display, &drect);
    
      // den veränderten Bildschirm-Bereich auffrischen
      SDL_UpdateRects(display,1,&drect);
    
      SDL_Delay(3000);
    
      // Das Bitmap-Surface löschen
      SDL_FreeSurface(image);
    }
    
    

    Detail:

    Zuerst deklarieren wir zwei SDL_Rects. Eines für den Quellbereich, von dem heraus kopiert werden soll und eines für den Zielbereich in den hinein kopiert werden soll. Den Zielbereich verwenden wir auch später als Bereich des displays, den wir refreshen wollen. Nach der üblichen Initialisierungsprozedur haben wir das Bild wie bereits gehabt in ein Surface geladen. Aber anstatt nun das komplette Surface zu kopieren, wollen wir nur das halbe Bild kopieren (in der Praxis werden oft nur Ausschnitte von Bildern benötigt).

    Wir setzten daher nun zuerst den Quellbereich, indem wir die Struktur des angelegten Source-Rectangels "srect" füllen. Das Bild beginnt bei den Koordinaten 0,0. Von dort aus wollen wir auch kopieren. Also wird der x- und y-Wert der Quelle auf 0 gesetzt. Die Breite des Bildes bekommen wir ja über die Surfacestruktur heraus. Da wir haben nur das halbe Bild kopieren willen, reicht es aus die kopierende Bildbreite auf "(image->w)/2" zu setzten. Die Höhe nehmen wir wieder komplett.

    Nachdem wir nun wissen, was wir kopieren wollen, setzen wir nun den Zielbereich. Dazu haben wir "drect" deklariert, also das destination-rectangle. Der x- und y-wert gibt an, an welcher Stelle der linke obere Pixel unseres Ausschnittes positioniert werden soll. Um dies zu demonstrieren, rücken wir das halbe Bild einfach um 200x100 Pixel ein. drect.w und drect.h haben beim blitting noch keine Funktion und wird ignoriert (ein resize des Ausschnittes ist also beim Blit nicht direkt möglich). Wir setzen es dennoch, um später schon die Bereichsgrösse zu kennen, wie dir beim neu zeichnen des display-surfaces benötigen.

    Im nächsten Schritt wird wieder der eigentliche Blit des Bildsurfaces (diesmal nur den definierten Ausschnitt) auf das display-surface vorgenomen. Wie bereits im vorhergehenden Kapitel beschrieben, nimmt SDL_BlitSurface() für das Quell- und Zielsurface auch jeweils ein SDL_Rect mit an. Da wird diese ja jetzt gesetzet haben, geben wir diese auch mit (laut SDL-Definition muss der Parameter als Adresse übergeben werden).

    Als letzten Schritt wollen wir nun das geänderte dispay-surface natürlich auch zur Anzeige bringen. Schon alleine aus Performancegründen sollte man immer nur den geänderten Bereich neu laden.

    Neben dem bereits kennen gelernten SDL_Flip(), das den ganzen Bildschirm neu zeichnet, gibt es auch noch zwei weitere Möglichkeiten des Bildschirmupdates. Diese zwei Funktionen bieten die Möglichkeit Bereiche zu definieren, die erneuert werden sollen:

  • void SDL_UpdateRect(SDL_Surface *display, Sint32 x, Sint32 y, Sint32 w,Sint32 h)
  • void SDL_UpdateRects(SDL_Surface *display, int numrects, SDL_Rect *rects)

  • Wie es wahrscheinlich der Name der Funktion bereits vermuten lässt, kann man durch SDL_UpdateRect() genau einen Bereich neu zeichnen. Die Parameter sind dafür die bereits auch von den SDL_Rects bekannten Werte x,y,h und w, die hier einfach direkt mit angegeben werden. Ein SDL_UpdateRect(display,0,0,0,0) würde wieder den gesamten Bildschirm neu zeichnen, und wäre (bei Grafikkarten ohne hardwaremässige Unterstützung des DoubleBuffer) funktional gleich des SDL_Flip().
    SDL_UpdateRects() kann dazu im gegensatz mehrere Rechtecke refreshen, jedoch muss man hier SDL_Rects inklusive der Gesamtanzahl der Bereiche mit übergeben. Wie man das Update über SDL_UpdateRects mit einem Bereich macht, ist im Beispiel oben gezeigt. Um mehrere Bereiche auf einmal neu zu zeichnen bedient man sich üblicherweise Arrays vom Typ SDL_Rect, welches man dann als Parameter mit der korrekten Anzahl der im Array gespeicherten SDL_Rects mit übergibt. Wir werden uns in diesem Tutoial aber auf das updaten eines Bereichs pro Frame begnügen.

    Nun sollte sich das halbe Bild, um 200x100 Pixel eingerückt, auf eurem Bildschirm zeigen. Am Ende der Applikation wird wieder der Bildspeicher frei gegeben und das Programm beendet sich:



    [7] Pixel zeichnen

    Bisher haben wir immer nur Bilder geladen und angezeigt. Jedoch ist es auch oft nötig einzelne Pixel zu zeichnen. Sei es um Linien und Kreise darzustellen (auch wenn es dafür wieder bereits fertige Bibliotheken gibt) oder auch aus Geschwindigkeitsgründen (wenn Pixelzeichnungen ausreichen). Das zeichnen eizelner Pixel auf den Bildschirm ist jedoch nicht so trivial wie es sich vielleicht anhören mag.

    RGB Grundlagen:
    Eine Pixelfarbe wird auf dem Bildschirm über 3 Farben bestimmt. Dies sind RGB, also Rot, Grün und Blau. Durch die Mischung dieser Farbtöne kann man jede andere Farbe erzeugen. Die Farbselektion in GIMP (WaterColor-Selection) verdeutlicht dies:


    Ein rein grüner Pixel wird also natürlich z.B: über die Werte: R=0, G=255 und B=0 erzeugt. Ein gelber Pixel jedoch wird nur durch die Mischung von Rot und Gelb generiert, was also R=255, G=255 und B=0 entsprechen würde. Weiss und Schwarzen sind laut Farblehre keine Farben. Weiss wäre durch R=255, G=255, B=255 zu erreichen. Schwarz durch R=0,G=0,B=0. Neben RGB kann es noch einen Alphawert geben. Dieser wird aber erst relevant, wenn wir später Transparenz behandeln.

    Nun wissen wir zwar, dass wir mindestens 3 Werte benötigen, um einem Pixel die Farbe zu geben. Wie zeichnen wir aber nun einen Pixel auf den Bildschirm ? Rein technisch gesehen müssen wir uns die Struktur eines Surfaces wieder in Erinnerung rufen. Dort gab es ein Element mit dem Namen: "void *pixels;" Dieses Element speichert die Farbwerte der einzelnen Pixel des display-Surfaces. "Was interessiert mich hier das display-Surface?", werden einige wahrscheinlich fragen. Das spezielle am Pixel zeichnen ist, dass wir direkt das dispay-surface verändern. Wir schreiben also direkt in den Grafikspeicher. Nicht wie zuvor immer auf separate Surfaces, die wir dann durch einen Blit kopieren. Dies hat den riesigen Vorteil, dass wir mit hoher Geschwindigkeit arbeiten können und nicht mit Surfaces und Blits hantieren müssen. Der Nachteil dabei ist jedoch, dass bei der direkten Manipulation des Grafikspeichers dieser zuvor gelockt, also für andere Anwendungen gesperrt, sein muss. Ist dies nicht der Fall könnten mehrere Zugriffe gleichzeitig auf den selben Speicherbereich stattfinden, was unweigerlich zu einem segmention fault führt.

    Da die eigentliche Manipulation der Pixelmap des display-surfaces am Anfang recht kompliziert erscheint, verwenden wir die Standardfunktion der offiziellen SDL-Dokumentation. Diesen Code findet man fast in allen SDL-Applikationen wieder, die direkt Pixel zeichnen:

    void DrawPixel(SDL_Surface *screen, int x, int y,Uint8 R, Uint8 G,Uint8 B)
    {
        Uint32 color = SDL_MapRGB(screen->format, R, G, B);
    
        if ( SDL_MUSTLOCK(screen) )
        {
            if ( SDL_LockSurface(screen) < 0 ) {
                return;
            }
        }
    
        switch (screen->format->BytesPerPixel) {
            case 1: { /* vermutlich 8 Bit */
                Uint8 *bufp;
    
                bufp = (Uint8 *)screen->pixels + y*screen->pitch + x;
                *bufp = color;
            }
            break;
    
            case 2: { /* vermutlich 15 Bit oder 16 Bit */
                Uint16 *bufp;
    
                bufp = (Uint16 *)screen->pixels + y*screen->pitch/2 + x;
                *bufp = color;
            }
            break;
    
            case 3: { /* langsamer 24-Bit-Modus, selten verwendet */
                Uint8 *bufp;
    
                bufp = (Uint8 *)screen->pixels + y*screen->pitch + x * 3;
                if(SDL_BYTEORDER == SDL_LIL_ENDIAN) {
                    bufp[0] = color;
                    bufp[1] = color >> 8;
                    bufp[2] = color >> 16;
                } else {
                    bufp[2] = color;
                    bufp[1] = color >> 8;
                    bufp[0] = color >> 16;
                }
            }
            break;
    
            case 4: { /* vermutlich 32 Bit */
                Uint32 *bufp;
    
                bufp = (Uint32 *)screen->pixels + y*screen->pitch/4 + x;
                *bufp = color;
            }
            break;
        }
    
        if ( SDL_MUSTLOCK(screen) )
        {
            SDL_UnlockSurface(screen);
        }
    
    }
    

    Funktionsdetails:

    Diejenigen, die mit mit dem Lesen dieses Tutorial das erste mal etwas mit SDL zu tun haben, können diesen Abschnitt getrost überspringen. Wir kopieren diese Funktion nachher einfach und verwenden sie. Es reicht wenn wir wissen, dass sie funktioniert und mit:
    DrawPixel(SDL_Surface *screen, int x, int y, Uint8 R, Uint8 G, Uint8 B)
    aufgerufen wird. Die Parameter sind relativ selbsterklärend. x und y sind hier die Positionen des zu verändernden Pixels auf dem Bildschirm. Die Maximalwerte sind auch hier natürlich wieder display->w und display->h. Die drei unsigned integers R, G und B spiegeln die bereits erklärte Farbmischung des Pixels wieder.

    Für alle, die es doch ein wenig genauer wissen wollen, hier die genauere Erklärung der Funktion:

    Die übergebenen RGB-Werte werden zunächst mit SDL_MapRGB() in einen 32-Bit unsigned Integer-Wert umgewandet. Dieser eine Wert spiegelt SDL-intern den RGB-Wert wieder. Um, wie bereits schon erwähnt, Konflikte im Zugriff auf den Grafikspeicher zu vermeiden, "locken" wir das Surface (und den somit für das Surface reservierten Grafikspeicher). Dies geschieht über die Funktion SDL_LockSurface(). Um sicherzustellen, dass wir wirklich locken müssen, wird dies immer mit SDL_MUSTLOCK() gekoppelt. Wenn wir viel zeichnen ist es sicherlich besser, das locken aus der eigentlich Drawfunktion zu nehmen und manuell im Hauptprogramm zu setzen. Somit wird speziell bei zeichenintensiven Schleifen das Surface nur einmal gelockt. Nun ist der Speicher jedenfalls auf eine der beiden Weisen gesichert und wir können problemlos hinein schreiben. "screen->format->BytesPerPixel" liefert natürlich die Farbtiefe in Byte zurück. Die Rückgabe wurde in Byte gewählt, dass der nachfolgende switch-Befehl übersichtlicher wirkt. Ein "screen->format->BitsPerPixel" und entsprechende Änderung der switches ist aber genauso möglich. Nun wird in jedem Zweig ein Pufferpointer mit der entsprechenden Grösse angelegt. Diesem wird die Adresse des screen->pixels plus der Entsprechenden Abweiung in x und y zugewiesen. Somit zeigt der Puffer direkt auf den zu manipulierenden Pixel an der richtigen Stelle. Jetzt wird nur noch dieser Adresse der richtige Wert, nämlich den durch MapRGB erstellten Farbwert, zugewiesen. Am Ende noch das gelockte Surface wieder frei geben und wir sind fertig. Der Bildpunkt hat nun unsere Farbe.

    Nun zurück zu unserem eigentlich Problem. Wir wollten einen Pixel auf dem Bildschirm ausgeben. Ohne uns weiter Gedanken zu machen, nutzen wir die oben beschriebene Funktion DraxPixel():

    #include <stdlib.h>
    #include "SDL.h"
    #include "SDL_image.h"
    
    // Fuer einen 24-Bit-Modus unabhaengig von der Bytereihenfolge.
    // Wird von also DrawPixel() benoetigt
    #include "SDL_endian.h"
    
    void DrawPixel(SDL_Surface *screen, int x, int y,Uint8 R, Uint8 G, Uint8 B);
    
    int main()
    {
        SDL_Surface *display;
    
        // init video stuff
        if ( SDL_Init( SDL_INIT_VIDEO) < 0 )
        {
           fprintf(stderr, "SDL konnte nicht initialisiert werden:  %s\n", SDL_GetError());
           exit(1);
         }
    
        atexit(SDL_Quit);
    
        // init screen
        display = SDL_SetVideoMode( 800, 600, 16, SDL_SWSURFACE);
        if ( display == NULL )
        {
          fprintf(stderr, "Konnte kein Fenster 640x480px oeffnen: %s\n", SDL_GetError());
          exit(1);
        }
    
    for( int x=0; x < 800; x++ )
    {
        DrawPixel( display, x, display->h/2, 255, 255, 255);
    }
    
    SDL_Flip(display);
    
    SDL_Delay(3000);
    }
    
    

    Detail:

    Das Locking ist bereits in der Funktion DrawPixel() geschehen (siehe Detailinformationen zu DrawPixel). Darum müssen wir uns also an dieser Stelle nicht mehr kümmern. Der Rest sieht alles sehr einfach aus; ist es auch. Eine einfache For-Schleife, die auf der Horizontalen 800 Pixel in weiss zeichnet. Eine einfache Linie also. Dies kann man na herzenslust erweitern: For-Schleifen ineinander schachteln, Positionen berechnen oder die RGB-Werte dynamisch gestalten.

    Noch zwei Anmerkungen zum Thema Pixelmanipulation:

  • Um Linien und Kreise zu zeichnen muss man das Rad nicht neu erfinden. Es gibt eine sehr gute Bibliothek (6) von Andres Schiffler, die sdl_gfx. Diese stellt die Basiszeichenfunktionen auf Pixeleben (und noch einiges mehr) über ein einfaches Interface zur Verfügung.

  • Um den ganzen Bildschirm oder ein bekanntes SDL_Rect mit einer Farbe zu füllen, muss man auch nicht selbst jeden Pixel selbst zeichnen. Hier stellt schon SDL nativ die Funktion SDL_FillRect() zur Verfügung, welche als Parameter das Surface, das SDL_Rect und die RGB-Farbe annimmt. Details: siehe SDL-Doc-Project (7)


  • [8] Input-Handling : Grundlagen

    Die libSDL wird gerne als Spielebibliothek verwendet. Dazu ist es aber erforderlich (um überhaupt Spielspass aufkommen lassen zu können), dass eine Interaktion zwischen User und Anwendung zustande kommt. SDL bietet dem Entwickler hierfür 3 Wege an: Maus, Tastatur und Joystick. Zumindest auf die ersten Zwei wollen wir hier nun näher eingehen.


    Detail: Tastatur-Handling:

    SDL kennt zwei Datentypen, welche Tasten repräsentieren. Zum einen den Typ SDLKey. SDLKey ist ein einfacher "enumerated type", der die entsprechenden ASCII-Werte der betätigten Tasten hält. Zum anderen den Typ SDLMod. SDLMod ist rein strukturell und funktional sehr ähnlich aber stellt die sogenannten Modifiers dar, also z.B. die ALT-, Shift- oder STRG-Taste, wenn sie in Verbindung mit anderen Tasten genutzt werden (z.B. Hotkey-Kombinationen).

    Was passiert also nun wenn eine Taste gedrückt wird ?
    Zuerst einmal muss man verdeutlichen, dass eigentlich nicht nur das Drücken der Taste wichtig ist, sondern auch das Lösen dieser. Das Ereignis des Tastedrucks ist also ein Anderes als das des Tastenlösens. Daher sprechen wir in Zukunft allgemein von Tastenereignissen. Damit ist dann das Drücken oder das Loslassen gemeint.

    Wird nun solch ein Ereignis ausgelöst, so kommen zwei SDL Strukturen zu tragen: SDL_keysym und SDL_KeyboardEvent. Bei einem Tastenereignis legt SDL die Struktur SDL_KeyboardEvent für genau dieses Ereignis an:

    typedef struct{
    
      Uint8 type;
      Uint8 state;
      SDL_keysym keysym;
    
    } SDL_KeyboardEvent;
    

    Wie man sieht, beschreibt diese Struktur den Status des Events. type und state habe im Prinzip die gleiche Information; nämlich ob das Tastenevent ein "Press" (also ein Tastendruck) oder ein "Release" (Loslassen) war. Wir werden in Zukunft "type" zur Bestimmung des Event befragen. type hat den Wert "SDL_KEYUP" oder "SDL_KEYDOWN".
    Mit der kontextlosen Statusinformation des Event können wir nichts anfangen. Daher wird in dieser Strukur des Events noch ein weiteres Element, vom Typ SDL_keysym, angelegt. SDL_keysum ist wie bereits kurz angesprochen wiederum eine Struktur. Diesmal eine Struktur für die eigentlichen Tastenwerte:

    typedef struct{
    
      Uint8 scancode;
      SDLKey sym;
      SDLMod mod;
      Uint16 unicode;
    
    } SDL_keysym;
    

    SDL_keysym speichert die ASCII-Werte der Taste in "sym". Die zugehörigen Modifier-Tasten werden in "mod" abgelegt (Modifiers werden mit konstanten Namen bespeichert, deren Zuweisung in SDL_keysym.h festgelegt ist). Da natürlich bei einem Tastenevent mehrer Modifier zu tragen kommen können, kann es als sein, dass Konstrukte wie ( KMOD_NUM | KMOD_CAPS | KMOD_LSHIFT ) für mod zustande kommen, was aber nur soviel bedeutet, als dass NumLock, CapsLock und die linke Shift Taste während dem Tastendruck aktiv waren. Hier gibt es natürlich keine Unterscheidung zwichen Press und Release; entweder ein Modifier war aktiv oder eben nicht.

    Scancode ist ein Hardwarescancode, der uns nicht wirklich zu interessieren hat. Der Unicodewert ist beinahe selbsterklärend, wenn auch seine Anwendung ein wenig komplexer erscheint. Unicode ist ein internationaler Zeichensatz. Dies kann also nur funktionieren, wenn eine Taste gedrückt wird. Dieses Feld wird somit nur beim SDL_KEYDOWN korrekt gesetzt. Und auch dann nur, wenn Unicode zuvor über SDL_EnableUnicode() aktiviert wurde. Wir werden Unicode in unseren Beispielen nicht mehr weiter beachten.

    Die verschachtelten Strukturen der Keyboardevents werden wohl am besten an folgendem Diagramm verdeutlicht:


    Es wird ersichtlich, dass SDL_KeyboardEvent noch nicht die letzte Station unseres Events war. Es wird noch mit den Strukturen von Maus, Keyboard und Joystick in ein Union vermischt: dem SDL_Event.


    Detail: Maus-Handling:

    Die Eventverarbeitung bei der Maus ist wesentlich simpler als die der Tastatur. Wir müssen eigentlich nur zwei Unterscheidungen machen: MouseMotion und MouseButton. Die Speicherung der Nutzdaten läft auch hier so ab wie beim Keyboard. Es gibt zwei Strukturen. SDL_MouseMotionEvent für die Mausbewegung und SDL_MouseButtonEvent für die Mausknöpfe. Dort werden die eigentliche Nutzdaten gespeichert:

    In SDL_event.h sind diese Mausstrukturen wie folgt definiert:

    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;
    

    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;
    

    Natürlich sind auch diese beiden Strukturen wieder in dem bereits oben erwähnten SDL_Event-Union vorhanden.


    SDL_Event:

    Was das SDL_Surface im SDL_Grafikbereich, ist das SDL_Event im Bereich des Event-Handling. Es ist das zentrale Element, über das auf alle Eventeigenschaften, egal von welchem Eingabegerät, zugegriffen wird. Wird ein Ereignis ausgelöst, so wird von SDL die Struktur des entsprechenden Eingabegaerätes gefüttert (z.B. SDL_KeyboardEvent oder auch SDL_MouseMotion und SDL_MouseButton). Diese gerätespezifischen Strukturen sind widerum alle zusammen in einem SDL_Event-Element vereint.

    Für jedes auftretende Ereignis wird ein SDL_Event angelegt und in einem FIFO-Puffer gespeichert. Das heisst konkret, dass ein Pufferspeicher mit SDL_Event-Elementen angelegt wird. Das erste Element, welche in diesen Speicher geschoben wird ist auch das Element, das später beim Auslesen auch wieder an erster Stelle steht.

    Grafisch verdeutlicht sieht das wie folgt aus:



    Zusammenfassung:

    Ein SDL_Event ist das eigentliche Eventelement. Jedoch enhält ein SDL_Event die internen Strukturen der Eingabeger&aul;te. Wir müssen daher immernoch zumindest die wichtigsten Elemente der Strukturen von Maus, Joystick und Tastatur kennen, um diese Geräte effektiv verwenden zu können.

    Und woher wissen wir als Entwickler nun, in welchen Strukturen die Nutzdaten gespeichert sind ? Und welches Gerät hat dieses Event eigentlich ausgelöst ? Dies wurde mit dem ersten Element in SDL_Event gelöst. Dies ist "type". Type gibt Auskunft darüber, von welcher Art das Event ist, das in diesem Element gespeichert ist. Folgende Tabelle zeigt die möglichen Einträge mit der entsprechend verantwortlichen Unterstruktur, auf die wir bei benötigten Dateilinformationen zugreifen müssen:

    Ereignis-NameEreignis-Datentyp
      
    SDL_ACTIVEEVENTSDL_ActiveEvent
    SDL_KEYDOWN/UPSDL_KeyboardEvent
    SDL_MOUSEMOTIONSDL_MouseMotionEvent
    SDL_MOUSEBUTTONDOWN/UPSDL_MouseButtonEvent
    SDL_JOYAXISMOTIONSDL_JoyAxisEvent
    SDL_JOYBALLMOTIONSDL_JoyBallEvent
    SDL_JOYHATMOTIONSDL_JoyHatEvent
    SDL_JOYBUTTONDOWN/UPSDL_JoyButtonEvent
    SDL_QUITSDL_QuitEvent
    SDL_SYSWMEVENTSDL_SysWMEvent
    SDL_VIDEORESIZESDL_ResizeEvent
    SDL_USEREVENTSDL_UserEvent

    Die Programmierlogik sollte wie folgt sein:
    Ein Event legt ein SDL_Event in den Puffer ab. Wir lesen das Event aus und schauen auf den Type. Ist dieser beispielsweise ein SDL_Mousemotion, dann wissen wir dass es (natürlich) ein MausMotion-Element ist und alle weiteren Mausinformationen (in diesem Falle Position, etc) in SDL_MouseMotionEvent gespeichert ist. Wir greifen dann auf die tieferen Strukturen und Elemente dieser Ereignis-Typen zu.

    An letzter Stelle noch die Erklärung zum auslesen des Puffers. Wir beobachten im moment nur den obersten Eintrag in der Queue, also das Element, das schon am längsten im Puffer ist. Dies können wir mit zwei Funktionen aus dem Speicher lesen (und somit herauslöschen):

  • int SDL_PollEvent(SDL_Event *event);
  • int SDL_WaitEvent(SDL_Event *event);

  • SDL_PollEvent und SDL_WaitEvent unterscheiden sich von der Funktionalität her nicht. PollEvent ist eben eine aktive Funktion, während WaitEvent passiv auf ein Event wartet.

    Es gibt noch einige weitere Funktionen, um den Puffer zu beeinflussen. Beispielweise kann man Filter auf Events setzen, Einträge beobachten ohne zu löschen, manuell neue Elemente in die Queue einfügen oder den Status der einzelnen Events ändern. Die detailierten Erklärungen gehen aber weit über den Umfang dieses Tutorials hinaus. Die Funktionen sind einfach zu verstehen und in ihrer Anwendung, wenn man die Grundlagen des Input-Handling verstanden hat. Ein Blick in die API-Referenz des Doc-Projects (7) sollte hier eine wertvolle Hilfe sein.

    Soweit die Theorie. Kommen wir nun zur ersten Anwendung mit Events.


    Input-Handling in der Praxis: Tastatur

    Die erste Anwendung nimmt nur unsere Tastatureingaben auf und gibt auf der Standardausgabe den Namen der Tastatur aus. Zusätzlich wird noch mit angegeben, um welchen Tastenstatus es sich handelt.

    #include <stdlib.h>
    #include "SDL.h"
    
    int main()
    {
            SDL_Event event;
            int quit = 0;
    
            if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
            { 
              fprintf( stderr, "Konnte SDL nicht initialisieren: %s\n", SDL_GetError() );  
              exit( -1 );  
            }
            
            if( SDL_SetVideoMode( 320, 200, 8, SDL_SWSURFACE ) < 0)
            { 
              fprintf( stderr, "Konnte video mode nicht setzen: %s\n", SDL_GetError() ); 
            }
            
            atexit(SDL_Quit);
    
            while( quit == 0)
            {
                while( SDL_PollEvent( &event ) )
                {
                    switch( event.type )
                    {
                      case SDL_KEYDOWN:
                        printf( "Press: " );
                        printf( " Name: %s\n", SDL_GetKeyName( event.key.keysym.sym ) );
                      break;
                        
                      case SDL_KEYUP:                    
                        printf( "Release: " );
                        printf( " Name: %s\n", SDL_GetKeyName( event.key.keysym.sym ) );
                      break;
    
                      case SDL_QUIT:  // SDL_QUIT  int ein schliessen des windows
                        quit = 1;
                      break;
    
                      default:
                      break;
                    }
                }
            }
      exit( 0 );
    }
    

    Detail

    Zuerst wird ein "event" vom Typ SDL_Event angelegt. Dies ist natürlich unser Union mit den Ereignis-Elementen. Wir initialisieren zuerst SDL, wie wir es bereits in den ersten Kapiteln gemacht haben. Die while-Schleife ist der Beginn des relevanten Code. Hier sind 2 while-Schleifen in einander geschachtelt. Die Äussere ist eine einfache Endlosschleife, um ständig von der Tastatur zu lesen. Diese wird abgebrochen, wenn die Variable "quit" auf "1" gesetzt wird. Die innere Schleife holt mittels "SDL_PollEvent( &event )" das erste Element aus der Queue. Der Nachfolgende Switch überprüft zu erst den Typ des abgeholten SDL_Event-Elements. Die ersten zwei Anweisungen sind die Fälle, wenn ein Tastendruck erfolgt. Die dritte Anweisung ist ein SDL_QUIT. SDL_QUIT steht im "event.type" wenn das SDL-Fenster geschlossen wird.

    Wie bereits in den Tastaturdetails wurde erwähnt, dass event.key.keysym.sym nur die ASCII-Werte der Tasten speichert. Nur wissen die meisten Menschen nicht auswendig, welcher Buchstabe von welchem Wert repräsentiert wird. Um eine lesbare Augabe der Tasten zu erzeugen kann das ASCII-Mapping für die Taste zu Hilfe gezogen werden. Dies wir durch SDL_GetKeyName() realisiert. Die Rückgabe dieser Funktion ist ein in SDL_keysym.h (gehört zu den Standardincludes der libsdl) festegelegter String, also der Name der zugehörigen Taste.

    Zu den Modifiers gibt es kein Mapping. Modifiers werden in "mod" bereits mit einer Konstanten gespeichert. Um dies aussagekräftiger zu machen, wird in der offizielle Dokumentation folgende Funktion als Lösungsvorschlag gemacht:

    
    void PrintModifiers( SDLMod mod )
    {
       printf( "Modifers: " );
    
       // wenn keine Modifiers aktiviert waren...
       if( mod == KMOD_NONE )
       {
           printf( "None\n" );
           return;
       }
    
       // Ueberpruefe die Praesenz jedes einzelnen SDLMod-Wertes.
       // Das sieht zwar unsauber aus, aber es gibt keinen besseren Weg.
       if( mod & KMOD_NUM ) printf( "NUMLOCK " );
       if( mod & KMOD_CAPS ) printf( "CAPSLOCK " );
       if( mod & KMOD_LCTRL ) printf( "LCTRL " );
       if( mod & KMOD_RCTRL ) printf( "RCTRL " );
       if( mod & KMOD_RSHIFT ) printf( "RSHIFT " );
       if( mod & KMOD_LSHIFT ) printf( "LSHIFT " );
       if( mod & KMOD_RALT ) printf( "RALT " );
       if( mod & KMOD_LALT ) printf( "LALT " );
       if( mod & KMOD_CTRL ) printf( "CTRL " );
       if( mod & KMOD_SHIFT ) printf( "SHIFT " );
       if( mod & KMOD_ALT ) printf( "ALT " );
       printf( "\n" );
    }
    
    


    Anmerkungen:

    Natürlich sollte man auch hier wieder ein Blick in die API-Referenz (7) werfen. Eine Funktion sei aber dennoch hier kurz erwähnt: "SDL_EnableKeyRepeat()". Diese gibt an, ab wann eine gedrückte und gehaltene Taste als Wiederholung dieser gilt.

    Um nun Inputhandling an einem richtigen Beispiel zu zeigen, wird im Folgenden die MouseMotion-Ereignisse dazu dienen, unseren Tux über den Bildschirm zu bewegen.


    Input-Handling in der Praxis: Maus

    Eine kurze Anmerkung zuvor:
    Die folgende Applikation ist natürlich noch bei weitem nicht optimiert. Die langsame Geschwindigkeit des blit von Bild und Hintergrund bemerkt man noch sehr stark. Codeoptimierung (z.B. durch RLE und/oder Surfaceformatierung) wird noch in späteren Kapiteln besprochen. Auch der reine Programmcode wirkt sehr unschön. Viel Funktionalität und Fehlerhandling gehört verbessert, wie auch das Auslagern in Funktionen die Übersichtlichkeit steigert. In der Praxis entwickelt der SDL-User gerne Funktionen, die er ständig wieder nutzt. Wiederverwertbarer Code ist, wie im normalen Programmiereralltag, auch hier das Stichwort. Dieser Code hier soll nun verständlich und strukturiert das Inputhandling erklären; auch wenn dies nicht der schönste Weg ist.

    #include <stdlib.h>
    #include <iostream.h>
    #include "SDL.h"
    #include "SDL_image.h"
    
    int main()
    {
      // Die zwei bereits bekannten Surfaces deklarieren
      SDL_Surface *display;
      SDL_Surface *image;
      SDL_Surface *hintergrund;
      
      SDL_Event event;
      int quit = 0;
    
      SDL_Rect drect;
      SDL_Rect srect;
      
      if ( SDL_Init( SDL_INIT_VIDEO) < 0 )
      {
         fprintf(stderr, "SDL konnte nicht initialisiert werden:  %s\n", SDL_GetError());
         exit(1);
       }
    
       atexit(SDL_Quit);
       
       display = SDL_SetVideoMode( 800, 600, 16, SDL_SWSURFACE);
       if ( display == NULL )
       {
         fprintf(stderr, "Konnte kein Fenster: Auflösung 800x600 oeffnen werden: %s\n", SDL_GetError());
         exit(-1);
       }
    
      // Bild laden
      image = IMG_Load("tux-transparent.gif");
      if (image == NULL)
      {
         fprintf(stderr, "Das Bild konnte nicht geladen werden: %s\n", SDL_GetError());
         exit(-1);
      }
    
      // Hintergrundbild laden
      hintergrund = IMG_Load("dawn.jpg");
      if (hintergrund == NULL)
      {
         fprintf(stderr, "Das Bild konnte nicht geladen werden: %s\n", SDL_GetError());
         exit(-1);
      }
    
      // Mauszeiger verstecken
      SDL_ShowCursor(0);
      
      // Kompletten Hintergrund aufbauen
     SDL_BlitSurface( hintergrund, 0, display, 0 );
     SDL_Flip( display );
      
      // Event-Handling  
            while( quit == 0)
            {
                while( SDL_PollEvent( &event ) )
                {
                    switch( event.type )
                    {
                        case SDL_MOUSEMOTION:
                        printf( "Mausposition: x: %i und y: %i\n", event.motion.x, event.motion.x);
    
                       // alte stelle wieder fuellen
                       SDL_BlitSurface(hintergrund, &drect, display, &drect);
                       SDL_UpdateRects(display,1,&drect);
    
                       // Setzen des Quellbereichs
                       srect.x = 0;
                       srect.y = 0;
                       srect.w = image->w;  // das gesamte Bild
                       srect.h = image->h;   // das gesamte Bild
    
                        // Setzen des Zielbereichs
                       drect.x = event.motion.x;
                       drect.y = event.motion.y;
                       drect.w = image->w;
                       drect.h = image->h;
    
                       // kopiere surface auf display-surface
                       SDL_BlitSurface(image, &srect, display, &drect);
    
                       // den veränderten Bildschirm-Bereich auffrischen
                       SDL_UpdateRects(display,1,&drect);
                       
                        break;
    
                        case SDL_MOUSEBUTTONDOWN:     // Quit beim druecken des Mousbuttons
                            quit = 1;
                            break;
    
                        default:
                            break;
                    }
                }
            }
      
      // Das Bitmap-Surface löschen
      SDL_FreeSurface(image);
    
      exit(0);  
    }
    

    Detail:

    Die komplette Initialisierung ist wie gehabt. Wir nutzen dieses mal ein transparentes Gif für unser Bild. In einem späteren Kapitel werde ich auch zeigen, dass wir auch Farbwerte als Transparenzfarbe angeben können und somit den selben Effekt auf normale Bilder erzeugen. Danach laden wir ein Hintergundbild, welches wir einfach direkt und komplett auf das display-surface blitten und mit SDL_Flip auch komplett neu zeichnen. Oft stört der Mauszeiger : SDL_ShowCursor(0) schaltet den Mauscursor aus. Setzt mal den Parameter wieder auf 1, so wird der Zeiger wieder eingeschaltet.

    Nun folgt das eigentliche Eventhandling. Auch hier sieht es aus, wie bei unserem Tastaturbeispiel. Ist es im Prinzip auch. Wir unterscheiden nur den event.type. Die Struktur des MouseMotionEvents ist so, dass die Elemente x und y die Position hält. Diese Werte nutzen wir, und setzen in der Schleife das SDL_Rect des Zielbereichs (hier also drect).


    [9] Grafik für Experten


  • Farbtransparenz
  • int32 trans; // int-Groesse >= Bildfarbtiefe trans = SDL_MapRGB( image->format, 0x0, 0x0, 0x0 ); SDL_SetColorKey( image, SDL_RLEACCEL | SDL_SRCCOLORKEY, trans);
  • Alpha-Kanal
  • SDL_DisplayFormatAlpha(image); SDL_SetAlpha( image, SDL_SRCALPHA, 200 ); Flags: flags is used to specify whether alpha blending should be used (SDL_SRCALPHA) and whether the surface should use RLE acceleration for blitting (SDL_RLEACCEL). flags can be an OR'd combination of these two options, one of these options or 0. If SDL_SRCALPHA is not passed as a flag then all alpha information is ignored when blitting the surface. The alpha parameter is the per-surface alpha value; a surface need not have an alpha channel to use per-surface alpha and blitting can still be accelerated with SDL_RLEACCEL. Note: The per-surface alpha value of 128 is considered a special case and is optimised, so it's much faster than other per-surface values. 0 = volle Transparenz 255 = volle Deckung
  • Sprites und Animationen
  • sdl_gfx Rotatezoom, etc von (6)
  • sdl_ttf
  • clipping
  • Collision Detection
  • SDL_DisplayFormat()
  • Kommt noch

    [10] Sound


  • gfx
  • music

  • [11] Tipps und Tricks


  • Windowmanagement
  • sdl_net
  • Codeoptimierung/Geschwindigkeitoptimierung
  • Ausgabe ueber andere Videodevices
  • - aalib mit eincompilieren mit --enable-video-aalib - Debainpakete mit allen switches gibt es framebuffer,aalib getesteet durch setzen der Umgebungsvariablen $SDL_VIDEODEVICE; siehe genaue FAW GEneral(?) Beschreibung

    [12] SDL_Ausblick


    Auf dir Frage nach SDL VErsion 2.0 sagt Sam in der offiziellen FAQ folgendes:

    A: SDL 2.0 will be a full redesign of the SDL functionality, based on what we've learned over the past four years. The architecture design is partially done, and we'll start prototyping the design soon. As soon as there's a working framework, we'll make it publicly available for comment and contributions. This new framework has about a year or so before we anticipate it being ready for stable release. --- cut ---

  • Language-Bindings
  • Kein 3D ? OpenGL -> MESA

  • Autor

    Marco Kraus betreut libSDL.de und ist unter marco@libsdl.de zu erreichen.
    Er freut sich über jegliche Form von Feedback.


    Quellen

    (1) http://www.devolution.com/~slouken
    (2) http://www.libsdl.org/mailman/listinfo
    (3) http://www.libsdl.de/liste.htm
    (4) http://www.libsdl.org
    (5) http://www.geekcomix.com/snh/files/docs/sdl-kdev/sdl-kdev-mini-how2.html
    (6) http://www.ferzkopp.net/Software.html
    (7) http://sdldoc.csn.ul.ie

    (c) 2002 by Marco Kraus