Wir haben nun das Zeitalter der KI erreicht, weshalb hier weniger Lösungen und mehr das Technik Tagebuch Posts kommen.
Lösungen verlinke ich Kategorisch extra auf den weiteren Seiten des Blogs
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.
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.
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)
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.
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)
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)
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.
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.
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'.
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.
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".
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 < mapOffsetX + mapTileSize &&
positionX + 10 > mapOffsetX &&
positionY < mapOffsetY + mapTileSize &&
positionY + 16 > 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.
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.
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
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.