„ContentPipeline Overdrive“

Die ContentPipeline des XNA-Frameworks ist ja eine ziemlich coole Sache, wenn man deren Sinn verstanden hat und diese richtig anwendet. Sie komprimiert Inhalte, kümmert sich zur Entwicklungszeit darum, daß die Inhalte in ein passendes Format konvertiert werden, kann diese noch dynamisch verarbeiten (z.B. Tangenten in einem 3D-Objekt berechnen) und kümmert sich später auch darum, daß diese im Spiel komfortabel geladen werden können.

Soweit so gut…

Auf der anderen Seite besteht ja ständig eine Nachfrage für Editoren aller Art und eine Infrastruktur für diese Editoren zu entwickeln ist nicht unbedingt eine Aufgabe, die innerhalb von einer Stunde erledigt ist, sondern dabei trennt sich die Spreu vom Weizen. Ein guter Editor benötigt eine gute Infrastruktur. Ich denke mal, daß mir hier die meisten zustimmen können und hoffentlich auch werden.

Ein sehr wichtiger Punkt ist in einem Editor, daß Inhalte geladen werden können. Hmmm, kurz überlegt: Heureka! Das ist doch das was die ContentPipeline macht. Da verwende ich doch einfach die…

Und genau jetzt fangen die Probleme an…

Die ContentPipeline ist nicht für solche Aufgaben ausgelegt. Wer mehr darüber erfahren möchte, den kann ich auf meinen Blog-Artikel Die Content-Pipeline: Grundlagen verweisen, da dies hier den Rahmen sprengen würde.

Wir haben in einem Editor und dem zugehörigen Spiel also folgende Anforderungen:

1. [Build-Time] Der ContentImporter importiert einen Inhalt I und macht daraus einen benutzerdefinierten Typ T
2. [Build-Time] Der ContentProcessor verarbeitet bzw. optimiert T
3. [Build-Time] T wird vom ContentWriter als .XNB-Datei gespeichert
4. [Runtime] .XNB-Datei wird vom ContentReader gelesen und die Content-Pipeline erzeugt daraus T, daß nun verwendet werden kann

Das sind Punkte, die bereits vorhanden sind und wunderbar funktionieren. Nur halt nicht für einen Editor. Der Editor arbeitet im Grunde genommen nur mit dem Inhalt I und benötigt den benutzerdefinierten Typ T nur um diesen interaktiv darzustellen. Wir haben also noch zwei weitere Anforderungen:

5. [Edit-Time] Inhalt I wird aus Datei geladen und daraus benutzerdefinierter Typ T gemacht
6. [Edit-Time] T wird als Inhalt I gespeichert

Dabei fällt auf, daß Punkt 1 und Punkt 5 sehr ähnlich sind, im Grunde genommen eigentlich das Gleiche. Punkt 6 ist der interessante Punkt, denn dort wird im Grunde genommen das erzeugt, was von den Punkten 1-3 zur Build-Time des Spiels verarbeitet wird und damit für Punkt 4 bereitgestellt wird.

Hier mal ein Beispiel, um das ganze etwas deutlicher zu machen. Ich habe eine Tile-Engine, die Maps per Content-Pipeline laden kann und ich habe einen Editor, der Tile-Maps erzeugen kann. Der Editor speichert Tile-Maps als .XML-Datei, so daß diese einfach verarbeitet werden können und auch vom Entwickler einfach gelesen werden können. Der Editor muss diese natürlich auch laden können, dies passiert ebenfalls aus einer XML-Datei. Zur Build-Time des Games springt nun die Content-Pipeline an: Ein Content-Importer importiert die Datei und es wird eine .XNB-Datei erzeugt. Diese wird mit dem fertigen Spiel ausgeliefert und ermöglicht es, per ContentManager ein TileMap-Objekt daraus zu erzeugen.

Bisher bin ich immer so vorgegangen, daß ich einen XML-Loader und einen XML-Writer für meine Klassen erzeugt habe, also z.B. für eine TileMap. Den XML-Loader habe ich dann sowohl im Editor verwendet, als auch im Content-Importer, da dieser ja die Gleiche Anforderung hat.

Der XML-Writer wurde bisher immer vom Editor verwendet um diese Objekte zu speichern. Damit wurde auch die Basis für das spätere Laden durch den Editor und den Content-Importer ermöglicht. Analog zum ContentImporter habe ich dazu eine abstrakte Basisklasse ContentExporter entwickelt, die im Grunde so funktioniert wie der ContentImporter, inklusive der Attribute zur Klassendekoration.

Um das Ganze schön zu gestalten und komfortabel zu verwenden, habe ich ganz einfach einen generischen ResourceManager erzeugt. Diesem kann man einen ResourceLoader und einen ResourceWriter zuweisen. Und das ist die interssante Sache. Es gibt einen EditorResourceLoader und einen EditorResourceWriter. Ersterer verwendet den Content-Importer der ContentPipeline und zweiterer verwendet meinen neu erzeugen ContentExporter. Diese werden bei der Initialisierung des ResourceManagers vom Editor zugewiesen. Fortan können – wie mit der Content-Pipeline – Inhalte geladen, aber auch gespeichert werden. Sehr komfortabel. Für das eigentliche Spiel habe ich dann noch einen ContentPipelineResourceLoader erzeugt. Dieser lädt Inhalte ganz einfach über die ContentPipeline, ist also nur ein sehr dünner Wrapper. Das eigentliche Spiel weist dem ResourceManager nun diesen Loader zu und das Spiel verwendet die ContentPipeline. Dem ResourceWriter weise ich ganz einfach null zu und werfe eine entsprechende Exception, sobald die Save-Methode aufgerufen wird, da dies zur Laufzeit des Spiels nicht möglich ist.

