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)


Kommentare

Beliebte Posts aus diesem Blog

Arduino Control (Teil 5) - PWM Signal einlesen

RC Fahrtenregler für Lego Kettenfahrzeug

Angular auf dem Raspberry Pi