Sitzung: Jeden Freitag ab 14:30 s.t. online. Falls ihr den Link haben wollt, schreibt uns.

C-Kurs/Tron

So in etwa soll dann unser Spiel aussehen

Hier wollen wir euch mal ein wenig SDL zeigen - am Beispiel von Tron. SDL steht für "Simple Direct media Layer" und kümmert sich um Tastatureingaben und um ein Zeichenfenster (eigentlich um noch viel mehr, siehe unten). SDL ist plattformunabhängig; ihr könnt euren Code also sowohl unter Linux als auch unter Windows oder Mac compilen und ausführen.

Tron: Tron ist ein kleines Spiel für zwei (oder mehr) Spieler, dass ihr vielleicht aus dem Film Tron oder dem Spiel Amagetronad kennt. In unserer Zwei-Spieler-2D-Variante startet jeder Spieler als ein Punkt auf dem Spielfeld. Während des Spieles zieht jeder Spieler eine Linie hinter sich her. Fährt man irgendwo gegen (gegnerische Linie, eigene Linie, Bildschirmrand), so hat man verloren und eine neue Runde beginnt.

Das Ganze machen wir Schritt für Schritt, vom Initialisieren des Fensters über Abarbeiten der Tastatur-Events und Erkennen der Kollisionen bis hin zum Zeichnen des Feldes. Falls ihr mehr über eine der SDL-Funktionen wissen wollt (also alle Funktionen/Datenstrukturen, die mit SDL_ anfangen), könnt ihr entweder auf http://www.libsdl.org/cgi/docwiki.cgi gucken oder (falls ihr ein Unix-artiges Betriebssystem mit installiertem SDL habt) einfach "man FUNKTION" (z.B. man SDL_Init) in eine Konsole eingeben um Hilfe zu dieser Funktion zu bekommen. Ich werde an entsprechenden Stellen einfach (man funktion) als Hinweis schreiben, dass man mit dieser Funktion noch mehr anstellen könnte.

Was ihr braucht

Ein erstes Fenster

Fangen wir mit unserem Fenster an.

SDL initialisieren

Zu Beginn müssen wir das SDL Headerfile und unsere pixel.h includen.

 #include <SDL/SDL.h>
 #include "pixel.h"

In der Main-Funktion rufen wir zu aller erst die Initialisierungsfunktion von SDL auf:

SDL_Init(SDL_INIT_EVERYTHING);

Der Einfachheit halber initialisieren wir alles, was SDL zu bieten hat. Man kann ggf. nur Teilsysteme initialisieren (man SDL_Init), das ist für uns aber gerade nicht so wichtig.

Als nächstes müssen wir SDL sagen, dass wir ein Fenster haben wollen und wie groß dieses sein soll. Das machen wir mit der Funktion SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags). Wir geben der Funktion die gewünschten Parameter für das Fenster und SDL gibt uns ein SDL_Surface (dieser Typ hält in SDL eine Grafik, in diesem Fall unser Fenster).

 SDL_Surface *screen = SDL_SetVideoMode(640, 480, 32, SDL_SWSURFACE);

Oookay.. SDL gibt uns hier ein

  • 640 Pixel breites
  • 480 Pixel hohes Fenster mit
  • 32 Bits pro Pixel (hat was mit der Farbtiefe zu tun, für uns nicht so wichtig, das passt schon so)
  • als SDL_SW_SURFACE (soll heißen wir benutzen keine Hardwarebeschleunigung)

Für den letzten Parameter, also die Window Flags, gibt es viele verschiedene Parameter (man SDL_VideoMode), die man einfach miteinaner verunden könnte (z.B. SDL_HWSURFACE | SDL_FULLSCREEN für Vollbild mit Hardwarebeschleunigung), aber auch das ist für uns gerade nicht wichtig.

Zu guter letzt rufen wir am Ende der main-Funktion noch ein SDL_Quit() auf, damit SDL sich deinitialisiert, wenn wir das Programm beenden.

Nun wollen wir unser Grundgerüst ausprobieren. Damit, wenn wir das Programm starten, das Fenster nicht sofort wieder zu geht packen wir hinter die Initialisierung (aber vor das SDL_Quit()) ein SDL_Delay(5000) (SDL_Delay nimmt die Zeit in Millisekunden, d.h. der Aufruf wartet 5 Sekunden). Und nun compilen und ausprobieren!

gcc -Wall tron.c pixel.c -lSDL -o tron 

Falls ihr (wie ich) zu faul seid das jedes mal einzugeben könnt ihr auch folgendes Makefile benutzen (soll heißen in die Datei Makefile pasten) und dann nur noch make bzw. make run eingeben.

