Navigation überspringen

Bewegung & Beschleunigung

Vektoren

Was ist ein Vektor? Das ist eine spannende Frage. Und eine sehr einfache und doch hilfreiche Antwort darauf ist: Ein Pfeil.

Ein Pfeil, kann mir entweder anzeigen, an welchem Ort sich etwas befindet oder in welche Richtung sich etwas bewegt. Außerdem hat so ein Pfeil eine Richtung und eine Länge. Damit haben wir schon dich wichtigsten Eigenschaften eines Vektors.

Wenn ich nun beschreiben möchte, wie ein Vektor aussieht, dann habe ich zwei Möglichkeiten:

  • Ich gebe zwei Werte an, z.B. (3|4). Das bedeutet in diesem Fall, dass das Ende des Pfeils 3 Schritte auf der x-Achse und 4 Schritte auf der y-Achse vom Start entfernt ist.
    • Du kannst dir das vorstellen, wie die Navigation mit einem Auto-Navi: "In 300m rechts abbiegen, danach in 400m geradeaus. Dann haben Sie ihr Ziel erreicht."
  • Ich gebe einen Winkel (eine Richtung) und eine Länge an, z.B. 45° und 10. Das bedeutet, dass ich mich in Richtung 45° drehen muss und dann 10 Schritte nach vorne gehen muss, um an die Spitze des Pfeils zu kommen.
    • Du kannst dir das vorstellen, wie die Navigation mit einer Schatzkarte mit einem Kompass: "Der Schatz befindet sich 100m in Richtung Osten."

Doch warum sind Vektoren eigentlich wichtig und warum müssen wir uns damit beschäfitgen? Es mag dir vielleicht schon aufgefallen sein: Wir haben bisher die ganze Zeit schon Vektoren verwendet! Jedes Mal, wenn wir mithilfe von x- und y-Werten angegeben haben, wo bzw. an welchem Ort sich etwas befindet, haben wir aus mathematischer Sicht einen Vektor verwendet - und zwar einen Ortsvektor.

float x = 20;
float y = 10;

rect(x, y, 50, 50);         // Der Ortsverktor ist (20|10)
point(75, 30);              // Der Ortsverktor ist (75|30)

Datentyp PVector

Du siehtst also, dass Vektoren ein wichtiger Bestandteil der graphischen Programmierung sind. Aus diesem Grund ist Processing so freundlich, uns einen eigene Datentyp dafür zur Verfügung zu stellen: PVector. Das hat zum einen den Vorteil, dass ich die beiden x- und y-Werte nicht in zwei, sondern in einer Variablen speichern kann. Und zum anderen hat der Datentyp PVector ein paar Funktionen, die mir das arbeiten mit Vektoren deutlich vereinfachen. Schauen wir uns also einmal an, wie ich einen Vektor mit PVector anlege:

// Bisher
float xPosition = 10;
float yPosition = 20;

// mit PVector
PVector position = new PVector(10, 20);

Du siehst hier ein paar Unterschiede zu dem, wie wir bisher Variablen deklariert und initialisiert haben:

  • PVector wird groß geschrieben: Das ist bei den anderen Datentypen, wie int und float, nicht der Fall. Außer String, was ebenfalls groß geschrieben wird. Warum das so ist, klären wir später. Wichtig ist nur: Merke dir: PVector wird groß geschrieben!
  • Das Schlüsselwort new: Das kennst du bereits von Arrays. Das darfst du hier auf keinen Fall vergessen!
  • Das Zuweisen der Werte in den runden Klammern: Bei der Initialisierung bzw. wenn du beide Werte festlegen willst, machst du das wie im Beispiel oben. Das geht am schnellsten und am einfachsten.

Du kannst auch auf die einzelnen Werte zugreifen. Das geht folgendermaßen:

PVector pos = new PVector(); // das geht auch - keine Werte angeben
// Ist aber unnötig umständlich das so zu machen

pos.x = 10;  // so kannst du Werte für x und y zuweisen
pos.y = 20;

println(pos.x); // So kannst du Werte verwenden und z.B. ausgeben

Außerdem hat der Datentyp PVector ein paar hilfreiche Funktionen:

  • add() & sub(): mit diesen Funktionen können Vektoren addiert bzw. subtrahiert werden.
    • Bsp.: PVector richtungsVektor = PVector.sub(zielPos, spielFigurPos);
      Nutze diese Schreibweise, wenn du einen Vektor von einem anderen abziehen möchtest und das Ergebnis in einem anderen Vektor speichern möchtest, wie bei a = b - c
    • Bsp.: PVector vektorA.sub(vektor);
      Nutze diese Schreibweise, wenn du von einem Vektor einen anderen abziehen möchtest, wie bei a = a - b
  • dist(): Berechnet die Distanz zwischen zwei Vektoren.
    • Bsp.: float distanz = zielPos.dist(spielFigurPos);
    • Bsp.: float distanz = PVector.dist(zielPos, spielFigurPos);
  • normalize(): Normalisiert eine Vektor (setzt ihn auf die Länge 1)
    • Bsp.: richtungsVektor.normalize();
  • mag(): Berechnet die Länge eines Vektors in Bezug zum Mittelpunkt (0|0).
    • Bsp.: float laenge = spieFigurPos.mag();
  • heading(): Berechnet den Winkel (im Bogenmaß) eines Vektors.
    • Bsp.: float richtung = spieFigurPos.heading();

Beachte: Alle diese Funktionen funktionieren nur, wenn sie mithilfe der Punktnotation entweder an den Vektor angehängt werden oder an den Datentyp (PVector). Je nachdem, woran man die Funktion anhängt, muss man unterschiedliche Parameter angeben. Schau am besten in der Referenz über PVector nach, wie man die Funktionen genau verwendet und welche weiteren Funktionen es noch gibt.

Jetzt wissen wir, was Vektoren sind und wie wir damit arbeiten können. Und wir wissen, wie wir mithilfe von Vektoren angeben können, wo sich etwas befindet. Gehen wir einen Schritt weiter: Wir geben eine Richtung an, in die wir uns bewegen.

Geschwindigkeit

Die einfachste Form von Bewegung, die wir kennen und bereits mehrmals umgesetzt haben, ist die Bewegung in eine Richtung. Ein einfaches Beispiel ist ein Kreis, der sich von links nach rechts über den Bildschirm bewegt:

float x = 0;

void draw() {

  // BERECHNEN
  x = x + 1;

  // ZEICHNEN
  ellipse(x, 50, 10, 10);

}

Schauen wir uns die Zeile 6 einmal genauer an - was passiert hier? Wie berechnen die nächste Position und sagen, die nächste Position ist gleich die aktuelle Position plus 1 (die Geschwindigkeit). Das ist eine wichtige Formel, die wir uns merken müssen, wenn wir mit Bewegung arbeiten. Denn was ist Geschwindigkeit eigentlich? Geschwindigkeit ist die Rate, mit der sich die Position ändert. In unserem Fall wäre die Rate 1. Die Position ändert sich um 1 Pixel pro Frame. Wie sieht das als Formel aus? Da Programmierung meist auf englisch erfolgt, verwende ich ab jetzt die englischen Begriffe location für position und velocity für Geschwindigkeit:

locationnew = locationold + velocity

Passen wir das Beispiel oben also mal in zwei Aspekten an: Als erstes machen wir aus der 1 eine Variable namens velocity. Und als zweites wandeln wir dir Variablen so um, dass wir PVetor verwenden. Die Geschwindigkeit gibt dabei die Richtung an, in die wir uns bewegen und ist somit ein Richtungsvektor. Wir wollen uns nur nach rechts bewegen, also in Richtung, der positiven x-Achse. Das bedeutet der Richtungsvektor für die Geschwindigkeit hat die Werte (1|0). 1 Schritt auf der x-Achse und 0 Schritte auf der y-Achse.

PVector location = new PVector(0, 50);
PVector velocity = new PVector(1, 0); // 1 nach rechts, 0 nach oben/unten

void draw() {
  // BERECHNEN
  location.add(velocity); // die add()-Funktion addiert einen Vektor zu einem anderen
 
  // ZEICHNEN
  background(200);
  ellipse(location.x, location.y, 10, 10);
}

