Eine Einführung in das XNA Framework, Teil 1

Get rich and famous, be a game designer
Kommentare

Mit dem XNA Framework und XNA Game Studio Express läutet Microsoft eine neue Ära der Spieleprogrammierung ein. Vorbei sind die Zeiten, in denen die Zielgruppe eines selbst programmierten Spiels im Wesentlichen aus dem Programmierer des Spiels bestand. Dank des XNA Framework können Spieleentwickler ihre Ergebnisse auch allen Besitzern einer Xbox 360 zur Verfügung stellen. Das ist aber nur ein Nebeneffekt – das XNA Framework bietet so viele Möglichkeiten, dass eine Xbox (sofern vorhanden) eine Weile ungenutzt bleiben dürfte.

Das XNA Framework ist weder eine Spiele-Engine noch ein Baukasten, mit dem ein Computerspiel zusammengeklickt wird. Das XNA Framework ist eine Plattform, die ein objektorientiertes Modell zur Verfügung stellt, um mit der Hardware zu kommunizieren. Der Abstraktionsgrad ist so hoch, dass die Programmierung fast unabhängig vom System wird. Im Hintergrund steht ein eigens für die Xbox 360 entwickeltes .NET Compact Framework. Wird bei der Entwicklung darauf geachtet, welche Klassen und Methoden jenes Framework unterstützt, bedarf es keiner Änderung am Code, um das Spiel zu portieren. Zielt die Anwendung ausschließlich auf die Windows-Plattformen ab, stehen alle Möglichkeiten offen, die Entwickler von der Windows-Programmierung gewohnt sind.

Gegen Ende April ist eine neue Version des XNA Framework und XNA Game Studio Express erschienen. Diese beiden neuen Versionen der zwei Komponenten sind mit der Versionsnummer 1.0 Refresh betitelt worden. XNA Game Studio Express ist ein Add-in für Visual C# 2005 Express. Die aktuellste Version setzt Visual C# Express mit Service Pack 1 voraus und unterstützt offiziell auch Windows Vista.

Die in dieser Artikelserie präsentierten Beispielprogramme wurden mit XNA Game Studio Express 1.0 Refresh entwickelt. Das Ziel der Serie ist die Entwicklung zweier Computerspiele, die auf beiden Systemen laufen. Zu Beginn liegt der Fokus auf einem zweidimensionalen Spiel, das im zweiten Teil als 3D-Variante umgesetzt wird. Der erste Teil beschäftigt sich mit den Grundlagen, die zur Umsetzung eines 2D-Computerspiels erforderlich sind. Jeder Abschnitt wird durch ein kleines Beispiel untermalt und konzentriert sich nur auf die entsprechende Thematik.

Visual Basic 2005 oder Visual Studio 2005? Eine häufig gestellte Frage ist, ob es möglich sei, mit Visual Studio 2005 bzw. mit Visual Basic 2005 Express und dem XNA Framework zu entwickeln. Bis dato lautet die Antwort: Nein. Es existieren noch keine (offiziellen) Aussagen, ob Microsoft Visual-Basic-Programmierern das Tor zu XNA öffnen wird. In Planung ist zumindest eine Professional-Version, die dann auf Visual Studio 2005 aufsetzt und noch in diesem Jahr erscheinen soll.

XNA Game Studio Express

Nach der Installation und dem Start von XNA Game Studio Express ist auf dem ersten Blick kein Unterschied zu Visual C# 2005 Express erkennbar. Ausgetauscht wurden die Projekttypen sowie die Item Templates. Weiterhin stellt das Add-in die Verbindung zur Content Pipeline her, in der alle Ressourcen (Modelle, Texturen und Shader) residieren.

Für beide Plattformen stehen je zwei Projekttypen zur Verfügung. Ein Projekttyp zum Erzeugen einer XNA-Anwendung und ein Typ zum Entwickeln von Klassenbibliotheken. Es ist ratsam, zunächst ein Windows-Projekt anzulegen und daraufhin im Xbox-360-Projekt auf dieselben Codedateien zu verweisen. Codesegmente können mit der #if-Direktive wie folgt auf die beiden Plattformen abgestimmt werden:

#if XBOX
//Xbox 360 Code
#else
//Windows Code

In der Hauptrolle: Die Game-Klasse

Grundlage eines jeden Spiels stellt eine Instanz der Game-Klasse dar. In der Regel existiert pro Anwendung genau eine Instanz jener Klasse, dessen Konstruktor parameterlos ist. Unter Verwendung der Projektvorlage Windows Game beziehungsweise Xbox 360 Game liegt zu Projektbeginn eine Klasse vor, die von Game erbt und innerhalb der Program.cs instanziiert wird. Weiterhin nennt jene neue Klasse bereits zwei Variablen, graphics und content, ihr eigen. Letztere Variable stellt die Verbindung zwischen Ihrem Code und der Content Pipeline her. Graphicsgewährt den Zugriff auf das Graphics Device und alle sonstigen Informationen bezüglich der Grafikkarte (Eigenschaft: Graphics-Device). Ein Graphics Device fungiert als Vermittler zwischen Ihrer Anwendung und der Grafik-Hardware. Das Graphics Device ist für alle Rendering-Operationen erforderlich.

Die Basisklasse erledigt diverse Standardaufgaben, die bei jedem Projektstart anfallen: Die Grafikkarte wird initialisiert, der Game Loop implementiert und zudem sorgt die Klasse dafür, dass unter Windows ein Fenster zur Verfügung steht. Der Game Loop ruft pro Frame die Methoden Update und Draw auf. Zudem sind folgende Methoden per Default überschrieben:

  • Initialize()
  • LoadGraphicsContent(bool loadAll-Content)
  • UnloadGraphicsContent(bool unload-AllContent)

Die Initialize-Methode ist für sämtliche Initialisierungsvorgänge angedacht, die nicht mit der Grafikkarte in Verbindung stehen. Hierfür existiert eine Funktion LoadGraphicsContent. Der Hintergrund hierfür ist, dass grafische Elemente Ressourcen im Speicher der Grafikkarte platzieren. Die Größenänderung des Fensters unter Windows führt etwa dazu, dass das Graphics Device verloren geht und neu initialisiert werden muss. Adressierte Ressourcen müssen neu im Speicher hinterlegt werden. Folglich ruft das so genannte Application Model immer dann die LoadGraphicsContent-Methode auf, wenn das Graphics Device verloren gegangen ist. Das Pendant ist die UnloadGraphicsContent-Methode….

[ header = Seite 2: Der Game Loop ]

Der Game Loop

Der Game Loop stößt pro Frame die Aktualisierung der Spiellogik (Update) sowie den Render-Prozess (Draw) an. Unterschieden wird zwischen einem statischen und einem variablen Game Loop. Ein statischer Game Loop begrenzt die maximale Anzahl der Frames bei schnellen Systemen auf eine fixe Rate. Der variable Game Loop reizt das System voll aus, um so viele Bilder pro Sekunde wie möglich darzustellen. Standard ist der statische Game Loop, der durch die folgende Zuweisung innerhalb des Game-Objekts umgeschaltet werden kann:

this.IsFixedStep = false;

Game Components

Game Components sind Klassen mit einer fest definierten Schnittstelle, sodass jene Komponenten möglichst ohne Änderungen in anderen Projekten übernommen und innerhalb der Community ausgestauscht werden können. Zum einen existieren nicht grafische Komponenten, die sich der Basisklasse GameComponent bedienen. Zum anderen erweitert die DrawableGameComponent-Klasse jenes Modell dahingehend, dass grafische Komponenten umgesetzt werden können.

Jedes Game-Objekt verfügt über eine Auflistung, in der Game Components hinterlegt werden können. Reiht sich eine neue Komponente in der Collection ein, bemüht das Application Model dieInitialize-Methode der Komponente. Zudem wird pro Frame die Aktualisierung der Logik veranlasst sowie der Komponente signalisiert, dass sie sich erneut zeichnen soll. Die EigenschaftenUpdateOrder und Enabled sowie DrawOrder und Visible steuern die Reihenfolge, in der die Komponenten verarbeitet werden und den Status einer jeden Komponente (Aktiviert/ Deaktiviert).

