In diesem Artikel möchte ich ein Problem angehen, welches vor allem für Spieleentwickler, die für Android entwickeln, einen Stolperstein darstellt: Boundingbox-Kollisionserkennung.
Was ist eine Boundingbox?
Eine Boundingbox ist eine MonoGame-Klasse, die in 3D Spielen oft für die Kollisionserkennung von einfachen Objekten herhalten muss. Sie stellt eine Box dar, die aus acht Punkten besteht, wobei jeder Winkel an den Ecken genau 90 Grad beträgt. Dank dieser einfachen Form, benötigt man lediglich zwei Punkte für die Erstellung einer Boundingbox: den Minimal- und Maximal-Punkt. Das sind zwei Ecken, die diagonal zueinander liegen. Die restlichen sechs Punkte berechnet für uns dann MonoGame
BoundingBox boundingBox = new BoundingBox(minVertex, maxVertex);
Stolperstein unter Android
Sicherlich fragt sich der ein oder andere, warum so eine einfache Klasse einen Stolperstein für die Android-Entwicklung darstellen soll. Die Antwort ist ganz einfach: wenn wir um unsere 3D-Modelle eine BoundingBox setzen wollen, müssen wir den Minimal- und Maximal-Punkt berechnen. Dies können wir nur anhand der Vertices. Allerdings liegen diese im VertexBuffer. Unter Windows bzw. DirectX können aus diesem Buffer die Vertices einfach ausgelesen werden, unter Android bzw. OpenGL bekommt man bei dem Versuch allerdings eine Exception.
System.NotSupportedException: Vertex buffers are write-only on OpenGL ES platforms
Um trotzdem an die Vertices zu kommen, kann man den Importvorgang des Modells anpassen. Dazu sehen wir uns zunächst einmal die Content-Pipeline an.
Die Content-Pipeline
Die Content-Pipeline ist dafür zuständig, all unsere Game-Assets wie Musik, Texturen oder 3D-Modelle in eine XNA-taugliche .xnb Datei zu kompilieren. Der Sinn davon ist, Berechnungen vor der Laufzeit abzuhaken, um mehr Performance zu erreichen.
Zu XNA-Zeiten reichte es, wenn man im Content Ordner die Dateien einfach ablegte, um von der Content-Pipeline gebrauch zu machen. Diese wurden dann von Visual-Studio kompiliert. In MonoGame muss man für diesen Job das Content-Pipeline-Tool verwenden. Wenn man in diesem Tool eine beliebige Datei auswählt, kann man unter Properties->Settings einen Importer bzw. Processor auswählen. Die Kernaufgabe des Importers ist das Lesen der Rohdatei, z.B. eine Bilddatei wie .png oder .jpg. Der Processor wiederum nimmt sich aus dem Inhalt das, was das Spiel benötigt. Aus den Vertex-Daten eines 3D-Models können z.B. erste Verarbeitungen erfolgen, die nicht unbedingt zur Laufzeit des Spiels geschehen müssen. Der Processor liefert ein Datenobjekt zurück, welches im Spiel verwendet werden kann, z.B. Texture2D oder Model. Auf den Processor folgen der Content-Type-Writer und -Reader, die unsere .xnb Datei erzeugen bzw. lesen. Der Reader wird im Gegensatz zu den anderen Bestandteilen der Content-Pipeline logischerweise zur Laufzeit ausgeführt.
Unser Ziel ist es nun, einen vorhandenen Standard-Processor so zu erweitern, dass dort unsere BoundingBox berechnet wird.
ModelProcessor erweitern
Der Processor, der z.B. auf den „Fbx Importer“ folgt, ist der ModelProcessor. Dieser soll nun erweitert werden. Dazu legen wir in Visual Studio ein neues Projekt an.

Nachdem das Projekt angelegt wurde, muss eventuell noch eine kleine Installation vorgenommen werden. Dazu klickt ihr einfach im Projektmappenexplorer mit der rechten Maustaste auf „Verweise“ und wählt „NuGet-Pakete verwalten..“ aus. Sucht nach „Monogame Pipeline Portable“ und wählt dann das passende Ergebnis aus. Installiert einfach die aktuellste Version, bei mir ist es die 3.6.0.1625.