CFLAGS=`sdl-config --cflags` -Wall
LDFLAGS=`sdl-config --libs`
OBJ=tron.o pixel.c

.PHONY: clean run

tron: $(OBJ)

run: tron
	./tron

clean:
	rm -f *.o tron


Jetzt solltet ihr für 5 Sekunden ein Fenster sehen, welches sich dann wieder schliesst.

SDL Events

Ein sich nach einiger Zeit wieder schliessendes Fenster bringt uns natürlich nicht so viel. Was wir wollen ist mit SDL zu interagieren. Wir wollen wissen, ob jemand eine Taste auf der Tastatur gedrückt hat oder das Fenster schliessen wollte. All das sagt uns SDL mit sog. Events. Davon gibt es viele für alle möglichen Gegebenheiten (man SDL_Event), aber wir wollen als erstes nur den Fall des sich schliessenden Fensters betrachten.

Als erstes brauchen wir eine Variable vom Typ SDL_Event in der wir das Event speichern. Um SDL nach einem Event zu fragen übergeben wir int SDL_PollEvent(SDL_Event*) eine Referenz zu dieser Variable.

 SDL_Event event;
 SDL_PollEvent(&event);

SDL_PollEvent hat als Rückgabewert 1, wenn es ein Event gefunden hat und 0, falls es kein neues Event gab. SDL_Event event ist ein struct, in event.type steht der Typ des aktuellen Events. Für unser Spiel sind folgende Events wichtig (Dies sind dann auch die Werte, welche in event.type stehen): SDL_QUIT falls das Programm beendet werden soll und SDL_KEYDOWN, falls eine Taste auf der Tastatur gedrückt wurde.

Schreibt nun in die main-Funktion eine while-Schleife. In dieser Schleife sollen SDL-Events abgefragt werden (unser sog. Event-Loop). Wenn ein SDL_QUIT-Event auftritt soll das Programm beendet werden. Damit die while-Schleife nicht all eure CPU-Zeit frisst solltet ihr mit SDL_Delay eine Verzögerung von 10-50 Millisekunden einbauen.

Nun kann man das Fenster über das ihm gegebene X (bei entsprechendem Windowmanager *hust*) schließen. Horray, unser Fenster hört auf uns!

Tron

Zustandsvariablen

Es wird Zeit, dass wir zu unserem Spiel kommen. Ein Spieler in Tron hat in etwa folgende Eigenschaften:

  • eine Position (x/y)
  • eine Richtung (up, down, left, right)
  • Punktestand
  • Farbe des Spielers (Definiert als Rot/Grün/Blau-Wert von 0-255)

Für Position und Punke können wir jeweils den Typ int wählen. Für die Farbe nehmen wir SDL_Color (das ist ein von SDL definiertes struct mit drei Uint8 r, g und b für den Farbwert rot, grün und blau (man SDL_Color)). Bei der Richtung könnt ihr euch überlegen, ob ihr lieber ein enum nehmen wollt anstatt eines ints. Wir packen diese Werte in ein struct Player, damit wir später Funktionen nur einmal schreiben müssen und dann für jeden Spieler jeweils einmal aufrufen können.

Schreibt nun eine Funktion void initPlayers(), welche die Variablen der beiden Spieler initialisiert (und ihnen auch eine schöne Farbe gibt ;). Die Startposition der Spieler machen wir abhängig von der Spielfeldgröße. Ich schlage halbe Höhe des Feldes als Y-Koordinate und 1/3 bzw. 2/3 der Breite als Startposition vor. Hier ein Codeschnipsel, wie ihr an das SDL_Surface vom Fenster und an dessen Höhe/Breite kommt:

 [...]
 SDL_Surface *screen = SDL_GetVideoSurface();
 int hoehe = screen->h;
 int breite = screen->w;

Der Rückgabewert von SDL_GetVideoSurface ist hierbei mit dem von SDL_SetVideoMode identisch (halt der Zeichenbereich unseres Fensters).

Ruft nun initPlayers() in eurer main-Funktion nach dem Initialisieren von SDL auf (sonst ist der Rückgabewert von SDL_GetVideoSurface() nicht definiert und ihr habt keine Fensterhöhe/breite).

Zeichnen der Spieler

Nun wollen wir anfangen unsere beiden Spieler zu zeichnen und sich bewegen zu lassen. Dazu setzen wir den Pixel, auf dem der Spieler sich gerade befindet auf seine Farbe.

 putpixel(SDL_Surface *screen, int x, int y, Uint32 pixel);

