Posts mit dem Label SPI 128x160 werden angezeigt. Alle Posts anzeigen
Posts mit dem Label SPI 128x160 werden angezeigt. Alle Posts anzeigen

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)

Montag, 16. Oktober 2017

Karte anlegen (Arduino Esplora, Part 4)


Eine Figur durch einen leeren Raum zu steuern, ist auf Dauer sehr öde. Man kann nun den Hintergrund zunächst eine Farbe geben, ist aber dennoch sehr eintönig ist. Schauen wir uns andere Spiele an, könnte man meinen, dass alles in der Umgebung in Blöcken unterteilt ist.
Und so wird dies auch in diesem Beispiel umgesetzt. Die Karte wird Blockweise angelegt. Das ermöglicht uns weiterhin nur die Bereiche neu zu rendern, die sich auch geändert haben.

Karten Eigenschaften
Mit der Unterteilung in Blöcken, kann ein Block verschiedene Eigenschaften aufweisen. Hier stellt die '0' die Frei Begehbaren Blocke da, in dem sich die Figur bewegen kann. Der Wert '1' wiederum stellt eine Mauer da, an dem die Figur nicht hindurch gehen kann. Die Fläche eines Blockes ist etwas größer als die der Figur. Daher weist die Kanten länge 16 Pixel mal 16 Pixel auf.


Ordnung ist das halbe Leben
Zunächst muss vorweg etwas Ordnung eingebracht werden. Zwar habe ich bereits mit dem letzten Post die Funktionen zu der Figur in einen eignen Tab/Seite eingesetzt, aber ich bin nicht weiter darauf eingegangen.
Damit nicht zu viel Code auf einer Seite ist, teilen wir die Inhalte in Zugehörigkeiten auf. Somit kommen die Funktionen/Methoden für die Farbe und das Ausfüllen eines Quadrates in einen eignen neuen Tab mit dem Namen 'RenderComponent'. Die Funktionen zur Karte werden unter 'MapComponent' abgelegt und 'FigureComponente' sollte nur noch die Inhalte zur Spielfigur haben.












Das Schreiben von Pixeln
Die Methode mit dem die Farbnummern, die die Farbwerte für die Ausgabe zurückgibt, kommt in den 'RenderComponent'.  Die zuvor verwendete Funktion/Methode drawFigurArray wird zusammengefasst und um weitere Parameter erweitert, die dann drawTile genannt wird. Im späteren Blog Post Teil, wird die Funktion/Methode nicht nur für das Rendern der Figur eingesetzt.

 void drawTile(int relationX, int relationY, byte tileWidth, byte tileHeight, byte tilePic[], boolean mirror) {  
  int index = 0;  
  for(int y = 0; y &lt; tileHeight; y++) {  
   for(int x = 0; x &lt; tileWidth; x++) {  
     int indexTarget = index;  
     if(mirror) {  
      indexTarget = index - x + (tileWidth - x) - 1;  
     }  
     byte colorNumber = tilePic[indexTarget];  
     // Nur Farbe  
     if(colorNumber != 0) {  
      EsploraTFT.drawPixel(relationX+x, relationY+y, mapNumberToColor(colorNumber));  
     }  
     index++;  
   }  
  }  
 }  

 uint16_t mapNumberToColor(byte c) {  
  uint16_t result = ST7735_RED;  
  switch(c) {  
   case(1):{ result = ST7735_BLACK; break; }  
   case(2):{ result = 0xF590; break; } // haut  
   case(3):{ result = 0x81E1; break; } // braun  
   case(4):{ result = 0xC2C2; break; } // hell braun  
   case(5):{ result = 0x8300; break; } // braun gelb  
   case(6):{ result = 0x5406; break; } // gruen  
   case(7):{ result = 0x32A4; break; } // dunkel gruen  
   case(8):{ result = 0xAE91; break; } // hell gruen  
   case(9):{ result = 0x2146; break; } // dunkel grau blau  
   case(10):{ result = 0x31E9; break; } // grau blau  
   case(11):{ result = 0x84B6; break; } // hell blau  
   case(13):{ result = 0xFC08; break; } // orange  
   case(14):{ result = 0xFA8A; break; } // hell rot  
   case(15):{ result = 0xD759; break; } // hell gruen 2  
   default: {  
    result = 0;  
    break;  
   }  
  }  
  return result;  
 }  

Sprite Render Methode ändert sich
Nun sollte auch der Programmcode in 'FigureComponent' angepasst werden. Die Methode 'drawFigure' hatte zum Zeichnen die Methode 'drawFigureArray' und wird nun mit der Methode 'drawTile' aus dem Tab 'RenderComponent' ersetzt. Folgender Code zeigt einen Ausschnitt der Änderung. Wie zu sehen ist, wird nun die Breite und Höhe das Sprite übergeben und der Parameter für 'Clear' entfällt.

…
    if(directionX == 0 && directionY == 1) {
      switch(animStep){
        case(0): { drawTile(relationX, relationY, 10, 16, spriteFigureFrontLeft, false); break; }
        case(1): { drawTile(relationX, relationY, 10, 16, spriteFigureFrontMiddle, false); break; }
        case(2): { drawTile(relationX, relationY, 10, 16, spriteFigureFrontLeft, true); break; }
        default: {  drawTile(relationX, relationY, 10, 16, spriteFigureFrontMiddle, false); break; }
      }
    }
…

Die Karte
Für das Anlegen einer Karte wird ein weiteres Byte Array angelegt. Ein Byte stellt eine Kachel Information da. Wie bereits am Anfang des Posts beschrieben, ist '0' Begehbar und '1' wiederum nicht. Da pro Kachel 16 Pixel mal 16 Pixel groß ist, können in der Breite zehn Kachel Nebeneinander aufgestellt werden. Untereinander werden sechs Kacheln angelegt. Der Restliche Bereich unten bleibt frei für später kommende Spieldaten.

Über die Kacheln und anecken
Die erste Methode 'renderMap' liest das Array ein, dass die Karteninformation auf das Display Zeichnet. Mit 'canEnterArea' wird auf einfache Weise die zu betretende Kachel geprüft, ob diese begehbar ist. Wo die Funktion eingesetzt wird, komme ich in einen späteren Absatz . In einen späteren Absatz gehe beschreibe ich, wo diese Funktion ihren Einsatz findet.
Hier sei Angemerkt, dass die Kollisionsabfrage wirklich sehr simple ist, so dass ein durchlaufen unter Umständen dennoch möglich ist. Eine bessere Lösung zu diesen Thema, gehe ich jedoch erst in einen späteren Post darauf ein.

Zuletzt für das Zeichnen einer Kachel, unternimmt die Methode 'renderMapTile' eigentlich zwei Aufgaben. Sie ruft zu dem Byte eine Farbnummer ab und verwendet diesen Wert, um eine Kachel Ausgefüllt auf dem Display zu zeichnen. Genauere Beschreibungen zu den Funktionen und Variablen findet im Sourcecode.

// Karte
byte mapContent[160] =  { 
  1,1,1,1,1,1,1,1,1,1,
  1,0,1,0,0,0,0,0,0,1,
  1,0,1,0,0,1,0,0,0,1,
  1,0,1,0,0,1,1,1,0,1,
  1,0,0,0,0,0,0,0,0,1,
  1,1,1,1,1,1,1,1,1,1,
};

// Groeße einer Kachel
byte mapTileSize = 16;
// Anzahl Kacheln auf der X Achse
byte mapCountX = 10;
// Anzahl Kacheln auf der Y Achse
byte mapCountY = 6;

void renderMap(int positionX, int positionY, boolean renderAll) {
  byte index = 0;
  for(byte y = 0; y < mapCountY; y++) {
    for(byte x = 0; x < mapCountX; 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) {
        renderMapTile(x, y, mapContent[index]);
      }
      index++;
    }
  }
}

// Einfache Kollisionsabfrage
boolean canEnterArea(byte positionX, byte positionY) {

  // Kachel Kordinate abrufen
  byte tileX = positionX / 16;
  byte tileY = positionY / 16;

  // Index aus dem Array abfragen
  byte index = (tileY * mapCountX) + tileX;

  // ist das Feld begehbar
  if(mapContent[index] == 0) {
    return true;
  }
  
  return false;
}

// rendert die Kacheln Einfarbig.
void renderMapTile(byte x, byte y, byte mapSegment) {
  
  byte mapTileColorNumber = 0;
  switch(mapSegment) {
    case(1): { mapTileColorNumber = 10; break; }
    default: { mapTileColorNumber = 15; break; }
  }

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

Blockade prüfen
In der 'loop' Funktion wird je nach Ausrichtung die Position um einen hoch oder runter gezählt. Mit der neuen Funktion 'canEnterArea' aus dem Tab 'MapComponent' kann nun verhindert werden, dass die Figur nicht in eine Blockwand laufen kann. Der folgende Code zeigt die Bedingung für Links mit der neuen Funktion und der Parameter Übergabe über die nächste Position.

…
if(buttonLeft && !buttonRight && lastPosX > 0) {
    // nach links und letzte Position Y ist groesser als '0'.
    if(canEnterArea(lastPosX - 1, lastPosY)) {
      lastPosX--;
    }
  }
…

Offenes und ausbessern
Es funktioniert zwar schon, aber so ganz Rund sind die gängigen Funktionen noch nicht. Die kollisionsabfrage funktioniert nicht unter jeder Bedingung und die Figur flimmert. Dennoch sind diese Groben Ausführungen keine Primären Probleme und werden daher später verbessert, sobald diese ein Problem darstellen.
Der Nächste Schritt ist die Arbeitsspeicherauslastung zu verbessern. Denn derzeitig werden noch nicht viele Inhalte angezeigt, aber der Arbeitsspeicher ist mit den jetzigen Inhalten ist fast voll.

Nächster Post: Voller Arbeitsspeicher (Arduino Esplora, Part 5)


Sonntag, 17. September 2017

Eigene Sprites erstellen (Arduino Esplora, Part 3.1)


Im Internet habe ich nach einer Einfachen Lösung gesucht, wie mein ein Sprite bzw. Bild  auf seine eigenen Anforderungen erstellen kann. Damit ist gemeint, dass eine durchgehende Farbpallette für das Eigene Ziel abbilde und Numerisch bezeichnen kann. Zudem sollte dies in einer Byte Folge ausgegeben werden, so dass ich diese im Programmcode ablegen kann.
Natürlich gibt es so ein Programm nicht. Im Grunde ist die Ausgabe eines Bildes durch ein Skript zu übersetzen relative einfach oder auch mal schnell ein eigenes kleines Programm schreiben. Denn das habe ich zunächst gemacht, um schnell eigene Sprites anzulegen. Also zwei Abende dran ran gesetzt und fertig war der Bildeditor. Die Benutzbarkeit beschränkte sich auf die mehr auf die Verwendung der Funktionen.
Mit der Zeit wurden dann noch ein paar Farben hinzugefügt und neu Sortiert, ansonsten hat sich nichts weiter geändert.


  
Übung ist trainieren
Irgendwann bekam es mich doch. Ich schrieb zu Übungszwecken weiter und räumte einiges an Programmcode auf, fügte ein paar weitere Funktionen hinzu und verpasste noch ein paar Optische Verbesserungen. Da ich nicht immer Zeit hatte, verstrichen Monate und ich stellte mal wieder fest, wenn man etwas ordentlich macht, dann kann dabei schon eine Menge Zeit vergehen. Oberflächlich wird man die Mühe nicht sehen, nur die Dinge die nicht funktionieren. Schließlich sind wir in Deutschland und gemeckert wird immer.

Wie verwende ich das Programm
Man schreibt ein Programm, kopiert sich die fertigen Code aus dem Beispiel, malt ein Bild und klickt auf Exportieren. Na gut, etwas mehr Detaillierter darf die Beschreibung dann doch sein.

Am besten verwendet ihr den folgenden Beispiel Programmcode. Je nach Größe des Bildes, müssen die Breite und Höhe angepasst werden. Das sind die Member Variablen 'pictureWidth' und 'pictureHeight', sowie auch die Array Größe und dessen eingetragenen Byte Werte. Im Beispiel ist ein Bild das 10 Pixel Breit und 16 Pixel hoch ist. Multipliziert man die beiden Werte, dann bekommen wir den Wert 160, dass hier die Byte Array Größe festlegt.

 #include <SPI.h>  
 #include <TFT.h>  
 #include <Esplora.h>  
 // zu renderndes Bild  
 int pictureWidth = 10;  
 int pictureHeight = 16;  
 byte picture[160] = {  
  0,0,1,1,1,1,1,1,0,0,0,1,3,3,3,3,3,3,1,0,1,3,3,3,3,3,3,3,3,1,1,3,3,4,4,2,4,3,3,1,1,3,5,5,2,2,5,5,3,1,1,3,4,1,2,2,1,4,3,1,0,1,2,1,2,2,1,2,1,0,0,0,1,2,2,2,2,1,1,0,0,1,8,8,3,3,8,8,6,1,1,8,8,8,8,8,8,6,2,1,1,2,1,8,8,8,8,1,1,0,0,1,1,10,10,7,7,1,0,0,0,1,9,10,1,7,6,1,0,0,0,0,1,1,1,7,6,1,0,0,0,0,0,0,1,9,11,1,0,0,0,0,0,0,0,1,1,0,0,0  
  };  
 void setup() {  
  // init display  
  EsploraTFT.begin();  
  EsploraTFT.initR(INITR_BLACKTAB);  
  EsploraTFT.setRotation(1);  
  EsploraTFT.background(0, 0, 0);  
 }  
 void loop() {  
   drawPictureArray(40, 40, picture);  
 }  
 uint16_t mapNumberToColor(byte c) {  
  uint16_t result = ST7735_RED;  
  switch(c) {  
   case(1): { result = ST7735_BLACK; break; }  
   case(2): { result = 0xF590; break; } // haut  
   case(3): { result = 0x81E1; break; } // braun  
   case(4): { result = 0xC2C2; break; } // hell braun  
   case(5): { result = 0x8300; break; } // braun gelb  
   case(6): { result = 0x5406; break; } // gruen  
   case(7): { result = 0x32A4; break; } // dunkel gruen  
   case(8): { result = 0xAE91; break; } // hell gruen  
   case(9): { result = 0x2146; break; } // dunkel grau blau  
   case(10):{ result = 0x31E9; break; } // grau blau  
   case(11):{ result = 0x84B6; break; } // hell blau  
   case(12):{ result = 0xFFE0; break; } // gelb  
   case(13):{ result = 0xFC08; break; } // orange  
   case(14):{ result = 0xFA8A; break; } // hell rot  
   case(15):{ result = 0xD759; break; } // hell gruen 2  
   case(16):{ result = 0xF800; break; } // rot  
   case(17):{ result = 0x8208; break; } // dunkel braun  
   case(18):{ result = 0xC618; break; } // grau  
   case(19):{ result = 0xF7BE; break; } // sehr hell grau  
   case(20):{ result = 0xFE97; break; } // hell haut  
   default: {  
    result = 0;  
    break;  
   }  
  }  
  return result;  
 }  
 void drawPictureArray(int relationX, int relationY, byte pictureArray[]) {  
  int index = 0;  
  for(int y = 0; y < pictureHeight; y++) {  
   for(int x = 0; x < pictureWidth; x++) {  
     EsploraTFT.drawPixel(relationX+x, relationY+y, mapNumberToColor(pictureArray[index]));  
     index++;  
    }  
   }  
 }  

Wenn ihr den Bild Editor Startet, dann wird gleich ein neues Bild von 10 mal 16 Pixel angelegt.


Über ‚Edit' kann die Größe des Pixelfeldes geändert werden. ACHTUNG: Nach Umstellung der Größe, gehen bereits eingetragene Pixel verloren.


Ist das Bild fertig, dann kann über File -> Export die Byte Kette selektiert und per STRG+C kopiert werden.


Über STRG+V wird zwischen den geschweifte Klammern der Member Variable 'picture' hinzugefügt. Ggf. Breite, Höhe und byte Array Größe anpassen und schon kann der Programmcode auf den Arduino Esplora geschrieben werden.


Sobald das Programm auf dem Arduino Esplora ausgeführt wird, sollte nun das Bild zu sehen sein, dass als Byte Kette hinein kopiert wurde.


Hinweise:
  • Im Programm sind bereits mehr Farben hinterlegt, da ich zu diesen Zeitpunkt mit dem Eigentlichen Projekt weiter gearbeitet und weitere Farben ergänzt habe.
  • Touch Fähigkeit wird noch bearbeitet.
  • Ein Bild mit Programmcode darf nicht mehr Bytes verbrauchen als der Maximale Arbeitsspeicher des Arduinos.

Der Bildeditor ist an für sich fertig. Fehler sind nicht ausgeschlossen und können über die Kommentar Funktion des Blogs eingetragen werden.

Nächster Post: Karte anlegen (Arduino Esplora, Part 4)

Low Pixel Maker Download
Anmerkung: Ich räume noch das kleine Programm auf. Sobald ich damit fertig bin, lade ich die Solution auf Github hoch.

Sonntag, 3. September 2017

Bildanimation (Arduino Esplora, Part Teil 3)


Die Adafruit GFX Bibliothek gibt uns die Möglichkeiten Pixel für Pixel auf das TFT zu schreiben, womit sich dann auch ein Bild zusammen setzen lässt. Bei größeren Bilder sollte klar sein, dass der Bildaufbau mit 16MHz nur langsam abläuft. Als Ziel ist jedoch eine Darstellung zur Laufzeit zu verändert, wie z.B. eine Runde Analoge Anzeige.


Relativ schnell stellte sich heraus, dass die Umsetzung einer solchen Anzeige zwar einfach ist, aber ab einer bestimmten Größe zu langsam gerendert wird. Alternative und einfacher ist die das Verwenden von bereits fertigen Bildern in 16 mal 16 Format. Zugegeben ist eine Analoge Anzeige mit dieser Auflösung sehr grob und auf Dauer nicht zu friedend stellend. Eine Low Pixel Figur wiederum würde passen und das kombiniert mit den Tasten, könnte die Figur auch über den Bildschirm gesteuert werden. An dieser Stelle erinnerte ich mich wieder an den Anfang von Octoawesome von Tom Wendel, der in seinen ersten folgen ähnliche Schritte unternahm ein Spiel zu entwickeln mit C# und Windows Forms (später wurde die Windows form Oberfläche abgelöst  durch MonoGame).

Technische Umgebung und Anforderung
Kommen wir zu den Bildern die zunächst auf ein Format gebracht werden müssen, die möglichst wenig Ressourcen verbrauchen. Mit dem Format 16x16 muss jeder Pixel eine Farbe zugewiesen werden. Der Bildschirm unterstützt 16Bit, womit zwei Byte pro Pixel anfallen würden. Das wären dann 512 Bytes für ein Bild, womit dann eine Sinnvolle Animation mit 2,5 Kilobyte SRAM nicht sinnvoll wäre.
An der Stelle sollten man sich vor Augen halten, wie viele Farbabstufungen 16Bit haben. Und dann schaut man nochmal auf die Anforderung. Daraus stellen sich die Fragen:
Wie viele Farben werden benötigt?
Wie viele Pixel braucht meine Figur?


Auf die Hälfte und weniger reduzieren
Anstatt zwei Byte als Pixelfarbinformation zu hinterlegen, wird dies auf ein Byte reduziert. Der Byte Wert trägt später nur die Nummer aus einer Farbpallette. Mit einer entsprechenden Mapping Funktion, wird dann später die Farbe für den Pixel abgerufen. Durch diesen Vorgang reduziert sich das Bild von 16x16 von 512 auf 256 Bytes. Die Figur selbst benötigt in der Breite nur zehn Pixel, womit der Speicher verbrauch sich auf 160 Bytes weiter reduziert.

 uint16_t mapNumberToColor(byte c) {  
  uint16_t result = ST7735_RED;  
  switch(c) {  
   case(1):{ result = ST7735_BLACK; break; }  
   case(2):{ result = 0xF590; break; } // haut  
   case(3):{ result = 0x81E1; break; } // braun  
   case(4):{ result = 0xC2C2; break; } // hell braun  
   case(5):{ result = 0x8300; break; } // braun gelb  
   case(6):{ result = 0x5406; break; } // gruen  
   case(7):{ result = 0x32A4; break; } // dunkel gruen  
   case(8):{ result = 0xAE91; break; } // hell gruen  
   case(9):{ result = 0x2146; break; } // dunkel grau blau  
   case(10):{ result = 0x31E9; break; } // grau blau  
   case(11):{ result = 0x84B6; break; } // hell blau  
   default: {  
    result = 0;  
    break;  
   }  
  }  
    
  return result;  
 }  

Bild Editor in Arbeit
Kommen wir zum Erstellen eines Bildes. Ein Bild Byte für Byte zu schreiben ist so spaßig wie einen Film auf Indisch mit Kantonesischen Untertitel zu schauen. Hier für stelle ich demnächst ein kleines Tool zur Verfügung, mit denen ihr Farbpixel für Farbpixel euer Bild Zeichnen und nach Fertigstellung die byte Kette in den Sketch kopieren könnt.




Keine Metadaten
Die Byte Kette selbst enthält keine Metadaten. Das bedeutet, dass das Bild nicht weist wie Breit und wie hoch sie ist. Diese Informationen müssen vom Programmcode festgelegt werden.

 void drawFigurArray(int relationX, int relationY, byte figureArray[], boolean mirror, boolean clearColor) {  
  int index = 0;  
  for(int y = 0; y < 16; y++) {  
   for(int x = 0; x < 10; x++) {  
    if(clearColor) {  
      EsploraTFT.drawPixel(relationX+x, relationY+y, mapNumberToColor(figureArray[1]));  
    }  
    else {  
     int indexTarget = index;  
     if(mirror) {  
      indexTarget = index - x + (10 - x) - 1;  
     }  
   
     EsploraTFT.drawPixel(relationX+x, relationY+y, mapNumberToColor(figureArray[indexTarget]));  
     index++;  
    }  
   }  
  }  
 }  

Sprite Animation
Den Sketch den ich zur Verfügung stelle, kann die Farbnummern übersetzen aus dem Byte Array und so mit ein Bild auf den TFT schreiben. Hier fehlt nur die Sequenz abfolge der verwendeten Sprites als Animation. Würde man die paar Bilder direkt hintereinander abspielen, würden diese zu schnell ablaufen. Mit 'delay' lässt sich dieses Problem provisorisch lösen, führt jedoch zu Problemen mit dem späteren Abfragen der Taster. Deshalb werden zwei Variable vom Typ long und int im Funktionsvariable angelegt. Der mit dem Typ long (hier gametime benannt), wird pro Methoden Loop durchlauf, um einen hochgezählt. Der zweite Wert von Type int Mit einer Modulo Abfrage wird dann immer der vierte Durchlauf verwendet, um den Sequenz Bildindex der Animation um einen fortzusetzen oder von vorne abzuspielen.
Die Figur kann sich in vier Richtungen bewegen, womit dann auch vier verschiedene Animationsabläufe sich abbilden. Für Links und Rechts können die selben Bilder verwendet werden, da für die gegenteilige Richtung gespiegelt werden kann.
UPDATE: Zuvor war an der Methode der Parameter 'walk' übergeben worden, der jetzt entfällt. Und 'default' führt jeweils nochmal das zweite Sprite aus, womit dann die Figur weniger flimmern sollte.

 void drawFigure(int directionX, int directionY, int relationX, int relationY) {  
   
  if(gameTime % 4 > 0) {  
   if(animStep > 2) {animStep = 0;}  
   else {animStep++;}  
  }  
   
   if(directionX == 0 && directionY == 1) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureFrontLeft, false, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureFrontMiddle, false, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureFrontLeft, true, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureFrontMiddle, false, false); break; }  
    }  
   }  
   else if(directionX == -1 && directionY == 0) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureSideLeft, false, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, false, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureSideRight, false, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, false, false); break; }  
    }  
   }  
   else if(directionX == 0 && directionY == -1) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureBackLeft, false, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureBackMiddle, false, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureBackLeft, true, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureBackMiddle, false, false); break; }  
    }  
   }  
   else if(directionX == 1 && directionY == 0) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureSideLeft, true, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, true, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureSideRight, true, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, true, false); break; }  
    }  
   }  
 }  

Die Animation sollte nur dann abgespielt werden, wenn eines der Tasten gedrückt wurde. Im zweiten Teil der Blogpost Reihe wurde nur ein Kreis bewegt und wurde von der Methode in der folgenden If Abfrage ausgeführt.

 // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.  
  if(lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {  
   drawPoint(lastPosXtemp, lastPosYtemp, false);  
   drawPoint(lastPosX, lastPosY, true);  
  }  

Der Inhalt der If Abfrage Zeichnete einen Kreis, der durch die Sprite Animation ersetzt wird. Hier werden nun die Joystick eingaben auf ihre Ausrichtung geprüft bevor die Figur mit den richtigen Sprite Bilder gezeichnet werden.

 // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.  
  if(lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {  
   
   int directX = 0;  
   int directY = 0;  
   
   if(lastPosXtemp > lastPosX) { directX = -1; }  
   if(lastPosXtemp < lastPosX) { directX = 1; }  
   if(lastPosYtemp > lastPosY) { directY = -1; }  
   if(lastPosYtemp < lastPosY) { directY = 1; }  
   
   drawFigure(directX, directY, lastPosX, lastPosY);  
  }  

So das sollte erstmal alles sein für diesen Post. Im nächsten Post kommt eine Beschreibung zum Bild Editor, dass für das Erstellen der Sprites erleichtert.

Nächster Post: Eigene Sprites erstellen (Arduino Esplora, Part 3.1)

Mittwoch, 5. April 2017

Umzug auf passende Plattform (Arduino Esplora, Part 2)


Nachdem ich viel probiert habe und dabei fast ein Spiel zusammen hatte (Nicht Pong, das ist zu einfach), entschied ich den Arduino Esplora zu bestellen. Normalerweise würde ich vom Breadboard umziehen und dann etwas selbst auf eine Platine mit den entsprechenden Komponenten zusammenlöten. Aber warum nicht eine fertige Plattform nutzen.

Etwas enttäuschend, fand ich die Suche im Internet, weil ich keine aufwendigen Spiele für den Arduino Esplora entdeckt habe. Damit will ich das nicht schlecht reden, aber etwas mehr hatte ich schon erwartet.

Der Umzug vom Breadboad auf den Arduino Esplora ist in wenigen Schritten erledigt. Als erstes werden die Adafruit Bibliotheken gegen die 'Esplora.h' und 'TFT.h' ausgetauscht. Die Beschreibung an welchen Pin vom TFT zum Arduino Uno (oder Nano) verbunden werden soll, sowie auch die Pin Variablen entfallen. Das Initialisieren der Pins sowie auch das TFT Display, wird durch ein 'EsploraTFT.begin()' ersetzt. Die Steuerrichtung wird durch den Joystick abgenommen, allerdings bleibt das Verhalten der Bewegungsgeschwindigkeit. Hinzu kommt noch, dass ein Offset benötigt wird. Bei analogen Eingängen kann es zu Abweichungen kommen die durch das Offset korrigiert werden können. Für diesen Zweck ist eine neue Methode hinzugekommen, mit der die Werte auf dem Display angezeigt werden können.


 #include <SPI.h>   
 #include <TFT.h>   
 #include <Esplora.h>   
   
 // ruft die letzte Postion X ab. (Pixel Position)   
 int lastPosX = 5;   
 // ruft die letzte Postion Y ab. (Pixel Position)   
 int lastPosY = 5;   
   
 // mittelstellung des Joystick   
 int offsetX = -3;   
 int offsetY = 4;   
   
 void setup() {  
   EsploraTFT.begin();   
   EsploraTFT.background(0, 0, 0);   
 }   
   
 void loop() {   
   
   // Eingänge einlesen   
   int stickX = Esplora.readJoystickX() + (offsetX * -1);   
   int stickY = Esplora.readJoystickY() + (offsetY * -1);   
   
   boolean buttonLeft = false;   
   boolean buttonRight = false;   
   boolean buttonUp = false;   
   boolean buttonDown = false;   
   
   if(stickX > 3 || stickX < -3) {   
     buttonLeft = stickX > 3;   
     buttonRight = stickX < -3;   
   }   
   
   if(stickY > 3 || stickY < -3) {   
     buttonUp = stickY < -3;   
     buttonDown = stickY > 3;   
   }   
   
   // Werte ausgeben auf dem Bildschirm   
   writeValue(10, 10, stickX, false);   
   writeValue(10, 20, stickY, false);   
   
   // Temporaer letzte Position merken   
   int lastPosXtemp = lastPosX;   
   int lastPosYtemp = lastPosY;   
   
   // Abfragen zu den gesetzten joystick   
   // Es kann nur in eine Richtung die Bedingung erfüllt werden.   
   
   // Wenn nach links oder rechts gedrückt wird.   
   if(buttonLeft && !buttonRight && lastPosX > 0) {   
     // nach links und letzte Position Y ist groesser als '0'.   
     lastPosX--;   
   }   
   else if(!buttonLeft && buttonRight && lastPosX < EsploraTFT.width()) {   
     // nach rechts und letzte Position X ist kleiner als die TFT Pixel Breite.   
     lastPosX++;   
   }   
   
   // wenn nach oben oder unten gedrückt wird.   
   if(buttonUp && !buttonDown && lastPosY > 0) {   
     // nach oben und letzte Position Y ist groesser als '0'.   
     lastPosY--;   
   }   
   else if(!buttonUp && buttonDown && lastPosY < EsploraTFT.height()) {   
     // nach unten und letzte Position X ist kleiner als die TFT Pixel hoehe.   
     lastPosY++;   
   }   
   
   // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.   
   if(lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {   
   
     // vorigen punkt entfernen mit den temporären Positionen.   
     drawPoint(lastPosXtemp, lastPosYtemp, false);   
   
     // neuen punkt zeichnen mit der neuen Position.   
     drawPoint(lastPosX, lastPosY, true);   
   }   
   
   writeValue(10, 10, stickX, true);   
   writeValue(10, 20, stickY, true);   
 }   
   
 void writeValue(int x, int y, int val, boolean clr) {   
   if(clr) {   
     EsploraTFT.stroke(0, 0, 0);   
   }   
   else {   
     EsploraTFT.stroke(30, 200, 50);   
   }   
   String accResult = String(val);   
   char valuePrint[5];   
   accResult.toCharArray(valuePrint, 5);   
   EsploraTFT.text(valuePrint, x, y);   
 }   
   
 // Einachen Punkt Zeichnen, der nicht ausgefuellt ist.   
 void drawPoint(int x, int y, boolean setColor) {   
   
   // Schwarz Zeichnen.   
   EsploraTFT.stroke(0, 0, 0);   
   
   // Farbe festlegen   
   if(setColor) {   
     EsploraTFT.stroke(120, 100, 200);   
   }   
   
   EsploraTFT.circle(x, y, 2);   
 }   

Sobald das Programm auf dem Esplora ist, sollte mit dem Lauf des Programms die Werte für X und Y des Joysticks zu sehen sein. Die angezeigten Wert zeigen die Abweichungen, die im Anschluss im Offset einzutragen sind.

Der Arduino Esplora ist eine schöne Plattform und kann wie jeder andere Arduino Programmiert und angesteuert werden. Das Potenzial an Möglichkeiten ist da. Abgesehen vom Joystick, Buttons und dem passenden TFT, sind noch weitere Sensoren und Aktoren auf der Plattform vorhanden. Falls diese nicht reichen sollten, dann können oben an die Stecker weitere Komponenten angeschlossen werden und Betrieben werden.

Einen wesentlichen Unterschied gibt es zwischen den Arduino Nano (den ich zuvor verwendet habe) und dem Arduino Esplora. Der SRAM vom ATMega32u4 ist um 512 Bytes größer 😃

Technische Daten
Microcontroller:   ATmega32u4
Flash Speicher:    32kb (4kb werden vom bootloader verwendet)
SRAM:                2,5 kb
EEPROM:           1 kb
Takt:                    16 MHz

Sensoren / Eingabemöglichkeiten: Analog X Y Joystick
                                                        4 Taster
                                                        Linear Potentiometer
                                                        Mikrofone
                                                        Lichtsensor
                                                        Drei Achsen Beschleunigungssensor

Aktor, Widergabe, Ausgabe:          Buzzer
                                                        RGB LED
                                                        TFT Display (gibt's wahlweise mit als Bundle)




Github - BlogPost_02_MoveLibrary

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...