XNA-Sample: MoonTaxi (Teil 1/3)

Veröffentlicht: 30 Dezember 2012 in Development, Visual Studio, XNA
Schlagwörter:
Kleiner Nachtrag vom 02.03.2015: In der Zwischenzeit gibt es auch eine kleine Video-Reihe zum Spiel. Leider nicht Deckungsgleich mit dem Code und der Vorgehensweise, aber trotzdem mit dem selben Konzept.

Dies hier ist ein Blogpost, den ich vor fast drei Jahren mal geschrieben habe, um die Funktionalität von XNA zu beschreiben – damals noch für Microsoft. Da sich dieser Artikel bis heute mit enormen Besucherzahlen schmücken kann und ich auch weiterhin auf Kommentare und Verbesserungsvorschläge reagieren will, habe ich den Eintrag entsprechend umgezogen und hoffe auch weiterhin auf rege Rückmeldung. Viel Spaß damit.

Worum geht es im diesem Spiel? Grundsätzlich handelt es sich um ein Spiel im 2D-Modus, bei dem die Camera eine seitliche Sicht auf die Szene ermöglicht. Ähnlich einem klassischen Jump&Run.

Der Spielbildschirm

Der Spieler steuert im Spiel ein kleines Taxi, das sich unter Einfluss von Trägheit und Gravitation durch ein verkzwicktes Netz aus Plattformen schlängeln muss, um einen Taxi-Fahrgast einzusammeln und an einen bestimmten Zielort zu bringen. Gelingt ihm das, bevor der Treibstoff ausgeht, gelangt er in das nächste Level. Bei diesem Taxi handelt es sich nicht im ein Taxi auf Rädern, sondern um ein, mit Steuerdüsen ausgestattetes Fluggerät.

Interessante Aspekte dieses Samples liegt in den Bereichen

  • Allgemeine Gameloop
  • Fenster-Setup
  • Parameterisierung
  • Einfaches 2D rendern
  • Einfache Headsup Displays (HUD)
  • Physikalische Bewegung
  • einfache Kollisionen
  • Spieler-Eingaben

Am besten arbeitet man sich in das Sample ein, indem man sich das vollständige Projekt herunter lädt, parallel öffnet und selbst Schritt für Schritt das Spiel nachentwickelt.

Man braucht dafür im Grunde nur eine Sache: Eine Installation des aktuellen XNA Game Studio in Version 3.1. Das Sample, soweit  es im ersten Teil des Tutorials erstellt wird, steht auf meinem Skydrive zur Verfügung.

Edit (30.12.2012): Inzwischen gibt es eine neuere Version von XNA und Visual Studio (4.0), aber ich bin mir auf die Schnelle nicht sicher, welche System-Voraussetzungen das Ganze hat. Der Code des Samples sollte aber gleich aussehen.

Schritt 1: Ein neues Projekt erzeugen

Startet man Visual Studio neu, so kann man direkt über das Menü ein neues Projekt erzeugen. Im Folgenden Assistenten wählt man einfach ein neues XNA 3.1 Windows Spiel aus, gibt einen sinnvollen Projektnamen an (in unserem Fall “MoonTaxi”), kann optional noch den Speicherort wählen und bestätigt mit OK.

Der "Neues Projekt" Dialog

Abb.1: Der Projekt-Template Browser

Anschließend verfügt man auch bereits über ein funktionierendes Spiel, das leider aber noch nichts anzeigt, wenn man es startet. Gestartet werden können XNA-Spiele direkt aus der Entwicklungsumgebung mit der Taste F5 oder dem grünen Pfeilchen in der Toolbar. Will man das Spiel aber an andere weiter geben, muss man noch ein paar andere Sachen erledigen. Dazu später mehr.

Im Projekt Explorer finden sich Dinge, die bei normalen C#-Projekten üblicherweise nicht vorhanden sind. Einerseits ist das ein Ordner namens “Content”. Er soll später ALLE (!!!) zusätzlichen Spiele-Assets wie Bilder/Texturen, Musik, Sound, Schriftarten,… enthalten, die im Spiel später benötigt werden. Andererseits ist auch bereits eine vorgefertigte Klasse “Game1” vorhanden. Ein Blick dort rein verrät einem, dass der Assistent bereits etwas Code hinterlegt hat. Den schauen wir uns auch gleich mal an.

Inhalt der Game1.cs

Abb. 2: Vorlage für Game1.cs

Das wichtigste an dieser Klasse ist, dass sie von der Basisklasse “Game” erbt. Diese Klasse stammt aus dem Namespace “Microsoft.Xna.Framework” ist ist damit Teil von XNA. Diese Klasse übernimmt die vollständige Initialisierungsarbeit der Grafikkarte und erzeugt bereits ein ordentliches Fenster für unser Spiel. Das ist auch der Grund, warum das Projekt bereits jetzt läuft, ohne, dass wir da eine Zeile Code geschrieben haben.

Außerdem scheint es schon fertige Methoden für bestimmte Aufgaben zu geben. Man findet beispielsweise eine Methode “Initialize” oder “LoadContent”. Diese Methoden werden verständlicher, wenn man sich man den groben Ablauf eines Spiels so anschaut.

Game Loop

Abb. 3: Schematischer Ablauf eines Spiels

In Abbildung 3 lässt sich sehen, wie der Ablauf eines Spiels grundsätzlich immer abläuft. Die Anwendung beginnt mit der Inistialisierung von Spieldaten. Das kann z.B. die Grundeinstellung eines Levels sein. Danach folgt der Ladevorgang der ganzen benötigten Assets wie Grafiken, Sounds,… Darauf startet das Spiel ins läuft in die sogenannte Gameloop. Eine ständige Schleife, die solange läuft, wie das Spiel gespielt wird. Diese Schleife wird im Idealfall sooft wie möglich durchlaufen und stellt die einzelnen Spielrunden dar. Bei Echtzeit-Spielen wie 3D-Shootern beispielsweise ist ein Schleifendurchlauf etwa mit einem Frame zu vergleichen. Innerhalb dieser Schleife werden zuerst die Spielereingaben abgeprüft (drückt der Spieler vielleicht gerade den Schuss-Knopf) um im darauffolgenden Schritt entsprechend darauf zu reagieren. Wenn die Szene dann neu berechnet wird, kommen gewöhnlich physikalische Einflüsse und die Usereingaben zum tragen, um die Szene auf den aktuellen Stand zu bringen. Gegenstände fallen, Waffe wird beim Betätigen eines Schussknopfes abgefeuert, der Munitionscounter muss eins nach unten gezählt werden,… solche Dinge. Sobald die Szene dann wieder auf dem aktuellen Stand ist, kann sie am Bildschirm im lezten Schritt auch wieder ausgegeben werden. Danach gehts wieder von vorne los. Beendet der Spieler das Spiel, so verlässt das Programm diese Schleife und räumt noch die Inhalte auf.

Diese Schritte lassen sich nun 1:1 im vorgenerierten Code wieder finden. “Initialize” wird zum initialisieren verwenden, “LoadContent” und “UnloadContent” werden vor und nach dem Start der Gameloop bemüht, um sich um die Assets zu kümmern und Loop selbst wird durch “Update” und “Draw” abgebildet. Wobei Update die Schritte der Eingabeabfrage und der Neuberechnung in sich vereint.

Schritt 2: Planung des Spielmodells

Damit so ein Spiel auch ordentlich funktioniert, muss man sich vorher ein kleines bisschen Gedanken über die Struktur, den Ablauf und die benötigten Rahmeninformationen machen. Im Konkreten heißt das, dass man sich Gedanken darüber machen muss, welche Dinge überhaupt relevant sind, wie groß das Level sein wird, in welcher Auflösung das Ganze dargestellt werden soll. Man macht sich auch Gedanken darüber, welche Objekte im Spiel vorhanden sind und über welche Informationen diese Objekte verfügen müssen.

Der Grundgedanke dabei ist, dass man während des Spiels ein Modell der Szene in objektorientierter Form vorliegen hat, damit die Berechnungen und der Rendervorgang strukturierter ablaufen kann. Für einfache, sehr einfache Spiele reichen naütlich einfache Variablen innerhalb der Game-Klasse, aber das führt schon sehr schnell zu einem großen Durcheinander, weshalb man nicht früh genug mit einer Modellplanung anfangen kann. Aber auch der umgekehrte Weg ist möglic. Zu viele Klassen und Objekte machen das Projekt auch sehr schnell wieder zu komplex. Ein guter Mittelweg ist da gefragt. Wie sieht das aus? Bei meiner ersten Analyse des Modells kam folgende Klassenstruktur heraus.