Du siehst hier, dass wir in Zeile 6 mithilfe von location.x und location.y die Werte in unserem rect()-Befehl ganz einfach verwenden können. Interessanter ist die Zeile 7: Hier addieren wir zu unserem Vektor location den Vektor velocity. Man könnte das auch "händisch" machen, indem man jeweils einzeln die x- und die y-Werte addiert. So geht das aber ein wenig schneller. Wichtig: location = location + velocity geht nicht!

// Der umständliche Weg
location.x = location.x + velocity.x;
location.y = location.y + velocity.y;

// Besser
location.add(velocity);
// Oder
location = PVector.add(location, velocity);

// Das geht NICHT !!!
location = location + velocity;
location += velocity;

Beschleunigung

Nun wissen wir, wie wir etwas in Bewegung versetzen können. Allerdings sieht es etwas unnatürlich aus, wenn Bewegungen abrupt beginnen und enden. Typischerweise wollen wir, dass Dinge beschleunigen und bremsen. Beschleunigung ist die Rate, mit der sich die Geschwindigkeit ändert. Moment mal. Das kennen wir doch, oder? Das ist dasselbe Prinzip, wie bei der Geschwindigkeit. Die Beschleunigung ändert die Geschwindigkeit und die Geschwindigkeit ändert die Position. Als Formel sieht das so aus:


velocitynew = velocityold + acceleration
locationnew = locationold + velocitynew

Dabei gibt es unterschiedliche Möglichkeiten, die Beschleunigung zu berechnen oder festzulegen. In den meisten Fällen ist aber einen konstante Beschleunigung das, was wir wollen.

Konstante Beschleunigung

Das ist die einfachste Form der Beschleunigung: Es gibt einen festen, unveränderlichen Wert für die Beschleunigung. Wir ergänzen unser Codebeispiel nun um einen neuen Vektor acceleration und addieren diesen jeden Frame auf velocity. Dadruch wird unser Ball mit jedem Frame schneller. Da wir mit ca. 60 Frames pro Sekunde arbeiten, müssen wir hier einen sehr kleinen Wert nehmen, da die Geschwindigkeit sonst zu schnell zu groß wird.

PVector location = new PVector(0, 50);
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector (0.05, 0);

void draw() {
  // BERECHNEN
  velocity.add(acceleration); // Beschleunigung auf Geschwindigkeit addieren
  location.add(velocity); 

  // ZEICHNEN
  background(200);
  ellipse(location.x, location.y, 10, 10);
}

Apropos zu groß: In diesem Beispiel wird die Geschwindigkeit immer schneller solange wir das Programm laufen lassen. Das ist natürlich nicht in unserem Sinne. In der Realität ist Geschwindigkeit immer begrenzt, sei es aufgrund von Reibung, Widerstand oder der Leistungsfähigkeit des Motors. Das sind alles Faktoren, die wir hier so komplex gar nicht berechnen können und wollen. Das Einfachste ist, dass wir dem Vektor velocity ein Limit geben (auch wenn das physikalisch nicht ganz korrekt ist). Hierfür gibt es im Datentyp PVector die Funktion limit(). Diese Funktion begrenzt den Vektor auf eine maximale Länge.

PVector location = new PVector(0, 50);
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector (0.05, 0);

void draw() {

  // BERECHNEN
  velocity.add(acceleration); 
  velocity.limit(2);  // velocity hat eine max. Länge von 2
  location.add(velocity); 

  // ZEICHNEN
  background(200);
  ellipse(location.x, location.y, 10, 10);
  
}

Auf diese Weise beschleunigt unser Ball zunächst und hat dann, sobald er seine Höchstgeschwindigkeit erreicht hat, eine konstante Geschwindigkeit von 2. Allerdings bewegt sich unser Kreis immer noch nur in eine Richtung. Langweilig. Die Richtung können wir mithilfe der Beschleunigung ändern. Das einfachste ist zunächst eine zufällige Richtung, sprich eine zufällige Beschleunigung zu wählen. Dafür gehen wird folgendermaßen vor:

  1. Mithilfe von PVector.random2D() erstellen wir einen zufälligen Vektor mit x- und y-Werten zwischen 0 und 1
  2. Wir normalisieren den Vektor (setzen seine Länge auf 1)
  3. Wir multiplizieren den Vektor mit der Länge, die wir haben wollen

So erhalten wir jeden Frame einen neuen Vektor, der in eine zufällige Richtung zeigt und dabei jedes Mal gleich lang ist. Das ist wichtig, damit wir auch jeden Frame im selben Maße beschleunigen. Im Code sieht das folgendermaßen aus:

PVector location = new PVector(50, 50); // Kreis mittig platzieren
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector (); 

void draw() {
   
  // BERECHNEN
  // Zufällige x- und y-Werte zw. 0 und 1
  acceleration = PVector.random2D(); 
  // Auf Länge 1 setzen
  acceleration.normalize();
  // Mit gewünschter Rate multiplizieren
  acceleration.mult(0.05);
  velocity.add(acceleration); 
  velocity.limit(2);  // velocity hat eine max. Länge von 2
  location.add(velocity); 

  // ZEICHNEN
  background(200);
  ellipse(location.x, location.y, 10, 10);
  
}

Jetzt haben wir es schonmal geschafft, dass sich unser Kreis in verschiedene Richtungen bewegen kann. Diese wollen wir jetzt aber gezielt steuern. Das geht, indem wir uns in Richtung eines Ziels bewegen oder lenken. Beide Varianten werden wir uns ansehen.

Beschleunigung in Richtung eines Ziels

Wenn ich zwei Vektoren für Position und Ziel habe, kann ich den Richtungsvektor  (der Vektor mit dem ich von meiner Position zum Ziel zu komme) berechnen, indem ich die Position vom Ziel abziehe. Wenn ich nun diesen Richtungsvektor wieder auf die Position addieren würde, wäre ich sofort bei meinem Ziel. Das wollen wir aber natürlich nicht. Wir wollen uns ja nur schrittweise in Richtung Ziel bewegen - und das mit einer Rate bzw. Schrittweite, die wir festlegen. Um das zu erreichen, müssen wir den Richtungsvektor normalisieren. Das bedeutet, dass die x- und y-Werte so skaliert werden, dass der Vektor genau eine Länge von 1 hat. Dann können wir den Richtungsvektor einfach mit einer Zahl, die unsere Beschleunigungsrate ist, multiplizieren und erhalten dann die Beschleunigung in Richtung Ziel.

Das folgende Beispiel, habe ich so angepasst, dass unser Ball jetzt der Maus folgt. Dafür habe ich einen neuen Vektor goal angelegt, in dem ich in Zeile 10 und 11 die Mauskoordinaten speichere. In Zeile 16-18 berechne ich die Richtung, wie oben beschrieben.

PVector location = new PVector(random(10, 90), random(10, 90));
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector ();
PVector goal = new PVector();

void draw() {
  
  // BERECHNEN
  // Ziel setzen
  goal.x = mouseX;
  goal.y = mouseY;

  // Richtung berechnen
  PVector direction = PVector.sub(goal, location); // Ziel von der Position abziehen
  direction.normalize(); // Richtung normalisieren
  direction.mult(0.05);  // Richtung multiplizieren

  // Beschleunigung setzen
  acceleration = direction;

  // Geschwindigkeit berechnen
  velocity.add(acceleration);
  velocity.limit(2);

  // Geschwindigkeit addieren
  location.add(velocity);

  // ZEICHNEN
  background(200);
  ellipse(location.x, location.y, 10, 10);
}

Warum hält der Kreis nicht an, wenn er das Ziel erreicht?

Das sich bewegende Objekt weiß nicht, dass es an einem Ziel anhalten muss, es weiß nur, wo das Ziel ist und versucht, so schnell wie möglich dorthin zu gelangen. Wenn es so schnell wie möglich vorankommt, wird es unweigerlich über das Ziel hinausschießen und umkehren müssen, um dann wieder so schnell wie möglich in Richtung des Ziels zu fahren und erneut über das Ziel hinauszuschießen, und so weiter, und so fort.

Richtung und Ausrichtung

Bei unserem Kreis sehen wir gar nicht, wo vorne und hinten ist. Er bewegt sich nur in Richtung des Ziel, aber dreht sich nicht in diese Richtung. Die meisten Objekte, haben aber eine Ausrichtung. Wie schaffen wir es nun, dass sich unser Kreis in Richtung Ziel dreht? Als erstes benötigen wir eine geometrische Form, die eine eindeutige Ausrichtung hat. Hier nehmen wir ein kleines Dreieck. Nun verschieben wir das Koordinatensystem mithilfe von translate an die Position des Dreiecks. Dann drehen wir das Koordinatensystem mithilfe von rotate in Richtung Ziel - doch woher kenne ich den Winkel (im Bogenmaß) dafür? Hierfür verwenden wir den Befehl acceleration.heading(). Diese Funktion liefert uns den Winkel eines Vektors im Bogenmaß. Das ist möglich, weil (wir erinnern uns an den Anfang des Kapitels) jeder Vektor entweder mithilfe von x- und y-Werten oder mithilfe des Winkels und der Länge angegeben werden kann.

PVector location = new PVector(random(10, 90), random(10, 90));
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector ();
PVector goal = new PVector();

void draw() {

  // BERECHNUNGEN
  
  // Ziel setzen
  goal.x = mouseX;
  goal.y = mouseY;

  // Richtung berechnen
  PVector direction = PVector.sub(goal, location); // Ziel von der Position abziehen
  direction.normalize(); // Richtung normalisieren
  direction.mult(0.05);  // Richtung multiplizieren

  // Beschleunigung setzen
  acceleration = direction;

  // Geschwindigkeit berechnen
  velocity.add(acceleration);
  velocity.limit(2);

  // Geschwindigkeit addieren
  location.add(velocity);

  // ZEICHNEN
  background(200);
  translate(location.x, location.y);
  rotate(direction.heading());
  triangle(- 5, - 5, - 5, 5, 10, 0);
}

Lenken

Schauen wir uns nun an, wie wir lenken können. Für diesen Fall möchten wir, dass unser Pfeil (ehemals Kreis) beschleunigt und dann konstant bewegt und wenn wir die linke oder rechte Pfeiltaste drücken, soll sich der Pfeil drehen und in die entsprechende Richtung bewegen. Hierfür ist es zunächst sinnvoll eine Variable angle zu haben, in der gespeichert ist, in welche Richtung sich unser Pfeil gerade bewegt. Ein Winkel ist dabei ja nur eine andere Art einen Vektor anzugeben (wir erinnern uns an den Anfang des Kapitels). Wenn wir dann unsere Pfeiltasten bewegen, verändern wir den Wert in angle

Als nächstes brauchen wir, wie in dem Beispiel davor auch, einen Vektor direction mit x- und y-Werten. Das bedeutet, ich muss irgendwie aus angle einen Vektor mit x- und y-Werten machen. Das geht mithilfe von Sinus und Kosinus und dem Satz des Pythagoras. Wenn ich einen Vektor habe, der mit einem Winkel winkel und einer Länge länge angegeben ist, kann ich folgendermaßen die x- und y-Werte berechnen:

x = cos(winkel) * länge
y = sin(winkel) * länge

Wenn man - wie in unserem Fall - einfach nur einen Winkel hat, dann hat der Vektor die Länge 1 und man kann die Multiplikation am Ende weglassen. Wir können also direction.x mihilfe von cos(angle) berechnen und direction.y mithilfe von sin(angle). Da direction bereits die Länge 1 hat, müssen wir ihn nicht normaliseren und können ihn gleich mit der Rate multiplizieren, mit der wir uns drehen bzw. beschleunigen wollen.

Das vollständige Codebeispiel sieht folgendermaßen aus:

PVector location = new PVector(150, 150);
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector ();
float angle = 0;

void setup() {
  size(300, 300);
}

void draw() {

  // BERECHNUNGEN
  boolean turnLeft = keyPressed && key == CODED && keyCode == LEFT;
  boolean turnRight = keyPressed && key == CODED && keyCode == RIGHT;

  if (turnLeft) angle -= 0.03;
  else if (turnRight) angle += 0.03;

  // Richtung berechnen
  PVector direction = new PVector();
  direction.x = cos(angle);
  direction.y = sin(angle);
  direction.mult(0.07);  // Richtung multiplizieren

  // Beschleunigung setzen
  acceleration = direction;

  // Geschwindigkeit berechnen
  velocity.add(acceleration);
  velocity.limit(1);

  // Geschwindigkeit addieren
  location.add(velocity);

  // ZEICHNEN
  background(200);
  translate(location.x, location.y);
  rotate(angle);
  triangle(- 5, - 5, - 5, 5, 10, 0);
}

Bremsen

Und wie entschleunige bzw. bremse ich? Hier gibt es rein physikalisch mehrere Faktoren, mit denen man berechnen kann, wie schnell ein Objekt bremst. Der Einfachheit halber sagen wir jetzt einfach, dass unser Objekt jeden Frame 5% langsamer wird, wenn wir die Pfeiltaste nach unten drücken. Hierfür habe ich einen boolean breaking angelegt. Wenn wir nicht bremsen, beschleunigen und lenken wir, wie bisher. Wenn wir aber bremsen, verringert sich unsere Geschwindigkeit (Zeile 37).

Als Code sieht das folgendermaßen aus:

PVector location = new PVector(150, 150);
PVector velocity = new PVector(0, 0);
PVector acceleration = new PVector ();
float angle = 0;

void setup() {
  size(300, 300);
}

void draw() {

  // BERECHNUNGEN
  boolean breaking = keyPressed && key == CODED && keyCode == DOWN;
  if(!breaking) {

  boolean turnLeft = keyPressed && key == CODED && keyCode == LEFT;
  boolean turnRight = keyPressed && key == CODED && keyCode == RIGHT;

  if (turnLeft) angle -= 0.03;
  else if (turnRight) angle += 0.03;

  // Richtung berechnen
  PVector direction = new PVector();
  direction.x = cos(angle);
  direction.y = sin(angle);
  direction.mult(0.07);  // Richtung multiplizieren

  // Beschleunigung setzen
  acceleration = direction;

  // Geschwindigkeit berechnen
  velocity.add(acceleration);
  velocity.limit(1);

  } else {
    // Bremsen
    velocity.mult(0.95);
  }
  
  // Geschwindigkeit addieren
  location.add(velocity);

  // ZEICHNEN
  background(200);
  translate(location.x, location.y);
  rotate(angle);
  triangle(- 5, - 5, - 5, 5, 10, 0);
}

Zusammenfassung

  • Vektoren geben an, wo sich etwas befindet oder in welche Richtung sich etwas bewegt.
  • Vektoren können mithilfe von x- und y-Werten angegeben werden oder mithilfe eines Winkels und einer Länge.
  • In Processing können Vektoren mit dem Datentyp PVector gespeichert und verarbeitet werden. Wichtige Funktionen von PVector sind unter anderem:
    • add() & sub(): mit diesen Funktionen können Vektoren addiert bzw. subtrahiert werden.
    • dist(): Berechnet die Distanz zwischen zwei Vektoren.
    • normalize(): Normalisiert eine Vektor (setzt ihn auf die Länge 1)
    • mag(): Berechnet die Länge eines Vektors in Bezug zum Mittelpunkt (0|0).
    • heading(): Berechnet den Winkel (im Bogenmaß) eines Vektors.
  • Die Geschwindigkeit ist die Rate mit der sich die Position eines Objekts ändert. Die neue Position kann berechnet werden mit locationnew = locationold + velocity
  • Die Beschleunigung ist die Rate mit der sich die Geschwindigkeit eines Objekts ändert (bzw. erhöht). Die neue Geschwindigkeit kann berechnet werden mit velocitynew = velocityold + acceleration
  • Ein Vektor, der in Form eines Winkels (und einer Länge) vorliegt, kann mithilfe von Sinus und Kosinus in x- und y-Werte umgerechnet werden:
    • x = cos(winkel) * länge
    • y = sin(winkel) * länge

Made with eXeLearning (New Window)