ArraysDatenreihen

Thema der Lesson ist die Einführung in Arrays - Datenreihen. Nach einer kurzen Wiederholung zu Variablen und Datentypen werden der Aufbau und Umgang mit Arrays erläutert, sowie praktische Beispiele aus der Animation fortgeführt die die Verwendung von Arrays verdeutlichen.

Wiederholung: Datentypen

Durch die Einführunf von Variablen (Variablen I & Variablen II) haben wir die Möglichkeit kennen gelernt unterschiedlich Formen von Daten zu speichern, auszulesen und zu verändern. Folgende Datentypen kennen wir bereits:
  • Interger - simple Ganzzahl, beispielsweise 0, 1, 8, -25, etc. int
    int number = 10;
  • Float - gebrochene Zahl (Fließkommazahl). Die Trennung erfolgt hierbei durch einen Punkt, nicht wie im Deutschen gewohnt durch ein Komma. float
    float number = 12.5819;
  • Character - speichert ein einzelnes Zeichen in der Unicode - Codierung, z.B.: 'a', '?', 'ä'. Beachte die einzelnen Anführungszeichen um das Zeichen auf der rechten Seite des Istgleich! char
    char a = 'a';
  • Boolean - Bool'scher Wert kann nur einer "wahr" - true oder "nicht wahr" - false beinhalten. Wir treffen diesen Datentyp meist bei if-Bedingungen an. boolean
    boolean nice = true;
  • Color - beinhaltet die Rot-, Grün- und Blauanteiligkeit (optional den Alphakanal) einer Farbe (color(43, 31, 22), color(44, 199, 199), color(235, 223, 167), etc.) color_datatype
    color pink = color(255, 0, 255);
Diese Datentypen werden auch primitiv genannt, da ihr Vorkommen, ihre Struktur und ihre Größe in der verwendeten Programmiersprache fest verankert sind. Beispielsweise kann eine Variable vom Typ int immer nur einen ganzzahligen Wert beinhalten. Dieser Fakt hat in unseren Programmen bisher kein Hindernis dargestellt - erlaubte uns aber, speziell im Bereich der Animation, nur eine stark begrenzte Anzahl von Elementen zu bearbeiten.
Neben den primitiven Datentypen gibt es noch die sogenannten zusammengesetzten Datentypen. Ihre Größe und ihr Umfang ist nicht vorgegeben. Ihre Struktur kann in einigen Fällen verändert werden, da sie im Grunde aus primitiven Datentypen zusammengesetzt sind.
Einen zusammengesetzten Datentypen haben wir bereits bei der Arbeit mit Text kennengelernt:
  • String - Kette aus Zeichen in Unicode - Codierung ("Creative", "Coding", "Creative Coding", etc. Umschlossen von doppelten Anführungszeichen!) String
    String text = "too much!";

Arrays

Variablen sind das Gedächtnis unserer Programme. Wir legen dort Informationen (Texte, Zahlen, Ja/Nein) ab um sie im weiteren Ablauf wiederverwenden zu können bzw. zu modifizieren. Die primitiven Variablen haben uns erlaubt eine Information pro deklarierte Variable zu speichern - dies ist gut, aber sehr wenig. Um Programme flexibel zu gestalten, beschäftigen wir uns nun mit dem Erstellen und Verarbeiten von sogenannten Arrays.
Arrays sind Listen, Reihen von Daten (Array = engl. Reihe). Ein Array besteht aus einer von uns bestimmten Anzahl von Feldern, welche alle einen Wert des gleichen Datentyps speichern können. Beispielsweise die Positionen von fünf Quadraten:
// Sketchsettings
fill(0);
stroke(0);

// Erstellen und Befüllen der Variablen
float rSize = 20.0;
float pos1 = 0.0;
float pos2 = 20.0;
float pos3 = 40.0;
float pos4 = 60.0;
float pos5 = 80.0;

// Zeichnen der Rechtecke nach den
// Werten der Variablen
rect(pos1, pos1, rSize, rSize);
rect(pos2, pos2, rSize, rSize);
rect(pos3, pos3, rSize, rSize);
rect(pos4, pos4, rSize, rSize);
rect(pos5, pos5, rSize, rSize);
Diese Schreibweise mag für fünf Elemente noch erträglich sein, ist aber beispielsweise bei 100 Elementen unbrauchbar (erst recht wenn wir die Positionen über die Zeit hinweg verändern wollen!). Hier kommen Arrays ins Spiel und verkürzen den Code beachtlich.

Deklarieren und Initialisieren

Arrays werden in folgender Form notiert:
// deklariere ein float Array
float[] pos;
Die beiden eckigen Klammern hinter der Datentypbezeichnung markieren, dass es sich hierbei um ein Array von float-Werten handelt. Als nächstes muss das Array initialisiert werden, d.h. wir bestimmen wie viele Werte das Array umfassen soll. Dies geschieht mithilfe eines neuen Begriffs in Processing, dem new. Mithilfe von new erzeugen wir ein 'neues' Array an float-Werten, in das wir ab sofort Werte speichern können. Als letztes geben wir noch die Anzahl an Werten an, die in dem Array gespeichert werden sollen. Die gesamte Zeile sieht dann wiefolgt aus:
// deklariere und erzeuge ein float-Array mit 5 Werten
float[] pos = new float[5];
Die '5' innerhalb der zweiten eckigen Klammern bezieht sich also auf die 5 float-Werte die ab sofort in dem Array gespeichert werden können.

Schreiben und Lesen

In einem so initialisierten Array können also ab sofort Werte gespeichert und ausgelesen werden. Dabei hat jeder Wert einen eigenen Index - beginnend bei 0 - über den auf den Wert an der jeweiligen Stelle zugegriffen werden kann. Beim Zugriff auf das Array werden wieder die eckigen Klammern benutzt:
/* Deklarieren und Erzeugen eines float-Array mit 
 * der Möglichkeit 5 Werte ablegen zu können
 */
float[] pos = new float[5];

/* Befüllen des Arrays mit Werten. Beginnend bei 0. 
 * Das letzte Feld im Array sprechen wir deswegen 
 * mit Länge-1 an. Array mit 3 Feldern --> 3-1 = 2
 */
pos[0] = 1.2; // speichere den Wert '1.2' an der ersten Stelle
pos[1] = -2.5; // speichere den Wert '-2.5' an die zweite Stelle
pos[2] = 12.5; // speichere den Wert '12.5' an die dritte Stelle

// Auslesen des ersten Wertes, Ausgabe in der Konsole
println (pos[0]);
Ausserdem gibt es die Mölichkeit Arrays mit festen Werten zu erzeugen. Dazu werden sie nach der Deklaration in geschweiften Klammern, getrennt durch Kommata, geschrieben. Damit ersparen wir uns die Zeilen für das Befüllen des Arrays, können unseren Quelltext aber schlechter lesen:
/* Erstelle ein float-Array mit drei Feldern und befülle 
 * diese sofort mit den Werten '1.2', '-2.5' und '12.5'.
 */
float[] pos = {1.2, -2.5, 12.5};

// gibt '12.5' in der Konsole aus
println (pos[2]);
Unter Verwendung dieser Schreibweise sieht das obere Beispiel nun wiefolgt aus:
// Zeicheneinstellungen
fill (0);
stroke (0);

// Varaibel für die Quadratgröße
float rSize = 20.0;
// float-Array mit Position der Rechtecke
float[] pos = {0.0, 20.0, 40.0, 60.0, 80.0};

/* Zeichnen der Rechtecke, x- und y-Position 
 * wird dabei dem Array 'pos' entnommen und 
 * jeweils für beide Achsen eingesetzt.
 */
rect(pos[0], pos[0], rSize, rSize);
rect(pos[1], pos[1], rSize, rSize);
rect(pos[2], pos[2], rSize, rSize);
rect(pos[3], pos[3], rSize, rSize);
rect(pos[4], pos[4], rSize, rSize);

Größe

Jedes Array bietet die Möglichkeit über die Eigenschaft length seine Größe abzufragen. Die Schreibweise dazu sieht wiefolgt aus:
float[] pos = {0.0, 20.0, 40.0, 60.0, 80.0};
println (pos.length); // gibt '5' in der Konsole aus
Die Länge eines Arrays ist demnach eine Eigenschaft auf die mit der Punkt-Schreibweise zugegriffen werden kann (wie z.B. width oder height von Bildern, als img.width).

Rechenoperationen

Modifizieren und Neudefinieren von Werten erfolgt bei Werten innerhalb eines Arrays wie bei den uns bekannten einfachen Variablen. Links des Istgleich (=) befindet sich das Feld, wessen Wert wir ändern wollen. Bei einer Variable war dies die Nennung dieser durch den Variablennamen. Um ein Feld in einem Array anzusprechen, bedienen wir uns, wie oberhalb bereits erwähnt, der eckigen Klammern.
float[] pos = {0.0, 20.0, 40.0};

/* legt die Summe aus Feld zwei 
 * und drei in Feld eins ab.
 */
pos[0] = pos[1] + pos[2];

/* definiert den Wert des dritten Feldes 
 * mit dem Wert '91.41' neu
 */
pos[2] = 91.41;

/* definiert den Wert des zweiten Feldes 
 * mit einem Viertel des dritten Feldes neu
 */
pos[1] = pos[2] / 4;

Arrays und for-Schleifen

Die häufigste Verwendung dieser Eigenschaft findet sich bei der Bearbeitung von Arrays mit Hilfe von for-Schleifen. Um ein Array, unabhänig vom Datentyp, einmal vollständig von Beginn bis Ende durchzulaufen sieht diese Schleife vom Aufbau immer gleich aus:
  1. Da der Wert immer im null'ten Feld abgelegt wird, startet unsere Zählvariable i bei 0
  2. Wir wollen bis zum letzten Wert vordringen, benötigen deswegen die Anzahl von Schleifendurchläufen wie unser Array Felder hat. Informatiker zählen, damit es nicht langweilg wird von 0 an - dem müssen wir uns beugen. Die Schleife läuft deshalb solange i kleiner als die Anzahl ist. Unseren letzten Wert erreichen wir mit Arraylänge - 1.
  3. Die Zählvariable muss nach jedem Schleifendurchlauf um 1 erhöht werden um kein Feld des Arrays auszulassen.
Alle drei Regeln kombiniert ergeben folgende Vorlage:
int[] array = {"Alles", "aus", "der", "Liste"};

for (int i=0; i < array.length; i = i + 1) {
    print (array[i] + " ");
}
Im folgenden Beispiel wenden wir das Muster in zwei Formen an. Zuerst füllen wir jedes Feld des Arrays mit einem zufälligen Wert zwischen 0 und der Breite unseres Sketchfensters. Danach durchlaufen wir es ein zweites Mal und Positionieren unsere Quadrate an den durch random() festgelegten Positionen.
fill(0);
stroke(0);

float rSize = 20.0;
// deklariere und initialisiere das Array
float[] pos = new float[5];

/* Gehe mit Hilfe einer for-Schleife durch jede einzelne 
 * Position des Arrays. Das Ende der for-Schleife wird 
 * über die Länge-Eigenschaft des Arrays bestimmt.
 */
for (int i=0; i < pos.length; i++) {
  pos[i] = random(width);
}

// zeichne die Rechtecke nach dem selben Prinzip
for (int i=0; i < pos.length; i++) {
  rect(pos[i], pos[i], rSize, rSize);
}

Array Beispiele

Lineare Bewegung mit Arrays (1D) in Processing

Eines der ersten Beispiele zum Thema Animation war die Lineare, ohne jegliche Beschleunigung auf der x-Achse von links nach rechts. Dieses kleine Projekt dient uns als Basis um 150 Kreise die selbe Aktion ausführen zu lassen. Wir übernehmen also den kompletten Quelltext und modifizieren als Erstes die Deklaration unserer globalen Variable xpos zum Speichern der Kreisposition - float xpos wird zu float[] xpos. Dieses Array ermöglicht uns mehrere Werte in einer Variable ablegen zu können - jeweils eine pro Kreis. Gleiche Form, andere Name für die y-Achse, damit sich die Kreise nicht überlagern.

float xpos[] = new float[150];
float ypos[] = new float[150];
Im setup() -Block durchlaufen wir mit einer for-Schleife beide Arrays und legen mittels random() eine zufällige Startposition in xpos und ypos für jeden Kreis fest:
for (int i=0; i < xpos.length; i++) {
  xpos[i] = random (width);
  ypos[i] = random (durchmesser / 2, height - durchmesser);
}
Die Struktur des draw() bleibt bestehen. Eine weitere for-Schleife bewirkt die Positionsabfragen und Änderungen auf alle Kreise anzuwenden.

// gloable Arrays für die Ablage der Position
float xpos[] = new float[150];
float ypos[] = new float[150];

float durchmesser = 30;

void setup () {
  size(500, 180);
  smooth ();
  
  // Einmaliges Festlegen der x- und y-Position
  for (int i=0; i < xpos.length; i++) {
    xpos[i] = random (width);
    ypos[i] = random (durchmesser / 2, height - durchmesser);
  }
}

void draw () {
  // Hintergrund leeren
  background (255);
  
  // Führe die Schleife für jeden einzelnen 
  // Kreis aus, angegeben durch die Länge von 'xpos'
  for (int i=0; i < xpos.length; i++) {
    // Position modifiziere
    xpos[i] = xpos[i] + 1;

    // Zurücksetzen der Kreisposition auf die linke 
    // Bildschirmseite wenn die Position größer als 
    // Bildschrimbreite + Durchmesser ist
    if (xpos[i] > width) {
      xpos[i] = 0;
    }
    
    fill (random (210, 255), random (25, 60), 0);
    
    // Zeichnen des Kreises an die aktuelle Position
    ellipse (xpos[i], ypos[i], durchmesser, durchmesser);
  }
}
Lineare Bewegung mit Arrays (2D) in Processing

Array Animation
Beispiel Nummer zwei treibt das oben Durchgeführte auf die Spitze (siehe 1D Version). Wir legen für alle Attribute: Position, Geschwindigkeit, Durchmesser und Farbe jeweils ein Array mit 100 Feldern an. Jedem Kreis gehört demnach ein Wert aus jedem Array. Die Startposition ist bei allen die Selbe (Sketchmitte). Durch die unterschiedlichen Geschwindigkeiten (können auch negativ sein) springt die Gruppe sofort auseinander und beginnt sich auf der Zeichenfläche zu verteilen. Wir setzen den Hintergrund nie mit background() zurück und erhalt deshalb diese komplex anmutende Collage.

// gloable Variable für
// die Ablage aller Positionen
float[] xpos = new float[100];
float[] ypos = new float[100];

// Positionsänderung pro Einzelbild
float[] xGeschwindigkeit = new float[100];
float[] yGeschwindigkeit = new float[100];

// Farbe der Kreise
color[] farbe = new color[100];
// Kreisdurchmesser der Kreise
float[] durchmesser = new float[100];

void setup () {
  size(500, 240);
  smooth ();
  noStroke ();

  // einmaliges Festlegen aller benötigten
  // Werte zu Beginn des Programms
  for (int i=0; i < xpos.length; i++) {
    xpos[i] = width / 2;
    ypos[i] = height / 2;
    xGeschwindigkeit[i] = random (-1, 1);
    yGeschwindigkeit[i] = random (-1, 1);
    durchmesser[i] = random (10, 40);
    farbe[i] = color (random (100, 255), 20);
  }
}

void draw () {
  // Führe die Schleife für jeden einzelnen 
  // Kreis aus, angegeben durch die Länge von 'xpos'
  for (int i=0; i < xpos.length; i++) {
    // rechter Fensterrand
    if (xpos[i] > width - durchmesser[i] / 2) {
      xGeschwindigkeit[i] *= -1;
    }
    // linker Fensterrand
    if (xpos[i] < durchmesser[i] / 2) {
      xGeschwindigkeit[i] *= -1;
    }
    // unterer Fensterrand
    if (ypos[i] > height - durchmesser[i] / 2) {
      yGeschwindigkeit[i] *= -1;
    }
    // oberer Fensterrand
    if (ypos[i] < durchmesser[i] / 2) {
      yGeschwindigkeit[i] *= -1;
    }

    // Position modifizieren, 'geschwindigkeit'
    // kann positiv oder negativ sein (siehe: * -1)
    xpos[i] = xpos[i] + xGeschwindigkeit[i];
    ypos[i] = ypos[i] + yGeschwindigkeit[i];

    // Zeichnen des Kreises an die aktuelle Position
    fill (farbe[i]);
    ellipse (xpos[i], ypos[i], durchmesser[i], durchmesser[i]);
  }
}
Mausverfolger mit Arrays in Processing

Ähnlichen Effekt kennen wir schon aus vergangenen Kursstunden. Bei dem Spielen mit der Mausposition hatten wir statt den Hintergrund komplett vollfarbig zu füllen ein semitransparentes Rechteck über die gesamte Zeichenfläche gelegt. Nun, in Kombination mit background(), speichern wir die letzten 70 Koordinaten der Maus und positionieren an ihnen unsere Kreise. Zwei Arrays vom Typ float beinhalten wieder die x- und y-Koordinaten. Bei jedem draw()-Durchlauf verschieben wir alle gespeicherten Werte, in den Arrays xpos und ypos, um einen Index an den Anfang der Liste (Richtung 0). Dadurch vergessen wir die Informationen über die älteste Position. An das Ende der Listen kommen die aktuellen Koordinaten der Maus.

// globale Variablen zum Ablegen
// der Mausposition(en)
int[] xpos = new int[70];
int[] ypos = new int[70];

void setup(){
  size(500, 200);
  smooth();
  noStroke();
}

void draw(){
  background (255);

  /* wir verschieben in jedem Einzelbild 
   * alle bisher gespeicherten Mauspositionen 
   * um eins nach vorn in unserem Array. In 
   * das letzte Feld kommt später die 
   * aktuelle Position der Maus.
   */
  for (int i=0; i< xpos.length - 1; i++) {
    xpos[i] = xpos[i + 1];
    ypos[i] = ypos[i + 1];
  }
  
  // aktuelle Mausposition in das letzte Feld speichern
  xpos[xpos.length - 1] = mouseX;
  ypos[xpos.length - 1] = mouseY;

  // Zeichnen aller Kreise
  for (int i=0; i < xpos.length; i++) {
    fill (0, 0, 0, 25);
    ellipse (xpos[i], ypos[i], i / 0.75, i / 0.75);
  }
}