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

Sonntag, 12. März 2023

Tool Lagerverwaltung (Teil 6) Umzug nach .NET MAUI


Irgendwann ist immer was Neues da und dann sind Runde Ecken dran. Ich las die aktuelle .NET Pro (2/23) und stellte fest, dass .NET MAUI, die neue Zukunft für Frontend Technologie wird. Oder zumindest vermute ich dies stark, da diese Technologie ein sehr guter Nachfolger zu WPF und Xamarin sein könnte.

Natürlich ist .NET MAUI noch sehr neu und sicherlich sind im Bereich für Plattformübergreifende Abdeckung noch das eine und andere Problem da.

Für mich bedeutet das, dass ich mein ‚Application Framework' umziehe in die neue Welt von .NET MAUI.

Benötigt

  • Visual Studio 2022
  • GitHub

 

Umzug

Eigentlich sag der Titel des Absatzes schon alles. Aber vielleicht gehen einige Sachen noch nicht so wie ich mir das Vorstelle. Daher ist das erste Ziel das alle Grundlegende Funktionen aus dem 'Codexzier's Application Framework' zu übernehmen. Die speziellen Benutzerdefinierten Steuerelemente werden später migriert.

 

Neues Projekt und Struktur aktualisieren

So ganz will ich die Inhalte nicht kopieren und ich bin mir nicht sicher, ob einige Vorgänge genau so funktionieren wie in WPF. Zudem sollen auch die Fehlenden Unit Tests kommen, was wiederum voraussetzt, dass ich für jede Komponente die Anforderungen und Erwartungen beschreibe. Stichwort: Test-Driven-Development oder auch kurz TTD. Ein Thema, dass man immer wieder hört, aber nie gemacht wird oder gefühlt nur von mir umgesetzt wird.

Zunächst erstelle ich das Projekt mit folgenden Einstellungen:

  • Projektvorlage: .NET MAUI Class Library
  • Projektname: Codexzier.Maui.ApplicationFramework


Die Ordner Struktur kann zum Teil übernommen werden. Allerdings ziehe ich vor, erst die Ordner zu erstellen, wenn eine Klasse darin erstellt wird. Damit soll vermieden werden, nicht genutzte Inhalte anzulegen.

Darüber hinaus wurden von der Vorlage Ordner angelegt mit den Namen zu verschiedenen Plattformen.


Aktuell belasse ich die Inhalte und entscheide erst mit späteren Tests auf den verschiedenen Betriebssystemen, ob ich die Ordner benötige.


Issues anlegen in GitHub

Eigentlich sollte das als erstes erfolgen, bevor überhaupt die erste Implementierung stattfindet. Ich habe nachträglich mich Anhand des bestehenden WPF Application Frameworks, meine Aufgaben definiert.

Dazu lernen

Bevor ich die weitere Hauptkomponenten implementieren kann, muss ich mehr über .NET MAUI Lernen. Denn zu diesem Zeitpunkt weiß ich nicht, ob meine Bisherigen Funktionen so weit übernommen werden kann und zum anderen will ich statt MVVM, das MVU Pattern verwenden, dass auf .NET MAUI besser geeignet ist.

Übersicht

Teil 1 - Wiki anlegen in GitHub

Teil 2 - Neue Solution

Teil 3 - Noch ein Wiki anlegen

Teil 4 - Datenbank und Schnittstelle

Teil 5 - Konzept ausschreiben

Teil 6 - Umzug nach .NET MAUI

Montag, 22. August 2022

Tool Lagerverwaltung (Teil 2) Neue Solution


Welche Architektur oder welche Form soll das Projekt haben. Am liebsten setze ich auf die Drei-Schichten-Architektur. Klinkt abstrakt und hochtrabend, aber kompliziert ist die Sache nicht. Sich dran halten ist anfangs schwierig. Vorzugsweise setze ich auf Desktop Anwendung.

Benötigt

  • Visual Studio 2022 oder anderen Compiler
  • .NET 6.0
  • Codexzier's Application Framework
  • GitHub

 

Ziel für diesen Blog-Eintrag

Eine neue Solution und die benötigten Projekte einrichten mit dem Ansatz der Drei-Schichten Architektur..

 

Drei-Schichten

Wenn ich Rückblicke, in welchen Formen ein Drei-Schichten Modell aussieht, dann waren diese immer unterschiedlich gestaltet und hielten dennoch erkennbar Drei Schichten. Grundlegen haben wir das Frontend oder auch Benutzeroberfläche genannt, dann die Service-Schicht in der die Daten verarbeitet werden und als drittes die Datenhaltung in einer Datenbank.

 

Solution anlegen

Aber bevor das Projekt angelegt wird, soll eine leere Solution erstellt werden. Idealerweise gibt ihr in die Suche 'Blank' ein.

Nach der Auswahl kommen wir zur Eingabe des Solution Name und das Festlegen des Speicherortes.


Git Repository

Bevor das WPF Projekt Eingesetzt wird, gehört die Pflege der Sourcecodes in das Git System. Also fehlt der Klick auf den Button 'Create Git Repository…'.

Falls die Anmeldung von GitHub noch nicht geschehen ist, dann wird dies jetzt gefordert. In meinem Fall ist das Projekt öffentlich, weshalb der Haken für 'Private repository' raus ist.

Ist das Repository erstellt, dann sollte der aktuelle Stand bereits Online in eurem GitHub Repositories zu finde sein.

 

Einrichten der Projekte

Als erstes kommt das WPF Projekt, das die Frontend Schicht abbildet. Hier verwende ich das Projekt Template, dass bereits die Referenzen zu meinem Application Framework enthält.

Im folgenden kommen zwei Möglichkeiten, um das Template in Visual Studio 2022 einzubinden.

 

Option 1

Die Zip-Datei Codexzier Application Framework Vorlage Augsut 2022 in den Ordner ../Visual Studio 2022/Templates/ProjectTemplates einfügen (zum Download)

 

Option 2

Das Codexzier Application Framework von meinem GitHub Repository herunterladen und in Visual Studio öffnen. Dann oben auf Projekt

Dann das Projekt Template 'Codexzier.Wpf.ApplicationTemplate' auswählen..

..und im nächsten Schritt einen Namen vergeben.

Wählt nun die Solution aus und fügt diesem ein neues Projekt zu.

Das WPF Vorlagen Projekt kann bei Bedarf über die Suche gefunden werden.

Für die Benutzer Oberfläche, solltet ihr für den Namensraum den Bereich Namentlich erkenntlich Beschreiben.


Service Schicht

Das nächste Projekt soll alle Service Komponenten enthalten, die nichts mit der Benutzer Oberfläche zu tun haben. Obwohl ich von Service Schicht gesprochen habe, wird eine einfachen Klassen Bibliothek angelegt. Hier darauf achten, dass ihr die Vorlage für das aktuelle .NET Framework verwendet.

Und wieder einen passen Namen vergeben für die Service Komponenten Schicht.

Im nächsten Schritt muss noch die .NET Version angegeben werden und dann kann das Projekt erstellt werden.


Ein Projekt noch

Eins fehlt noch und zwar das für die Automatischen Tests. Für die UI kann eigentlich auch ein Test Projekt angelegt werden, aber aktuell reicht ein Unit Test Projekt für die Service Komponenten Schicht.

Der Namespace bekommt den Zusatz 'Test'. Damit sollte das Unit Test Projekt direkt unter dem zu testenden Projekt sein.

Zum Schluss wieder die .NET Version auswählen und erstellen.


Initialen Stand sichern

Hier braucht der Text schlichtweg aussagen, dass grundlegend die Projekte angelegt wurden.


Grundlage für das gesamt Projekt

Der zweite Schritt ist erledigt. Eine neue Solution mit den Grundlegenden Projekten ist angelegt. Die Inhalte habe ich nicht zu stark unterteilt, schließlich handelt es sich hier um ein sehr kleines Privat Projekt. Im nächsten Schritt geht’s dann mit dem Konzept und Funktionsumfang weiter.


Übersicht

Dienstag, 2. August 2022

Tool Lagerverwaltung (Teil 1) Wiki anlegen in GitHub

Für das Tool soll das eigene Framework verwendet werden, dass ich in den letzten Jahren immer weiter ausgebaut hatte. Jedoch ist noch offen, eine Dokumentation anzulegen, die den Funktionsumfang sowie Verwendung des Applikation Framework zeigt.

 

Benötigt

  • Visual Studio 2022 oder anderen Compiler
  • .NET 6.0
  • Codexzier's Application Framework
  • GitHub

 

Was ist mein Ziel?

Primär soll ein einfaches Tool entwickelt werden, dass zur Lagerverwaltung verwendet werden kann. Mit der der Entwicklung möchte ich beschreiben, welche Vorgehensweisen und Lösungen ich verwende. Zudem soll zu jeder Funktion aus dem Applikation Framework dokumentiert werden, das ich für den aktuellen Schritt verwendet wird.

 

Neben Produkt

Immer wenn ich eine neue Anwendung geschrieben habe, benötigte ich Grundlegende Inhalte für eine WPF Anwendung mit bestimmten Funktionen die ich selbst mal geschrieben habe. Und weil ich meine Anwendung auch ein Bestimmtes Aussehen haben sollen, habe ich auch die Styles immer mit kopiert. Damit ergaben sich Vorteile aber auch Nachteile.

 

Vorteile

  • Auf den Bisherigen Lösungen etwas Besseres oder neues entwickeln
  • Übung, Übung, Übung

Nachteil

  • Pflege älterer Anwendung erschwert
  • Doppelte Arbeit

 

Ok, gehen wir von der selbst Erklärung rüber zum ersten Schritt.

 

Henne-Ei-Problem

Fange ich mit der Dokumentation an, was funktionieren soll oder fange ich mit dem Programmieren an und dokumentiere? Die Frage, ob man mit der Dokumentation anfängt, ist davon abhängig, um was für eine Dokumentation angelegt werden soll. Bevor etwas entsteht, kann im Grunde nur ein Konzept- oder ein Grundbeschreibungen zu einer Anwendung angelegt werden.

In meinem Fall liegt der Programmcode vor und die Dokumentationsbeschreibung fehlt. Also wie in vielen Projekten.

 

GitHub Wiki

Die Sourcen zu dem Applikation Framework von mir habe ich bereits auf GitHub hochgeladen und dort kann zusätzlich ein Wiki gepflegt werden. Und damit soll's neben dem Hauptprojekt beginnen.

 

Als erstes hilft eine Grobe Inhaltsangabe anzulegen, so dass eine Grundlage vorliegt, an der man sich orientieren kann.

 Home  
 Lizenz  
 Grundlagen  
      Vorlage  
      Neues Projekt einrichten  
      Grundaufbau  
      Weiter Einstellungen  
 Components  
      Event Bus Manager  
      User Settings  
      Animation Helper  
 Styles  
      Blue Gray  
      White Gray  
      Gray White  
 Steuerelemente  
      Button  
      Diagram  
      Folder Browser  
 Game Tree 

So sieht der Text in der Bearbeitung aus in der GitHub Wiki (Zuvor hatte ich am Ende jedem Eintrag immer Page stehen gehabt, leider ist das dann auch der Name der Seite und habe das deshalb abgeändert)

 * [[Lizenz|Lizenz]]  
 * [[Grundlagen|Grundlagen]]  
 > - [[Vorlage|Grundlagen - Vorlage]]  
 > - [[Neues Projekt einrichten|Grundlagen - Neues Projekt einrichten]]  
 > - [[Grundaufbau|Grundlagen Grundaufbau]]  
 > - [[Weiter Einstellungen|Grundlagen - Weitre Einstellungen]]  
 - [[Components|Components]]  
 > - [[Event Bus Manager|Components - Event Bus Manager]]  
 > - [[User Settings|Components - User Settings]]  
 > - [[Animation Helper|Components - Animation Helper]]  
 * [[Styles|Styles]]  
 > - [[Blue Gray|Styles - Blue Gray]]  
 > - [[White Gray|Styles - White Gray]]  
 > - [[Gray White|Styles - Gray White]]  
 * [[Steuerelemente|Steuerelemente]]  
 > - [[Button|Steuerelemente - Button]]  
 > - [[Diagram|Steuerelemente - Diagram]]  
 > - [[Folder Browser|Steuerelemente - Folder Browser]]  
 > - [[Game Tree|Steuerelemente - Game Tree]]  

Und so sieht's dann nach dem Speichern aus.



Gefärbter Text
Im Bild ist zu sehen, dass nur der Text 'Lizenz' in blauer schrift hinterlegt ist. Wenn du suggestiv einen Link vermutest, dann liegst du richtig. Für die Seite Lizenz, habe ich bereits eine Seite angelegt. Alle die noch rot eingefärbten sind, müssen noch erstellt werden. Wenn man auf eines der rot gefärbten Text klickt, kommt statt einem Fehler eine Bearbeitungsmaske, um diese Seite zu erstellen.


 

Grundlage

Der erste Schritt ist damit getan. Nun muss man nur dranbleiben und hin und wieder schauen, ob die Struktur weiterhin passt.


Links

https://docs.github.com/en/communities/documenting-your-project-with-wikis/editing-wiki-content

 

Sonntag, 10. Dezember 2017

Von Händlern, Kisten und Münzen (Arduino Esplora, Part 8)


Ok, so richtiger Handel wird hier nicht stattfinden. Dafür reicht der Speicher nicht. Oder? In erster Linie sollen nur Grund Funktionen Umgesetzt werden. Ziel wird sein, wenn die Figur vor dem Händler oder einer Kiste steht, dann sollte sich der Inhalt Zeigen. Anschließend kann ein Objekt Ausgewählt und in die eigene Tasche übertragen werden. Leider passt das nicht alles in einen Blogpost, so dass der Inhalt mit der Waren Anzeige in einem späteren Post kommt.

Anforderung
Beim Händler können Gegenstände erworben werden und diese in Kisten abgelegt werden. Das erfordert einige mehr Programmcodezeilen und daher muss an der Stelle wieder eine neue Seite  mit dem Namen 'TraderComponent' angelegt werden.


Am Anfang werden die Werte für Händler und Kisten hinterlegt, die später über den Flashspeicher abgerufen werden. Die Münzen werden hier ebenfalls abgelegt als Funktionsvariable, wird aber erst in einen späteren Post weiter behandelt. (im Folgender Programmcode sind Kommentare und Bilddaten gekürzt, ggf. schaut ihr am besten in die Github Sourcen)

 // # Coins, im Besitz  
 int16_t coins = 25;  
 int16_t lastStateCoins = 0;  
 // # Common Text   
 // Begruessungstext (Sollte immer verschieden sein.)  
 const PROGMEM char traderStartText[] = "Hallo, was darf ich ihnen verkaufen?";  
 // Wenn zu wenig Muenzen zum Kaufen da sind  
 const PROGMEM char traderNotEnough[] = "Du hast nicht genug Muenzen.";  
 // Frage zum Kauf  
 const PROGMEM char traderYouWantToBuy[] = "Kaufen?";  
 // # Common Sprite  
 // Bild vom Handler / Die Farbe des Shirts, kann veraendert werden.  
 const PROGMEM byte traderSpriteFrontMen[160] = { … };  
 const PROGMEM byte traderSpriteFrontWomen[160] = { … };  
 const PROGMEM byte boxSpriteFront[100] = { … };  
 const PROGMEM byte coinSpiteIcon[49] = { … };  
 // # TRADER  
 // temp Variablen zum zwischen laden.  
 char traderName[1];  
 char traderdescription[1];  
 byte traderItems[4];  
 // '0' bedeutet immer nicht belegt.  
 // #######################################  
 // ID 1  
 // Name des Handlers  
 const PROGMEM char trader01Name[5] = "Surie";  
 // Kurze Beschreibung  
 const PROGMEM char trader01Description[11] = "Verkaeferin";  
 // Dinge zum verkauf  
 const PROGMEM byte trader01Items[4] = { 2, 0, 0, 0 }; // 2 = Kamera  
 // 0 = Taschenplaetze werden wie angegeben befullt.  
 // Stellen werden Stellenweise in Bit herausgenommen  
 byte trader01ItemsClear = 0;  
 // # Box  
 // '0' bedeutet immer nicht belegt.  
 // #######################################  
 // ID 1  
 // Name des Handlers  
 const PROGMEM char box01Name[11] = "Meine Kiste";  
 // Kurze Beschreibung  
 const PROGMEM char box01Description[25] = "Dinge die man so braucht.";  
 // Dinge zum verkauf  
 const PROGMEM byte box01Items[4] = { 3, 0, 0, 0 }; // 3 = Foto  
 void memCopyItems(byte arrayContent[], byte traderItemsClear) {  
   if(traderItemsClear == 128) {  
   traderItemsClear-= 128;  
   traderItems[0] = 0;  
  }  
  else { traderItems[0] = pgm_read_byte_near(arrayContent + 0); }  
  if(traderItemsClear >= 64) {  
   traderItemsClear-= 64;  
   traderItems[1] = 0;  
  }  
  else { traderItems[1] = pgm_read_byte_near(arrayContent + 1); }  
  if(traderItemsClear >= 32) {  
   traderItemsClear-= 32;  
   traderItems[2] = 0;  
  }  
  else { traderItems[2] = pgm_read_byte_near(arrayContent + 2); }  
  if(traderItemsClear >= 16) {  
   traderItemsClear-= 16;  
   traderItems[3] = 0;  
  }  
  else { traderItems[3] = pgm_read_byte_near(arrayContent + 3); }  
 }  
 void drawTrader(int16_t traderId, int16_t positionX, int16_t positionY) {  
  if(!mapFigureRerender) {  
   return;  
  }  
  mapFigureRerender = false;  
  switch(traderId) {  
   case(1): { // Farbe des Haenderls/in  
    spriteHairColor1 = 0xEEEC; spriteHairColor2 = 0xE662; // hell Braun 1, hell braun 2  
    spriteShirtColor1 = 0xD69A; spriteShirtColor2 = 0xB596; // hell grau, grau  
    spritePantsColor1 = 0x0418; spritePantsColor2 = 0x0312; // Blau 1, blau  
    memCopy(traderSpriteFrontWomen);             // sprite einer Weiblichen figur  
    memCopyItems(trader01Items, trader01ItemsClear);     // Taschen Inhalt  
    break;  
   }  
   case(2): { // Farbe des Haenderls/in  
    spriteHairColor1 = 0xD615; spriteHairColor2 = 0xBD30; // hell Braun 1, hell braun 2  
    spriteShirtColor1 = 0xD69A; spriteShirtColor2 = 0xB596; // hell grau, grau  
    spritePantsColor1 = 0x0418; spritePantsColor2 = 0x0312; // Blau 1, blau  
    memCopy(traderSpriteFrontMen);  
    break;  
   }  
   default: { break; }  
  }  
  drawTile(positionX, positionY, 10, 16, tempArray, false);  
 }  
 void drawBox(int16_t boxId, int16_t positionX, int16_t positionY) {  
  switch(boxId) {  
   case(1): {  
    boxColor = 0xDCFE;  
    break;  
   }  
   default: { break; }  
  }  
  memCopy(boxSpriteFront);  
  drawTile(positionX, positionY, 10, 10, tempArray, false);  
 }  
 void drawCoinsStatus(bool redraw) {  
  if(coins != lastStateCoins || redraw) {  
   EsploraTFT.fillRect(2, 2, 30, 9, mapNumberToColor(1));  
   memCopy(coinSpiteIcon);  
   drawTile(3, 3, 7, 7, tempArray, false);  
   writeValue(12, 3, coins, false);  
   lastStateCoins = coins;  
  }  
 }  

Der Händler oder Händlerin sollten für die Kollisionsabfrage den selben Raum einnehmen, wie die eigene Spielfigur. Damit dies funktioniert und der Händler nicht wie ein Karten Block (Kachelgröße) registriert wird, ist eine kleine Erweiterung an der Methode "CanEnterArea" mit "checkCollideOther" notwendig. Etwas abwegig ist die Abfrage der Position, weil diese wiederum über das Byte Array der Karte weiterhin abgefragt wird. Dafür habe ich eine relativ simple Lösung (ggf. in den Github Source schauen)

boolean checkCollideOther(boolean resultColide, int positionX, int positionY) {
  // anderes bewegbares objekt
  if(resultColide) {

    int overlap = 4;
    resultColide = checkCollide(positionX, positionY, mapFigurePositionX + (overlap / 2), mapFigurePositionY + (overlap), 10 - overlap, 16 - (overlap * 2));

    // zum testen Fenster oeffnen
    showWindow = !resultColide;
    menueNavigation = showWindow;
  }

  return resultColide;
}

Message Box
Der Text bekommt sein Platz in einem eigenen Fenster Bereich. Für diese Funktion wird ebenfalls eine weiter Seite angelegt mit dem Namen "WindowComponent". Das Fenster (MessageBox) wird angezeigt, sobald man mit seiner gesteuerten Figur in den Kollisionsradius des Händlers kommt.
Solange der Dialog offen ist, sollte die Figur nicht mehr bewegbar sein und mit dem Joystick kann nur noch in den Taschenplätzen Navigiert werden. Nachdem der Spieler die Schließen-Option Auswählt, verschwindet das Fenster und die Figur sollte sich wieder frei bewegen können.
Was im folgenden Code nicht zu sehen ist, ist die Ausführung des Schließen der MessageBox über den Button 2 bzw. Switch 2.

// Legt ein Fenster in den Vordergrund
bool lastStateShowWindow = false;
bool windowHasRendered = false;

void drawWindow(bool rightSide) {
  if(lastStateShowWindow != showWindow && !showWindow) {
    lastStateShowWindow = showWindow;
    drawStack(true);
  }

  lastStateShowWindow = showWindow;
  
  if(!showWindow) {
    windowHasRendered = false;
    return;
  }

  if(windowHasRendered) {
    return;
  }
  
  // Mitte des Bildschirm schreiben
  int sizeX = 100; int sizeY = 40;
  int winPosX = (EsploraTFT.width() / 2) - (sizeX / 2);
  int winPosY = (EsploraTFT.height() / 2) - (sizeY / 2);

  EsploraTFT.fillRect(winPosX, winPosY, sizeX, sizeY, mapNumberToColor(0));
  EsploraTFT.drawRect(winPosX, winPosY, sizeX, sizeY, mapNumberToColor(18));
  EsploraTFT.drawRect(winPosX + 2, winPosY + 2, sizeX - 4, sizeY - 4, mapNumberToColor(18));

  // Text schreiben
  writeText(winPosX + 5, winPosY + 5, "Hallo!");
  writeText(winPosX + 5, winPosY + 28, "Schliessen [2]");
   windowHasRendered = true;
}


Das Stehenbleiben der Figur muss wiederum auf der Hauptseite festgelegt werden. Dazu muss die Funktion für das Laufen erweitert werden, damit die Figur sich erst nach der Option "Schließen" bewegen kann. Zudem müssen alle Inhalte nach dem Schließen neu gerendert werden mit der  Methode 'drawStack'.

void loop() {
  …  
  // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.
  if(!menueNavigation && lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {

    drawStack(false);
  }
  else if(menueNavigation) {
    menueNavigateWithDelay();
  }

  drawWindow(lastPosX > EsploraTFT.width() / 2);
  drawCoinsStatus(false);
}

Der Dialog ist noch nicht ganz fertig. Die Taschenplätze sollten mit dem Joystick erreichbar sein. Das fehlt derzeitig auch für den Rucksack. Dies würde jedoch den Rahmen des Posts sprengen und kommt daher im übernächsten. Für den nächsten Part wird der Programmcode dringend aufgeräumt, auf dass ich näher eingehen will.



Sonntag, 29. Oktober 2017

Ich packe in meinen Rucksack (Arduino Esplora, Part 7)


Was wäre ein Abenteuer ohne einen Rucksack, in dem man seine Gefundenen Gegenstände einsammeln kann. Um diese Funktion Übersichtlich zu halten, wird der Rucksack sechs Plätze haben. Im Vorfeld muss festgelegt werden, wie zunächst die Informationen im Rucksack gehalten werden. Auch hier wird weiterhin eine Datenbanklose Lösung erzielt. Die Gegenstände müssen als Abstrakt betrachtet werden, so dass diese auf wesentliche Informationen eingeschränkt wird.

Ein wichtiger Punkt wird sein, die Funktionsvariablen entsprechend zu kommentieren. Das wird später hilfreich sein, die Informationen auch wieder zu zuordnen.

Ein Objekt sollte Grundlegende Eigenschaften haben:

  • Name
  • Bild (ein 16x16 Pixel Sprite)
  • Beschreibung (sollte nur für bestimmte Gegenstände verwendet werden)
  • Verwendungszweck

Damit der Gegenstand Zugeordnet werden kann, ist zusätzlich eine Identifikationsnummer erforderlich oder auch kurz ID. Diese wird z.B. für den Rucksack Funktion verwendet. Allerdings muss die ID Nummer nicht als Funktionsvariable angelegt werden und steht nur als Kommentar zu den verwendeten Daten.

 // ID 01  
 // Name  
 const PROGMEM char itemKey01[10] = "Schluessel";  
 // Icon / Bild  
 const PROGMEM byte itemKey01Icon[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,10,10,10,10,10,10,10,10,10,10,10,0,0,10,0,0,10,10,0,10,0,0,0,0,10,0,0,0,0,10,0,0,10,0,0,0,0,0,0,0,10,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,10,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };  
 // Beschreibung  
 const PROGMEM char itemKey01Description[] = "Oeffnet eine Box";  
 // Verwendungszweck Id => kombinierte funktions abruf fur position und verknuepfte Box mit der selben Id  
 const PROGMEM uint16_t itemKey01Usage = 1;  
 // #######################################  
 // ID 02  
 // Name  
 const PROGMEM char itemCamera[6] = "Kamera";  
 // Icon / Bild  
 const PROGMEM byte itemCameraIcon[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,9,9,9,9,9,9,1,0,0,0,0,0,1,1,1,1,9,19,19,19,19,9,1,1,1,1,0,1,9,9,9,9,9,9,1,1,9,9,9,9,9,9,1,1,9,9,9,9,1,1,11,11,1,1,9,19,19,9,1,1,9,9,9,9,1,11,11,11,11,1,9,19,19,9,1,1,9,9,9,1,11,11,11,11,11,11,1,9,9,9,1,1,9,9,9,1,11,11,11,11,11,11,1,9,9,9,1,1,9,9,9,9,1,11,11,11,11,1,9,9,9,9,1,1,9,9,9,9,1,1,11,11,1,1,9,9,9,9,1,1,9,9,9,9,9,9,1,1,9,9,9,9,9,9,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };  
 // Beschreibung  
 const PROGMEM char itemCameraDescription[] = "Mach ein paar Fotos!";  
 // Verwendungszweck  
 const PROGMEM uint16_t itemCameraUsage = 2;  

Der Name ist klar, Bild muss sein und Beschreibung zu einem Objekt ist auch hilfreich. Aber wie sieht der Einsatz für die Eigenschaft 'Verwendungszweck' aus? Im Programmcode wird dort eine Nummer stehen. Hier kommt die Kollisionsabfrage ins Spiel.

Ein Fallbeispiel
Die Figur hat den Gegenstand 'Schlüssel' und kann damit eine Bestimmte Tür öffnen. Über die Kollisionsabfrage wird geprüft, ob das Hindernis eine Tür ist. Wenn ja, dann wird der Rucksack nach einem Objekt abgefragt, dass dem Verwendungszweck entspricht.


Weiteres zum Verwendungszweck, kommt im späteren Abschnitt und bleiben zunächst bei der Umsetzung Gegenstände einzusammeln.

Der Rucksack
Als erstes sollten die Taschenplätze im Unteren Bildschirm Bereich abgebildet werden. Im Aus übersichtlichen Gründen wird im Programmcode eine weitere Seite (Tab) angelegt mit dem Namen 'BackpackComponent'. Für die Anforderungen kommen einige Funktionen hinzu, um ein Item in den Rucksack zu schreiben, abzurufen oder zu entfernen.

// Grundeinstellung des Rucksackes
#define backpackPlacesCount 6
uint16_t backPlaces[backpackPlacesCount] = { 0, 0, 0, 0, 0, 0 };
byte tempIcon[256];

// … Item Objekte …

// Kopiert das array aus dem flash in den Arbeitsspeicher
void setItemIconToTemp(byte icon[]) {
  for(int index = 0; index < 256; index++) {
    tempIcon[index] = pgm_read_byte_near(icon + index);
  }
}

// Pruefen ob das Item bereits vorhanden ist
boolean isItemInBackback(uint16_t itemId) {

  for(byte index = 0; index < 6; index++) {
    if(backPlaces[index] == itemId) { return true; }
  }
  return false;
}

// Legt das Item in die Tasche ab und Zeichnet es in einen offen Taschenplatz
bool setItemToBackpack(uint16_t itemId) {

  if(isItemInBackback(itemId)) { return false; }
  
  // id ablegen in ersten freien Taschenplatz
  byte place = 0;
  for(byte index = 0; index < sizeof(backPlaces); index++) {
    if(backPlaces[index] == 0) {
      backPlaces[index] = itemId;
      place = index;
      break;
    }
  }
  
  byte relationPlaceX = 0;
  byte relationPlaceY = 0;
  setItemRelationPlace(place, &relationPlaceX, &relationPlaceY);

  // Abruf des Icon zu dem Item
  boolean isArrayCopy = true;
  switch(itemId) {
    case(1): { setItemIconToTemp(itemKey01Icon);  break; } // Schluessel
    case(2): { setItemIconToTemp(itemCameraIcon); break; } // Fotoapparat
    case(3): { setItemIconToTemp(itemPhoto01Icon); break; } // Foto
    default: { isArrayCopy = false; break; } // Nicht belegt, darf aber auch nicht eintreten
  }

  if(isArrayCopy) { drawTile(relationPlaceX, relationPlaceY, mapTileSize, mapTileSize, tempIcon, false); }
  else { EsploraTFT.fillRect(relationPlaceX, relationPlaceY, mapTileSize, mapTileSize, 0xF800); }
  
  // einen Rahmen darueber zeichnen
  EsploraTFT.drawRect(relationPlaceX, relationPlaceY, mapTileSize, mapTileSize, mapNumberToColor(12));

  return true;
}

// Holt die anfangs Position des Taschenplatzes das auf dem Bildschirm gerendert werden soll.
void setItemRelationPlace(byte place, byte* relationPlaceX, byte* relationPlaceY) {

  if(place == 0 || place == 1 || place == 2) { *relationPlaceY = 96; }  // erste Zeile
  else if(place == 3 || place == 4 || place == 5) { *relationPlaceY = 112; }  // zweite Zeile

  if(place == 0 || place == 3) { *relationPlaceX = 0; } // erste Spalte 
  else if(place == 1 || place == 4) { *relationPlaceX = 16; } // zweite Spalte
  else if(place == 2 || place == 5) { *relationPlaceX = 32; } // dritte Spalte
}

// Prueft die Karten Id mit einem Objekt aus dem Rucksack.
bool getItemToUsed(int16_t mapUsageId) {

  int16_t itemId = 0;

  // hole itemId aus der Karteneigenschaft ab.
  if(mapUsageId == mapBarrierUsageDoor01) {
    itemId = 1; // Id des zu verwendenden Schlussels
  }

  // pruefe die Tasche, ob das Item vorhanden ist und dann aus dem inventar nehmen
  for(byte index = 0; index < sizeof(backPlaces); index++) {

    //       Item einmalig verwenden
    if(itemId != 0 && backPlaces[index] == itemId) {

      // Verwendungszweck
      if(mapUsageId == mapBarrierUsageDoor01) {

        mapBarrierDoorIsOpen = true;
        backPlaces[index] = 0; // aus dem Inventar entfernen
      }
    }
  }
  if(mapUsageId == mapBarrierUsageDoor01 && mapBarrierDoorIsOpen == true) {
    return true;
  }
   return false;
}

Die Tasche ist nun da. Jetzt fehlt noch das Einsammeln, dass mit Hilfe der Kollisionsabfrage ermöglicht. Bisher wurden nur die Werte für Begehbar und Wand geprüft. Auf der Karte kommt nun ein weiterer Wert hinzu, das für ein einzusammelndes Objekt steht. Damit wir diese Stelle wiedererkennen, muss auch das Rendern der Karte noch angepasst werden.

 ...
void renderMap(int positionX, int positionY, boolean renderAll) {

  // zum probieren wird zunächst ein Grid gerendert.
  byte index = 0;
  for(byte y = 0; y < mapTileCountY; y++) {
    for(byte x = 0; x < mapTileCountX; x++) {

      if(((positionX >= (int)(x * mapTileSize) - (int)mapTileSize && positionX <= (int)(x + 1) * (int)mapTileSize && 
          positionY >= (int)(y * mapTileSize) - (int)mapTileSize && positionY <= (int)(y + 1) * (int)mapTileSize)) || 
          renderAll) {
            byte bTile = pgm_read_byte_near(mapContent + index);

            // TODO: Kartenspezifische abhangigkeit, 
            //       Eigenschaften andern sich mit Kartenwechsel
            if(bTile == 2 && mapKeyIsGet) { bTile = 0; }
            if(bTile == 5 && mapBarrierDoorIsOpen) { bTile = 0; }
           
           renderMapTile(x, y, bTile);
      }
      index++;
    }
  }
}
...

Momentan werden die zwei Werte noch direkt in der Funktion 'renderMap' aufgerufen. Die ergänzende Ausführung ist Simple. Solange sich noch die Objekte an ihren Stellen befinden, werden die Kacheln in der vorgesehenden Farbe eingefärbt. Die Funktion 'renderMapTile' benötigt daher weitere 'case´s'.

...
void renderMapTile(byte x, byte y, byte mapSegment) {
  
  byte mapTileColorNumber = 0;
  switch(mapSegment) {
    case(1): { mapTileColorNumber = 10; break; }
    case(2): { mapTileColorNumber = 12; break; }
    case(5): { mapTileColorNumber = 13; break; }
    default: { mapTileColorNumber = 15; break; }
  }

  EsploraTFT.fillRect(x * mapTileSize, y * mapTileSize, mapTileSize, mapTileSize, mapNumberToColor(mapTileColorNumber));
}
...

Einsammeln und Verwenden
Die Kacheln, an dem eine Tür oder ein Schlüssel liegt, erfüllen zwei Eigenschaften. Die Kachel ist weiterhin begehbar und hat ein Objekt. Wurde das Objekt aufgenommen, wird jedoch im Karten Array der Wert nicht auf '0' gesetzt. Denn die Karte wird immer aus dem Flashspeicher geladen und kann nur gelesen werden. Deshalb werden neue Funktionsvariablen angelegt die den Status der Kachel wiedergeben. Das wird bereits in der Funktion 'renderMap' erledigt. Später erfüllen die Variablen auch für andere Karten dieselbe Rolle. Die Information wird jedoch für die Karte hinfällig, wenn sie verlassen wird. Aber dazu in einen späteren Post.

Die Kollisionsabfrage 'checkCollideNeighbor' wurde erweitert, um den Wert '2' und '5'. Die Werte '3' und '4' werden jetzt noch nicht verwendet, sollen aber später die selbe Eigenschaft haben, wie der Wert '2'. Der folgende Vorgang prüft ähnlich wie bei einer Kollision mit einer Wand. Allerdings wird hier nach einem Objekt geprüft, dass in der zu betretenden Kachel vorhanden ist.

 ...
  if(bTile == 2) {

    resultColide = checkCollide(positionX, positionY, mapOffsetX, mapOffsetY);

    // Abruf des Objektes, dass zu der Karte gehoert an der Position.
    if(!resultColide) {
      if(setItemToBackpack(1)) {
        mapKeyIsGet = true;
      }
      // nicht blockieren
      resultColide = true;
    }
  }
...

Der Wert '5' benötigt ein anderes Vorgehen, hält sich jedoch ebenfalls simpel. Auch hier wird vorher abgefragt, ob ein Hindernis besteht. Wenn nicht, dann prüfe ob die Tür offen ist oder der Schlüssel die Tür öffnet. In diesem Fall verschwindet der braune Block.

...
  if(bTile == 5) {

    resultColide = checkCollide(positionX, positionY, mapOffsetX, mapOffsetY);

    // Uebergabewert des Verwendungswecks > Tuer oeffnen.
    // kollision aufheben
    if(!resultColide) {
      
      // ID 1 ist der Schlüssel und entscheidet,
      // ob die Tuer sich oeffen laest.
      resultColide = getItemToUsed(1); 
    }
  }
...

Animationslos verschwindet die Tür. Hier färbt sich die braune Kachel in hell grün (sieht leider mehr grau aus), sowie die anderen Kacheln die begehbar sind.
So dass sollte Inhaltlich vom Blogpost reichen. Das Thema ist länger geworden als vorgesehen und dabei habe ich einiges noch gekürzt. Alles weiter sowie Kommentar Beschreibungen sind in den Sourcen eingetragen, die ich wieder auf Github hoch geladen habe.


Lange noch nicht fertig
Dass die Grundfunktionen noch nicht reichen, dürfte klar sein und viele würden lieber ein Schwert ziehen und Monster bekämpfen. Aber, wie bereits ein weiser Mann Sprach: "Wie soll das Schwert richtig geschwungen werden, wenn das nicht mal mit einem Stock geht".

Nächster Post: Von Händler, Kisten und Münzen (Arduino Esplora, Part 8)


Montag, 23. Oktober 2017

Du kommst hier nicht vorbei (Arduino Esplora, Part 6)


Sicherlich habt ihr entweder am Programcode oder beim Testen der Spielfunktionen bemerkt, dass die Kollisionsabfrage nur bedingt funktioniert. Sie ist zwar einfach, aber hier fehlt die Einschränkung, dass man sich nur von Block zu Block bewegen kann. Offen gestanden war ich kein Fan davon, das sich die Figur weiter bewegt bis der nächste Feld oder Kachel erreicht wurde.
Zu dem Thema Spieleprogrammierung und Kollisionsabfrage für 2D Spiele, können verschiedene Lösung im Internet gefunden werden. Ein Beispiel wird hier auf spieleprogrammierer.de/wiki beschrieben, wie man mit Geometrischen Objekten die Kollision Abfragen kann.


Die simple Form für die Kollisionserkennung ist das Verwenden von zwei Rechtecken. Im folgenden Code zeigt die Methode die wesentliche Abfrage von überschneidenden Rechtecken.

 // Kachel Position mit zukuenftiger Position der Figur abgeleichen,  
 // durch ansetzten von Rechtecken und ob diese sich ueberschneiden.  
 boolean checkCollide(byte positionX, byte positionY, byte mapOffsetX, byte mapOffsetY) {  
  if(positionX &lt; mapOffsetX + mapTileSize &amp;&amp;  
     positionX + 10 &gt; mapOffsetX &amp;&amp;  
     positionY &lt; mapOffsetY + mapTileSize &amp;&amp;  
     positionY + 16 &gt; mapOffsetY)  
   {  
    // DEBUG: Nur fuer debug und visuelle kontrolle  
    EsploraTFT.drawRect(positionX, positionY, 10, 16, 0xFA8A);  
    return false;  
   }  
  return true;  
 }  

Die Abfrage reicht jedoch nicht aus, um zu verhindern, dass die Figur wieder durch die Wand geht. Oft müssen auch übereinander oder nebeneinander liegende Kacheln zusätzlich geprüft werden. Für einen späteren Blogpost wird die Kachelgröße Reduziert von 16x16 auf 8x8. Spätestens dann wird die jetzige Abfrage erforderlich sein, alle Hindernisse zu erkennen. Das war leider nicht ganz ohne und zugegeben habe ich daran relativ viel Zeit damit verbracht, die Kollisionen durch zu debuggen.

 // Prüfen, ob in diesem Bereich sich bewegt werden kann.
boolean CanEnterArea(int positionX, int positionY) {
  boolean resultColide = true;
  
  // umliegende Kacheln auf hindernis prüfen
  // wenn hoch oder runter
  if(directionX == 0) {
    
    for(uint8_t i = 0; i < 3; i++) {
      
      int tileX = (positionX + (collisionTiles[i] * mapTileSize)) / mapTileSize;
      int tileY = -1;
 
      int tileYTemp = tileY;
      int positionYShift = 0;
 
      while(tileY == tileYTemp && tileY != 0) {
        if(directionY == -1) { 
          tileY = (positionY + directionY + positionYShift) / mapTileSize; 
          }
        else { tileY = (positionY + directionY + 16 + positionYShift) / mapTileSize; }
 
        positionYShift += mapTileSize * directionY;
      }
      
      resultColide = checkCollideNeighbor(positionX, positionY + directionY, tileX, tileY);
 
      if(!resultColide) {
        break;
      }
    }
  }
 
  // wenn links oder rechts
  if(directionY == 0) {
 
    for(uint8_t i = 0; i < 3; i++) {
      int tileX = positionX / mapTileSize;
      int tileY = (positionY + (collisionTiles[i] * mapTileSize)) / mapTileSize;
      
      int tileXTemp = tileX;
      int positionXShift = 0;
 
      while(tileX == tileXTemp) {
        if(directionX == -1) {  tileX = (positionX + directionX + positionXShift) / mapTileSize; }
        else { tileX = (positionX + directionX + 10 + positionXShift) / mapTileSize; }
        
        positionXShift += mapTileSize * directionX;
      }
  
      resultColide = checkCollideNeighbor(positionX + directionX, positionY, tileX, tileY);
  
      if(!resultColide) {
        break;
      }
    }
  }
  
  return resultColide;
}
 
// Laedt aus dem Flashspeicher die Kachelelemente ab und 
// prueft die Kollision mit neben anliegende Kacheln.
// Verhindert speziel den Fehhler zwischen zwei Kacheln, nur eine zu pruefen.
boolean checkCollideNeighbor(int positionX, int positionY, int tileX, int tileY) {
 
  boolean resultColide = true;
 
  int mapOffsetX = tileX * mapTileSize;
  int mapOffsetY = tileY * mapTileSize;
 
  int indexStart = (tileY * mapTileCountX) + tileX;
  // hole die content Nummer ab um die kollisionsart zu bestimmen
  byte bTile = pgm_read_byte_near(mapContent + indexStart);
 
  if(bTile == 1 && resultColide) {
 
    // DEBUG: Nur fuer debug und visuelle kontrolle
    EsploraTFT.drawRect(mapOffsetX, mapOffsetY,  mapTileSize, mapTileSize, 0xFA8A);
    resultColide = checkCollide(positionX, positionY, mapOffsetX, mapOffsetY);
  }
 
  return resultColide;
}
 

Nun eckt die Figur in positiven Sinne überall an und kann sich nicht mehr wie ein Geist durch die Wand bewegen. In einen späteren Post wird die Kollisionsabfrage auch für Türen verwendet, um z.B. einen Kartenwechsel auszulösen.
Für Debug und Demo Zwecke, werden die Rechtecke mit eingezeichnet, die visuell die Kollision abbilden.


Der Clip zeigt die ungenaue Kollisionsabfrage, wie sie zuvor war. Wie bereits beschrieben, war diese simple und schnell umgesetzt.


Mit der implementieren der Abfrage von überschneidenden Rechtecken sieht das Ergebnis besser aus.




Nächster Post: Ich packe in  meinen Rucksack (Arduino Esplora, Part 7)

Zu guter letzt der gesamte Programmcode auf Github

Github - BlogPost_06_BetterCollision

Samstag, 21. Oktober 2017

Voller Arbeitsspeicher (Arduino Esplora, Part 5)


Der Arduino oder auch vielmehr der verwendete Mikrocontroller hat für viele Anwendungen genügend Arbeitsspeicher. Im ersten Teil der Blogpost Reihe verwendete ich einen Arduino Nano, der einen ATmega328 hat und einen Arbeitsspeicher von 2kByte besitzt. Der Arduino Esplora verwendet den ATmega32u4 der wiederum 2,5kByte Arbeitsspeicher aufweist. Trotz des etwas größeren Arbeitsspeichers muss für dieses Projekt dennoch sparsam damit umgegangen werden.

ATmega328P und ATmega32u4

Arbeitsspeicher verbrauch
Ein Sprite Bild besteht selbst aus 160 Bytes. Das klingt jetzt nicht viel, aber verbraucht den Arbeitsspeicher bereits mit über 6%. Würde man die Sprite Animation der Figur nicht mit dem Trick einzelner Bilder spiegeln, dann würden insgesamt 2,92kByte Arbeitsspeicher anfallen. Stattdessen werden momentan 1,12kB verwendet, dass allerdings für das Ziel immer noch zu viel ist. Und dann kommt noch die Karte mit 160 Bytes hinzu, die noch sehr grob ist. Da bleibt am Ende nicht viel übrig. Der jetzige Sketch verwendet ca. 1,575kBytes Arbeitsspeicher.

Vom Flashspeicher
Im Gegensatz zu dem insgesamten Flash Speicher mit 32kB, ist dieser gerade mal mit 12,45kB belegt. Damit liegt nahe, dass Sprites und weitere Daten am besten zur Laufzeit geladen werden. Hier kommt ein Kompromiss zustande über die Lesegeschwindigkeit von Flasch und RAM.
Eine kleine Umstellung und die Byte Array lassen sich aus dem Flashspeicher lesen, wenn diese zur Laufzeigt benötigt werden. Die folgenden Ergebnisse nach dem Kompilieren zeigen den Unterschied zwischen dem Sketch vom letzten Stand mit dem Anlegen der Karte und das gleiche jedoch nach der Umstellung mit PROGMEM.

Ohne PROGMEM

Mit PROGMEM

Weitere Informationen könnt ihr auf der Arduino Seite über PROGMEM erfahren.

Das folgende Code Ausschnitt zeigt die Änderung der Funktionsvariable eines Byte Array ergänzt wird.

// Figur Sprites load from lokal
byte spriteFigureFrontLeft[160] = { …

// Figur Sprites load from flash
const PROGMEM byte spriteFigureFrontLeft[160] = { …

Sobald alle Byte Arrays mit dem Präfix 'const' und 'PROGMEM' erweitert wurden, dürfte der Belegte Speicher deutlich gesunken sein. Ein Byte Array bleibt allerdings immer im Speicher, dass ist der Buffer oder wie im Beispiel 'tempArray' benannt wird aus dem Flash in das Byte Array geladen, das über eine einfache Funktion übertragen wird.

// kopiert den Array Inhalt vom Flashspeicher in den SRAM
void memCopy(byte arrayContent[]) {
  for(byte index = 0; index < 160; index++) {
    tempArray[index] = pgm_read_byte_near(arrayContent + index);
  }
}

Wann ist der Einsatz von PROGMEM Sinnvoll
Alle Funktionsvariablen die nicht zur aktuellen Ausführung verwendet werden, könnten über die Funktion erweitert werden. Also eine Spriteanimation rendert immer nur eines der angelegten Sprites.

Nächster Post: Du kommst hier nicht vorbei (Arduino Esplora, Part 6)

Ameisen Simulation und andere Dinge

Wer lange sich schon mit C# beschäftig, hat sicherlich schon mal was von AntMe gelesen oder gehört. Diese Idee hatte ich aufgegriffen und mi...