Sprites

Selbst in Zeiten, wo beeindruckende, komplexe 3D-Welten den Markt der Computerspiele beherrschen, haben zweidimensionale Grafiken nicht ausgedient. Im Gegenteil, 2D-Grafiken werden auch in professionellen Spielen herangezogen, um Menüs oder Head Up Displays (HUDs) zu realisieren. Speziell das erste Spielprojekt dieser Serie bedient sich ausschließlich zweidimensionaler Grafiken (Abbildung 1). Doch was sind Sprites?

Abb. 1: Ein 2D-Flugzeugspiel

Sprites sind Bitmaps, die unabhängig von einer Kamera-Einstellung „auf den Monitor“ gerendert werden. Perspektivische Verzerrungen treten folglich nicht auf. Jene Konstrukte werden mittels so genannten Bildschirmkoordinaten platziert. Bildschirmkoordinaten sind in einem zweidimensionalen Koordinatensystem angesiedelt, bei dem das Tupel (0, 0) die obere linke Ecke des Monitors beschreibt. Welche Koordinaten der unteren rechten Bildschirmecke entsprechen sind von der gewählten Auflösung abhängig. Angenommen die Auflösung beträgt 640×480 Pixel, dann beschreibt das Koordinatenpaar (640, 480) die untere, rechte Ecke. Ein Sprite besitzt einen Ursprung, der standardmäßig bei (0, 0) liegt. Wird das Sprite an der Position (50, 50) gerendert, platziert Direct3D die obere, linke Ecke der Grafik an der genannten Position. Insbesondere bei Rotationen spielt der Origo des Sprites eine entscheidende Rolle. In der Standardeinstellung führt eine Rotation des Sprites dazu, dass sich die komplette Grafik um diese eine Ecke dreht. Dem Problem wird begegnet, indem der Ursprung innerhalb der Grafik zentriert wird – doch dazu später mehr.

Starter Kits Insgesamt stellt Microsoft drei Starter Kits für das XNA Framework zur Verfügung. Im Installationspaket von XNA Game Studio Express ist ein Remake des Klassikers Space War enthalten. Zwei weitere komplette Computerspiele (Marblets und XNA Racer) inkl. Sourcecode stehen unter creators.xna.com zum kostenlosen Download bereit.

Vorbereitungen

Bevor eine erste Grafik in Form eines Sprites ausgegeben werden kann, bedarf es eines neuen Projektes vom Typ Windows Game und einer Ressource innerhalb der Content Pipeline. Zur Umsetzung der letzteren Anforderung wird die Projektstruktur aus dem ersten Schritt im Projektmappen- Explorer um zwei Ordner Content und Textures erweitert, wobei der Textures- Ordner dem ersten untergeordnet wird. Im zweiten Schritt wird dem Projekt eine Grafikdatei im Format BMP, JPEG, PNG, TGA oder DDS hinzugefügt und im Textures-Ordner platziert. Jetzt zeigt das Eigenschaftenfenster diverse Optionen an, die spezifizieren, wie die Content Pipeline die Ressource verarbeitet. Als Build Action ist Content voreingestellt und signalisiert, dass die Datei zum Inhalt der Content Pipeline gehört. Der Eintrag Texture – XNA Framework fungiert als Content Importer und Texture (Sprite, 32bpp) – XNA Framework als Content Processor (Abbildung 2). Alle Ressourcen eines Computerspiels finden die Bezeichnung „Game Asset“. Per Default erhält jedes neue Asset den Namen der Datei ohne Dateierweiterung. Der Name dient der Identifikation einer Ressource innerhalb des Codes.

Abb. 2: Konfiguration eines Game Assets

Am Ende der Content Pipeline steht die Grafik in Form eines Objekts vom Typ Texture2D bereit. Im Sourcecode stellt der Content Manager die Verbindung zur Content Pipeline her. Jedes einzelne neue Projekt verfügt über eine Variable content, die den Content Manager repräsentiert. Ein Aufruf der generischen Methode Load lädt die Ressource, sofern der übergebene Bezeichner korrekt ist. Dieser Bezeichner setzt sich aus der Ordnerstruktur und dem Asset-Namen zusammen – der folgende Sourcecode soll als ein kleines Beispiel dienen:

Texture2D oTexture = content.Load
(“\Content\Textures\Mauer“);

[ header = Seite 3: Sprites rendern ]

Sprites rendern

Die Grafikdatei liegt jetzt zur Laufzeit in Form eines Objekts vor. Damit jene Grafik als Sprite verwendet werden kann, ist eine Instanz der SpriteBatch-Klasse erforderlich. Deren Konstruktor erwartet die Referenz des GraphicsDevice-Objekts. Der GraphicsDeviceManager offeriert jenes Objekt über die GraphicsDevice-Eigenschaft. Der GraphicsDeviceManager schließlich wird über diegraphics-Variable angesprochen:

Private SpriteBatch m_oSprite = null;
protected override void LoadGraphicsContent(
bool loadAllContent)
{
m_oSprite = new SpriteBatch(graphics.GraphicsDevice);
}

Instanzen jenes Typs sind nicht an eine spezielle Grafik gebunden. Stattdessen besteht die Option, unter Zuhilfenahme eines SpriteBatch-Objekts mehrere Sprites mit „einem Rutsch“ auf den Back Buffer zu kopieren. Die Methoden Begin und End bilden einen gedachten Block, in dem Aufrufe der Draw-Methode gültig sind:

protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
m_oSprite.Begin();
//Sprites rendern
m_oSprite.End();
base.Draw(gameTime);
}

Die Draw-Methode steht in einer Reihe von Variationen zur Verfügung. Eine der Signaturen lautet wie folgt: Public void Draw(Texture2D texture, Vector2 position, Color color); Neben der Grafik fordert die Draw-Methode eine Position und einen Farbwert ein. Der Farbwert gewichtet die Farbkanäle der Grafik, um diese beispielsweise rot einzufärben. Weiß gilt als neutrale Farbe und belässt die Grafik in dessen Originalzustand (Abbildung 3):

Abb. 3: Darstellung eines Sprites

Transparenz

In den seltensten Fällen sollen quadratische Objekte mithilfe eines Sprites auf den Monitor gebracht werden. Stattdessen weisen die Objekte unterschiedlichste Silhouetten auf. Bitmaps sind aber immer quadratisch. Abhilfe schafft ein Alpha- Kanal, der die Transparenz pro Pixel widerspiegelt und alle nicht erforderlichen Pixel ausblendet.

Per Default führt der Aufruf von SpriteBatch.Begin dazu, dass das Objekt die Konfiguration des Graphics Device dahingehend abändert, dass theoretisch, ohne eine Zeile Code schreiben zu müssen, Grafiken transparent dargestellt werden können. Voraussetzung ist, dass die Textur ein Format aufweist, das einen zusätzlichen Kanal für die Transparenz bereithält und dass dieser Kanal mit entsprechenden Werten gefüllt ist. Wurde das DirectX SDK installiert, steht ein Tool namens DirectX Texture Tool zur Verfügung, mit dem sich das Format der Grafik ändern und zudem um einen Alpha-Kanal bereichern lässt. Der Alpha-Kanal wird mit einer Graustufengrafik gefüllt, wobei Schwarz zur Laufzeit transparent erscheint und weiß vollständig sichtbare Bereiche kennzeichnet. Das Tool erzeugt daraufhin eine neue Datei im DDS-Format (Direct Draw Surface).

Die Content Pipeline Die Content Pipeline hat zweierlei Daseinsberechtigungen. Zum einen erleichtert sie die Arbeit mit diversen Dateiformaten, da das Produkt immer ein Objekt eines bestimmten Typs ist. Zum anderen konvertiert die Content Pipeline alle Ressourcen in ein Format, das die Konsole Xbox 360 versteht. Eine aufwändige Verarbeitung der Daten entfällt und die Ladezeiten werden verkürzt. Zudem besitzen Konsolen lediglich einen beschränkten Befehlssatz, sodass die Verarbeitung der Ressourcen in den Kompiliervorgang ausgelagert wird.

Sprite-Animationen

Wird eine Reihe von Sprites hintereinander abgespielt, erweckt dies den Anschein einer Animation. Diese Vorgehensweise ist mit einem Daumenkino vergleichbar. Sprite-Animationen dienen im 2D-Bereich zum Beispiel der Darstellung von Explosionen oder der Charakteranimation. Derartige Animationen lassen sich durch zwei Wege realisieren. Entweder werden die Einzelbilder jeweils als Asset der Content Pipeline hinzugefügt und durchnummeriert oder alle Frames werden in einer Textur hinterlegt und zur Laufzeit referenziert – zu sehen in Abbildung 4:

Abb. 4: Eine Textur als Frames-Container

Das nachfolgende Beispiel bestreitet den zweiten Weg. Die Logik wird in einer Klasse gekapselt, die von DrawableGame-Component erbt, weshalb dem Konstruktor als Minimum ein Game-Objekt übergeben werden muss. Für eine Sprite-Animation sind zusätzliche Informationen erforderlich:

  • Der Bezeichner des Assets
  • Die Breite bzw. Höhe eines Bildes innerhalb der Textur
  • Die abzuspielenden Bilder pro Sekunde

Sowie Breite als auch Höhe eines Bildes speichert eine Rectangle-Struktur. Da die X- und Y-Komponente auf 0 gesetzt ist, selektiert die Struktur den ersten Frame der Animation. Anhand der Bilder pro Sekunde berechnet der Konstruktor die Zeitperiode, die zwischen zwei Frames liegt (Listing 1):

public SpriteAnimation(Game game, string sAsset,
int iImageWidth, int iImageHeight,
int iFramesPerSecond) : base(game)
{
m_sAsset = sAsset;
m_oSourceRect.Height = iImageHeight;
m_oSourceRect.Width = iImageWidth;
m_fTimePerFrame = 1.0f / iFramesPerSecond;
this.Enabled = false;
}

Back Buffer Jeder Rendervorgang führt dazu, dass die berechneten Farbwerte pro Pixel in den Back Buffer geschrieben werden. Der Back Buffer ist ein sequentiell aufgebauter Speicherbereich der letztlich die Pixel der Szene speichert. Sobald die Szene komplett ist, wird der Inhalt des Back Buffers auf den Monitor „kopiert“.

[ header = Seite 4: Game Services ]

Game Services

Aus einer Komponente heraus existiert per Default keine direkte Zugriffsmöglichkeit auf den Content Manager. Stattdessen ist ein Game-Objekt mit einem Game Service Container ausgestattet (Eigenschaft: Services), über den diverse Dienste geboten werden. Die Integration eines Dienstes zum Zugriff auf die Content Pipeline obliegt jedem Programmierer selbst:

public interface IContentService
{
ContentManager Content { get; }
}

Obige Schnittstelle stellt den Vertrag dar, dem der neue Service genügen muss. Implementiert wird die Schnittstelle in der Ableitung von der Game-Klasse. Im Rahmen der Initialisierung registriert die Ableitung den Service:

protected override void Initialize()
{
this.Services.AddService(typeof(IContentService), this);
//...
}

Nun können Assets direkt innerhalb einer Komponente geladen werden. Die Sprite Animation benötigt ein Texture2D-Objekt, das alle Frames beinhaltet. Im Anschluss berechnet die Methode die Anzahl der Spalten und Zeilen und zentriert den Ursprung des Sprites (Listing 1). Rein didaktischen Zwecken dient die Initialisierung der SpriteBatch-Klasse innerhalb der Komponente. Standardmäßig ist das Game-Objekt mit dem IGraphicsDevice- Service-Dienst augestattet, der die Referenz des Graphcis Device liefert, um die Sprite-Batch-Klasse zu instanziieren (Listing 2):

protected override void LoadGraphicsContent(
bool loadAllContent)
{
oDevice = ((IGraphicsDeviceService)this.Game
.Services.GetService(
typeof(IGraphicsDeviceService))).GraphicsDevice;
m_oSpriteBatch = new SpriteBatch(m_oDevice);
if (loadAllContent)
{
if (string.IsNullOrEmpty(m_sAsset))
throw new ArgumentNullException(“No asset“);
m_oTexture = ((IContentService)this.Game.Services
.GetService(
typeof(IContentService))).Content.Load
(m_sAsset);
if (m_iColumns == 0 && m_oSourceRect.Width !=
0 && m_oTexture != null)
m_iColumns = m_oTexture.Width / m_oSourceRect
.Width;
if (m_iRows == 0 && m_oSourceRect.Height !=
0 && m_oTexture != null)
m_iRows = m_oTexture.Height / m_oSourceRect.Height;
m_oOrigin.X = m_oSourceRect.Width / 2;
m_oOrigin.Y = m_oSourceRect.Height / 2;
}
base.LoadGraphicsContent(loadAllContent);
}

Selektion und Rendern des Frames Dank des Game Loop liefert jeder Aufruf der Update- bzw. der Draw-Methode eine Angabe zur verstrichenen Zeit seit dem letzten Frame. Anhand jener Zeitmessung prüft die Komponente, ob der nächste Animationsframe angezeigt werden muss. Pro Aufruf summiert die Methode die vergangen Sekunden. Ist die Zeitperiode, die zwischen zwei Animationsframes liegt, verstrichen, verschiebt die Methode den Zeiger, der in Form einer Rectangle-Struktur vorliegt. Jene Struktur selektiert den Quellbereich der Textur, der in den Render-Vorgang einbezogen werden soll (Listing 3):

public override void Update(GameTime gameTime)
{
if (!this.Enabled)
return;
m_fTimeElapsed += (float)gameTime
.ElapsedGameTime.TotalSeconds;
if (m_fTimeElapsed > m_fTimePerFrame)
{ //Spalten/ Zeilennr. Aktualisieren }
m_oSourceRect.X = m_iActualColumn * m_oSourceRect
.Width;
m_oSourceRect.Y = m_iActualRow * m_oSourceRect
.Height;
m_fTimeElapsed -= m_fTimePerFrame;
}

Damit Direct3D nur den ausgewählten Bereich der Grafik auf den Bildschirm bringt, kommt folgender Methodenaufruf zum Einsatz:

m_oSpriteBatch.Draw(m_oTexture, oPosition,
m_oSourceRect, Color.White, 0.0f,
m_oOrigin, fScale, SpriteEffects.None, 1.0f);

Die Methode erwartet neben der Textur und der Zielposition auf dem Back Buffer eine Rectangle-Struktur, die in der Update-Methode mit Daten versehen wurde. Dem folgt eine Gewichtung der Farbkanäle, ein Rotationswinkel im Bogenmaß, der Ursprung des Sprites als Vector2-Struktur, ein Fließkommawert zur Skalierung der Grafik (Original: 1.0f) sowie eine Angabe ob die Grafik gespiegelt werden soll und auf welcher Ebene sich das Sprite befindet. Beim letzten Argument sind Zahlen im Bereich von 0.0 und 1.0 erlaubt, wobei 0.0 der höchsten Ebene entspricht. Alle Sprites, die darunter liegen können vom obersten Element überlappt werden.

Einsatz der Komponente

Da die Komponente nun komplett ist, muss jene irgendwie in der Hauptanwendung integriert werden. Dazu genügen die folgenden drei Anweisungen:

protected override void Initialize()
{
m_oSpriteAnimation = new SpriteAnimation(
this, “content\textures\explosion“,
256, 256, 50);
m_oSpriteAnimation.Position = new Vector2(100, 100);
this.Components.Add(m_oSpriteAnimation);
base.Initialize();
}

Dadurch, dass die Komponente der Components-Auflistung hinzugefügt wird, kümmert sich das Application Model um die Initialisierung der Komponente, um die Aktualisierung und um den Render-Prozess.

Ausblick aus Episode 2

Der nächste Teil dieser Serie beschäftigt sich mit dem Background Scrolling, kümmert sich um die Kommunikation mit den Peripheriegeräten und behandelt die Kollisionskontrolle. Der Entwurf eines Menüs für das erste Spiel rundet den Teil ab.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -