XNA in 3-D

3-D-Spiele mit XNA erstellen (Teil 2)
Kommentare

Das Modell lernt laufen
Als bekennendem Sadisten bereitet es dem Autor enormes Unbehagen, den Affen auf dem Bildschirm faulenzen zu sehen. Der nächste Schritt besteht daher darin, dem Affen ein paar Bälle

Das Modell lernt laufen

Als bekennendem Sadisten bereitet es dem Autor enormes Unbehagen, den Affen auf dem Bildschirm faulenzen zu sehen. Der nächste Schritt besteht daher darin, dem Affen ein paar Bälle auf den Kopf zu werfen, sodass seine friedliche Existenz beendet ist und das Herz des Autors wieder lacht. Als Erstes erweitern wir unser SmartModel deshalb um die Fähigkeit, sich selbsttätig über den Bildschirm zu bewegen. Dazu brauchen wir drei Werte, die die Bewegungsrichtungen abbilden: public float myMoveX, myMoveY, myMoveZ;. Im nächsten Schritt erweitern wir die Klasse um eine Update()-Routine, die das Aktualisieren der Position pro frame vornimmt (Listing 6).

Sie wird in Update nach dem hier aus Platzgründen nicht noch einmal abgedruckten Abfragen der Steuerelemente aufgerufen:

Listing 6

public void Update()
{
  myPos.X += myMoveX;
  if (myPos.X < -1000 || myPos.X > 1000) myMoveX *= -1;

  myPos.Y += myMoveY;
  if (myPos.Y < -1000 || myPos.Y > 1000) myMoveY *= -1;

  myPos.Z += myMoveZ;
  if (myPos.Z < -1000 || myPos.Z > 1000) myMoveZ *= -1;
}
  

Sie wird in Update nach dem hier aus Platzgründen nicht noch einmal abgedruckten Abfragen der Steuerelemente aufgerufen:

protected override void Update(GameTime gameTime)
{
. . .
foreach (SmartModel sm in myModels)
{
sm.Update();
}
base.Update(gameTime);
}

Damit wären wir mit dem Bewegen fertig und müssen einige Bälle erstellen. Das erledigen wir am besten in LoadContent (Listing 7). Auch diese Methode ist keine Raketenphysik: Die Bälle werden erstellt, mit zufälligen Parametern belebt und in die Liste der SmartModels der Klasse eingefügt.

Listing 7

protected override void LoadContent()
{
  . . .
  for (int i = 1; i < 10; i++)
  {
    Model aModel2 = Content.Load("roterball");
    float x = (float)((newRand.NextDouble()) - 0.5) * 2000;
    float y = (float)((newRand.NextDouble()) - 0.5) * 2000;
    float z = (float)((newRand.NextDouble()) - 0.5) * 2000;
    SmartModel aSmartModel2 = new SmartModel(aModel2, new Vector3(x, y, z), new Vector3(0, 0, 0), new Vector3(100, 100, 100));
    aSmartModel2.myMoveX = (float)((newRand.NextDouble()) - 0.5) * 20;
    aSmartModel2.myMoveY = (float)((newRand.NextDouble()) - 0.5) * 20;
    aSmartModel2.myMoveZ = (float)((newRand.NextDouble()) - 0.5) * 20;
    myModels.Add(aSmartModel2);
  }
  
Nur nicht zusammenknallen

Führte man das Beispiel in der vorliegenden Form aus, würden die Bälle auf dem Bildschirm herumeiern und weder auf den Affen noch aufeinander Rücksicht nehmen, denn es fehlt die so genannte Kollisionserkennung. Das Feld der Kollisionserkennung ist weit. Ganze Bücher werden nur über dieses eine Thema (und seine Optimierung) veröffentlicht und auch im außeruniversitären Umfeld gerne gelesen beziehungsweise gekauft. In unserem Beispiel werden wir auf die Methode der Bounding Spheres zurückgreifen, da sie von Microsoft gegenüber der Methode der Bounding Boxes favorisiert wird. Eine Bounding Sphere ist primitiv betrachtet ein Ellipsoid, der alle Teile des Körpers umfasst und so die Kollisionserkennung erleichtert. Berühren sich die beiden Ellipsoide nicht, berühren sich auch die Objekte nicht. Berühren sich die Ellipsoide hingegen, kann man entweder detaillierter nachprüfen oder wie in unserem Beispiel einfach von einer Kollision ausgehen. Das Zurückliefern der Bounding Sphere wird durch eine Methode in SmartModel realisiert (Listing 8).

Listing 8

public BoundingSphere getBoundingBox()
{
  BoundingSphere mySphere = new BoundingSphere(Vector3.Zero, 0);
  foreach(ModelMesh m in myModel.Meshes)
  {
    BoundingSphere transformedSphere = m.BoundingSphere.Transform(myTransforms[m.ParentBone.Index]);
    mySphere = BoundingSphere.CreateMerged(transformedSphere, mySphere);
  } //cache the value after this for speed, but not in article due to length

  Matrix transformMeToWorld=Matrix.CreateScale(myScale)*Matrix.CreateTranslation(myPos);
  return mySphere.Transform(transformMeToWorld);
}
  

Im ersten Schritt wird hier eine leere BoundingSphere erstellt, die im nächsten Schritt um die BoundingSpheres der einzelnen Meshes erweitert wird bis die Modell-Bounding-Box bereitsteht. In einem realen Spiel würde man diese cachen, da sie sich ohne Veränderung des zugrunde liegenden Modells nicht verändern kann. Im nächsten Schritt wird sie an ihre derzeitige Position kopiert, sodass sie das Modell an seiner realen Position umschließt. Dann müssen wir nur noch die einzelnen Boxen auf Kollision prüfen. Dazu erweitern wir die Routine Update() um das in Listing 9 gezeigte Codestück. Der wichtigste Teil dieser Routine sind die beiden verschachtelten for-Schleifen. Ihre Parametrierung ist auf den ersten Blick verwirrend, lässt sich aber anhand von Abbildung 4 leicht erklären.

Listing 9

for (int i = 0; i < myModels.Count; i++)
{
  for (int j = i + 1; j < myModels.Count; j++)
  { 
    if(myModels[i].getBoundingBox().Contains(myModels[j].getBoundingBox())!=ContainmentType.Disjoint)
    { // boom!
      float dxold = myModels[i].myMoveX;
      float dyold = myModels[i].myMoveY;
      float dzold = myModels[i].myMoveZ;
      myModels[i].myMoveX = myModels[j].myMoveX;
      myModels[i].myMoveY = myModels[j].myMoveY;
      myModels[i].myMoveZ = myModels[j].myMoveZ;

      myModels[j].myMoveX = dxold;
      myModels[j].myMoveY = dyold;
      myModels[j].myMoveZ = dzold;

      myModels[i].Update();
      myModels[j].Update();
    
  }
}
  

Abb. 4: Mit jedem neuen Durchlauf der Schleife werden immer weniger neue Kombinationen sichtbar
Abb. 4: Mit jedem neuen Durchlauf der Schleife werden immer weniger neue Kombinationen sichtbar

Beginnen wir unsere Überlegungen in einer Flughafenlounge im Osten: Der Autor knallt mit seiner Lebensgefährtin zusammen, die ein Red Bull abernten will. Beide entschuldigen sich und gehen ihrer Wege. Allen feministischen Überlegungen zum Trotz: Es ist nicht nur der Autor mit der Dame kollidiert, sondern auch die Dame mit dem Autor. Würde die Kollisionsroutine - ganz primitiv - einfach für jedes Objekt die Kollision mit allen anderen Objekten prüfen, hätte man zumindest den Vergleich Autor - Lebensgefährtin zweimal (also einmal zu oft) durchgeführt. Auch der Vergleich Autor - Autor ist Verschwendung von Rechenleistung. Doch damit nicht genug: Kollisionsroutinen verändern die Parameter der kollidierenden Objekte, um sie voneinander "wegzulotsen". In unserem Beispiel erfolgt das (physikalisch nicht korrekt) durch das Vertauschen der Bewegungsvektoren. Würde nun der Autor mit der Lebensgefährtin Vektoren tauschen und nachher die Lebensgefährtin mit dem Autor, wäre der Ausgangszustand wiederhergestellt und die nächste Kollision vorprogrammiert. Nach sorgfältiger Betrachtung der obigen Analyse und der in Abbildung 3 dargestellten Tabelle macht die Parameterbestimmung Sinn. Damit fehlt nur der nochmalige Aufruf von Update. Doch auch dieser ist sinnvoll: Die Objekte sollen ja "auseinandergeschoben" werden, sodass sie nicht beim nächsten Durchlauf der primitiven Kollisionsroutine als "zusammenklebend" vorgefunden werden.

Aufbruch in neue Welten

Microsoft wirbt damit, dass in XNA erstellte Programme auf dem PC, der Xbox 360 und (in begrenztem Rahmen) am Windows Phone 7 ausgeführt werden können. Unser kleines Projekt verwendet (noch) keine Shader-Effekte und benötigt deshalb nicht unbedingt das nur auf PC und Xbox 360 verfügbare HiDef-Profil. Zum Erstellen einer auf PC oder Windows Phone 7 lauffähigen Version des Programms klickt man das Projekt im Solution Explorer rechts an und wählt im Kontextmenü entweder CREATE COPY OF PROJECT FOR PC oder CREATE COPY OF PROJECT FOR WINDOWS PHONE 7 aus. Visual Studio erstellt daraufhin eine neue .csproj-Datei, die die Einstellungen für die neue Plattform zusammenfasst. Es ist sehr wichtig, darauf zu achten, dass dadurch keine Duplizierung der Inhaltsdateien auftritt (Abb. 4).

Abb. 5: Quellcode wird nicht dupliziert
Abb. 5: Quellcode wird nicht dupliziert

Je nach aktivem "Startprojekt" und geöffneter Datei wird der Debugging-Prozess entweder für Xbox, Windows Phone oder den Desktop gestartet. Wenn man auf Nummer sicher gehen will, entfernt man die nicht benötigten .csproj-Dateien durch Rechtsklick und_ REMOVE aus dem Solution Explorer und sichert so die Plattformauswahl. Die entfernten .csproj-Dateien kann man später ohne Probleme als Existing Project wieder zur Solution hinzufügen. Zum Unterscheiden der aktiven Codeplattform im Code greift man auf den Preprozessor zurück. Das verwendete Schema zeigt den folgende Code:

#if WINDOWS
#elif XBOX
#elif WINDOWS_PHONE
#elif ZUNE
#endif

Abbildung 5 zeigt alle drei Versionen in Aktion nebeneinander stehend.

Abb. 6: Vom Samsung Omnia 7 bis zur XBox - und das Notebook mittendrin
Abb. 6: Vom Samsung Omnia 7 bis zur XBox - und das Notebook mittendrin
Fazit

Sowohl Affe als auch Kugel sind auf dem Bildschirm und tauschen Zärtlichkeiten aus. Nach diesem Schema könnte man ein erstes kleines Spielchen zusammenbauen. Leider reicht das noch lange nicht für ein passables, verkaufbares Spiel. Im nächsten Teil werden wir unser Affenbeispiel um eine Soundausgabe erweitern, die Kameraperspektive an einen der Bälle "anhängen" und erste Schritte in die faszinierende Welt der Shader machen. Weichen Sie den roten Kugeln aus und bleiben Sie am Ball, es lohnt sich!

Tam Hanna befasst sich seit der Zeit des Palm IIIc mit Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht unter tamhan@tamoggemon.com für Fragen, Trainings und Vorträge gern zur Verfügung.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -