Posts mit dem Label XNA werden angezeigt. Alle Posts anzeigen
Posts mit dem Label XNA werden angezeigt. Alle Posts anzeigen

Sonntag, 20. Januar 2019

Nicht ich, sondern der Boden bewegt sich (MonoGame)


Für die Lösung, wie eine Figur auf der Karte Bewegt werden kann, ist davon abhängig, wieviel Leistung mein Ziel System hat. In den meisten Fällen hat man mehr als genügend Leistung, so dass man sich hierbei entscheiden kann. Für relativ schwache System, z.B. wie bei einem Arduino Esplora kann eine Figur auf dem Bildschirm bewegt werden, ohne einem starken flackern. Eine Karte auf dem Bildschirm zu bewegen sieht dagegen wieder schlecht aus, weil der Bildaufbau zu lange dauert. Und das bei einer Auflösung von 128x160 Pixeln.

Nicht die Figur, sondern die Karte bewegt sich
Nun zurück zum Eigentlichen. Die Figur ist immer mittig auf dem Bildschirm und bewegt sich auf einer Karte. Aber Technisch gesehen, wird die Karte bewegt und die Figur hat eine Laufanimation.

Neues MonoGame Projekt
Aktuell wird für dieses Beispiel eine Neue Solution angelegt mit dem Ziel einer UWP Anwendung. Die Zielplattform und Version hat jedoch keinen Effekt für das Beispiel. Der Grund für UWP ist, das die Lauffähigkeit auf PC, Tablet, Raspberry Pi 2&3 und wenn ich mich nicht irre, auch auf der Xbox funktionieren soll. Sollte die Anwendung/Spiel auch auf dem Windows Phone laufen, dann ist eine ältere Minimum Ziel Version von Windows 10 einzustellen.



Ordnung ist das halbe Leben
Als erstes brauchen wir eine Projekt Struktur und legen ein paar Ordner an. Components wird es später drei Komponenten geben, die für die Benutzereingaben, die Karteverarbeitung und das Zeichnen zuständig sind.





















Der Umfang für das Realisieren benötigt einige Zeilen Code, so das eine Aufteilung der Inhalte Notwendig sind. Deshalb kann zunächst in der 'Game1.cs' fast alles weg. Nur der Konstruktor bleibt.

 using Microsoft.Xna.Framework;  
 namespace ExampleWalkingOnMap  
 {  
   public class Game1 : Game  
   {  
     private GraphicsDeviceManager _graphics;  
     public Game1()  
     {  
       this._graphics = new GraphicsDeviceManager(this);  
       this.Content.RootDirectory = "Content";  
       this.Window.Title = "Example Walking on map";  
       this.IsMouseVisible = true;  
     }  
   }  
 } 

Eingabegerät
Für das erfassen von Tasteneingaben, wird die Klasse 'ComponentInputs.cs' und für die Steuerinformation 'InputData.cs' angelegt.








Zunächst muss die 'InputData.cs' bearbeitet werden, um die verarbeiteten Steuerinformationen weiter zutragen.

 using Microsoft.Xna.Framework;  
 namespace ExampleWalkingOnMap.Components.Inputs  
 {  
   public class InputData  
   {  
     public Vector2 Move => new Vector2(this.MoveX, this.MoveY);  
     public float MoveX { get; set; }  
     public float MoveY { get; set; }  
   }  
 } 

Mit der ‚InputData‘ können die Eingaben in ComponentInputs.cs aufgenommen werden. Im Beispiel wird die Tastatur und ein Xbox Controller eingelesen. Damit wäre die Eingabe Komponente fertig.

using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Input;  
 using System;  
 namespace ExampleWalkingOnMap.Components.Inputs  
 {  
   public class ComponentInputs : GameComponent  
   {  
     public InputData Inputs { get; private set; } = new InputData();  
     public ComponentInputs(Game game) : base(game) {  }  
     public override void Update(GameTime gameTime)  
     {  
       this.Inputs.MoveX = 0;  
       this.Inputs.MoveY = 0;  
       KeyboardState stateKeyboard = Keyboard.GetState();  
       this.Inputs.MoveX += stateKeyboard.IsKeyDown(Keys.A) ? 1 : 0;  
       this.Inputs.MoveX -= stateKeyboard.IsKeyDown(Keys.D) ? 1 : 0;  
       this.Inputs.MoveY += stateKeyboard.IsKeyDown(Keys.W) ? 1 : 0;  
       this.Inputs.MoveY -= stateKeyboard.IsKeyDown(Keys.S) ? 1 : 0;  
       GamePadState stateGamePad = GamePad.GetState(PlayerIndex.One);  
       this.Inputs.MoveX += stateGamePad.ThumbSticks.Left.X * -1;  
       this.Inputs.MoveY += stateGamePad.ThumbSticks.Left.Y;  
       // Normalize  
       this.Inputs.MoveX = Math.Min(1, Math.Max(-1, this.Inputs.MoveX));  
       this.Inputs.MoveY = Math.Min(1, Math.Max(-1, this.Inputs.MoveY));  
     }  
   }  
 }