Klassenstruktur

Abb. 4: Erste Klassenstruktur

Die Gameklasse sollte also ein Level-Objekt halten, in dem zum einen ein Fahrgast- und ein Fahrziel-Objekt enthalten sein sollte. Ebenso ein Array aus Plattform-objekten, die die Landeplattformen repräsentierten. Game sollte außerdem ein Taxi-Objekt haben, das optional, je nachdem, ob das Taxi über einen Gast verfügt, auch die Referenz auf einen solchen enthalten. Natürlich sind innerhalb jeder dieser Klassen Rahmeninfos für das Objekt gespeichert.

Auf den ersten Blick scheint das sinnvoll, wird aber etwas absurder, wenn man sich mal das Level-Layout genauer überlegt und schaut, welche Infos denn nun wirklich in diesen Objekten stecken.

Wie könnte denn beispielsweise das Level-Layout so aussehen? Bei Spielen bedient man sich oft einem Konzept, das das ganze Level-Layout in ein Raster passt – quasi alles aus Blöcken gebaut ist. Das kann man bei Klassikern wie Super Mario sehen, zieht sich aber bis in die modernen Spiele. Man unterteilt die verfügbare Fläche in gleichgroße Blöcke und verwendet diese Einteilung als Grundlage für das Level-Design. Das bringt unglaublich viele Vorteile: Texturen können in festen Größen erstellt werden, die Berechnung der Kollisionen ist etwas einfacher und das Spielmodell wird überschaubar.

Raster der Spielfläche

Abb. 5: Rastern der Spielfläche

Das Spiel-Modell muss dann Plattformen, Fahrgäste oder Fahrziele nur noch als Flags in einem 2-dimensionalen Array aus Zellen speichern und keine eigenen Objekte mehr dafür erstellen. Das macht die Berechnung von Kollision und den Zeichenvorgang sehr, sehr einfach. Sehen wir später. Auf jeden Fall benötigen wir nun nur noch 2 zusätzliche Klassen: Das Taxi, um die aktuelle Position und den aktuellen Zustand zu speichern. Das Level, damit wir das aktuelle Level halten können.

Schritt 3: Die erste Zeichnung

Es wird nach dieser Theorie auch endlich mal Zeit was zu zeichnen. Dazu brauchen wir aber erstmal ein paar Texturen. Am einfachsten ist es wohl, diese Texturen aus dem Demo-Projekt zu übernehmen. Du findest die entsprechenden Dateien im Projektordner unter “MoonTaxi/Content”. Suche dort nach der Datei “Taxi.png” und ziehe die Datei per Drag&Drop einfach in dein Projekt zum Content-Ordner des Solution Explorers.

Assets importieren

Abb. 6: Assets importieren

Nachdem das Bild im Content-Ordner liegt, kann man in den Eigenschaften sehen, unter welchem Namen man das Bild im Code ansprechen kann (Asset Name). Um das Bild im Code jetzt aber wirklich verwenden zu können, muss im Code selbst noch eine Variable erstellt werden. Im oberen Teil der Game-Klasse wird dazu einfach eine Variable vom Typ “Texture2D” und benennt diese nach Wahl – vorzugsweise taxiTexture. Gefüllt wird diese dann innerhalb der Methde “LoadContent” mit der Zeile

taxiTexture = Content.Load<Texture2D>("Taxi");

geladen.  Soeben hast du eine der großen Vorteile von XNA verwendet: Die Content Pipeline. Ein großes Problem der Spieleentwicklung sind die Formate von Bildern, Sounds und sonstigen externen Daten. Üblicherweise müssen diese Daten vor Verwendung in einem Spiel noch entsprechend vorformatiert werden. Dabei gibt es sogar Unterschiede auf welchem System das Spiel später laufen soll. Die XBox erwartet beispielsweise ein anderes Sound-Format als das beim PC der Fall ist. Mit dem Verfahren über den Content-Ordner braucht man sich mit XNA nicht weiter darum kümmern, in welchem Format die Daten vorliegen. Die entsprechende Datei wird beim kompilieren von XNA passend aufbereitet und liegt dann für den Ladevorgang bereit.

Die Textur zu zeichen ist nun auch keine große Sache mehr. Wo zeichnen wir? natürlich in der Methode “Draw”. Da steht auch schon was drin. Das hier verwendete Objekt “GraphicsDevice” repräsentiert sozusagen unsere Grafikkarte. Sie verfügt über eine Methode Clear, die den gesamten Bildschirm wieder mit der angegebenen Farbe überschreibt. CornflowerBlue. Wer will, kann damit gerne mal etwas rum spielen.

Wir brauchen allerdings ein Hilfsobjekt, das Dank Game-Vorlage bereits existiert: Ein SpriteBatch. Ein SpriteBatch erlaubt die vollkommen unkomplizierte Zeichnung von 2D-Dingen direkt auf dem Bildschirm. Man muss das Ding aber erstmal starten und am Ende des Zeichenvorgangs auch wieder beenden, damit die Sachen auch wirklich gezeichnet werden. Zwischen diesen beiden Aufrufen werden dann die eigentlichen Texturen gezeichnet. Zum Test können wir da gerne mal das Taxi hin zeichen, damit man schonmal was sieht.

spriteBatch.Begin(); 
spriteBatch.Draw(taxiTexture, new Vector2(100, 100), Color.White); 
spriteBatch.End();

Wenn alles glatt gelaufen ist, kannst du jetzt beim Starten des Projektes (F5 oder den grünen Pfeil oben) schon eine blaue Fläche mit unserem Taxi sehen. Was haben wir da denn gemacht? In Draw werden 3 Parameter erwartet. Der erste ist die Textur, die gezeichnet werden soll. Parameter zwei ist ein zweidimensionaler Vector, der die Position der Textur angibt. Dabei ist die Position der linken, oberen Ecke der Textur, betrachtet von der linken, oberen Ecke des Zeichenfensters gemeint. Der dritte Parameter nimmt eine Farbe entgegen. Damit lässt sich die Textur nachträglich einfärben. Möchte man aber die original Farben der Textur beibehalten, gibt man hier einfach die Farbe Weiß (Color.White) an.

Das Resultat

Abb. 7: Erste Render-Ergebnisse

Schritt 4: Gravitation

Wir zeichnen! Hurra! Leider lässt die Interaktion mit dem Spieler zur Zeit etwas zu wünschen übrig. Es passiert nämlich noch garnichts. Es fehlen uns noch die äußeren Einflüsse und der Spieler-Input.

Damit wir das Taxi nun ordentlich bewegen können, müssen ein paar variable Informationen in unser Spielmodell einfließen. Das ist also der richtige Moment, um die Taxi-Klasse zu implementieren. Wir fügen also eine neue Klasse mit dem Namen “Taxi” zum Projekt hinzu und fügen den folgenden Code in die Klasse ein.

public Vector2 Position { get; set; } 
public Vector2 Speed { get; set; }

Für Vector2, einem Datentypen aus dem XNA-Framework, muss noch die Using-Direktive für das Framework eingefügt werden.

using Microsoft.Xna.Framework;

Wir speichern also in unserem Taximodell einerseits die Position – die soll ja in Zukunft variabel sein – und die Geschwindigkeit. Die Geschwindigkeit brauchen wir, weil sich das Taxi ja selbständig in einem physikalischen Raum bewegt. Es handelt sich um Masse die auf Gravitation reagiert und eine gewisse Trägheit besitzt. Wenn es sich einmal in Bewegung gesetzt hat, muss es sich im Laufe der Simulation auch weiterhin in diese Richtung bewegen, bis der User was unternimmt. Damit wir das ordentlich ausrechnen können, brauchen wir die Information der Geschwindigkeit.

Bewegung

Abb. 8: Bewegung des Taxis in Bezug auf die Runde

Jetzt brauchen wir noch eine Variable für das Taxi, die wir oben in der Game-Klasse deklarieren. Am besten in der Nähe der Texture von eben. Die passende Instanz dazu erzeugen wir natürlich in der Methode Initialize. Damit wir später keine Probleme kriegen, wenn wir das Level neu resetten wollen, führen wir an dieser Stelle schon eine zusätzliche Methode “Reset” in der Klasse Game ein, die uns das Level und das Taxi wieder in die Startposition versetzen sollen. Zu beginn legen wir die Startposition am besten mal auf P(100/100) und die Start-Geschwindigkeit natürlich auf 0/0.

private void Reset() 
{ 
    taxi.Position = new Vector2(100, 100); 
    taxi.Speed = new Vector2(); 
}

Aufgerufen wird diese Methode erstmal nur am Ende von Initialize. Später soll der Aufruf dieser Methode das Spiel zurück setzen. z.B. wenn der Spieler gegen eine Wand stößt.

Endlich können wir mit der Berechnung der Bewegung beginnen. Das passiert – na? – natürlich in Update. Die Bewegungberechnung besteht aus 2 Teilen. Im ersten Teil wird die Geschwindigkeitsangabe des Taxis neu berechnet. Wir müssen hier die Gravitation und später auch die Usereingabe berücksichtigen, bevor wir das Taxi dann tatsächlich in Schritt zwei bewegen und aus der Geschwindigkeitsangabe die neue Position des Taxis berechnen und setzen.

Im Schritt 1 schauen wir uns erstmal alle Kräfte an, die auf das Taxi wirken. Erstmal ist das nur die Gravitation. Diese wird mit ihrem Beschleunigungswert angegeben. Beschleunigung gibt man in der Regel mit der Geschwindigkeitsänderung pro Zeiteinheit an. Die Physik macht das üblicherweise in Meter pro Sekunde im Quadrat (m/s²). Dabei ist Meter pro Sekunde die Geschwindigkeitsangabe (m/s). Die Zeiteinheit ist auch wieder die Sekunde – daher das Quadrat (m/(s * s)).

Die Kräfte, die auf den Körper wirken müssen also nur addiert werden, um die Gesamtänderung der Geschwindigkeit zu ermitteln. Es gibt, wenn man diese Berechnung mit der Realität vergleicht, zwei wichtige Dinge zu beachten: Die Geschwindigkeit eines Fahrzeuges gibt man gerne in km/h an (um Grunde das selbe wie m/s, nur eben mit anderen Multiplikatoren), aber die Richtung zu kennen ist in diesem Zusammenhang sehr wichtig. Bei einem Auto würde man vielleicht von der Himmelsrichtung sprechen. Im Spiel verwenden wir aber eher ein 2-dimensionales Koordinatensystem, in dem die beiden Achsen X und Y existieren. Kräfte, Geschwindigkeiten und Beschleunigungen können dort für jede Achse getrennt betrachtet werden. Kommt eine dritte Dimension hinzu, kann das System einfach um diese Achse erweitert werden und funktioniert weiterhin. Deshalb besteht unsere Geschwindigkeitsangabe auch aus einem 2-dimensionalen Vektor.

Wir arbeiten hier übrigens nicht mit Metern, sondern mit Pixeln. Unsere Geschwindigkeit wird also in Pixel/Sekunde und die Beschleunigung mit Pixel pro Sekunde im Quadrat angegeben.

Geschwindigkeitsberechnung

Abb. 9: Geschwindigkeitsberechnung mit einwirkenden Kräften

Die zweite wichtige Sache, die man bei der rundenbasierten Berechnung beachten muss ist die Zeiteinheit. Natürliche Vorgänge wie die Beschleunigung eines Fahrzeugs passieren kontinuierlich. Leider sind wir im Spiel aber darauf angewiesen diese Berechnungen in den Simulationsrunden, also Schrittweise zu berechnen. Gerne werden Beschleunigungswerte so angegeben, dass sie pro Runde einfach zur Geschwindigkeit addiert werden können. Das ist aber physikalisch nicht korrekt, da der Zeitaspekt nicht korrekt berücksichtigt wurde. Man beachte, dass diese Simulationsrunden nicht immer in der selben Geschwindigkeit abgearbeitet werden. Es kann also sein, dass man im ersten Moment die Schleife 200 mal pro Sekunde durchläuft, im zweiten Augenblick aber mehr Berechnungen notwendig sind und deshalb nur 100 Runden zustande kommen. Würde man nun in jeder Runde den selben Wert addieren, bekäme man unregelmäßige Bewegungen des Taxis heraus. Lange Story, kurzer Sinn: Man muss bei der Berechnung von Bewegungen und Geschwidigkeitsänderungen immer darauf achten wie viel Zeit seit dem letzten Durchlauf vergangen ist. XNA übergibt den Methoden Update und Draw deshalb einen Parameter gameTime, aus dem sich diese Zeitdifferenz ermitteln lässt.

In der Update-Methode erzeugen wir zwei Variablen vom Typ float, die die Geschwindigkeitsänderung für die beiden Achsen X und Y enthalten sollen. Wir machen das, weil die Gravitation nur eine von vielen Kräften darstellt, die auf das Taxi wirken. Im Laufe des Tutorials werden noch weitere Kräfte hinzu kommen, die wir dann immer auf die beiden Variablen addieren können, um anschließend alles zusammengefasst auf das Taxi anzuwenden.

// Geschwindigkeitsänderungen 
float deltaY = 0; 
float deltaX = 0;

// Gravitation wirken 
deltaY = GRAVITY * (float)gameTime.ElapsedGameTime.TotalSeconds;

// Geschwindigkeitsänderung auf das Taxi anwenden 
taxi.Speed = new Vector2(taxi.Speed.X + deltaX, taxi.Speed.Y + deltaY);

// Neue Position des Taxis ermitteln 
taxi.Position += taxi.Speed * (float)gameTime.ElapsedGameTime.TotalSeconds;

Anstatt die Gravitationskraft hier direkt einzutragen, arbeite ich hier mit einer Konstante “GRAVITY”, die wir ganz oben in der Klasse noch deklarieren müssen. Diese Art der Konstanten erlauben einem Programmierer noch nachträglich an den Parametern des Spiels zu drehen. Später kommen noch weitere dieser Werte hinzu, um die Rahmenbedingungen des Spiels noch zu balancieren.

public const float GRAVITY = 9.81f;

Wir verändern jetzt zwar schon den Positionswert des Taxis, wir zeichnen die Textur aber immernoch an der festen Position. Das müssen wir noch ändern. In der Draw-Methode müssen wir dazu nur noch den Draw-Befehl durch die folgende Zeile ersetzen und ersetzen damit die feste Texturposition durch die berechneten Position des Taxis.

spriteBatch.Draw(taxiTexture, taxi.Position, Color.White);

Wenn wir starten, sehen wir bereits ein abstürzendes Taxi 🙂

Schritt 4: Spieler-Eingabe

Abstürzende Taxis kommen sicher nicht allzu gut an auf dem Spielemarkt, weshalb wir das Taxi nun mit Raketendüsen ausstatten, die der User steuern kann. XNA bietet Unterstützung für unterschiedliche Eingabegeräte. Zum einen der klassische XBox360-Controller, dann noch die Tastatur und auch eine Maus. Das Konzept ist aber für alle 3 Eingabegeräte gleich: Man fordert vom Framework zu einem bestimmten Zeitpunkt eine Momentaufnahme des aktuellen Gerätezustandes an und kann daraus dann ermitteln, welche Knöpfe gerade gedrückt sind, in welche Richtung sich die Maus bewegt oder wie stark einzelne Controller-Sticks gerade vom Spieler betätigt werden. Die passenden Klassen dazu finden sich im Namespace “Microsoft.Xna.Framework.Input”.

Es gibt dort statische Klassen Keyboard, GamePad und Mouse, die jeweils über eine statische GetState-Methode verfügen. Rückgabewert ist dann eine State des entsprechenden Gerätes. GamePad liefert ein struct vom Typ GamePadState.

Das Spiel soll später sowohl mit der Tastatur, als auch mit dem Gamepad spielbar sein, weshalb wir die Eingaben erst einmal auf sammeln und in eigene Variablen packen müssen. Dazu macht eine Sammlung der Möglichkeiten eines Spielers Sinn. Dieser kann 4 Steuerdüsen an den Seiten des Taxis aktivieren. Beim Gamepad soll der Schub auch entsprechend der Stärke, mit dem der Spieler den Stick betätigt, wirken. Verpackt wird das ganze in die beiden floats “forceX” und “forceY”. Der Wertebereich reicht von –1 (voller Gegenschub) bis 1 (voller Schub).

Das Schöne ist, dass das Gamepad genau diese Werte für die analog-Sticks zurück liefert. Wir können diese also direkt zuweisen. Die Tastatur hat aber leider keine analogen Eingabemöglichkeiten, weswegen wir direkt immer auf maximum Schub gehen müssen.

// Gerätestatus abfragen 
GamePadState gamePad = GamePad.GetState(PlayerIndex.One); 
KeyboardState keyboard = Keyboard.GetState();

// Zwischenvariablen für Schub 
float forceX = 0f; 
float forceY = 0f;

// Tastatur anwenden 
if (keyboard.IsKeyDown(Keys.Left))  forceX += -1f; 
if (keyboard.IsKeyDown(Keys.Right)) forceX += 1f; 
if (keyboard.IsKeyDown(Keys.Up))    forceY += 1f; 
if (keyboard.IsKeyDown(Keys.Down))  forceY += -1f;

// Gamepad anwenden 
forceX += gamePad.ThumbSticks.Left.X; 
forceY += gamePad.ThumbSticks.Left.Y;

// Limitierung auf den Wertebereich -1 bis 1 
forceX = Math.Min(Math.Max(-1f, forceX), 1f); 
forceY = Math.Min(Math.Max(-1f, forceY), 1f);

Fügst du den Code in deine Update-Methode ein (am besten an den Anfang, da wir die Variablen gleich bei der Bewegung brauchen), dann verfügst du ab hier in den force-Variablen über die Usereingabe und kannst darauf basierend die Krafteinwirkung auf das Taxi berechnen.

Was heißt das eigentlich? Mit den Raketen-Düsen am Taxi kann der Spieler jetzt Kräfte auf das Fluggerät wirken lassen. Diese verhalten sich recht ähnlich der Gravitationskraft, nur eben auf unterschiedlichen Achsen. Die linke Düse beispielsweise beweigt das Taxi auf der X-Achse nach rechts. Je stärker die Düse (das ist der Wert aus forceX), umso stärker auch die Kraft. Welchen Beschleunigungswert eine solche Düse hat, müssen wir dan wieder als Konstante weiter oben definieren. Ich habe diesen Wert jetzt einfach mal auf 30 Pixel pro Sekunde Quadrat festgelegt. bei vollem Schub ist die Düse also etwa 3 mal so stark wie die Gravitation.

public const float JETPOWER = 30f;

Die Kraft nun auch tatsächlich wirken zu lassen ist mit ein paar Zeilen Code in der Update-Methode erledigt. Wir hatten ja vorhin schon die beiden Variablen deltaX und deltaY erstellt, damit wir jetzt noch weitere Kräfte einfügen können. Folgende Zeilen, unter die Gravitationsberechnung eingefügt, sorgen für die notwendigen Berechnungen zur Taxi-Geschwindigkeit. Dass forceY hier mit einem Minus invertiert wird, liegt daran, dass die Y-Achse eines normalen Koordinatensystems nach oben größer wird. Im Zeichenfenster ist das aber umgekehrt, weshalb Y erstmal umgedreht werden muss, damit das Taxi nicht plötzlich in die falsche Richtung fliegt.

// Antriebsdüsen wirken lassen
deltaX += forceX * (float)gameTime.ElapsedGameTime.TotalSeconds * JETPOWER;
deltaY += -forceY *  (float)gameTime.ElapsedGameTime.TotalSeconds * JETPOWER;

Das Taxi lässt sich jetzt beim Start direkt steuern und man kann mit dem Taxi bereits etwas umher fliegen. Leider noch etwas langweilig, da man noch nichts machen kann, aber immerhin bewegt sich das Taxi jetzt nach realistischen, physikalischen Bedingungen. Levels, Düsen und einen Taxi-Gast gibts in Teil 2 des Tutorials.

Source zum Tutorialweiter zu Teil 2

Kommentare
  1. […] vor Jahren zum Lernen von Abläufen innerhalb von Spielen geschrieben habe. Natürlich gibts auch Teil 1 zu […]

  2. […] ausführlichen Blog-Einträgen hatte ich damals detailiert beschrieben, wie das Spiel funktionert. (Teil 1, Teil 2, Teil 3). Damals noch mit XNA. Letzte Woche hatte ich aber mal spontan Lust das Ganze […]

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