Dies ist das Verfahren, daß ich in den letzten beiden Jahren immer wieder angewendet und immer weiter verfeinert habe. Mittlerweile setze ich dieses Verfahren im starLiGHT.GameStudio ein und habe damit sehr gute Erfahrungen gemacht.

Advertisements

Veröffentlicht am 28.06.2011 in Algorithmen, Content-Pipeline, Mini-Tutorial, XNA und mit , , , , , , , getaggt. Setze ein Lesezeichen auf den Permalink. 6 Kommentare.

  1. DarkPrisma

    das Problem an XML ist leider der Overhead der produziert wird. Bei meinem Editor und auch dem Spiel bin ich von der Content-Pipeline genau deswegen weggegangen. Sie war einfach zu langsam. Die Content-Pipeline ist sicherlich gut um Texturen und sowas zu laden, aber nich für größe Sachen wie eine Tilemap…..

    • Nein, nein, nein… Du hast was nicht verstanden 🙂

      Der Editor arbeitet mit XML (oder jedem anderen Format)… Damit lädt der ContentImporter XML und erzeugt z.B. ein TileMap-Objekt daraus. Mein ContentExporter speichert eine TileMap in eine XML-Datei. So kann der Editor gut damit arbeiten. Wenn hier die Ladezeiten etwas länger sind, dann ist dies ja nicht so schlimm, zu dieser Zeit geht’s ja eher um Komfort für den Editor. Ausserdem wird hier die Content-Pipeline auch komplett umgangen. Der ContentImporter wird so angesteuert, daß er direkt per XDocument oder XmlSerializer oder was auch immer eine XML-Datei lädt.

      Wenn die Content-Pipeline ins Spiel kommt, dann passiert folgendes:

      1. Der ContentImporter lädt die XML-Datei und macht eine TileMap daraus (Es wird aber der Gleiche Loader verwendet, wie auch vom Editor)
      2. Der ContentProcessor (optional) optimiert die TileMap
      3. Der ContentWriter schreibt die TileMap in eine Datei und hier passiert die Magie. Hier kann JEDES beliebige Format verwendet werden, es muss nur einen ContentReader geben, der das wieder laden kann. Hochoptimierte Binärformate werden von den Basisklassen „ContentWriter“ und „ContentReader“ jedenfalls unterstützt. Diese werden dann zusätzlich noch komprimiert.

      So hat man das Beste aus allen Welten und für das fertige Spiel ein anständig optimiertes Dateiformat.

    • Kleiner Nachtrag noch: Als „Editor-Format“ kann übrigens auch jedes beliebige Dateiformat verwendet werden, es muss nicht XML sein.

  2. DarkPrisma

    achso, ok, dann habe ich da etwas missverstanden.
    kannst ja bei Gelegenheit ein Beispiel Projekt hochladen 😀

  3. Schön zu sehen dass andere die gleichen Probleme haben und ähnliche Lösungen bauen :), denn ich verwende ein ähnliches Verfahren.
    In einer meiner Libraries habe ich mir einen abstrakten Persistor definiert. Von diesem leiten verschiedene Reader/Writer für verschiedene Lese/Schreib-Strategien ab. In meinem Fall sind es z.B. XElementReader/Writer, TextReader/Writer, BinaryReader/Writer. Die Funktionsweise ist analog zu XNA: man implementiert einen Reader für einen bestimmten Typ und markiert die Klasse mit einem Attribut. In meiner Engine beispielsweise mit [XElementReaderAttribute]. Als besonderheit erlaube ich eine Formatangabe. So kann ich verschiedene Reader/Writer für denselben Typ definieren, also [XElementReaderAttribute(format=“.xml“)] oder [XElementReaderAttribute(format=“.dae“)]

    Wie in Deinem Fall verwende ich im ContentImporter meinen eigenen Reader um die Daten zu importieren. Dann kommt optional der ContentProcessor und letzendlich der ContentWriter. Für die Laufzeit muss es da natürlich wieder einen entsprechenden ContentReader geben.

    Man spart sich also Arbeit im ContentImporter, wobei es auch hier bei manchen Objekten Probleme gibt. Beispielsweise solche, die zur Laufzeit das GraphicsDevice brauchen. In solchem Fall versuche ich die Grunddaten die ich Laden/Speichern will aus dem Laufzeitobjekt herauszutrennen und wiederverwendbare Reader/Writer zu schreiben um den Grunddatentyp sowohl im Editor als auch im ContentImporter zu Lesen/Schreiben. Dann braucht man nur noch einen Reader für den Laufzeittyp der den Grunddatentyp und das Graphicsdevice verwendet um den Laufzeittyp zu instantiieren.

  1. Pingback: Content Pipeline Ersatz « "Mit ohne Haare"

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: