3d Karte Bewegen (MonoGame)

Zur Vollständigkeit zu meinem letzten Blog-Post wird das Beispiel in 3d Abgebildet. Abgesehen von der 3d Ansicht und Verwendung einer 3d Kamera ist die Steuerung nahe zu gleich, so das ich hier die Ausführung des Inputs auslasse.


Welches 3d Format
Normalerweise werden *.fbx und *.x 3d Formate unterstützt. Für mich war es jedoch neu, daß auch Wavefront *.obj 3d Format unterstütz wird. Das erfreuliche ist, daß z.B. viele frei verwendete 3d Modell in diesem Format vorliegen. Das erspart fehlerhafte Konvertierungen mit anderen Programmen. Wie immer verwende ich hier die Assets von Kenny.nl die nicht nur Texturen anbieten, sondern auch 3d Modell in *.obj und teilweise in *.fbx.


Pipeline 3d Importer
Vermutlich ist das noch ein Fehler in MonoGame Version 3.7.1. Wenn das 3d Model im Pipline Tool hinzugefügt wird, dann werden einige initiale Einstellungen gesetzt die bei einen Punkt manuell umgestellt werden muss.
Bei dem Wavefront Format, liegt neben der *.obj Datei auch eine *.mtl. Diese brauchen wir zunächst nicht in das Pipeline Tool hinzufügen.



Klickt ihr auf die Datei, dann werden in Properties die Einstellungen angezeigt. Hier wurde eigentlich der richtige Importer Ausgewählt. Dies las ich auch in einigen Foren Einträgen.



Fehler beim Laden des 3d Models
Falls ihr einen Ausnahmefehler erhält, das beim Laden des 3d Models auftritt.
Could not find ContentTypeReader Type. Please ensure the name of the Assembly..... Naja, ist im folgenden Bild zu sehen.


Kommt also dieser Ausnahmefehler, dann sollte der Importer von Open Asset Import Library - MonoGame umgestellt werden auf Fbx Importer - MonoGame.


Und nicht vergessen, nach dem Hinzufügen einmal auf F6 oder Build (Rebuild) klicken.


Laden des 3d Models
In das Unterverzeichnis zu Component werden die ComponentMap.cs und PlateGrassTile.cs angelegt.


Das verwendete 3d Model in dem Beispiel ist einfach eine Grüne Platte und soll dann später eine größere Fläche Abbilden. Im folgenden Code ist zu sehen, dass das 3d Objekt gedreht wird um 90 Grad. Aus der Gewohnheit her, ist Z hier die Höhe und Y die Tiefe. In Büchern wird oft Z als Tiefe und Y als Höhe angeben, was auch Technisch gesehen auch richtig ist.
Das im Konstruktor der Parameter 'Scale' angegeben wird, findet erst in im nächsten Post eine Verwendung.

public class PlateGrassTile  
 {  
   private Vector3 _offsetPosition;  
   private readonly float _scale = 1;  
   private readonly Model _model;  
   private readonly Matrix[] _transform;  
   private readonly ModelMesh _mesh;  
   private readonly bool _onlyOneMesh = false;  
   private Vector3 _offsetRotation;  
   public Vector3 Position { set; get; } = new Vector3();  
   public PlateGrassTile(Vector3 offsetPosition, float scale, Model model)  
   {  
     this._offsetPosition = offsetPosition;  
     var offsetRotateX = MathHelper.ToRadians(90);  
     this._offsetRotation = new Vector3(offsetRotateX, 0, 0);   
     this._scale = scale;  
     this._model = model;  
     this._transform = new Matrix[this._model.Bones.Count];  
     this._model.CopyAbsoluteBoneTransformsTo(this._transform);  
     this._mesh = this._model.Meshes.First();  
     this._onlyOneMesh = !this._model.Meshes.Any();  
   }  
   public void Draw(Matrix view, Matrix projection)  
   {  
     foreach (ModelMesh mesh in this._model.Meshes)  
     {  
       foreach (BasicEffect effect in mesh.Effects)  
       {  
         foreach (EffectPass effectPass in effect.CurrentTechnique.Passes)  
         {  
           effectPass.Apply();  
         }  
         effect.EnableDefaultLighting();  
         this.SetTransform(effect, mesh);  
         effect.View = view;  
         effect.Projection = projection;  
       }  
       mesh.Draw();  
     }  
   }  
   private void SetTransform(BasicEffect effect, ModelMesh mesh)  
   {  
     effect.World = this._transform[mesh.ParentBone.Index] *  
             Matrix.CreateRotationX(this._offsetRotation.X) *  
             Matrix.CreateRotationY(this._offsetRotation.Y) *  
             Matrix.CreateRotationZ(this._offsetRotation.Z) *  
             Matrix.CreateTranslation(this.Position + this._offsetPosition) *  
             Matrix.CreateScale(this._scale);  
   }  
 } 

Karte erstellen
Die Kartenerstellung wird hier simple gehalten und besteht daher aus 3x3 Felder. Für das Beispiel haben die Objekte einen kleinen Abstand zu einander. So kann man sehen, daß die neun Grasflächen erzeugt wurden. Ansonsten einfach in der Methode 'GetPosition' die Variable 'distance' auf '0f' setzten. Das 3d Objekt wird hier Initial geladen und dessen Referenz zusammen mit der Position zugewiesen in das 'PlateGrassTile' Klassen Objekt.

public class ComponentMap : GameComponent  
 {  
   private const float _speed = 0.3f;  
   private readonly ComponentInputs _componentInputs;  
   private PlateGrassTile[] _groundTiles = new PlateGrassTile[9];  
   public ComponentMap(Game game, ComponentInputs componentInputs) : base(game)  
   {  
     this._componentInputs = componentInputs;  
   }  
   public override void Initialize()  
   {  
     var grass = this.Game.Content.Load("Plate_Grass_01");  
     int index = 0;  
     for (int iY = 0; iY < 3; iY++)  
     {  
       for (int iX = 0; iX < 3; iX++)  
       {  
         this._groundTiles[index] = new PlateGrassTile(this.GetPosition(iX, iY),  
                                 0.5f,   
                                 grass);  
         index++;  
       }  
     }  
   }  
   public override void Update(GameTime gameTime)  
   {  
     foreach (var item in this._groundTiles)  
     {  
       item.Position += new Vector3(this._componentInputs.Inputs.MoveX,   
                       this._componentInputs.Inputs.MoveY, 0)   
                       * _speed;   
     }  
   }  
   public void DrawContent(Matrix view, Matrix projection)  
   {  
     foreach (var item in this._groundTiles)  
     {  
       item.Draw(view, projection);  
     }  
   }  
   private Vector3 GetPosition(int x, int y)  
   {  
     float mapLength = 3f;  
     float distance = .02f;  
     float centerMap = (mapLength + distance)   
               * (float)Math.Sqrt(this._groundTiles.Length) / 2; ;  
     return new Vector3((y * (mapLength + distance)) - centerMap,  
               (x * (mapLength + distance)) - centerMap,   
               0);  
   }  
 }  

Kamera
So richtig entscheiden konnte ich mich an der stelle nicht, wie ich die Komponente nenne. Render oder Camera. Es wird gerendert, aber auch die Kameraausrichtung vorgenommen. Auftrennen wäre hier zunächst übertrieben, dennoch macht es Sinn die wesentlichen Aspekte der Kamera zu einer Klasse zusammen zufassen.



Die Klasse 'CameraView.cs' setz alle Einstellungen in der Initialize Methode fest. Initial muß auch die AspectRatio gesetzt werden, da mit der UWP vorher nicht festgelegt wird, welche Auflösung verwendet wird.

 public class CameraView  
 {  
   private readonly Game _game;  
   private readonly ComponentMap _componentContent;  
   public Matrix View { get; private set; }  
   public Matrix Projection { get; private set; }  
   public CameraView(Game game, ComponentMap componentMap)  
   {  
     this._game = game;  
     this._componentContent = componentMap;  
   }  
   public void Initialize()  
   {  
     var aspectRatio = this.GetAspectRatio();  
     var position = new Vector3(-1f, 5f, 5f);  
     var target = new Vector3(0, 0, 0);  
     var farPlaneDistance = 10000;  
     this.View = Matrix.CreateLookAt(position, target, Vector3.Backward);  
     this.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,  
                                   aspectRatio,  
                                   1,  
                                   farPlaneDistance);  
   }  
   public void Draw()  
   {  
     this._componentContent.DrawContent(this.View, this.Projection);  
   }  
   private float GetAspectRatio()  
   {  
     var w = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width;  
     var h = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;  
     return w / h;  
   }  
 } 

Die 'ComponentRender' Klasse ist hier sehr übersichtlich und hat hier auch sonst keine Besonderheit. Mit dem nächsten Blogpost wird jedoch dieser Bereich erweitert.

 public class ComponentRender : DrawableGameComponent  
 {  
   private readonly CameraView _cameraView;  
   public ComponentRender(Game game, ComponentMap componentContent) : base(game)  
   {  
     this._cameraView = new CameraView(game, componentContent);  
     this._cameraView.Initialize();  
   }  
   public override void Draw(GameTime gameTime)  
   {  
     this.GraphicsDevice.Clear(Color.CornflowerBlue);  
     this._cameraView.Draw();  
   }  
 }  

Der Rest des Codes ist nahe zu Identisch wie bei dem letzten Post, wie man eine 2d Karte bewegt.

Nachwort
Diese Ausführung funktioniert noch gut auf dem Raspberry Pi 3. Hektische Spiele sollten man denn noch nicht anversieren.


Kommentare

Beliebte Posts aus diesem Blog

Arduino Control (Teil 5) - PWM Signal einlesen

RC Fahrtenregler für Lego Kettenfahrzeug

Angular auf dem Raspberry Pi