Die Karte
Die Realisierung der Karte wird in Kacheln unterteilt und ist auch relativ leicht um zusetzten. Jede Kachel hat eine eigene Position und eine Textur, womit schon mal das nächste Objekt definiert ist.





















Für die Textur mit verschiedenen Untergründen wird eine Bilddatei zusammen gefasst, die die Unterschiedlichen Bodentexturen beinhaltet. Jedes Objekt kennt dann nur den Ausschnitt aus der Bilddatei. Für das Beispiel verwende ich die Texturen von https://kenney.nl/assets.











Im Projekt wurde bereits mit der Vorlage, der Ordner 'Content' angelegt. Hier wird nun die Textur abgelegt.






Nun muss die Bilddatei noch mit dem Pipline Tool verarbeitet werden, indem ihr doppelkick auf 'Content.mgcb' ausführt. Im Ausschnitt Projekt wird die Bilddatei hinzugefügt.
















Wirkt leider etwas doppelt, aber dies ist notwendig, damit die Textur später im Programmcode geladen werden kann.

















Anschließend auf den Button 'Build' oder F6 drücken und fertig ist dieser Teil und könnt das Tool  wieder schließen.















Nun zurück zum Code für die Kachel Information. Wie bereits erwähnt, kennt die Kachel nur die Position der Textur, die später über die Methode Draw hinein gereicht wird. Mit dem weiteren Parameter 'offset' wird bestimmt, wo im Fenster gerendert wird.

using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Graphics;  
 namespace ExampleWalkingOnMap.Components.Map  
 {  
   public class GroundTile  
   {  
     private Rectangle _texturePosition;  
     public GroundTile(int x, int y, int width, int height)  
     {  
       this._texturePosition = new Rectangle(x, y, width, height);  
     }  
     public void Draw(Texture2D textureMapTiles, SpriteBatch spriteBatch, Vector2 offset)  
     {  
       spriteBatch.Draw(textureMapTiles,  
                offset,  
                this._texturePosition,  
                Color.White,  
                0f,  
                new Vector2(0, 0),  
                new Vector2(1, 1),  
                SpriteEffects.None,  
                0f);  
     }  
   }  
 } 

Die Karte soll aus 10x10 Kacheln bestehen und wird in einer Hilfsklasse erstellt.






Mit dem zwei Dimensionalen Integer Array, lässt sich visuell schnell eine Karte dieser Größe  zusammenstellen, die wiederum noch übersichtlich ist. Zumindest im ersten Abschnitt der Methode. Anschließend wird hier in das Ziel Array gemappt mit den eigentlichen Ziel Objekt.

using System;  
 namespace ExampleWalkingOnMap.Components.Map  
 {  
   internal class MapHelper  
   {  
     internal static GroundTile[,] CreateMap()  
     {  
       int[][] map = new int[10][];  
       map[0] = new int[10] { 0, 0, 0, 0, 0, 0, 0, 0, 2, 2 };  
       map[1] = new int[10] { 0, 0, 0, 0, 0, 0, 0, 0, 2, 2 };  
       map[2] = new int[10] { 1, 1, 1, 1, 1, 1, 1, 0, 2, 2 };  
       map[3] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[4] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[5] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[6] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[7] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 };  
       map[8] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 3, 3 };  
       map[9] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 3, 0 };  
       GroundTile[,] groundTiles = new GroundTile[10, 10];  
       for (int iY = 0; iY < 10; iY++)  
       {  
         for (int iX = 0; iX < 10; iX++)  
         {  
           groundTiles[iY, iX] = GetMappingGroundTile(map[iY][iX]);  
         }  
       }  
       return groundTiles;  
     }  
     private static GroundTile GetMappingGroundTile(int groundTile)  
     {  
       switch (groundTile)  
       {  
         case 0:  
           return new GroundTile(0, 0, 64, 64);  
         case 1:  
           return new GroundTile(64, 0, 64, 64);  
         case 2:  
           return new GroundTile(0, 64, 64, 64);  
         case 3:  
           return new GroundTile(64, 64, 64, 64);  
       }  
       throw new ArgumentException("groundTile can be only 0, 1, 2 and 3");  
     }  
   }  
 }  

Für die Verwaltung der Karte wird die Klasse 'ComponentMap.cs' erstellt. Dort werden die Karteninhalte gesteuert und gehalten.






Der Umfang der Klasse ist hier größer als bei den anderen, auch wenn dessen Ausführung sehr klein gehalten ist.

using ExampleWalkingOnMap.Components.Inputs;  
 using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Graphics;  
 namespace ExampleWalkingOnMap.Components.Map  
 {  
   public class ComponentMap : GameComponent  
   {  
     private readonly ComponentInputs _inputs;  
     private GroundTile[,] _mapTiles;  
     private Texture2D _textureMapTiles;  
     private int _mapTileWidth, _mapTileHeight;  
     private Vector2 _screenOffset, _movePosition;  
     public Vector2 Position => new Vector2(this._movePosition.X + this._screenOffset.X,  
                         this._movePosition.Y + this._screenOffset.Y);  
     private int _mapWidth, _mapHeight;  
     public ComponentMap(Game game, ComponentInputs inputs) : base(game) => this._inputs = inputs;  
     internal void SetScreenOffset(float screenWidth, float screenHeight)  
     {  
       var centerScreenWidth = screenWidth / 2;  
       var centerScreenHeight = screenHeight / 2;  
       this._screenOffset = new Vector2(centerScreenWidth, centerScreenHeight);  
       this._movePosition = new Vector2(0, 0);  
     }  
     public override void Initialize()  
     {  
       using (var stream = TitleContainer.OpenStream("Content/MapTiles.png"))  
       {  
         this._textureMapTiles = Texture2D.FromStream(this.Game.GraphicsDevice, stream);  
       }  
       this._mapTiles = MapHelper.CreateMap();  
       this._mapTileWidth = this._textureMapTiles.Width / 2;  
       this._mapTileHeight = this._textureMapTiles.Height / 2;  
       this._mapWidth = this._mapTileWidth * this._mapTiles.GetLength(0);  
       this._mapHeight = this._mapTileHeight * this._mapTiles.GetLength(1);  
     }  
     public override void Update(GameTime gameTime)  
     {  
       this._movePosition += this._inputs.Inputs.Move * 2f;  
     }  
     public void DrawMapTiles(SpriteBatch spriteBatch)  
     {  
       for (int iY = 0; iY < this._mapTiles.GetLength(0); iY++)  
       {  
         for (int iX = 0; iX < this._mapTiles.GetLength(1); iX++)  
         {  
           Vector2 textureOffset = new Vector2(iX * this._mapTileWidth, iY * this._mapTileHeight);  
           Vector2 offset = this.Position + textureOffset;  
           this._mapTiles[iY, iX].Draw(this._textureMapTiles, spriteBatch, offset);  
         }  
       }  
     }  
   }  
 }

In der Update Methode wird die Bewegung mit 2 Multipliziert. Wenn die Bewegung zu langsam erscheint, dann kann hier der Wert erhöht werden. Wie bereits angemerkt, zeigt dieser Teil das Nötigste, um eine Karte anzulegen und für das Rendern vorzubereiten.

Rendern
Am Ende muss die Karte auf dem Bildschirm gerendert werden. Dazu wird noch eine weitere Komponente angelegt mit dem Namen 'ComponentRender.cs'.




Damit die Karte auf die Start Position in die Mitte zentriert wird, wird die Größe des Fensters eingelesen und dessen Werte für den Offset der Karte zugewiesen.

using ExampleWalkingOnMap.Components.Map;  
 using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Graphics;  
 using Windows.UI.ViewManagement;  
 namespace ExampleWalkingOnMap.Components.Render  
 {  
   public class ComponentRender : DrawableGameComponent  
   {  
     private readonly ComponentMap _componentMap;  
     private SpriteBatch _spriteBatch;  
     public ComponentRender(Game game, ComponentMap componentMap) : base(game)  
     {  
       this._componentMap = componentMap;  
     }  
     public override void Initialize()  
     {  
       this._spriteBatch = new SpriteBatch(this.GraphicsDevice);  
       var screenWidth = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width;  
       var screenHeight = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;  
       this._componentMap.SetScreenOffset(screenWidth, screenHeight);  
     }  
     public override void Draw(GameTime gameTime)  
     {  
       this.GraphicsDevice.Clear(Color.DarkBlue);  
       this._spriteBatch.Begin();  
       this._componentMap.DrawMapTiles(this._spriteBatch);  
       this._spriteBatch.End();  
     }  
   }  
 }  

Zurück zum Start
Am Ende müssen die Komponenten in die GameComponentCollection hinzugefügt werden, damit die Inhalte ausgeführt werden. Dazu geht es wieder in die 'Game1.cs' Code Datei. Hier werden in den Member die Komponenten eingetragen und im Konstruktor wird die 'UpdateOrder' zugewiesen und schließlich in die Collection aufgenommen.

 using ExampleWalkingOnMap.Components.Inputs;  
 using ExampleWalkingOnMap.Components.Map;  
 using ExampleWalkingOnMap.Components.Render;  
 using Microsoft.Xna.Framework;  
 namespace ExampleWalkingOnMap  
 {  
   public class Game1 : Game  
   {  
     private GraphicsDeviceManager _graphics;  
     private ComponentInputs _componentInputs;  
     private ComponentMap _componentMap;  
     private ComponentRender _componentRender;  
     public Game1()  
     {  
       this._graphics = new GraphicsDeviceManager(this);  
       this.Content.RootDirectory = "Content";  
       this.Window.Title = "Example Walking on map";  
       this.IsMouseVisible = true;  
       this._componentInputs = new ComponentInputs(this);  
       this._componentInputs.UpdateOrder = 1;  
       this.Components.Add(this._componentInputs);  
       this._componentMap = new ComponentMap(this, this._componentInputs);  
       this._componentMap.UpdateOrder = 2;  
       this.Components.Add(this._componentMap);  
       this._componentRender = new ComponentRender(this, this._componentMap);  
       this._componentRender.UpdateOrder = 3;  
       this.Components.Add(this._componentRender);  
     }  
   }  
 }  

Wenn ich hier im Blog-Post keinen Code ausgelassen habe, dann sollte sich die Anwendung starten lassen und die Karte mit den Tasten AWSD sich bewegen lassen. Oder auch mit dem Xbox Controller.



Nachwort
Mit ein wenig mehr Code, kann man dann einige Unreinheiten beheben, wie z.B. die Übergänge von Kachel zu Kachel sehen nicht immer sauber aus oder das der Untergrund Einfluss auf die Laufgeschwindigkeit hat. Und Überhaupt fehlt die Laufende Figur in der Mitte.

Die Anwendung läuft soweit auch auf einem Raspberry Pi mit Windows 10 IoT. Jedoch sollte man keine hohen Performance erwarten, da hier keine DirectX Unterstützung vorhanden ist. Im Folgenden Video zeige ich die Anwendung  auf einem Bildschirm mit einer Eingestellten Auflösung von 640x350 Pixeln (Nativ sind jedoch 480x320). In HD, also 1920x1080p, sind weniger als fünf Bilder pro Sekunde zu erwarten. Zudem ist der Boot Vorgang relativ lang und bis die MonoGame App gestartet ist. Da vergehen schon einige Minuten.
Der Aufbau und das Prinzip funktioniert auch mit Windows Forms, WPF und XAML. Auch wenn diese UI Technologien nicht dafür gedacht sind, kann man sehen zumindest sehen wie Leistungsfähig die anderen sind.

Die fertige Solution habe ich wie immer auf dem GitHub geladen. Zusätzlich sind die meisten Inhalte kommentiert.




Mittwoch, 10. August 2011

Schwierigkeiten mit dem GMax Plugin MiddleMan v1.3beta

Nach einer langen Suche einer aktuellen Version vom Plugin zu finden blieb bisher erfolgslos. Dabei ist es gar nicht mal so schlecht, wenn da nicht dieser Bug ist. Offenbar habe ich diesen auch erst seit ich das auf Windows 7, GMax und das Plugin nutze. Nun es kommt der Fehler, wenn ich mein Model exportiere, um es als .X Format zu speichern, dann sind die Checkboxen für .X ausgeschaltet. Das ist aber dann nicht, wenn es zum ersten mal genutzt wird. Irgendwann stellte ich fest, das der Fehler dann erzeugt wird, wenn im Plugins Ordner, die Einstellungen speichert. Die Datei “mman.sys” lässt den Fehler erscheinen. Offensichtlich, wenn die Einstellungen aus der Datei geladen wird, wird beim laden, die eine oder andere Auswahl deaktiviert.

Lösung ist, die Datei “mman.sys” nach jedem Export zu löschen.

Leider reicht das nicht. Denn offensichtlich scheint das Plugin sich nicht selbst zu finden, nachdem es den Export durch geführt hat. Zwar wird die Exportierte Datei des 3D Modells erzeugt, wird aber nach dem Schließen des Plugins wieder entfernt. Ähhh… Sagt ich mir. Solange das Plugin offen ist, geht es ja. Die erzeugte .X Datei ist auch entsprechend beschrieben. Also was kann man hier machen.

Die Datei kopieren, bevor das Plugin geschlossen wird.

Eigentlich nicht so schön, wenn man vielleicht mehrere Modelle exportieren möchte, und im Grunde ist es auch lästig, diese Vorgänge durch zu führen.

Also habe ich kurz mal ein Programm geschrieben, dass genau diese Vorgänge durch führt. Es ist sehr Simpel auf gebaut. Kopiert das Programm .exe wohin ihr wollt, startet es, weist entsprechend die Ordner zu. Fertig.

Wenn die x.Datei erstellt wurde, also dann wenn ihr auf den Button Go geklickt habt, dann wird ein im Meshes Odner ein Unterordner erzeugt und dort die Sicherung mit dem selben Namen gespeichert.

Also wenn ihr GMax startet, dann startet dieses kleine Programm, dass ich GMaxPluginCleaner genannt habe.

Download GMaxPluginCleaner

Mittwoch, 3. August 2011

3D Model einbinden mit C# und XNA

Wer bereits Erfahrungen mit C# und Visual Studio hat, und schon immer ein kleines Programm schreiben wollte, in dem sein eigenes 3D Model sich dreht, der dürfte mit XNA 4.0, schnell zu einem Erfolg kommen. Mein Artikel beschreibt eine in Kurzfassung, was ich brauche und die Codezeilen. Warum, es gibt leider relative wenige Seiten die dieses beschreiben oder ich habe vielleicht nach den falschen Begriffen gesucht.
Eine lange Fassung findet ihr für gewöhnlich auf der MSDN oder AppHub Seite, in denen auch die einzelnen Schritten genauer erklärt werden, anhand von Tutorials.

Also, was benötige ich:

  • Visual Studio 2010 C# Express
  • XNA 4.0 SDK


Wir beginnen mit Visual Studio C# Express und legen ein neues Projekt an:
XNA_und_CSharp_01

Nach dem Erstellen, dürfte euch gleich die Hauptseite geöffnet vorliegen. Standard mäßig ist das hier die Game1.cs:

XNA_und_CSharp_02

Als erstes tragen wir die erforderlichen Members ein:

XNA_und_CSharp_03

Nun zum Constructor “Game1()”. Da benötigen wir nur den TimerSpan einrichten.
XNA_und_CSharp_04

In unseren LoadContent() wird unser Model im .X Format geladen und wir legen das Seitenverhältnis fest.

XNA_und_CSharp_05

Über “Update(GameTime gameTime)” können wir Veränderungen eingeben, die zur Laufzeit gerendert werden soll. Hier verändern wir die Rotation des Objektes.

XNA_und_CSharp_06

So, letzte Methode ist “Draw(GameTime gameTime)”. Mit der foreach Schleife werden die Parameter festgelegt, die später gerendert wird pro Durchgang.

XNA_und_CSharp_07

Zu guter Letzt, darf das Model nicht fehlen. Einfach in den WindowsGame1Content hinzufügen.

XNA_und_CSharp_08

Fertig. Drücke dann F5 und wir können uns freuen.

XNA_und_CSharp_09


So, dazu muss ich noch hinzufügen, dass leider mein Model nicht ganz richtig dargestellt wird. Ein Teil des Rumpfes wird von der Innenseite gerendert. Aber vielleicht erkennt es der eine oder andere ja, aus welchen Spiel ich das Model nach gebaut habe als Low Polygone Model Zwinkerndes Smiley.

Gehäuseentwurf für Signalleuchten (Part 2)

Heute morgen sah ich mir mein Druckergebnis an und dachte: Nein, das sieht nicht nach einem Kristall aus. Also im Internet gesucht und ein T...