Dienstag, 27. August 2019

Gästebuch mit Wemos


Als ich mit meiner Freundin zum Geocaching unterwegs war, fiel mir ein das ich ein Programm schreiben wollte, das die Funktion eines Gästebuch ausführt und das auf einem Mikrocontroller. Wer Geocaching kennt, der weis dass man nach einem gefundenen Geocach sich auf einem beiliegenden Zettel eintragen kann. Und genau das will ich als digitale Form abbilden.

Was brauche ich:
  • Wemos
  • SD Shield
  • SD Karte

Anforderung
Für ein Gästebuch, soll ein (Nick)Name und Datum hinterlegt werden. Datum wird Automatisch mit dem Eintrag gesetzt, so das nur der Nickname eingetragen wird. Hier sollte der Wemos als eigenständiger Access-Point funktionieren, so das man sich mit dem Smartphone verbinden kann. Auf der Webseite sollten zwei Textboxen für die Eingabe des Namens und Datum vorhanden sein. Das Speichern erfolgt dann über ein Button. Neben der Eintrage Möglichkeit, soll auch eine Tabelle mit bereits bestehenden Einträgen zusehen sein. Und natürlich sollte der Zugang per Smartphone funktionieren.

Webseite schreiben
Leider kann die Webseite nicht mit Bootstrap aufgebaut werden. Na ja, eigentlich schon, aber das wäre ein eigenes Thema. Diesmal wird die Seite wieder Klassisch und einfach aufgebaut nach Standard HTML Bordmitteln.
Die Webseite wird mit in dem Programmcode eingebettet, so das diese nicht von der SD Karte erst gelesen werden muss. Zudem sollte bei defekter SD Karte trotzdem etwas angezeigt werden, wie eine Fehlermeldung.
Da die Darstellung des Code mit HTML auf dem Blog sich schwierig gestaltet, kann der Programmcode für den Seitenaufbau auf dem Github Repository betrachtet werden [Link zu WebsiteContent].
Die Seite habe ich zuvor ganz normal in HTML geschrieben und das Ergebnis im Browser immer wieder betrachtet. Erst danach kam die Seite in den Programmcode, das jedoch optisch grausig aussieht. Daher habe ich den Teil in einen eigenen Tab bzw. Code-Datei verfrachtet. Am Ende kam fast das selbe Ergebnis wie in den Folgendem Bild zu sehen ist.



Das Aktuelle Datum wird durch ComboBoxen ermöglicht, in dem man das aktuelle Datum und Uhrzeit einrichten kann. Das testen auf dem Windows Phone erfolgt ohne Probleme. Auf dem Iphone jedoch nicht.








SD Karte prüfen
Mit dem Start soll zunähst geprüft werden, ob die SD Karte verfügbar ist. Zuerst wird jedoch geprüft, ob bereits die Datei mit den Gästeeinträgen vorhanden ist. Wenn nicht, dann wird diese angelegt.

 void InitSdCard() {  
  SD.begin(mChipSelect);  
  if(!SD.exists(mGuestList)){  
   Serial.println("Create file...");  
   File dataFile = SD.open(mGuestList, FILE_WRITE);  
   dataFile.close();  
   Serial.println("...finish");  
  }  
  else {  
   Serial.println("the guest list exist");  
  }  
  if(!mCard.init(SPI_HALF_SPEED, mChipSelect)) {     // initialize sd card and check insert sd card  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   Serial.println("\t\tERROR: can not init sd card");  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   return;  
  }  
  if(!mVolume.init(mCard)) {               // initialize sd card  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   Serial.println("\t\tERROR: Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");  
   Serial.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");  
   return;  
  }  
  Serial.println("SD Card: Exist and partion was found");  
 } 

Wemos zum Access Point machen
Auf der Webseite von Wemos ist es eigentlich schon beschrieben, aber zur Vollständigkeit, nehme ich das hier denoch auf. Um den Wemos zum Access Point zu machen, bedarf dies nur weniger Code zeilen. In den Globalen Variablen muss dann nur die Instanz vom WiFiServer gehalten werden. Für den Zugang wird kein Passwort festgelegt.

 void StartWebserver() {  
  Serial.println("Start access point");  
  WiFi.mode(WIFI_AP);            
  byte setMode = 0;               // 0 = disable, 1 = enable router information  
  wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &setMode);  
  WiFi.softAP("VisitorsBook", "");  
  mServer.begin();                // Start the HTTP Server  
  Serial.print("Ip Address: ");  
  Serial.println(WiFi.softAPIP());// Send the IP address of the ESP8266 to the computer  
 } 

Aufruf der Seite
Mit dem Aufruf der Seite wird zunächst geprüft, ob der Benutzer einen Request sendet. Wenn nicht, wird die Methode zum Speichern umgangen. Den Request nach dem Inhalt zuprüfen, sieht hier etwas kryptisch aus, das jedoch nur schlimm aussieht. Beim auslesen des Datums wird ein Doppelpunkt mit '%3A' ausgefüllt, der sich jedoch einfach mit einem Replace wieder in einen Doppelpunkt gesetzt werden kann. Nach dem aufbereiten des Namens und Datum kann das Ergebnis gespeichert werden.

void printOnWebside() {  
  WiFiClient webclient = mServer.available();  
  if (!webclient) {  
   return;  
  }  
  String request = webclient.readStringUntil('\r');  
  webclient.flush();  
  int indexGuest = request.indexOf("guestname=");  
  if(indexGuest != -1) {  
   int indexEndGuestname = request.indexOf('&');  
   String guestName = request.substring(indexGuest + 10, indexEndGuestname);  
   int indexSetTime = request.indexOf("settime=");  
   int indexEndSettime = request.indexOf(" HTTP");  
   String settime = request.substring(indexSetTime + 8, indexEndSettime);  
   // Example: 2019-08-25T19%3A40  
   // filter technical colon  
   settime.replace("T", " ");  
   settime.replace("%3A", ":");  
   WriteGuest(guestName, settime);  
  }  
  BuildWebside(webclient);  
 } 

Datenspeichern
Das Speichern erfolgt Zeilenweise. Hier wird der Eintrag mit einem Zeilenumbruch in die Datei hinzugefügt. Zuerst hatte ich nur Name und Datum, aber das Parsen wäre relativ aufwendiger geworden. Daher wird die Zeile bereits mit dem Ziel HTML Format eingetragen. (Wegen HTML Inhalt ist der folgende Code nur ein Bild)



















Daten abrufen
Ist etwas lang die Methode. Aber auch nur, weil wieder etwas HTML mit eingeflossen ist, um die Anzahl der gespeicherten Einträge wieder zugeben. Anschließend werden die Gast Einträge aus der Datei Zeilenweise mit dem bereits HTML Anteil abgerufen und hinzugefügt.































Offene Punkte
Das Hübsch machen der Webseite habe ich weggelassen, sowie das Fehlerbehandlung. Denn ein Eintrag kann ohne Namen und Datum erfolgen und führt dazu, dass der Eintrag in der Liste wirklich leer sein kann. Zudem war auch ein RTC Timer angedacht, der jedoch für den ersten Schritt nicht benötigt wird. Im Main Branch habe ich diesen Teil stehen gelassen.
Und dann noch der Fehler, dass das Datum nicht geht, wenn versucht wird vom Iphone ein Request zu senden.


Datenschutz
Die Anmeldung erfolgt anonym, weil hier keine Personenbezogenen Daten erhoben werden. Um genau zu sein, dürfen hier keine Personendaten erhoben werden, wie Email (Macht in diesen Kontext auch kein Sinn), Namen und Geburtsdatum. Für den Eintrag reicht dann der Hinweis:

Mit meinem Eintrag ins Gästebuch erkläre ich mich einverstanden, dass dieser Beitrag mit Datum und Uhrzeit auf der Gästebuch-Seite hier Lokal öffentlich lesbar wird.

Weitere Informationen findet ihr auf der Seite zu "Datenschutz auf Webseiten"

Wie immer das ganze Beispiel auf meinem Github Repository


Mittwoch, 7. August 2019

Control Style Template mit gleichen Inhalt und unterschieden



Manchmal sind Lösungen so einfach, dass man sie in der Komplexität nicht mehr sieht. So hatte ich tatsächlich eine Menge XAML Code schreiben müssen, die sich praktisch immer wiederholten. Die Premisse war, dass eigentlich nur ein Wert anders war im Template. Die beschriebenen Trigger und Style Aufbauten im Template sind in den meisten fällen identisch für ein Steuerelement. Eine andere Lösung wäre den Aufbau per Behind Code zu schreiben, aber das war tatsächlich die aufwendigere Lösung. Aufwendig, weil mehr code entstehen würde als nötigt.

Umgebung
  • WPF, UWP oder XAML Anwendung

Verschieden und doch gleich
Beide Buttons, sollen die gleichen überschriebenen Animationen erhalten. Der Butten bekommt ein Path Objekt, um die Darstellung für jeden Button zu individualisieren. Dabei soll kein Redundanter Code entstehen.

Los geht’s
Der Ordnung halber, wird der Style in eine eigene Resourcendatei geschrieben. Falls Ihr ein Xaml Path Objekt braucht, gibt es auf dem folgenden Link eine Seite mit vielen verschienden Icons.


Ausgangslage
In diesem Beispiel verwende ich das File-Icon und ein Folder-Icon. Im folgenden XAML wird zunächst gezeigt, wie zwei Styles die fast identisch sind und sich nur in dem eingesetzten Path Control unterscheiden.
Der XAML Auszug kommt aus 'ButtonWithRepeadTemplate.xaml' (Leider lies sich das nicht vernüfitg hier im Blog abbilden, mit ausnahme eines Bildes). Die orange markierten Zeilen, sind die Stellen die sich unterscheiden und grün sind die Teile, dessen Inhalt gleich sind. (Ellipse habe ich nicht mit einbezogen)


Die Lösung ist nahezu offensichtlich, dass dieser Aufbau selbst als Basis Style festegelgt werden kann. Da es sich um einen Wert handelt, kann über die nicht verwendete DependencyProperty 'Content' später die eigentliche Zieländerungen eingesetzt werden. Das sieht dann im folgendem XAML Code aus.
BLAU: Da kann ein Path Element gesetzt werden mit einer Default Zeichnung.
GRÜN: Der Teil, der später nicht ein zweites mal geschrieben werden muss. Wobei hier angemerkt werden muss, dass auch der Teil in der 'Viewbox' nur in der Basis besteht.


Kommen wir zu den Ziel Button Styles, die als Basis Style (BaseOn) von 'ButtonTemplateBase' verwenden. Durch zuweisung eines Path Element in Content, wird das Aussehen in form eines Icon festgelegt. Also der Teil, der wirklich sich unterscheidet.


Ausprobieren
Mit diesen neunen Button Styles geht das Zuweisen auf der Windows.xaml (oder andere View) weiter, um dort die Resource einzubetten und dessen Styles auf die Ziel Buttons festzulegen.


Fertig
Und so sollte das Ergebnis aussen. Beide Buttons sollte die gleichen Trigger Effekte haben.



Zum Schluss
Wie ich bereits am Anfang geschrieben habe, ist die Lösung simpel und sehr überschaubar. Selbst wenn deutlich mehr Wpf Steuerelemente mit dem gleichen Style Grundaufbau kommen, bleibt die Übersicht. 
Aber wie sieht es aus, wenn sich zwei Inhalte im Template unterscheiden? Kurz gesagt, das geht. Aber dazu gehe ich in einem späteren Blogpost darauf ein.

Sonntag, 21. Juli 2019

Daten in JSON speichern auf dem Wemos


Um seine Daten auf eine SD Karte zu speichern, muss am Anfang klar sein, in welches Format gespeichert werden soll. Das ist dann besonders entscheidend, wenn die Daten wieder weiter verarbeitet werden.
Wenn für den Moment nur ein Wert gespeichert wird, dann reicht Zeilenweise. Bei mehreren Werten müssen die durch ein Zeichen getrennt werden. Das kann dann über Leerzeichen, Semikolon oder Komma sein, bzw. CSV entsprechend. CSV kann prima in Excel betrachtet und weiter verarbeitet werden. Will man die Daten aber weiter verarbeiten, müssen am Ende viele Zeichenketten zu Werten umgewandelt werden. Grundsätzlich ist das nicht verkehrt, aber man muss sich selbst Standards festlegen, um Aufwände gering zu halten.

Was benötigt wird:
  • Arduino oder Wemos
  • SD Shield
  • SD Karte

Vorhaben
Für das Ziel Projekt werden einige Werte aus den Messungen gespeichert. Daher wird ein Daten Objekt benötigt, dass alle Werte enthält. Um dieses nicht im Arbeitsspeicher zu halten, muss der Inhalt persistiert werden und später auf Abruf wieder hergestellt werden.

Datenformat zum Speichern
Die Entscheidung war für mich schon ein eigenes Thema Wert. Neben CSV blicke ich seit langem wieder auf JSON und XML. Nachdem ich ein wenig gesucht habe, kam ich zu dem Entschluss JSON zu verwenden. Für das JSON Format existiert bereits eine fertige Bibliothek.



Was man beachten sollte
Nach einigen Tutorials und veralteten Beispielen, hatte ich die nötigen Informationen zusammen. Zum Schreiben und lesen der Daten können zwei verschiedene Klassen zum Verarbeiten von JSON verwendet werden.
  • StaticJsonDocument
  • DynamicJsonDocument

Für meinen Einsatz reicht der StaticJsonDocument aus. Dieser legt die Daten im Stack ab und verbraucht durch die Eingrenzung auch weniger Arbeitsspeicher. Das ist bedingt durch den Mikrocontroller, vorzugsweise zu verwenden.
Mit DynamicJsonDocument werden die Daten im Heap abgesetzt. Für mein Beispiel konnte ich evaluieren, das nur zwölf Datensätze zuverlässig verarbeitet werden, ohne dass das Programm anhält bzw. abstürzt.

Datenobjekt
Der Zeitpunkt der Aufnahme und zu speichernden Werte werden für den späteren verlauf verwendet. Sekunden habe ich hier nicht einbezogen, da solche Zeitlich genauen Messungen für die Darstellung keinen Nutzen hat. Die Stunden und Minuten werden wiederum verwendet, um später Durchschnittswerte zu berechnen. Der Tag, Monat und Jahr werden ebenfalls für die Durchschnittswerte verwendet.

 class MeasureData {  
  public:  
   int16_t Day;  
   int16_t Month;  
   int16_t Year;  
   int16_t Hour;  
   int16_t Minute;  
   float Temperature;  
   float Humidity;  
   float Pressure;  
 };  


Speichern (Serialisieren)
Bevor die Daten in das JsonObject geschrieben werden können, muss die Größe definiert werden. Im folgenden Programmcode in "JSON_ARRAY_SIZE" der Wert 1 eingetragen, weil hier nur einmal gespeichert wird. Im Github Repository zeige ich dieses mit mehreren Datensätzen.

 void SaveData() {  
  const size_t capacity = JSON_ARRAY_SIZE(1) * JSON_OBJECT_SIZE(8);        
  StaticJsonDocument jsonBuffer;     // set the content size  
  JsonArray data = jsonBuffer.to();   // setup array to write  
  JsonObject measure = data.createNestedObject();  // created to add a new object  
  measure["Day"] = 12;  
  measure["Month"] = 12;  
  measure["Year"] = 2019;  
  measure["Hour"] = 12;  
  measure["Minute"] = 12;  
  measure["Temperature"] = 21.4f;  
  measure["Humidity"] = 50.5f;  
  measure["Pressure"] = 12345.6f;  
  File file = SD.open(mFilename, FILE_WRITE);  
  serializeJson(jsonBuffer, file);       // write json document to file  
  file.close();   
 } 

Öffnen (Deserialisierung)
Genau wie beim Speichern, wird ebenfalls die Kapazität angegeben. Dann kann die eingelesene Datei aus dem JSON Inhalt eingelesen und als JsonObject abgerufen werden.

 void OpenData() {  
  File file = SD.open(mFilename);  
  String content = file.readString();  
  const size_t capacity = JSON_ARRAY_SIZE(1) * JSON_OBJECT_SIZE(8);        
  StaticJsonDocument doc;  
  deserializeJson(doc, content);  
  JsonObject measure = doc[0];  
  Serial.print(" Day:"); Serial.print((int16_t)measure["Day"], DEC);  
  Serial.print(" Month:"); Serial.print((int16_t)measure["Month"], DEC);  
  Serial.print(" Year:"); Serial.print((int16_t)measure["Year"], DEC);  
  Serial.print(" Hour:"); Serial.print((int16_t)measure["Hour"], DEC);  
  Serial.print(" Minute:"); Serial.print((int16_t)measure["Minute"], DEC);  
  Serial.print(" Temperatur:"); Serial.print((float)measure["Temperature"], DEC);  
  Serial.print(" Humidity:"); Serial.print((float)measure["Humidity"], DEC);  
  Serial.print(" Pressure:"); Serial.println((float)measure["Pressure"], DEC);  
 }  


Fazit
Das JSON Format kann ich jeden nahe legen, dieses zu verwenden. Sobald mehrere Daten erfasst und gespeichert werden, ist der Aufwand beim Einlesen nahezu gleich. Das mag anfangs etwas ungewohnt sein. Spätestens wenn ihr Daten aus einer CSV einlesen wollt, das innerhalb des Mikrocontroller Bereiches geschieht, spart ihr eine Menge Zeit für das Entwickeln einer Methode, dass die Daten einließt.

Wie immer ein Beispiel:

Sonntag, 17. März 2019

Wellenschlagen (MonoGame)



Normalerweise sind 3D Objekte die Importiert wurden oder programmatisch erstellt wurden sehr statisch. Man kann zwar das 3d Objekt per Transformation bewegen, aber wenn man aus dem Objekt einzelnen Polygone bewegen will, dann sieht das ganze schon etwas schwierig aus.
Vor Jahren hatte ich mal ein PDF Dokument erhalten, das die Animationen eines 3D Models beschrieb. Leider bin ich nie dazugekommen, dies auszuprobieren und mit der Zeit ist auch das Dokument auch verloren gegangen.
Die folgende Beschreibung ist sicherlich für Performante Ausführungen nicht geeignet und ist daher nur für weniger Aufwendige Anwendungen gedacht. Wenn man nicht auf alternativen und kompromisse eingeht.

Grundaufbau
Für dieses Beispiel verwende ich einen Auszug von meinem bereits erstellten Beispiel Projekt 'ExampleMoveOnMap3d' und habe dazu einen neuen Branch mit dem Namen Waterwaves erstellt. In diesen Beispiel werden diesmal keine 3d Objekte importiert, sondern hier wird komplett eine Fläche erzeugt, auf die eine Textur zugewiesen wird.

Die neue Klasse AnimatedWaterwaves wird in den Ordner Map eingesetzt.



Parameter Angaben
Die ersten Members legen die Einstellungen der Fläche fest. Das gewählte Raster 20 mal 20 sollte auf den meisten derzeitigen Rechner ohne weiteres flüssig darstellbar sein. Die sich daraus ergebenen 1600 Polygone sind nicht viel, aber werden vor jedem Rendern neu erzeugt und das kostet Rechenzeit. Aber dazu später mehr.
Die Member Variablen _waveStartX und _waveStartY, legen den aktuellen Startwert beim erzeugen der Wellen fest. Die restlichen Member werden allgemein benötigt, um die Fläche zu halten.
Die üblichen Methoden wie Initialize, Draw und Update sind hier weniger Interessant. Dafür ist die Erzeugung der Wellen ansprechender. Die Methode 'RegenerateVertexBuffer' ist relative lang, was dem geschuldet ist, das ich den Hauptteil in der doppelten Schleife nicht ausgelagert habe.

public class AnimatedWaterwaves  
   {  
     private readonly int _tilesX = 20;  
     private readonly int _tilesY = 20;  
     private readonly float _squareLength = 1f;  
     private float _waveStartX = 0f;  
     private float _waveStartY = 0f;  
     private VertexBuffer _vertexBuffer;  
     private IndexBuffer _indexBuffer;  
     private int _vertexCount;  
     private int _indexCount;  
     private BasicEffect _effect;  
     private readonly Texture2D _texture;  
     private List _vertexPositions;  
     public AnimatedWaterwaves(Texture2D texture)  
     {  
       this._texture = texture;  
     }  
     public void Initialize(GraphicsDevice graphicsDevice)  
     {  
       this._effect = new BasicEffect(graphicsDevice);  
       this._effect.World = Matrix.Identity;  
       this._effect.TextureEnabled = true;  
       this._effect.Texture = this._texture;  
       this._effect.EnableDefaultLighting();  
       this._vertexPositions = this.RegenerateVertexBuffer(graphicsDevice);  
     }  
     private List RegenerateVertexBuffer(GraphicsDevice graphicsDevice)  
     {  
       this.ClearBuffers();  
       this._waveStartX += .05f;  
       this._waveStartY += .2f;  
       List vertices = new List();  
       List index = new List();  
       float setHeight = 1.4f;  
       float waveLengthX = 10f;  
       float waveLengthY = 16f;  
       float aa = 5f / this._tilesY;  
       for (int iY = 0; iY < this._tilesY; iY++)  
       {  
         for (int iX = 0; iX < this._tilesX; iX++)  
         {  
           this.SetIndex(ref index, vertices.Count);  
           float waveX = iX + this._waveStartX;  
           float waveY = iY + this._waveStartY;  
           float heightY = (float)Math.Sin(((waveY + 1) * Math.PI) / waveLengthY);  
           float heigthX1 = (float)Math.Sin((float)(waveX * Math.PI) / waveLengthX);  
           float height1 = (heigthX1 + heightY) * setHeight;  
           float heightY2 = (float)Math.Sin(((waveY + 1) * Math.PI) / waveLengthY);  
           float heigthX2 = (float)Math.Sin((float)((waveX + 1) * Math.PI) / waveLengthX);  
           float height2 = (heigthX2 + heightY2) * setHeight;  
           float heightY3 = (float)Math.Sin((waveY * Math.PI) / waveLengthY);  
           float heigthX3 = (float)Math.Sin((float)(waveX * Math.PI) / waveLengthX);  
           float height3 = (heigthX3 + heightY3) * setHeight;  
           float heightY4 = (float)Math.Sin((waveY * Math.PI) / waveLengthY);  
           float heigthX4 = (float)Math.Sin((float)((waveX + 1) * Math.PI) / waveLengthX);  
           float height4 = (heigthX4 + heightY4) * setHeight;  
           float aaX = aa * iX;  
           float aaY = aa * iY;  
           float aaX1 = (aa * iX) + aa;  
           float aaY1 = (aa * iY) + aa;  
           if (aaX > 1f)  
           {  
             aaX = aaX % 1f;  
             aaX1 = aaX1 % 1f;  
           }  
           if (aaY > 1f)  
           {  
             aaY = aaY % 1f;  
             aaY1 = aaY1 % 1f;  
           }  
           var verticePos1 = new Vector3((iX * this._squareLength) + 0, (iY * this._squareLength) + this._squareLength, height1);  
           var verticePos2 = new Vector3((iX * this._squareLength) + this._squareLength, (iY * this._squareLength) + this._squareLength, height2);  
           var verticePos3 = new Vector3((iX * this._squareLength) + 0, (iY * this._squareLength) + 0, height3);  
           var verticePos4 = new Vector3((iX * this._squareLength) + this._squareLength, (iY * this._squareLength) + 0, height4);  
           vertices.Add(new VertexPositionNormalTexture(verticePos1, Vector3.Up, new Vector2(aaX, aaY1)));  
           vertices.Add(new VertexPositionNormalTexture(verticePos2, Vector3.Up, new Vector2(aaX1, aaY1)));  
           vertices.Add(new VertexPositionNormalTexture(verticePos3, Vector3.Up, new Vector2(aaX, aaY)));  
           vertices.Add(new VertexPositionNormalTexture(verticePos4, Vector3.Up, new Vector2(aaX1, aaY)));  
         }  
       }  
       this._vertexCount = vertices.Count;  
       this._indexCount = index.Count;  
       this.InitVertexBuffer(graphicsDevice, vertices);  
       this._indexBuffer.SetData(index.ToArray());  
       return vertices;  
     }  
     public void Update(GraphicsDevice graphicsDevice)  
     {  
       this._vertexPositions = this.RegenerateVertexBuffer(graphicsDevice);  
     }  
     internal void Draw(GraphicsDevice graphicsDevice, Matrix view, Matrix projection)  
     {  
       this._effect.View = view;  
       this._effect.Projection = projection;  
       graphicsDevice.SetVertexBuffer(this._vertexBuffer);  
       graphicsDevice.Indices = this._indexBuffer;  
       foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)  
       {  
         pass.Apply();  
         graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, this._vertexCount);  
       }  
     }  
     private void InitVertexBuffer(GraphicsDevice graphicsDevice, List vertices)  
     {  
       this._vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionNormalTexture.VertexDeclaration, this._vertexCount, BufferUsage.WriteOnly);  
       this._vertexBuffer.SetData(vertices.ToArray());  
       this._indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.ThirtyTwoBits, this._indexCount, BufferUsage.WriteOnly);  
     }  
     private void ClearBuffers()  
     {  
       if (this._vertexBuffer != null)  
       {  
         this._vertexBuffer.Dispose();  
         this._vertexBuffer = null;  
       }  
       if (this._indexBuffer != null)  
       {  
         this._indexBuffer.Dispose();  
         this._indexBuffer = null;  
       }  
     }  
     private void SetIndex(ref List index, int localOffset)  
     {  
       index.Add(localOffset + 0);  
       index.Add(localOffset + 1);  
       index.Add(localOffset + 3);  
       index.Add(localOffset + 0);  
       index.Add(localOffset + 3);  
       index.Add(localOffset + 2);  
     }  
   }

Nun muß noch in ComponentMap mit der neuen Klasse umgestellt werden. Dort wo die 3d Objekte geladen wurden, wird nun die AnimatedWaterwaves instanziiert und initialisiert.

public class ComponentMap : GameComponent  
   {  
     private const float _speed = 0.3f;  
     private readonly ComponentInputs _componentInputs;  
     private AnimatedWaterwaves _animatedWaterwaves;  
     public ComponentMap(Game game, ComponentInputs componentInputs) : base(game)  
     {  
       this._componentInputs = componentInputs;  
     }  
     public override void Initialize()  
     {  
       var texture = this.Game.Content.Load("rpgTile029");  
       this._animatedWaterwaves = new AnimatedWaterwaves(texture);  
       this._animatedWaterwaves.Initialize(this.Game.GraphicsDevice);  
     }  
     public override void Update(GameTime gameTime)  
     {  
       RasterizerState raster = new RasterizerState();  
       raster.CullMode = CullMode.CullCounterClockwiseFace;  
       raster.FillMode = FillMode.WireFrame;  
       this.Game.GraphicsDevice.RasterizerState = raster;  

       this._animatedWaterwaves.Update(this.Game.GraphicsDevice);  
     }  
     public void DrawContent(Matrix view, Matrix projection)  
     {  
       this.Game.GraphicsDevice.DepthStencilState = DepthStencilState.Default;  

       this._animatedWaterwaves.Draw(this.Game.GraphicsDevice, view, projection);  
     }  
   }  

Ausprobieren
Auf dem PC sollte nun nach dem Start die Animation flüssig abgespielt werden. Vergrößert man die Fläche und gibt mehr Polygone hinzu, dann wird irgendwann der Rechner in die Knie gehen. Wie immer testete ich dies auch auf dem  Raspberry Pi, das auf dem folgenden Video zu sehen ist.


Gleich verbessern
Ehrlich gesagt, hat mich das dann doch gestört. Also schrieb ich am Beispiel weiter und auch weil ich bereits eine Lösung im Kopf hatte, die ich ausprobieren wollte. Stellt man die Variablen _tilesX und _tilesY auf 100, dann kommen wir hier auf 400.000 Polygone, aber die Animation ruckelt.

  • Das Problem ist das ständig neu erzeugen der Wellen. 
  • Die Lösung hierfür wäre die Wellenanimation vorher zu generieren und dann die fertige Wellen abzuspielen.


Gitter Erstellen auslagern
Als nächstes wird ein Teil aus der Klasse AnimatedWaterwaves in eine neue Klasse ausgelagert und zwar der Bestandteil für die Polygonen. In diesen Zug wird dann auch erstmal aufgeräumt. Alles was doppelt erscheint wird zu einer Methode zusammengezogen. Damit wird auch die Hauptmethode für das erstellen der Fläche etwas kleiner. Einstellungen werden als const oder einfach als readonly Member angelegt. Der Konstruktor wird nicht gebraucht, auch wenn man den Teil in Initialize, dort hätte einsetzen können.

public class BufferedWave  
 {  
   private float _waveStepX = .05f;  
   private float _waveStepY = .2f;  
   private readonly int _tilesX = 20;  
   private readonly int _tilesY = 20;  
   private readonly float _squareLength = 1f;  
   private float _textureSize;  
   public VertexBuffer VertexBuffer;  
   public IndexBuffer IndexBuffer;  
   public int VertexCount;  
   private int _indexCount;  
   public List VertexPositions;  
   public const float WaveHeight = 1.4f;  
   public const int WaveLengthX = 10;  
   public const int WaveLengthY = 10;  
   public void Initialize(int step, GraphicsDevice graphicsDevice)  
   {  
     this._textureSize = 5f / this._tilesY;  
     this.VertexPositions = this.GenerateVertexBuffer(step, graphicsDevice);  
   }  
   private List GenerateVertexBuffer(int step, GraphicsDevice graphicsDevice)  
   {  
     this.ClearBuffers();  
     var waveStartX = this._waveStepX * step;  
     var waveStartY = this._waveStepY * step;  
     List vertices = new List();  
     List index = new List();  
     for (int iY = 0; iY < this._tilesY; iY++)  
     {  
       for (int iX = 0; iX < this._tilesX; iX++)  
       {  
         this.SetIndex(ref index, vertices.Count);  
         float waveX = iX + waveStartX;  
         float waveY = iY + waveStartY;  
         float height1 = this.GetHeight(waveX, waveY, 0, 1);  
         float height2 = this.GetHeight(waveX, waveY, 1, 1);  
         float height3 = this.GetHeight(waveX, waveY);  
         float height4 = this.GetHeight(waveX, waveY, 1, 0);  
         var verticePos1 = new Vector3((iX * this._squareLength), (iY * this._squareLength) + this._squareLength, height1);  
         var verticePos2 = new Vector3((iX * this._squareLength) + this._squareLength, (iY * this._squareLength) + this._squareLength, height2);  
         var verticePos3 = new Vector3((iX * this._squareLength), (iY * this._squareLength), height3);  
         var verticePos4 = new Vector3((iX * this._squareLength) + this._squareLength, (iY * this._squareLength), height4);  
         float aaX = this.GetTextureCoordinate(iX);  
         float aaY = this.GetTextureCoordinate(iY);  
         float aaX1 = this.GetTextureCoordinate(iX) + this._textureSize;  
         float aaY1 = this.GetTextureCoordinate(iY) + this._textureSize;  
         vertices.Add(new VertexPositionNormalTexture(verticePos1, Vector3.Up, new Vector2(aaX, aaY1)));  
         vertices.Add(new VertexPositionNormalTexture(verticePos2, Vector3.Up, new Vector2(aaX1, aaY1)));  
         vertices.Add(new VertexPositionNormalTexture(verticePos3, Vector3.Up, new Vector2(aaX, aaY)));  
         vertices.Add(new VertexPositionNormalTexture(verticePos4, Vector3.Up, new Vector2(aaX1, aaY)));  
       }  
     }  
     this.VertexCount = vertices.Count;  
     this._indexCount = index.Count;  
     this.InitVertexBuffer(graphicsDevice, vertices);  
     this.IndexBuffer.SetData(index.ToArray());  
     return vertices;  
   }  
   private float GetHeight(double waveX, double waveY, int x = 0, int y = 0)  
   {  
     float heightY = (float)Math.Sin(((waveY + y) * Math.PI) / WaveLengthY);  
     float heigthX1 = (float)Math.Sin((float)((waveX + x) * Math.PI) / WaveLengthX);  
     return (heigthX1 + heightY) * WaveHeight;  
   }  
   private float GetTextureCoordinate(int index)  
   {  
     var textureCoordinate = this._textureSize * index;  
     if (textureCoordinate >= 1f)  
     {  
       textureCoordinate %= 1f;  
     }  
     return textureCoordinate;  
   }  
   private void InitVertexBuffer(GraphicsDevice graphicsDevice, List vertices)  
   {  
     this.VertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionNormalTexture.VertexDeclaration, this.VertexCount, BufferUsage.WriteOnly);  
     this.VertexBuffer.SetData(vertices.ToArray());  
     this.IndexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.ThirtyTwoBits, this._indexCount, BufferUsage.WriteOnly);  
   }  
   private void ClearBuffers()  
   {  
     if (this.VertexBuffer != null)  
     {  
       this.VertexBuffer.Dispose();  
       this.VertexBuffer = null;  
     }  
     if (this.IndexBuffer != null)  
     {  
       this.IndexBuffer.Dispose();  
       this.IndexBuffer = null;  
     }  
   }  
   private void SetIndex(ref List index, int localOffset)  
   {  
     index.Add(localOffset + 0);  
     index.Add(localOffset + 1);  
     index.Add(localOffset + 3);  
     index.Add(localOffset + 0);  
     index.Add(localOffset + 3);  
     index.Add(localOffset + 2);  
   }   
}

In der AnimatedWaterwaves bleiben in den Member die Textur und der BasicEffect. Neu ist das Dictionary das später die alle erstellten schritte der Welle beinhaltet. Der index wird in der Update Methode hochgezählt oder zurück gesetzt, wenn das ende des Index Bereiches erreicht ist.
Mit der Methode Initialize werden nun alle schritte generiert. Der BufferIndexSize von 1600 ist mehr zufällig geschätzt, der genau paßt um die Animation in der Schleife abzubilden. Am Ende wird in der Methode Draw noch die bereits angelegten Vertex Werte zugewiesen, nach dem diese per Index Wert aus dem Dictionary abgerufen werden. In den Parametern kommt noch eine offsetPosition und ein offsetIndex rein und ermöglicht den Inhalt zu rendern, der gerade gebraucht wird.

public class AnimatedWaterwavesBuffered  
 {  
   private BasicEffect _effect;  
   private readonly Texture2D _texture;  
   private Dictionary _dictionaryVertexPositions = new Dictionary();  
   public int Index { get; private set; } = 0;  
   public AnimatedWaterwavesBuffered(Texture2D texture)  
   {  
     this._texture = texture;  
   }  
   public void Initialize(GraphicsDevice graphicsDevice)  
   {  
     this._effect = new BasicEffect(graphicsDevice);  
     this._effect.World = Matrix.Identity;  
     this._effect.TextureEnabled = true;  
     this._effect.Texture = this._texture;  
     this._effect.EnableDefaultLighting();  
     // is estimated  
     int bufferIndexSize = 1600;  
     for (int i = 0; i < bufferIndexSize; i++)  
     {  
       BufferedWave bufferedWave = new BufferedWave();  
       bufferedWave.Initialize(i, graphicsDevice);  
       this._dictionaryVertexPositions.Add(i, bufferedWave);  
     }  
   }  
   public void Update()  
   {  
     if (this.Index >= this._dictionaryVertexPositions.Count - 1)  
     {  
       this.Index = 0;  
     }  
     else  
     {  
       this.Index++;  
     }  
   }  
   internal void Draw(GraphicsDevice graphicsDevice, Matrix view, Matrix projection, Vector3 offsetPosition, int offsetIndex)  
   {  
     this._effect.View = view;  
     this._effect.Projection = projection;  
     var index = this.GetIndex(this.Index + offsetIndex);  
     var item = this._dictionaryVertexPositions[index];  
     graphicsDevice.SetVertexBuffer(item.VertexBuffer);  
     graphicsDevice.Indices = item.IndexBuffer;  
     this._effect.World = Matrix.CreateTranslation(offsetPosition);  
     foreach (EffectPass pass in this._effect.CurrentTechnique.Passes)  
     {  
       pass.Apply();  
       graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, item.VertexCount);  
     }  
   }  
   private int GetIndex(int index)  
   {  
     if (index >= this._dictionaryVertexPositions.Count)  
     {  
       index -= this._dictionaryVertexPositions.Count;  
       return this.GetIndex(index);  
     }  
     return index;  
   }  
 }  

Umstellung und Fläche vergrößern
In Update entfällt nun die Übergabe des GraphicsDevice und in DrawContent, wird mit einer doppelten Schleife die Fläche mehrmals an verschiedenen Positionen gerendert.

public class ComponentMap : GameComponent  
 {  
   private const float _speed = 0.3f;  
   private readonly ComponentInputs _componentInputs;  
   public AnimatedWaterwavesBuffered AnimatedWaterwavesBuffered;  
   public ComponentMap(Game game, ComponentInputs componentInputs) : base(game)  
   {  
     this._componentInputs = componentInputs;  
   }  
   public override void Initialize()  
   {  
     var texture = this.Game.Content.Load("rpgTile029");  
     // the buffered waves  
     this.AnimatedWaterwavesBuffered = new AnimatedWaterwavesBuffered(texture);  
     this.AnimatedWaterwavesBuffered.Initialize(this.Game.GraphicsDevice);  
   }  
   public override void Update(GameTime gameTime)  
   {  
     RasterizerState raster = new RasterizerState();  
     raster.CullMode = CullMode.CullCounterClockwiseFace;  
     raster.FillMode = FillMode.WireFrame;  
     this.Game.GraphicsDevice.RasterizerState = raster;  
     // the buffered waves  
     this.AnimatedWaterwavesBuffered.Update();  
   }  
   public void DrawContent(Matrix view, Matrix projection)  
   {  
     this.Game.GraphicsDevice.DepthStencilState = DepthStencilState.Default;  
     // the buffered waves  
     for (int iY = 0; iY < 10; iY++)  
     {  
       for (int iX = 0; iX < 10; iX++)  
       {  
         this.AnimatedWaterwavesBuffered.Draw(this.Game.GraphicsDevice, view, projection, new Vector3(200 - (iX * 20), 200 - (iY * 20), 0), 400 * iY);  
       }  
     }  
   }  
 }  

Die CameraView sollte eine neue Start Position erhalten, damit die neu erzeugte Fläche in der Mitte des RenderScreens erscheint. Zuvor war es, das die Fläche bewegt wurde. In diesen Branch kann man allerdings auch die Kmaera mit den Tasten WASD bewegen.

Leistungsgewinn
Mit 400.000 Polygonen fing die nicht gepufferte Version schnell an zu Ruckeln. Diese Anzahl sollte für ein durchschnittlichen PC oder Notebook möglich sein. Leider existieren auf dem Markt Spiele, die bei sehr geringen Grafikdetails auf kleinen System nur rückelig angezeigt werden.
Hier muß man sich bewußt sein, daß der Teil nicht von der Grafikkarte berechnet wird. Die Umstellung mit gepufferten VertexBuffern führt zu einer geringeren CPU Last und wirkt sich dementsprechend mit einer flüssigen Animation aus.

Fazit
Eigentlich hatte ich nicht vor die gepufferte Version mit rein zu nehmen. Wie immer habe ich den Rapberry Pi verwendet, um zu sehen, wie sich diese Lösung auswirkt. Zwar ruckelt es dort, aber bei weiten nicht so schlimm wie vorher.
Im Grunde war dies auch eine Problem Darstellung und eine mögliche Lösung, ohne gleich HLSL Programmierung anzusetzen. Manipulationen von Drahtgitter Modellen lassen sich besser durch die Grafikarte berechnen und das ging schon bereits mit DirectX 8.1.




Sonntag, 24. Februar 2019

Splitscreen mit MonoGame


Spiele machen mehr Spaß, wenn man sie zu zweit spielen kann. Mit MonoGame lässt sich ein Splitscreen mit geringen Aufwand umsetzen. Hierfür verwende ich von mir bereits angelegten Beispiel Code "ExampleMoveOnMap3d" aus dem Blogpost "3d Karte Bewegen".

Eingabegeräte
Für die Steuerung werden als Beispiel Tastatur Eingaben und Xbox Controller aufgeteilt. Daher wird nun vom Typen InputData eine zweite Property angelegt und entsprechend benannt. Die Eingabe muss beim jeden Aufruf immer zurück gesetzt werden, deshalb kommt ein Reset, sowie eine Normalize Methode und halten damit unsere Übersicht in ComponentInput Klasse.

   public class InputData  
   {  
     public Vector2 Move => new Vector2(this.MoveX, this.MoveY);  
     public float MoveX { get; set; }  
     public float MoveY { get; set; }  
     public void Reset()  
     {  
       this.MoveX = 0;  
       this.MoveY = 0;  
     }  
     public void Normalize()  
     {  
       this.MoveX = Math.Min(1, Math.Max(-1, this.MoveX));  
       this.MoveY = Math.Min(1, Math.Max(-1, this.MoveY));  
     }  
   } 

Bei mehreren Gamepads, könnte man die Eingabeerfassung auch in der Schleife abfragen, aber bleiben wir zunächst beim Hartverdrahten.

public class ComponentInputs : GameComponent  
   {  
     public InputData Inputs1 { get; private set; } = new InputData();  
     public InputData Inputs2 { get; private set; } = new InputData();  
     public ComponentInputs(Game game) : base(game)  
     {  
     }  
     public override void Update(GameTime gameTime)  
     {  
       this.Inputs1.Reset();  
       this.Inputs2.Reset();  
       KeyboardState stateKeyboard = Keyboard.GetState();  
       this.Inputs1.MoveX += stateKeyboard.IsKeyDown(Keys.A) ? 1 : 0;  
       this.Inputs1.MoveX -= stateKeyboard.IsKeyDown(Keys.D) ? 1 : 0;  
       this.Inputs1.MoveY += stateKeyboard.IsKeyDown(Keys.W) ? 1 : 0;  
       this.Inputs1.MoveY -= stateKeyboard.IsKeyDown(Keys.S) ? 1 : 0;  
       GamePadState stateGamePad = GamePad.GetState(PlayerIndex.One);  
       this.Inputs2.MoveX += stateGamePad.ThumbSticks.Left.X;  
       this.Inputs2.MoveY += stateGamePad.ThumbSticks.Left.Y;  
       // invert   
       this.Inputs1.MoveX *= -1;  
       // Normalize  
       this.Inputs1.Normalize();  
       this.Inputs2.Normalize();  
     }  
   }  

Die Kartenbewegung
Die Karte muss nun zweimal geladen werden, damit beide Spieler eine Ansicht über ihre Karten Position erhalten. Eine "IF" Abfrage fragt die aktuelle Eingabe ab.
Die im Titelbild zu sehenden Fahrzeuge, habe ich hier nicht mit reingenommen, da der Inhalt ähnlich des Types PlateGrassTile ist. Allerdings ist es wiederum auf meinem Githab Repository zu finden.

public class ComponentMap : GameComponent  
   {  
     private const float _speed = 0.3f;  
     private readonly ComponentInputs _componentInputs;  
     private readonly int _player;  
     private PlateGrassTile[] _groundTiles = new PlateGrassTile[9];  
     public ComponentMap(Game game, ComponentInputs componentInputs, int player) : base(game)  
     {  
       this._componentInputs = componentInputs;  
       this._player = player;  
     }  
     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)  
     {  
       Vector3 positon = new Vector3();  
       if (this._player == 1)  
       {  
         positon = new Vector3(this._componentInputs.Inputs1.MoveX, this._componentInputs.Inputs1.MoveY, 0) * _speed;  
       }  
       else  
       {  
         positon = new Vector3(this._componentInputs.Inputs2.MoveX, this._componentInputs.Inputs2.MoveY, 0) * _speed;  
       }  
       foreach (var item in this._groundTiles)  
       {  
         item.Position += positon;  
       }  
     }  
     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);  
     }  
   }  

Von der 'ComponentMap' wird nun eine zweite Instance angelegt in der Game1.cs. Die 'UpdateOrder' muss dann entsprechend angepasst werden für den ComponentRender und im Konstruktor Parameter wird die zweite Instance eingereicht.

public class Game1 : Game  
   {  
     private readonly GraphicsDeviceManager _graphics;  
     private readonly ComponentInputs _componentInputs;  
     private readonly ComponentMap _componentMapPlayer1;  
     private readonly ComponentMap _componentMapPlayer2;  
     private readonly ComponentRender _componentRender;  
     public Game1()  
     {  
       this._graphics = new GraphicsDeviceManager(this);  
       this.Content.RootDirectory = "Content";  
       this.Window.Title = "Move on map 3d";  
       this.IsMouseVisible = true;  
       this._componentInputs = new ComponentInputs(this);  
       this._componentInputs.UpdateOrder = 1;  
       this.Components.Add(this._componentInputs);  
       this._componentMapPlayer1 = new ComponentMap(this, this._componentInputs, 1);  
       this._componentMapPlayer1.UpdateOrder = 2;  
       this.Components.Add(this._componentMapPlayer1);  
       this._componentMapPlayer2 = new ComponentMap(this, this._componentInputs, 2);  
       this._componentMapPlayer2.UpdateOrder = 3;  
       this.Components.Add(this._componentMapPlayer2);  
       this._componentRender = new ComponentRender(this, this._componentMapPlayer1, this._componentMapPlayer2);  
       this._componentRender.UpdateOrder = 4;  
       this.Components.Add(this._componentRender);  
     }  
   }  


Bildausgabe aufteilen
Nun kann auch schon der Render Bereich angepasst werden. Zunächst wird eine Anpassung an der 'CameraView' Klasse vorgenommen. In der Methode 'Initialize()' muss die Projektion auf die Hälfte reduziert werden. Für die Rechte Seite muss der zu anzufangende Viewport auf die Mitte des Bildschirms verschoben werden. In 'Draw()' muss dann der Eingerichtete ViewPort für den Renderbereich zugewiesen werden.

public class CameraView  
   {  
     private readonly Game _game;  
     private readonly ComponentMap _componentContent;  
     public Vector3 Position { get; set; }  
     public Matrix View { get; private set; }  
     public Matrix Projection { get; private set; }  
     private bool _isRightView = false;  
     private Viewport _viewPort;  
     public CameraView(Game game, ComponentMap componentMap, bool isRightView)  
     {  
       this._game = game;  
       this._componentContent = componentMap;  
       this._isRightView = isRightView;  
     }  
     public void Initialize()  
     {  
       this._viewPort = this._game.GraphicsDevice.Viewport;  
       this._viewPort.Width = this._viewPort.Width / 2;  
       if (this._isRightView)  
       {  
         this._viewPort.X = this._viewPort.Width;  
       }  
       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._game.GraphicsDevice.Viewport = this._viewPort;  
       this._componentContent.DrawContent(this.View, this.Projection);  
     }  
     private float GetAspectRatio()  
     {  
       var w = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width / 2;  
       var h = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;  
       return w / h;  
     }  
   }

Aus einer 'CameraView' werden nun zwei. Beide haben nun den zusätzlichen Parameter mit der sich dann der Splitinhalt von Links oder Rechts einstellen lässt. Das Initialisieren der 'CamereView', muss dann über die zu überschreibende Methode 'Initialize()' abgearbeitet werden von der geerbten Klasse 'DrawableGameComponente'.

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

Nachwort
Wie zu sehen ist, ist die Umsetzung für Split Screen nicht sonderlich kompliziert und ist auch noch in wenigen Schritten machbar. Mit ein wenig mehr Aufwand kann das OpenClosePrinzip angewendet werden, womit dann anschließend eine Umsetzung eines vier Spieler Modus erleichtert.


Links ist die Tastatur, Rechts wird vom GamePad gesteuert.

Sonntag, 17. Februar 2019

Win 10 IoT und MonoGame auf dem Raspberry Pi 3 Testen


Ohne DirectX Hardware Beschleuniger ist die Performance eher auf dem Level, hmm.. Ich drücke das mal so aus: "Es geht". Dennoch ist interessant zu testen, was man eigentlich so auf dem Raspberry Pi erreichen kann. Für diese Tests verwende ich den Raspberry Pi 3 Model B+.

Testanwendung
Vom meinem letzten Blogpost Eintrag "3d Karte bewegen (MonoGame)" habe ich einen Branch angelegt, in dem ich die Inhalte für einen kleinen Benchmark aufbereite. Zum Beispiel wird die Eingabe mit dem GamePad oder Tastatur abgestellt. Hinzu kommen dann noch weitere 3D Objekte hinzu, die im Verlauf geändert werden. In der UI selbst wird dann die FPS und Durchschnittsergebnis der verschiedenen drei Durchläufen wiedergegeben.

Testumfeld, Quick and dirty
Der Test selbst ist nur sehr einfach gestrickt und sicherlich kann dort auch Optimiert werden. Aber das sollte diesmal nicht das Thema sein.

Durchläufe:
- 9 Objekte
- 100 Objekte
- 400 Objekte

Die Objekte sind auf 0.25 skaliert. Nur bei dem ersten können wirklich alle Kacheln gesehen werden. Bei den anderen Mengen, übertreten diese den Bildschirmrand.

Auf dem PC werden immer die 60 FPS erreicht. Der eingestellte Vsync beschränkt die FPS, was jedoch nicht weiter schlimm ist. Für den Raspberry Pi sollte die Beschränkung  kein Problem darstellen. Kommen wir zu den Testergebnissen

Ergebnis (Debug)

Die Anzahl der Bilder waren nicht sonderlich stabil und schwanke sogar relativ stark. Dennoch hat man erstmal so ein paar  Ergebnisse, die man versucht zu verbessern. Da wäre das Auslassen von 3d Objekten, die nicht zu sehen sind.

Was ist mit Core Application
Bisher noch nicht genutzt, aber ich dachte, es paßt gerade hier beim Testen. Also nahm ich den ganzen Code von der aktuellen Test Branch und erstellte ein Projekt mit Core Application. Klingt zunächst super.


Ergebnis (Debug)







Ergebnis (Release)







Nun, die Ergebnisse verhalten sich fast identisch wie der Normalen UWP Projekt. Teilweise hatte ich den Eindruck, daß die Ergebnisse schlechter waren. Das lag mit unter das die Ergebnisse bei mehreren Durchläufen sehr stark schwankten. Das änderte sich auch nicht, als ich den Test für eine Stunde laufen ließ.


Was ist mit 2D?
Vielleicht sollte man nicht zuviel auf 3D Inhalte setzen und lieber auf 2D setzen. Also habe ich von meinem vorigen Post und dessen Beispiel ebenfalls ein Branch erstellt und ähnliche Test Umgebung geschaffen in 2D und zunächst als UWP.


Ergebnis (Debug)







Zugegeben, das hatte ich nicht erwartet. Vielleicht liegt das an der zu rendernden Anzahl von Pixeln, die letztendlich in den Backbuffer landen. Offensichtlich  macht die Ausführung in 2D oder 3D keinen Unterschied. Die Schwankungen verhielten sich nicht anders.

Fazit
Das Testumfeld ist nicht sonderlich Realistisch, wenn man vom Optimieren absah. Die Frage ob man MonoGame verwenden kann auf dem Raspberry Pi, dann ist die Antwort ja. Wir stellen fest, die Antwort passt nicht zu den Ergebnissen. Warum ja? Mit Kompromissen, kann man durch aus MonoGame auf dem Raspberry Pi verwenden, wenn man Kompromisse eingeht. Mein Kompromiß ist Interesse.

Kompromisse die in erster Linie bestehen:
- Geringe FPS
- Geringe Auflösung um den FPS hoch zuhalten
- Ich will sehen, wie mein MonoGame Spiel auf dem Raspberry Pi (nicht) performt

Damit will ich das Thema Raspberry und MonoGame nicht schlecht schreiben. Denn nicht alle Spiele setzen auf hohe FPS. Das könnte ein 'Click and Point Adventure' sein.


Im Beispiel Video mit dem Raspberry Pi TFT, kann leider die Native Auflösung von 480x320 vom TFT nicht eingestellt werden.

Gedruckte Bewegliche Erweiterung für die Arduino UNO Q Plattform

  Nach drei Korrekturen am 3D Model, habe habe ich ein Gerüst, dass funktionieren könnte. Als nächstes kommt der Boden, der mit dem Schritt ...