Da putpixel einen Uint32 als Farbwert erwartet müssen wir unser SDL_Color wohl umwandeln. Dies passiert mit der Funktion

 Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b);

wobei mit SDL_PixelFormat das PixelFormat unseres screens gemeint ist (screen->format (man SDL_Surface)).

  • Schreibt eine Funktion Uint32 color2rgb(SDL_Color) welche ein SDL_Color in den entsprechenden Uint32 umwandelt.
  • Schreibt eine Funktion void paintPlayer(struct Player*) welche einen Spieler zeichnet.

Wichtig: Eure Zeichenoperationen (also alles, was etwas in das Fenster bringt) werden erst sichtbar, wenn ihr SDL sagt, dass ihr eure "Änderungen" sehen wollt. Der Einfachheit halber benutzt nach euren Zeichenoperationen int SDL_Flip(screen) (wobei screen unser Fenster (SDL_Surface*) ist) um dies zu tun (wer hier mehr drüber wissen will kann sich mal SDL_UpdateRect anschauen).

Nun zeichnet in eurer main-Funktion vor dem Event-Loop die beiden Spieler und startet das Programm. Jetzt können wir unsere beiden Spieler als Pixel sehen.

Bewegen der Spieler

Ohne Bewegung wird das alles auf Dauer ziemlich langweilig. Deshalb brauchen wir eine Funktion void movePlayer(struct Player*), welche abhängig von der Richtung des Spielers dessen Position verändert. Diese Funktion müssen wir nun in unserer Event-Loop unterbringen. Wenn wir kein Event haben, dann wollen wir unsere Spieler beide bewegen (movePlayer), beide zeichnen (paintPlayer), unseren Bildschirm aktualisieren (SDL_Flip) und dann ein wenig warten (SDL_Delay).

Wenn das fertig ist und wir unser Programm starten, dann sehen wir, dass beide Spieler sich in ihre Richtung bewegen und sich ggf. übermalen. Sobald aber einer aus dem Bildschirm verschwindet passieren komische Sachen: Entweder sie kommen auf der anderen Seite wieder raus oder unser Programm stürzt ab. Hmmmm, da fehlt uns wohl...

Kollisionen erkennen und behandeln

In unserem Spiel gibt es drei Arten von Kollision

  • Ein Spieler fährt aus dem Fenster raus
  • Ein Spieler fährt gegen eine Linie (eine eigene oder eines Gegners)
  • Beide Spieler befinden sich auf der gleichen Position (unentschieden!)

Die ersten beiden Fälle wollen wir mit der Funktion behandeln int playerCollided(struct Player*). Sie soll überprüfen, ob der Spieler in seiner aktuellen Position ausserhalb des Bildschirms ist oder ob dort schon einer vor ihm war. Letzteres könnt ihr leicht überprüfen, indem ihr guckt, ob an dieser Stelle ein schwarzer Pixel ist (soll heißen: leeres Spielfeld). Das könnt ihr überprüfen, indem ihr schaut, ob der Wert, den Uint32 getpixel(SDL_Surface *screen, int x, inty) zurückliefert dem entspricht, was SDL_MapRGB für Schwarz (0, 0, 0) zurückgibt. Die Funktion soll 1 für Kollision und 0 für keine Kollision zurückgeben.

Nun brauchen wir die Funktion int gotCollison() welche 0 für keine Kollision, 1 für Player1 kollidiert, 2 für Player2 kollidiert und 3 für Unentschieden zurückgibt. Benutzt hierzu playerCollided() um herauszufinden, ob ein Spieler kollidiert ist und implementiert ausserdem den 3. Fall.

Aber was machen wir, wenn wir eine Kollision erkannt haben? Wir setzen alles wieder auf den Startzustand. Also brauchen wir eine Funktion void resetGame(), die

  • unser Fenster komplett schwärzt (uns von den alten Linien befreit)
  • die Spieler auf ihre Startpositionen setzt (Richtung nicht vergessen!)
  • die beiden Spieler zeichnet

Zum Schwärzen des Bildschirms könnt ihr SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0, 0, 0)) benutzen (man SDL_FillRect). NULL steht hierbei für das zu füllende Rechteck - wir übergeben keines, denn dadurch wird automatisch der gesamte Bildschirm angenommen.

Nun kombinieren wir all diese schönen Funktionen miteinander in unserer Event-Loop in unserer main-Funktion:

  • Fortbewegen beider Spieler
  • Überprüfen, ob eine Kollision stattfand
    • Wenn ja: Dem jeweiligen Spieler, der nicht kollidiert ist, einen Punkt geben und das Spielfeld resetten
    • Wenn nicht: Beide Spieler auf ihrer neuen Position zeichnen
  • Den Bildschirm updaten ("flippen")

Puh! Nun haben wir es aber auch schon fast geschafft. Fehlt nur noch die Bewegung der Spieler und eine Punkteanzeige.

Tastatureingaben zu Richtungswechsel

Um die Richtung eines Spielers zu wechseln müssen wir ihm eigentlich nur einen anderen Wert in seine Richtungsvariable schreiben. Den Rest macht dann schon unsere movePlayer-Funktion in der Event-Loop. Aber wie kriegen wir raus, welche Taste gedrückt wurde? Genau, per Event. Momentan dümpelt in unserer Event-Abfrage nur unser SDL_QUIT-Event rum. Nun wollen wir ausserdem gucken, ob es sich nich doch um ein SDL_KEYDOWN-Event handelt. Wenn es eines ist (event.type == SDL_KEYDOWN, schon vergessen?), steht in event.key.keysym.sym die Taste, die gedrückt wurde. sym ist vom Typ SDLKey. Alle benutzbaren Tasten findet ihr unter (man SDLKey), aber momentan sollten SDLK_LEFT, SDLK_RIGHT, SDLK_UP, SDLK_DOWN, SDLK_a, SDLK_w, SDLK_s und SDLK_d reichen.

Also: Behandelt das SDL_KEYDOWN-Event, schaut ob eine Taste für einen der Spieler gedrückt wurde und behandelt diese. Wenn ihr das Spiel etwas einfacher machen wollt, dann passt auf, das kein Spieler in sich selbst steuern kann (d.h. keine 180° Wendung, also z.B. von left zu right wechseln).

Tja, nun habt ihr ein spielbares (und plattformunabhängiges) Multiplayer 2D Tron Spiel!

Punkte?

Naja gut, was noch fehlt ist eine Punkteanzeige. Die realisieren wir ganz einfach über unseren Fenstertitel. Den kann man mit

 SDL_WM_SetCaption("Mein tolles Fenster!1!!", 0);

setzen.

Schreibt eine Funktion void updateWindowTitle() welche (z.B. mit sprintf) einen Titel für das Fenster (z.B. C-Kurs Tron) und dahinter den aktuellen Punktestand zusammensetzt und unserem Fenster diesen gibt. Diese Funktion muss immer aufgerufen werden, wenn sich der Punktestand verändert (und natürlich auch einmal am Anfang). Sucht euch einen schönen Platz dafür aus.

Bonusrunde

Wer jetzt noch nicht genug hat...

Einsteiger-Bonus: Quit on Esc

Das Spiel soll sich beenden, wenn jemand Escape drückt.

Bonus: Fenster resizen

Macht das Fenster vergrößerbar bzw. verkleinerbar! Denkt dran, dass bei einer neuen Runde die Spieler auch an der richtigen Stelle anfangen (falls ihr diese Werte direkt als Zahlen reingeschrieben habt).

Bonus: Ein Torus als Spielfeld

Warum sollte man an der Spielwand crashen? Man könnte doch an der jeweils anderen Seite des Feldes herauskommen...

Bonus Bonus: Massive Multiplayer

Baut das Spiel so um, dass man beim Starten auf der Kommandozeile eine Spielerzahl übergeben kann (z.B. 2-4 Spieler). Damit es nich zu eng auf der Tastatur wird könnte es nützlich sein, wenn ihr das Spiel so umschreibt, dass man nur noch zwei Tasten für Links/Rechts hat um den Spieler zu kontrollieren (Megabonus: Ausser ihr baut Maus+Joysticksupport ein).

Further reading oder "Was kann SDL denn noch?"

SDL hat noch einige interessante Zusatzmodule

  • SDL_Image - Kann ein haufen Bildformate laden und in ein SDL_Surface packen (von sich aus kann SDL nur .bmp)
  • SDL_Mixer - SDL Soundausgabe
  • SDL_Net - SDL Netzwerkbibliothek

Ausserdem kann SDL ziemlich einfach geladene Bilder auf den Bildschirm bringen (man SDL_BlitSurface), Tastatur/Maus/Joystick eingabe behandeln, Musik/Soundeffekte zusammenmixen oder auch (mit der entsprechenden Bibliothek) Netzwerkkram machen. Gerne wird es für 2D Spiele benutzt (z.B. supertux, xmame, frozen-bubbles, ...), aber auch als Wrapper für OpenGL (sauerbraten, der Quake3 Linux Port, warsow, armagetronad...); teilweise nutzen auch einige Anwendungen SDL (mplayer, virtualbox, dosbox, ...).

Einsteigerfreundliche SDL-Tutorials:

Happy Hacking ;)