Multimediaentwicklung mit SDL
Über dieses TutorialBeim 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.
[2] EntstehungsgeschichteSDL 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 SchritteNatü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:
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:
Hier passiert nun nicht mehr, als dass SDL initialisiert wird
und sich das Programm wieder beendet.
Detail:
SDL wird in 6 Funktionsbereiche eingeordnet:
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] VideomodusWir 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:
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:
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.
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:
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 anzeigenSDL 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:
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:
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:
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:
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] BildbereicheWie 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:
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.
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.
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:
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 zeichnenBisher 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:
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:
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:
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():
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:
[8] Input-Handling : GrundlagenDie 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 ?
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:
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".
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:
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:
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):
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.
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
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:
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:
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).
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 ---
|