Nach der Installation könnt ihr die Klasse „ContentProcessor1“ öffnen und, wenn noch nicht geschehen, folgende Bibliotheken einbinden
using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Processors;
Übrigens könnt ihr die ContentImporter1-Klasse löschen, da wir diese nicht benötigen.
Die vordefinierten Input- und Output-Typen könnt ihr ebenfalls löschen, da wir keinen komplett neuen Processor schreiben, sondern einen vorhandenen nur erweitern. Aus dem selben Grund soll unsere Klasse auch nur den ModelProcessor erben
namespace ExtendedModelProcessor { [ContentProcessor(DisplayName = "ExtendedModelProcessor.ContentProcessor1")] public class ContentProcessor1 : ModelProcessor { } }
Wir möchten eine Methode überschreiben, welche aus dem Input ein ModelContent-Objekt generiert. Bei dieser Klasse handelt es sich um das Pendant der Model-Klasse
public override ModelContent Process(NodeContent input, ContentProcessorContext context) { return base.Process(input, context); }
Nun können wir in dieser Methode unsere BoundingBox-Berechnung vornehmen. Zuerst deklarieren wir eine neue Variable vom Typ ModelContent, welche als Wert den Rückgabewert der Basisklassen-Methode „Process“ erhält. Diese liefert nämlich wie erwähnt zu einem Input ein ModelContent. Dieses würde normalerweise direkt an den Content-Type-Writer gehen, der daraus unsere .xnb-Datei erzeugt. Doch vorher wollen wir genau dieses Objekt um eine weitere Information ergänzen. Dazu nutzen wir die Tag-Eigenschaft unseres ModelContent-Objekts. Diese Eigenschaft ist genau für solche Fälle da, nämlich um weitere Informationen zu speichern. Normalerweise hat diese den Wert NULL. In einer weiteren Variablen wollen wir unsere BoundingBox zwischenspeichern, ich nenne sie boundingBox.
ModelContent modelContent = base.Process(input, context); BoundingBox boundingBox;
Im Internet finden sich zahlreiche Berechnungsalgorithmen für die BoundingBox. Daher werde ich den Algorithmus nicht näher erläutern. Simpel zusammengefasst werden alle Vertices aus unserem ModelContent-Objekt durchgegangen und dabei der minimalste und maximalste Punkt herausgefunden. Anschließend wird unsere BoundingBox initialisiert und der Tag-Eigenschaft unseres ModelContent-Objekts zugewiesen. Nun kann das Objekt als Rückgabewert zurückgegeben werden.
public override ModelContent Process(NodeContent input, ContentProcessorContext context) { if (input == null) { throw new ArgumentNullException("input"); } else { BoundingBox boundingBox; // Create variables to keep min and max xyz values for the model Vector3 modelMax = new Vector3(float.MinValue, float.MinValue, float.MinValue); Vector3 modelMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); ModelContent modelContent = base.Process(input, context); foreach (ModelMeshContent mesh in modelContent.Meshes) { //Create variables to hold min and max xyz values for the mesh Vector3 meshMax = new Vector3(float.MinValue, float.MinValue, float.MinValue); Vector3 meshMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); // There may be multiple parts in a mesh (different materials etc.) so loop through each foreach (ModelMeshPartContent part in mesh.MeshParts) { // The stride is how big, in bytes, one vertex is in the vertex buffer int stride = (int)part.VertexBuffer.VertexDeclaration.VertexStride; byte[] vertexData = part.VertexBuffer.VertexData; // Find minimum and maximum xyz values for this mesh part // We know the position will always be the first 3 float values of the vertex data Vector3 vertPosition = new Vector3(); for (int ndx = 0; ndx < vertexData.Length; ndx += stride) { vertPosition.X = BitConverter.ToSingle(vertexData, ndx); vertPosition.Y = BitConverter.ToSingle(vertexData, ndx + sizeof(float)); vertPosition.Z = BitConverter.ToSingle(vertexData, ndx + sizeof(float) * 2); // update our running values from this vertex meshMin = Vector3.Min(meshMin, vertPosition); meshMax = Vector3.Max(meshMax, vertPosition); } } // Expand model extents by the ones from this mesh modelMin = Vector3.Min(modelMin, meshMin); modelMax = Vector3.Max(modelMax, meshMax); } boundingBox = new BoundingBox(modelMin, modelMax); modelContent.Tag = boundingBox; return modelContent; } }
Das Kompilieren sollte fehlerfrei eine .dll erzeugen. Diese Muss im Content-Pipeline-Tool eingebunden werden. Dazu öffnet ihr eure .mgbc Datei, die standardmäßig Content heißt, und wählt das oberste Element namens „Content“ aus. Klickt unter den Properties auf „References“ und fügt eure zuvor kompilierte .dll hinzu.

Verwendung in eurem Game
Die Verwendung ist wie gewohnt einfach. Wenn ihr in eurer LoadContent-Methode euer 3D-Modell ladet.
myModel = Content.Load("MyAsset"); myBoundingBox = (BoundingBox)myModel.Tag;
Beachtet, dass bei meinem BoundingBox-Algorithmus keine Transformationen im World-Space vorgenommen wird. Mittels Vector3.Transform(pos, worldMatrix) könnt ihr die BoundingBox transformieren.
Bei Fragen könnt ihr sie gerne im Kommentarbereich stellen, ich werde diese zeitnah beantworten. Zum Abschluss möchte ich euch noch einige Links mit auf den Weg geben, die mit diesem Thema zusammenhängen und ich als nützlich erachte.
Empfehlungen
Anleitung für einen eigenen Content-Importer, -Processor, -Writer und -Reader.
http://blog.dylanwilson.net/posts/2015/creating-custom-content-importers-for-the-monogame-pipeline/
MonoGame-Community Thema über das hier behandelte Problem