SDL Tutorial (C#)

Das Tutorial, was du grade angefangen hast zu lesen, dreht sich um SDL und die Verwendung von SDL in C#, bzw. in allen .Net-Sprachen.

Vorwort – Hier steht noch nichts sehr wichtiges drin. Wenn du willst, kannst du direkt zum nächsten Punkt springen, aber ich fände es toll, wenn du es lesen würdest. Hab mir ja Mühe gegeben^^
Was ist SDL?
SDL steht für Simple DirectMedia Layer
Wieso SDL?
SDL ist eine Bibliothek, die für alle großen Sprachen umgesetzt wurde. Das heisst, dass die Verwendung unter vielen Sprachen ähnlich ist. Ich bin C++ Quellcodes das erste mal mit SDL in Berührung gekommen, habe in Python gelernt, damit umzugehen und konnte mein Wissen fast problemlos in C# umsetzen, da die Befehle genau die selben sind.
SDL bietet Schnittstellen zur Grafikausgabe(2D und mit z.B. OpenGL 3D), zur Tonausgabe(Selbst auf CDs kann man damit zugreifen, das werde ich aber nicht erklären ;)), für Eingabe(Tastatur, Maus, Joystick…) sowie zur Netzwerkkommunikation. (Es gibt aber sicherlich auch bessere Frameworks für Netzwerk^^)
Alles in allem beinhaltet SDL also alles, was man zum Spieleprogrammieren braucht!
Wieso nicht XNA?
Ganz einfach: Während die aktuelle Version von XNA bereits .NET 4.0 benötigt, (Afaik. Aber mindestens .NET 3.5 ;)) kommt SDL.NET auch locker mit .NET 2.0 klar, was hilfreich sein kann, wenn man das ganze mit Mono machen will, denn es wird auch kein DirectX vorrausgesetzt, womit SDL-Programme auch auf sehr alten Rechnern laufen können. Das sollte aber für uns ziemlich unwichtig sein 😉
Des weiteren kann man SDL wie bereits gesagt auch in anderen Sprachen einsetzen, was mit dem XNA-Framework nicht möglich ist. Ursprünglich wollte ich das Tutorial für Python schreiben, aber ich bezweifle, dass sich hier viele für Python interessieren.
Ein Nachteil gegenüber XNA ist, dass die Migration auf Zune oder die XBox360 wesentlich komplizierter, eigentlich für den Normaluser unmöglich ist. Normaluser bist du, wenn du keinen Zugang zu speziellen Dev-Tools für Konsolen hast 😛
Brauche ich für das Tutorial Vorkenntnisse?
Ich gehe davon aus, dass du bereits ein wenig Ahnung von C# und .NET hast, du solltest auf jeden Fall damit umgehen können. Ausserdem solltest du ein wenig Mathe können^^ Ich werde vorraussichtlich nicht bspw. Integral- oder Vektorrechnung verwenden – Aber die Grundregeln solltest du beherrschen.
Und wann gehts jetzt los?
Jetzt.

Vorbereitung – Erfahrene Benutzer können sich das ganze klemmen und einfach die Zusammenfassung lesen!
Zuallererst brauchen wir die Bibliothek:
http://sourceforge.net/projects/cs-sdl/files/SDL.NET/
Als allererstes brauchen wir Visual Studio. Wie gesagt funktioniert das auch mit Mono, ich werde aber hier Visual Studio 2008 Express verwenden, da ich zu faul bin, die 2010er Version zu registrieren oder gar eine Profiversion zu saugen.
Wenn wir jetzt also unsere Entwicklungsumgebung haben, erstellen wir ein neues Programm und wählen “Konsolenanwendung” aus.

Als erstes müssen wir ein paar Verweise hinzufügen. Fangen wir einfach mit System.Drawing an. Im Projektmappenexplorer Rechtsklick auf “Verweise” und “Verweis hinzufügen” auswählen. Unter dem Reiter “.NET” musst du die Assembly “System.Drawing” hinzufügen. Wenn du das getan hast, kannst du nun SDL.NET einfügen. Unter dem Reiter “Durchsuchen” wählst du die Assemblies “sdlDoNet.dll” und “Tao.SDL” aus dem “bin”-Ordner vom SDL.NET Paket aus. Nun musst du aus dem “lib” Ordner von dem Archiv alle Dlls nehmen und in den Ausgabeordner(bei mir bin/debug, bzw. bin/release) kopieren. Dazu musst du die Projektmappe allerdings gespeichert haben!
Das sieht nun nicht schön aus, aber es ist das, was wir brauchen.
Zusammenfassung

  • Download:
    http://sourceforge.net/projects/cs-sdl/files/SDL.NET/
  • Konsolenanwendung erstellen
  • Assembly System.Drawing hinzufügen
  • DLLs aus dem bin/ Ordner des SDL.NET Paketes als Verweise hinzufügen
  • DLLs aus dem lib/ Ordner des SDL.NET Paketes in den Ausgabeordner kopieren

Bitte beachtet die Lizensierung der Bibliotheken, da SDL unter GPL lizensiert ist!

Erster Sourcecode
Aus der Mainmethode erstellen wir erstmal eine Instanz der Klasse Program. Das sieht dann aus wie folgt:

namespace Tutorial.SDL
{
    class Program
    {
        static void Main(string[] args)
        {
            Program app = new Program();
        }
        public Program()
        {
            Video.SetVideoMode(500, 400);
        }
    }
}

In dem hier gezeigten Code erstellen wir auch gleichzeitig ein Fenster mit den Maßen 500×400. Dafür ist – unglaublich – Der Befehl Video.SetVideoMode() zuständig. Die Klasse Video ist für jegliche Video- bzw. Fensterarbeit zuständig.
Die möglichen Parameter für SetVideoMode() sind folgende:

  • Keine Parameter: Es wird ein großes Fenster(Auflösung ist vermutlich die deines Bildschirms) angezeigt, dessen Größe unveränderlich ist und schwarz gefüllt ist.
  • (int width, int height): Es wird ein Fenster angezeigt, dessen Größe unveränderlich ist und schwarz gefüllt ist. Die Parameter geben die Auflösung des Fensters an.
  • (int width, int height, bool resizeable): resizeable gibt an, ob die Größe des Fensters verändert werden kann.
  • (int width, int height, bool resizeable, bool OpenGL): openGL gibt an, ob das Fenster OpenGL gerendert werden soll.
  • ([…], bool fullscreen): fullscreen gibt an, ob das Fenster im Vollbildmodus angezeigt werden soll.
  • ([…], bool hardwareSurface): hardwareSurface gibt an, ob das Bild über die Grafikkarte gerendert werden sollte. Aus Stabilitätsgründen sollte dieser Wert auf false stehen, es sei denn, du weist wirklich, was du tust!
  • ([…], bool frame): Wenn frame true ist, wird ein Rahmen um das Fenster gezeichnet. (Standard!) Wenn fullscreen true ist, wird dies ignoriert und der Rahmen einfach weggelassen.
  • (int width, int height, int bitsPerPixel): bitsPerPixel kann ich nicht besser erklären als der Name es schon selber tut. Wenn du nicht weißt, was das heißt, versuch es einfach zu umgehen
  • Die restlichen bool-Variablen sind auch für die Varianten mit bpp vorhanden und haben die selbe Bedeutung.

Wenn du jetzt auf F5 drückst bzw. über die Toolbar das debugging startest, wirst du sehen, dass das Bild nur ganz kurz aufflackert und dann das Programm sich wieder beendet. Um das zu umgehen, musst du

Events.Run();

aufrufen. Was das bedeutet, erkläre ich gleich.
Wenn du den Fenstertitel ändern willst, hast du dafür die Property WindowCaption(string).
Das Icon oben links kannst du mit der Funktion WindowIcon() (void) setzen.
Der Zyklus
Wie bereits erwähnt, musst du, damit sich das Fenster nicht direkt wieder schließt, die Funktion

Events.Run();

aufrufen. Doch wofür steht das Objekt Events?
Events ist ein sealed Member von SdlDotNet.Core. Die Klasse ist für jegliches Aktualisieren zuständig(Nicht das zeichnen!) und organisiert den Event-queue. Ich gebe eine kurze Zusammenfassung der wichtigsten Events, Methoden und Eigenschaften. (ein ! davor steht für ein Event und wird nicht mitgeschrieben!)

  • FPS (int) – Gibt die aktuellen FPS zurück und setzt die gewünschte FPS-Rate
  • TargetFPS (int) – Gibt die gewünschte FPS-Rate zurück und setzt diese auch.
  • Close() (void) – Schließt alle Untersysteme von SDL
  • PushUserEvent(UserEventArgs) (void) – Setzt ein User-Event an den Event-Queue. Ich definiere es hier nicht näher, das vertiefe ich später.
  • QuitApplication() (void) – Beendet das Programm
  • Start() (void) – Startet den Event-Queue. Ohne diesen Befehl wird der framerate-ticker nicht gestartet und das Fenster schließt sich sofort.
  • Wait() (void) – Stoppt den ticker solange, bis ein Event vorhanden ist. Für Performance-Zwecke sehr gut zu benutzen. Ich werde es hier vorerst nicht benutzen, aber ihr solltet es wissen 😉
  • ! AppActivate – Wird ausgelöst, wenn die App aktiv oder inaktiv wird.
  • ! KeyboardDown – Wird ausgelöst, wenn eine Taste auf der Tastatur gedrückt wurde. Die Starttaste wird nicht abgedeckt. Alle anderen Tasten werden als KeyboardEventArgs an den Eventhandler weitergegeben.
  • ! KeyboardUp – Wird ausgelöst, wenn eine Taste auf der Tastatur losgelassen wurde. Siehe oben.
  • ! MouseButtonDown – Wird ausgelöst, wenn mit der Maus geklickt wurde.
  • ! MouseButtonUp – Wird ausgelöst, wenn eine Maustaste losgelassen wurde.
  • ! MouseMotion – Wird ausgelöst, wenn sich die Maus bewegt.
  • ! Quit – Wird ausgelöst, wenn das Fenster geschlossen werden soll. Um einen reibungslosen Ablauf zu garantieren, sollte im Handler Events.QuitApplication() aufgerufen werden.
  • ! Tick – Wird jeden Frame ausgelöst.
  • ! UserEvent – Wird ausgelöst, wenn ein UserEvent kommt.
  • ! VideoResize – Wird ausgelöst, wenn die Größe des Fensters verändert wird.

Tipp: Um ein gutes Tastaturhandling zu erreichen, kann eine boolsche Variable eingeführt werden, die anzeigt, ob eine Taste gedrückt wurde, bzw. ein Array/eine Liste, in dem alle Tasten die gedrückt wurden gespeichert wurden. Diese Tasten werden beim KeyboardUp-Event wieder gelöscht, damit man auch in anderen Frames weiß, ob eine Taste gedrückt wurde.

2D-Grafik
Bevor ich dich mit Code bombardiere, möchte ich dir ein bischen Theorie vermitteln. In SDL werden Flächen zum draufzeichnen verwendet, sogenannte Surfaces. Auf solchen Surfaces werden alle grafischen Operationen angewandt, bspw. Pixel, Rechtecke, Sprites oder andere Surfaces können auf Surfaces gezeichnet werden. A propos Sprites: Du weißt nicht, was Sprites sind? Wikipedia-Link
Sprites können alle Objekte sein, allerdings werden Sprites für alle beweglichen Dinge verwendet, bspw. Spielfiguren. Sprites unterscheiden sich in der Hinsicht von Surfaces, dass auf Sprites nicht gezeichnet werden kann, sowie einige andere Funktionen für Surfaces für Sprites nicht verfügbar sind. Dafür gibt es für Sprites Funktionen, die es für Surfaces nicht gibt, bspw. eine einfache Überprüfung, ob sich zwei Sprites überschneiden.
Man zeichnet also alles auf diesen Surfaces. Zum zeichnen von Grafiken (Bitmap aus dem Namespace System.Drawing), Sprites, Surfaces, … stellt SDL im allgemeinen die Funktion blit() bereit. Diese kann man mit den zu zeichnenden Objekten und der gewünschten Position als Parameter aufrufen. Probier dich daran einfach aus, das ist wirklich einfach 😉
Aber das einfache blitten (kA, ob das überhaupt richtig ist, blit kann man nicht übersetzen, aber es bedeutet, einen rechteckigen Ausschnitt im Grafik-Speicher zu verschieben) reicht nicht aus, um etwas auf den Bildschirm zu bringen. Wenn man sich die Methoden durchliest, kann man sehen, dass es eine Funktion Update() gibt. Diese bewirkt, dass die vorberechnete Fläche auch wirklich übernommen wird. Ohne Update() wird die Fläche also berechnet, aber nicht verarbeitet!
Sprites und Surfaces können einfach als Objekt erstellt werden (Namespaces SdlDotNet.Graphics und SdlDotNet.Graphics.Sprites) und je nach belieben verwendet werden, die restlichen Methoden und Eigenschaften sind selbsterklärend (es sei denn, ich komme nochmal auf sie zurück)
Aber wie genau zeichnet man nun auf den Bildschirm? Der Bildschirm hat eine ganz eigene Surface, auf die man über Video.Screen zugreifen kann. Dies ist ein Surface-Objekt, das in eine Variable geschmissen werden kann. Danach werden alle Arbeiten, die auf dieser Surface gemacht werden, direkt auf den Bildschirm übertragen. Der Vorteil (oder auch der Nachteil, da es das OOP-Prinzip zerstört) ist, dass aus jeder x-beliebigen Klasse auf Video.Screen zugegriffen werden kann und auch global jede Kopie davon automatisch aktualisiert wird.
Ich komme nun zu etwas wesentlich interesanterem: Animierten Sprites. Dazu muss ich gleich drei Klassen einführen: AnimationCollection, AnimationDictionary und AnimatedSprite.
Eine AnimationCollection hält alle Frames, die zu einer bestimmten Animation dazugehören. Des weiteren kann man einer Collection auch einen Namen geben. Folgend ein Code-Beispiel:

AnimationCollection walk = new AnimationCollection(); // Erstellt ein neues Objekt
walk.Add("tilesheet.png", new Size(16,16)); // Lädt alle Sprites aus den Spritesheet tilesheet.png, wobei alle Sprites als 16x16 angenommen werden. Möchte man nur ein Sprite hinzufügen, reicht es, eine einzelne Surface hinzuzufügen.
walk.Delay = 1000;  // Wartet 1sek zwischen jedem Frame

AnimationCollection run = new AnimationCollection(); //Das selbe hier
run.Add("run.bmp", new Size(32, 32));run.Delay = 250;
AnimationCollection stop = new AnimationCollection(); // Und hier
stop.Add(new Surface("stop.bmp"));

Die beschriebenen Befehle sind netterweise selbsterklärend. Das sind aber auch schon alle, die du brauchst, um eine AnimationCollection zu erstellen. Aber was fängt man jetzt damit an? Dazu komme ich jetzt. Wir können die verschiedenen Collections in Dictionaries werfen. Lustig, oder?
Mal vorher zur Zusammenfassung: Eine AnimationCollection enthält alle Frames einer Animation. Eine AnimationCollection enthält alle Animationen und ein AnimatedSprite stellt einfach nur den Sprite dar.

AnimationDictionary animations = new AnimationDictionary();
animations.Add("Walk", walk); //Füge eine AnimationCollection hinzu und gebe ihr ein Namen
animations.Add("Stop", stop);
animations["Walk"].Delay = 2000; // Ändere im Nachhinein eine Eigenschaft

Extrem einfach, oder?

Um jetzt das ganze zu einem Sprite hinzuzufügen, nutzen wir die bereits vorhandene Animations-Eigenschaft von der Klasse animatedSprite:

AnimatedSprite hero = new AnimatedSprite();
hero.Animations.Add(animations);
hero.CurrentAnimation = "Walk";

Die Aufrufe erstellen einen animierten Sprite, fügen die bereits vorhandenen Sprites hinzu und setzen die aktuelle Animation. Beim zeichnen wird dann automatisch das passende gezeichnet.

Sound
Sound ist wichtig. Das solltest du wissen^^
SDL kann folgende Formate abspielen: Wave-Dateien, Ogg-Dateien, Midi-Dateien, VOC-Dateien sowie MikMod-Dateiformate. Leider ist mp3 nicht unterstützt, da mp3 eigentlich kein freies Format ist.
Um einen Sound zu erstellen, benutzt man die Klasse Sound. Code-Beispiel:

Sound scream = new Sound("scream.ogg");
scream.Play();

So einfach wird ein Sound abgespielt. Da das aber nicht alles sein soll, kann man mit der Play-Methode auch festlegen, wie oft das wiederholt wird. Auch kann man über die Volume-Eigenschaft die Lautstärke einstellen. Da das aber ziemlich selbsterklärend ist, komme ich zu etwas wesentlich interessanterem: Channels. Wer ein wenig geschaut hat, wird bemerkt haben, dass die Funktion Play() ein Objekt Channel zurückgibt. Channels bieten erweiterte Funktionen, um die Lautstärke zu setzen, um eine Position im Raum zu verschaffen (Souround-Sound, Woohoo), und vieles mehr. Beachtet bitte, dass per Default maximal 8 Channel bespielt werden können, d.h. es können max. 8 Sounds und Musiken gleichzeitig abgespielt werden. Um die Anzahl zu erhöhen, könnt ihr folgende Variable verwenden:

Mixer.ChannelsAllocated = 100;

würde also die maximal gleichzeitig bespielbaren Channels auf 100 setzen. Bitte beachtet, dass mehr Channels mehr Speicherverbrauch bedeuten!
Die Klasse Mixer stellt Funktionen für alle Channels bereit. Ist sozusagen der Master-Controller 😉
Mehr folgt bald. Unten seht ihr eine TODO-Liste!

Beispiele
Das hier ist mal etwas, was ich mir dabei so zusammengeschrieben habe. Stil ist total schlecht, das weiß ich, aber es soll erstmal nur dazu dienen, es euch zu zeigen 😉

using System.Drawing;
using SdlDotNet.Audio;
using SdlDotNet.Core;
using SdlDotNet.Graphics;
using SdlDotNet.Graphics.Sprites;
using SdlDotNet.Input;

namespace Tutorial.SDL
{
	class Program
	{
    	Surface screen;
    	AnimatedSprite hero = new AnimatedSprite();
    	SoundDictionary sounds = new SoundDictionary();
    	MusicDictionary musics = new MusicDictionary();

    	static void Main(string[] args)
    	{
        	Program app = new Program();
    	}

    	Program()
    	{
        	initVideo();
        	initEvents();
        	initSounds();
        	initMusic();
    	}

    	private void initVideo()
    	{
        	screen = Video.SetVideoMode();
        	Video.WindowCaption = "SDL-Tutorial";
        	Video.WindowIcon();
    	}

    	private void initEvents()
    	{
        	Events.KeyboardDown += new System.EventHandler(keyDown);
        	Events.KeyboardUp += new System.EventHandler(keyUp);
        	Events.Quit += new System.EventHandler(quit);
        	Events.Tick += new System.EventHandler(Events_Tick);
        	Events.Run();
    	}

    	void Events_Tick(object sender, TickEventArgs e)
    	{
        	screen.Fill(Color.Black);
        	screen.Blit(hero);
        	screen.Update();
    	}

    	void quit(object sender, QuitEventArgs e)
    	{
        	Events.QuitApplication();
    	}

    	private void initSounds()
    	{
        	Sound scream = new Sound("scream.ogg");
        	sounds.Add("Scream", scream); //Füge in das globale Dictionary einen Schrei ein
        	sounds.Add("Shoot", new Sound("shoot.ogg")); //Füge in das globale Dictionary einen Schuss ein
    	}

    	private void initMusic()
    	{
        	Music ambient = new Music("ambient1.ogg");
        	musics.Add("ambient1", ambient);
        	musics.Add("ambient2", new Music("ambient2.ogg"));
        	musics["ambient1"].QueuedMusic = musics["ambient2"]; //Nächste Musik ist ambient2
        	musics["ambient2"].QueuedMusic = musics["ambient1"]; //Wieder Zurück
        	musics["ambient2"].Play();
    	}

    	private void initPlayer()
    	{
        	AnimationCollection walk = new AnimationCollection(); // Erstellt ein neues Objekt
        	walk.Add("tilesheet.png", new Size(16, 16)); // Lädt alle Sprites aus den Spritesheet tilesheet.png, wobei alle Sprites als 16x16 angenommen werden. Möchte man nur ein Sprite hinzufügen, reicht es, eine einzelne Surface hinzuzufügen.
        	walk.Delay = 1000;  // Wartet 1sek zwischen jedem Frame

        	AnimationCollection run = new AnimationCollection(); //Das selbe hier
        	run.Add("run.bmp", new Size(32, 32)); run.Delay = 250;
        	AnimationCollection stop = new AnimationCollection(); // Und hier
        	stop.Add(new Surface("stop.bmp"));
        	AnimationDictionary animations = new AnimationDictionary();
        	animations.Add("Walk", walk); //Füge eine AnimationCollection hinzu und gebe ihr ein Namen
        	animations.Add("Stop", stop);
        	animations["Walk"].Delay = 2000; // Ändere im Nachhinein eine Eigenschaft
        	AnimatedSprite hero = new AnimatedSprite(); hero.Animations.Add(animations); hero.CurrentAnimation = "Walk";
        	Mixer.ChannelsAllocated = 100;
    	}

    	void keyUp(object sender, KeyboardEventArgs e)
    	{
        	// Hier alle Ereignisse beim loslassen einer Taste!
    	}

    	void keyDown(object sender, KeyboardEventArgs e)
    	{
        	switch (e.Key)
        	{
            	case Key.LeftArrow:                	//Links gedrückt
                	break;
            	case Key.Q:
            	case Key.Escape: Events.QuitApplication(); break;
        	}
    	}
	}
}

TODO:

  • Sprite-Dragging
  • AudioChannels weiterführen(evtl.)
  • OpenGL
  • Partikel
  • Videos
  • Fonts
Advertisements
Tagged , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: