Im ersten Artikel zum Thema Computer-Simulationen haben wir uns mit dem eher abstrakten Ziegenproblem beschäftigt. Im heutigen Artikel soll es um ein Problem gehen, welches uns im Alltag zum allgemeinen Ärger öfter begegnet, nämlich um die Entstehung von Staus.
Zu diesem Thema wurden bereits verschiedene Experimente angestellt; ein recht bekanntes mit der Kreisfahrt kann zum Beispiel hier bewundert werden:
Der Kern des Experimentes ist es also, mehrere Autos so lang im Kreis fahren zu lassen, bis sich ein (kleiner) Stau bildet. So weit, so einfach. Nun ist es aber natürlich etwas mühselig und kostspielig, für derartige Experimente immer eine Menge Fahrer mit ihren Autos zu organisieren und diese dann stundenlang im Kreis fahren zu lassen. Effizienter (und günstiger) ist es da, die Autos stattdessen am Computer zu simulieren. Das kostet nicht viel, ist recht einfach zu realisieren und erlaubt es vor allem, schnell Parameter des Experimentes zu ändern, um so verschiedene Hypothesen testen zu können. Ich habe das Experiment einmal in eine einfache Simulation gegossen, und das sieht folgendermaßen aus (die Farbe der Fahrzeuge gibt deren Geschwindigkeit wieder, eine Verringerung des Mindestabstandes führt sehr schnell zu sichtbaren “Ergebnissen”):
Die Simulation zeigt sehr schön, welche Auswirkung das Einhalten eines größeren Mindestabstandes zum vorausfahrenden Fahrzeug hat, um einen Stau zu vermeiden: je größer der Abstand gewählt wird, desto länger dauert es, bis sich im Fahrzeug-Kreis ein Stau entwickelt.
Um zu zeigen, wie einfach sich eine derartige Simulation umsetzen lässt, stelle ich im folgenden den zugehörigen Programmcode vor. Umgesetzt wurde das ganze mit Hilfe von ActionScript 3 und dem Flex Framework – auf Deutsch, mit Flash. Wer Lust hat, das ganze einmal nachzuprogrammieren, findet hier eine passende, kostenlose Entwicklungsumgebung für Flash (als Projekttype “Flex 4” auswählen). Und los geht es.
Unsere Stau-Simulation besteht am Ende aus drei Dateien. Die erste enthält die Beschreibung der Oberfläche (der Knopf, der Schieberegler und die Fläche mit den dargestellten Fahrzeugen), die zweite die allgemeine Programmlogik und die dritte Datei schließlich eine spezielle Beschreibung der Fahrzeuge. Im Detail sehen diese drei Dateien wie folgt aus; wie auch im letzten Artikel zeige ich zuerst die gesamte Datei und erkläre dann die einzelnen Zeilen im Detail (wer an der Optik kein Interesse hat und nur die Programmlogik sehen möchte, möge direkt zur Datei Main.as springen):
Main.mxml:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="https://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx"> <fx:Script source="Main.as"/> <s:VGroup horizontalCenter="0" horizontalAlign="center"> <s:HGroup verticalAlign="middle"> <s:Button label="Fahrzeuge zurücksetzen" click="reset()" /> <s:Label text="Mindestabstand:"/> <s:HSlider id="dist" minimum="5" maximum="50" stepSize="5" value="20" change="reset()" /> <s:Label text="{dist.value}" /> </s:HGroup> <s:BorderContainer id="panel" width="400" height="400" addedToStage="reset()" enterFrame="update()" /> </s:VGroup> </s:Application>
Keine Angst – das sieht jetzt erst einmal schlimmer aus, als es ist, da die Datei in erster Linie die Optik der Anwendung beschreibt. Schauen wir uns den Code zeilenweise einmal an. Die Datei beginnt hiermit:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="https://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx">
Das dient der Initialisierung des Flash-Scripts (ist also eher kosmetischer Natur und nur am Rande von Interesse); wir sagen damit, dass wir mit dieser Datei ein Flash-Script beschreiben und dabei die Programm-Bibliotheken Flex im Allgemeinen und Spark im besonderen nutzen wollen. Die nächste Zeile ist auch eher technischer Natur; mit ihr sagen wir lediglich, dass die benannte Datei (sprich: Main.as) die weitere Programmlogik enthält:
<fx:Script source="Main.as"/>
Hier geht es nun schon stark Richtung Optik des Flash-Scripts. Diese Zeile öffnet eine sogenannte vertikale Gruppe; das ist nichts weiter als eine Menge von (im weiteren beschriebener) Elemente, die vertikal, also untereinander, angeordnet werden. Die Information horizontalCenter=”0″ besagt, dass sich die gesamte Gruppe in der Bildmitte befinden soll, wohingegen horizontalAlign=”center” bewirkt, dass alle Elemente in der Gruppe auch zentriert werden:
<s:VGroup horizontalCenter="0" horizontalAlign="center">
Genau wie die mit VGroup beginnende Zeile eine vertikale Gruppe öffnet, leitet die folgende Zeile eine horizontale Gruppe ein, also eine Menge von Elementen, die nebeneinander angeordnet sind; das Attribut verticalAlign=”middle” sorgt dabei dafür, dass sich alle Elemente in der Gruppe auf gleicher Höhe befinden:
<s:HGroup verticalAlign="middle">
Nun kommt das eigentlich interessante. Die folgende Zeile beschreibt den Knopf oben links im Flash-Script, mit welchem sich die Fahrzeuge zurücksetzen lassen. Das interessanteste an dieser Zeile ist das Attribut click=”reset()” – es sorgt dafür, dass beim Klicken des Buttons die Funktion reset aufgerufen wird (deren Beschreibung folgt in der nächsten Datei):
<s:Button label="Fahrzeuge zurücksetzen" click="reset()" />
Diese Zeile ist vergleichsweise langweilig und fügt nur den Text “Mindestabstand:” in das Script ein:
<s:Label text="Mindestabstand:"/>
Diese Zeile ist nun wieder interessanter; sie generiert den Schieberegler mit seinen verschiedenen Informationen wie Minimalwert (minimum), Maximalwert (maximum), Schrittweite (stepSize) und Anfangswert (value). Das Attribut change=”reset()” sorgt, ähnlich wie beim oben beschriebenen Button, dafür, dass bei Änderungen am Schieberegler die Methode reset aufgerufen wird. Wichtig ist auch id=”dist”, denn hiermit wird dem Schieberegler ein Name (nämlich dist) gegeben, über welchen er später wieder angesprochen und sein Wert abgefragt werden kann:
<s:HSlider id="dist" minimum="5" maximum="50" stepSize="5" value="20" change="reset()" />
Auch diese Zeile ist eher langweilig und definiert einen weiteren Textbereich. Einigermaßen interessant ist hier vor allem das Attribut text=”{dist.value}” – es besagt, dass der anzuzeigende Text genau dem Wert des Schiebereglers entsprechen soll (wir erinnern uns: der Schieberegler trägt den Namen dist, welcher wiederum über ein Attribut value verfügt):
<s:Label text="{dist.value}" />
Diese Zeile beendet lediglich die horizontale Gruppe.
</s:HGroup>
Das zweite Element der vertikalen Gruppe ist ein sogenannter BorderContainer; damit ist im Grunde eine umrahmte Fläche ohne einen genau spezifizierten Anwendungszweck gemeint. Der BorderContainer verfügt über eine Breite (width) und Höhe (height), beides jeweils mit dem Wert 400, ebenso wieder über einen Namen (diesmal “panel“). Interessant sind auch die letzten beiden Attribute: mit addedToStage=”reset()” wird angegeben, dass zu Beginn der Anwendung wieder einmal die reset-Funktion aufgerufen wird; noch wichtiger ist das Attribut enterFrame=”update()” – es bewirkt, dass in jedem Frame (also jedem zu zeichnenden Bild – wir erinnern uns) die Methode update aufgerufen wird (was sie tut, werden wir auch gleich noch sehen):
<s:BorderContainer id="panel" width="400" height="400" addedToStage="reset()" enterFrame="update()" />
Der Rest der Datei ist langweilig, denn er beendet nur die vertikale Gruppe…
</s:VGroup>
…und die Anwendung an sich:
</s:Application>
Die nächste Datei wurde bereits erwähnt; sie enthält die für die Anwendung relevante Logik, beschreibt also, was wir genau tun wollen (nämlich die Fahrzeuge auf den Bildschirm malen). Und so sieht sie aus:
Main.as:
private var radius:int = 180; private var perimeter:int = 2 * Math.PI * radius; private var cars:Vector.<Car> = new Vector.<Car>(); public function reset() : void { var car:Car; for each ( car in cars ) panel.removeElement( car ); var numCars:int = perimeter / ( dist.value + 10 ); var degreeInc:Number = 360 / numCars; cars = new Vector.<Car>(); for ( var i:int = 0; i < numCars; ++i ) { car = new Car(); car.rotation = degreeInc * i; panel.addElement( car ); cars.push( car ); } } public function update() : void { for ( var i:int = 0; i < cars.length; ++i ) { var car:Car = cars[i]; var nextCar:Car = ( i < cars.length - 1 ) ? cars[i + 1] : cars[0]; car.rotation += car.calcSpeed( nextCar, dist.value * 0.9 ); var circleRot:Number = car.rotation * Math.PI / 180; car.x = 200 + Math.cos( circleRot ) * -radius; car.y = 200 + Math.sin( circleRot ) * -radius; } }
Gehen wir auch hier die Zeilen im einzelnen durch. In den ersten drei Zeilen werden drei globale (das heißt, überall verfügbare) Variablen definiert. Die ersten beiden dienen der Visualisierung und beschreiben den Radius des Kreisen, den wir zeichnen wollen, sowie den Umfang ebenjenen Kreises (der sich, wie wir alle wissen, zu u = 2 * π * r berechnet); die dritte Variable ist schließlich eine Menge von Fahrzeugen (im Informatik-Bereich als Vector bezeichnet – bitte nicht mit dem Vektor aus der Mathematik verwechseln, denn obschon eine gewisse Ähnlichkeit der beiden Konzepte besteht, hat der Informatik-Vektor einen entscheidenden Vorteil: seine Größe ist änderbar). Was genau ein Fahrzeug (im Code mit Car bezeichnet) ist, werden wir in der nächsten Datei noch sehen:
private var radius:int = 180; private var perimeter:int = 2 * Math.PI * radius; private var cars:Vector.<Car> = new Vector.<Car>();
Die nächste Zeile leitet die schon öfter erwähnte Funktion reset ein. Sie hat die Aufgabe, alle sich in der Szene befindlichen Fahrzeuge zu entfernen sowie neu zu setzen, die Szene also zurückzusetzen:
public function reset() : void {
In der ersten Zeile der Funktion wird eine Variable car vom Typ eines Fahrzeugs (sprich: Cars) zur späteren Verwendung angelegt (zum Thema Variablen siehe hier, zum Thema Datentypen hier):
var car:Car;
Die folgenden Zeilen sorgen dafür, dass alle sich bereits in der Szene befindlichen Fahrzeuge (gespeichert in der globalen Variable cars) gelöscht werden; zu diesem Zweck wird mittel for each über alle Fahrzeuge in cars iteriert und jedes Fahrzeug aus der Szene (dem panel – wir erinnern uns: der BorderContainer) mittels removeElement entfernt:
for each ( car in cars ) panel.removeElement( car );
Anschließend werden zwei benötigte Werte für die Neuerstellung der Szene berechnet; da wären zum einen die Anzahl der neu hinzuzufügenden Fahrzeuge numCars; sie berechnet sich aus dem Kreisumfang (perimeter), geteilt durch den einzuhaltenden Abstand der Fahrzeuge (dist.value; wir erinnern uns: dist ist der Schieberegler und dist.value der Wert des Schiebereglers) plus die Länge der Fahrzeuge selber – für unser Script soll jedes Fahrzeug “10” lang sein – wofür die 10 auch immer steht (für den Interessierten: gemeint sind natürlich Pixel). Der zweite Wert beschreibt, wie viel Grad Abstand die Fahrzeuge auf ihrer Kreisbahn jeweils voneinander haben und berechnet sich natürlich aus der Gesamtzahl der Grad im Kreis geteilt durch die Anzahl der Fahrzeuge:
var numCars:int = perimeter / ( dist.value + 10 ); var degreeInc:Number = 360 / numCars;
Diese Zeile beherbergt ein wenig Magie: sie sorgt dafür, dass die gespeicherte Menge aller Fahrzeuge geleert wird (bisher wurden die Fahrzeuge ja nur aus der Szene entfernt – gespeichert waren sie aber immer noch in der Variable cars):
cars = new Vector.<Car>();
Nun kommt der eigentlich interessante Code der reset-Funktion; im folgenden werden die einzelnen Fahrzeuge der Szene hinzugefügt. Hierzu benötigen wir eine Schleife, die so oft durchläuft, wie wir Fahrzeuge setzen wollen und dabei die Laufvariable i, beginnend bei 0, hochzählt:
for ( var i:int = 0; i < numCars; ++i ) {
Innerhalb der Schleife erstellen wir zunächst einmal ein neues Fahrzeug (was es genau mit dem Car auf sich hat, sehen wir wie gesagt in der nächsten Datei) und speichern es in der Variablen car:
car = new Car();
Das Fahrzeug verfügt über ein Attribut rotation, welches entsprechend des Namens seine Rotation angibt. Ein Fahrzeug ist in unserem Script nichts anderes als ein Rechteck; die Rotation des Fahrzeuges entspricht der Rotation des Rechtecks um seinen Mittelpunkt. Da wir die einzelnen Fahrzeuge genau auf einer Kreisbahn platzieren (wie die Platzierung erfolgt, sehen wir gleich noch), entspricht die Rotation genau ihrem Winkelabstand auf dem Kreis; das i-te Fahrzeug muss dabei also um degreeInc * i gedreht werden:
car.rotation = degreeInc * i;
Die letzten beiden Zeilen der reset-Funktion sorgen nun noch dafür, dass das erstelle Fahrzeug auch der Szene hinzugefügt (addElement) und in der cars-Variable gespeichert wird (push):
panel.addElement( car ); cars.push( car );
Die nächsten beiden Zeilen beenden die Schleife und die Funktion:
} }
Aufmerksame Leser werden sich nun natürlich fragen, wo denn die Position der Fahrzeuge gesetzt wird. Das habe ich in der Tat unterschlagen, oder, um genauer zu sein, gar nicht programmiert. Wie wir nämlich in der Datei Main.mxml bereits gesehen haben, wird in jedem Frame die Funktion update aufgerufen, und in dieser werden einfach die Positionen der Fahrzeuge gesetzt. Die folgende Zeile leitet ebenjene Funktion ein:
public function update() : void {
In der update-Funktion wollen wir über alle Fahrzeuge iterieren; dazu benötigen wir eine Laufvariable (den Grund sehen wir gleich noch), nutzen also wieder eine normale Schleife mit der Laufvariablen i, um über alle in cars gespeicherten Fahrzeuge (cars.length an der Zahl) zu iterieren:
for ( var i:int = 0; i < cars.length; ++i ) {
Als erstes speichern wir innerhalb der Schleife das aktuelle Fahrzeug in der Variablen car; der Ausdruck cars[i] bedeutet, dass wir das i-te Element aus der Menge cars haben wollen (in der Mathematik würde man carsi schreiben):
var car:Car = cars[i];
Um die neue Position eines Fahrzeugs zu bestimmen, müssen wir es etwas auf der Kreisbahn bewegen; zu diesem Zweck müssen wir dem Fahrzeug die Möglichkeit geben, seine Geschwindigkeit an die Situation anzupassen. Hierfür benötigen wir das Fahrzeug, welches sich in Fahrtrichtung vor dem aktuellen befindet (sprich, das nächste Fahrzeug, da wir uns auf einer Kreisbahn bewegen und kein Fahrzeug das nächste überholen kann). Die folgende, etwas kryptisch anmutende Zeile extrahiert aus der cars-Menge ebenjenes Fahrzeug. Ohne allzu sehr ins Detail der Zeile gehen zu wollen, sei so viel gesagt: ist das aktuelle Fahrzeug nicht das letzte in der Menge, wird das sich an der folgenden Stelle befindliche extrahiert; ist das aktuelle Fahrzeug dagegen das letzte, so extrahieren wir das erste Fahrzeug der Menge (da dieses auf das letzte folgt, immerhin haben wir einen Kreis):
var nextCar:Car = ( i < cars.length - 1 ) ? cars[i + 1] : cars[0];
Die folgende Zeile ist die wichtigste für die Programmlogik, denn hier wird berechnet, wie weit sich das Fahrzeug fortbewegt. Das schöne an der Bewegung auf der Kreisbahn ist, dass die Rotation der Fahrzeuge direkt mit ihrer Position auf der Kreisbahn korrespondiert. Wenn wir ein Fahrzeug vorwärts bewegen wollen, müssen wir es also nur ein wenig weiter rotieren; wie stark diese Rotation sein soll, hängt von der Geschwindigkeit des Fahrzeuges ab. Also fragen wir das Fahrzeug mit Hilfe der Funktion calcSpeed (mehr dazu in der nächsten Datei), wie schnell es sich denn bewegt und geben zur Unterstützung der Berechnung sowohl das nächste Fahrzeug an als auch den Abstand, den die Fahrzeuge zueinander halten sollen (dist.value, wir erinnern uns). Der Faktor 0.9 beim einzuhaltenden Abstand hat stabilisierende Wirkung für die Simulation; da unser Kreis initial sehr dicht gepackt ist, geben wir den Fahrzeugen damit etwas mehr Platz, um ihren Abstand zu organisieren. Die von der calcSpeed-Funktion berechnete Geschwindigkeit (für den Interessierten: die Geschwindigkeit wird in Pixel Grad pro Frame berechnet) rechnen wir auf die aktuelle Rotation drauf (die Schreibweise x += y ist eine Kurzform von x = x + y):
car.rotation += car.calcSpeed( nextCar, dist.value * 0.9 );
Zur Bestimmung der neuen Position des Fahrzeugs benötigen wir nun nur noch ein wenig Mathematik. Wie bereits erwähnt, korrespondiert die Rotation des Fahrzeuges mit seiner Position auf dem Kreis; wir müssen also nur die Fahrzeugrotation in eine Grad-Zahl umrechnen, welche ebenjene Position beschreibt. Die folgende Zeile tut eben genau das (Math.PI repräsentiert die Kreiszahl π):
var circleRot:Number = car.rotation * Math.PI / 180;
Die Position des Fahrzeugs auf dem Kreis (car.x und car.y) lässt sich nun recht einfach mit Hilfe der Winkelfunktionen Sinus und Cosinus berechnen; der Wert 200 steht dabei für den Mittelpunkt des Kreises, der immer genau in der Bildmitte sein soll (wir erinnern uns: das Bild ist 400 Pixel breit und hoch) – der mathematische Hintergrund dieser Berechnung kann hier (insbesondere in den verschieden Abbildungen) einfach nachgelesen werden:
car.x = 200 + Math.cos( circleRot ) * -radius; car.y = 200 + Math.sin( circleRot ) * -radius;
Damit haben wir also nun die neue Position des Fahrzeuges berechnet; bleibt nur noch, die Schleife und die update-Funktion zu beenden, was die folgenden Zeilen tun:
} }
Die eigentliche Programmlogik ist nun also beschrieben. Fehlen nur noch Informationen über diese ominösen Fahrzeuge und vor allem die Funktion calcSpeed. Diese finden sich in der letzten Datei:
Car.as
package { import spark.primitives.Rect; import mx.graphics.SolidColor; public class Car extends Rect { public var speed:Number = 0; public var color:SolidColor; public var reactionTime:Number; public function Car() { reactionTime = 1.0 - Math.random() / 10.0; width = 5; height = 10; color = new SolidColor(); fill = color; } public function calcSpeed( nextCar:Car, distanceToKeep:Number ):Number { var distToNext:Number = Math.sqrt( ( nextCar.x - x ) * ( nextCar.x - x ) + ( nextCar.y - y ) * ( nextCar.y - y ) ) - 10; if ( distToNext > distanceToKeep ) { if ( speed < 0.5 ) speed += 0.01 * reactionTime; } else if ( distToNext < distanceToKeep ) { if ( speed > 0 ) speed -= 0.02 * reactionTime; if ( speed < 0 ) speed = 0; } color.color = ( ( 255 * ( 1 - speed ) ) << 16 | ( 255 * speed ) << 8 ); if ( distToNext + 10 < 12 ) speed = 0; return speed; } } }
Schauen wir uns also nun zum Abschluss auch diese Datei noch zeilenweise an. Sie beginnt mit diesen technisch motivierten Zeilen, die uns an dieser Stelle nicht weitere interessieren sollen:
package { import spark.primitives.Rect; import mx.graphics.SolidColor;
Die erste interessante Zeile ist die folgende. In ihr wird ein neuer Datentyp mit der Bezeichnung Car definiert; der Zusatz extends Rect bedeutet dabei, dass dieses Fahrzeug eigentlich ein Rechteck (von englisch rect = rectangle) mit all seinen Eigenschaften ist (wenn Interesse daran besteht, einen Artikel zu diesem Thema – der sogenannten Objektorientierung – zu haben, bitte Bescheid sagen). Dieser Datentyp wurde in der Datei Main.as bereits mehrfach referenziert und sollte mittlerweile bekannt sein:
public class Car extends Rect {
Die folgenden 3 Zeilen definieren einige Attribute für die Fahrzeuge (zusätzlich zu denen, die es bereits in seiner Eigenschaft als Rechteck hat). Das wichtigste Attribut ist hier natürlich die Geschwindigkeit (speed). color dient lediglich der Einfärbung der Fahrzeuge und soll hier nicht weiter interessieren. Das Attribut reactionTime dagegen benötigen wir; es simuliert eine Reaktionszeit des Fahrzeuges beim Beschleunigen und Bremsen und sorgt für etwas Variation zwischen den verschiedenen Fahrzeugen (mehr dazu weiter unten):
public var speed:Number = 0; public var color:SolidColor; public var reactionTime:Number;
Diese Funktion des Fahrzeuges, die den gleichen Namen wie der Datentyp trägt, ist ein sogenannter Konstruktor; er wird automatisch aufgerufen, sobald ein neues Objekt des Datentyps erstellt wird. Innerhalb des Fahrzeug-Konstruktors initialisieren wir einige Variablen mit Standardwerten; die reactionTime bekommt dabei einen Zufallswert im Intervall [0.9, 1], die Breite und Höhe des Fahrzeuges (als Attribute des zugrundeliegenden Rechtecks) werden auf 5 bzw. 10 gesetzt (die 10 entspricht dabei der Länge des Fahrzeugs) und die Sache mit den Farben ist technischer Natur und soll hier nicht weiter interessieren:
public function Car() { reactionTime = 1.0 - Math.random() / 10.0; width = 5; height = 10; color = new SolidColor(); fill = color; }
Die eigentlich interessante Funktion ist calcSpeed; sie berechnet für das aktuelle Fahrzeug die Geschwindigkeit in Abhängigkeit von der aktuellen Geschwindigkeit, der Distanz zum nächsten Fahrzeug (gegeben über den Parameter nextCar) und die einzuhaltende Distanz (distanceToKeep) und stellt damit eine primitive Form einer künstlichen Intelligenz dar. Die folgende Codezeile leitet die Funktion ein:
public function calcSpeed( nextCar:Car, distanceToKeep:Number ):Number {
Die erste Zeile der Funktion berechnet die Distanz zum nächsten Fahrzeug aus der eigenen Position (x und y) sowie der Position des nächsten Fahrzeugs (nextCar.x und nextCar.y); die mathematische Grundlage dieser Berechnung ist die Bestimmung der Länge eines Vektors (diesmal ist der mathematische Vektor gemeint) – hier der Vektor, der die beiden Positionen verbindet. Der Wert 10 wird von der Distanz abgezogen, da die Fahrzeuge 10 (Pixel) lang sind und die Fahrzeuglänge natürlich nicht mit berücksichtigt werden soll:
var distToNext:Number = Math.sqrt( ( nextCar.x - x ) * ( nextCar.x - x ) + ( nextCar.y - y ) * ( nextCar.y - y ) ) - 10;
Nun erfolgt die eigentliche Bestimmung der Geschwindigkeit. Wenn sich das Fahrzeug weiter als die Minimaldistanz vom nächsten entfernt befindet (distToNext > distanceToKeep) und zusätzlich noch nicht schneller als 0.5 (Pixel Grad pro Frame) ist, so wird die Geschwindigkeit erhöht. Die Erhöhung erfolgt dabei in Abhängigkeit der reactionTime (die damit eigentlich keine Reaktionszeit ist, sondern eine Beschleunigungsverzögerung – zur Simulation der Reaktionszeit ist da aber ausreichend):
if ( distToNext > distanceToKeep ) { if ( speed < 0.5 ) speed += 0.01 * reactionTime; }
Befindet sich das Fahrzeug dagegen zu nah am nächsten (distToNext < distanceToKeep), so wird (wieder in Abhängigkeit der Reaktionszeit) gebremst. Die weiteren Abfragen in den folgenden Zeilen dienen nur dazu, die Geschwindigkeit nicht negativ werden zu lassen:
else if ( distToNext < distanceToKeep ) { if ( speed > 0 ) speed -= 0.02 * reactionTime; if ( speed < 0 ) speed = 0; }
Damit wäre die Geschwindigkeit angepasst. Die folgende Zeile ist nur optischer Natur und ändert die Farbe des Fahrzeugs in Abhängigkeit zur Geschwindigkeit (je langsamer, desto röter – bei Interesse kann ich diese Formel auch noch erklären):
color.color = ( ( 255 * ( 1 - speed ) ) << 16 | ( 255 * speed ) << 8 );
Eine Sicherheitsprüfung brauchen wir noch: befindet sich das Fahrzeug zu nah am nächsten Fahrzeug, so setzen wir die Geschwindigkeit direkt auf 0 und stoppen es damit also. Der Wert von 12 entspricht dabei der Länge der Fahrzeuge plus einer kleinen zusätzlichen Distanz:
if ( distToNext + 10 < 12 ) speed = 0;
Die letzte Zeile der Funktion gibt nun lediglich noch die neu berechnete Geschwindigkeit als Ergebnis der Funktionsberechnung zurück:
return speed;
Und damit alles seine syntaktische Richtigkeit hat, müssen alle geöffneten Klammern natürlich noch geschlossen werden:
} } }
Und das war es auch schon. Diese (relativ gesehen) geringe Menge an Code reicht, um unsere kleine Stau-Simulation vom Anfang des Artikels zu programmieren. Man sieht: selbst mit geringen Mitteln kann bereits eine (zugegeben relativ einfache) Simulation programmiert werden, die dennoch einen Aspekt der Wirklichkeit realitätsnah wiedergeben kann.1 Um die Simulation zu erweitern, könnten nun noch verschiedene weitere Aspekte eingebracht werden, zum Beispiel fahrzeugabhängige Variationen bei den eingehaltenen Sicherheitsabständen, eine erweiterte künstliche Intelligenz oder auch ein erweitertes Modell der Simulation, bei welchem nicht auf eine Kreisbahn gesetzt , sondern eine reale Straße simuliert wird. Die Möglichkeiten sind – dank der Computertechnologie – beinahe unbegrenzt. Der große Vorteil von Simulationen gegenüber realen Experimenten liegt darin, dass außer den Ressourcen für die Programmierung und Durchführung der Berechnungen keine weiteren Mittel benötigt werden; sie sind damit in der Regel bedeutend günstiger und schneller/einfacher durchführbar als reale Experimente (im Falle von zum Beispiel Simulationen im Bereich der Astrophysik gibt es manchmal sogar überhaupt keine Möglichkeit für reale Experimente). Und wie wir in diesem Artikel gesehen haben, steckt hinter einer Simulation auch keineswegs Magie, sondern pure Logik. Natürlich gilt es hierbei zu bedenken, dass eine Simulation auch Fehler enthalten kann; die Ergebnisse müssen also in der Regel auch gegen die Wirklichkeit geprüft werden. Und wie wir im Falle des Stau-Experiments gesehen haben, haben wir zumindest bei unserer Stau-Simulation alles richtig gemacht, denn die Ergebnisse der Simulation werden durch das Experiment bestätigt.
1 An dieser Stelle eine Frage: besteht überhaupt Interesse daran, dass ich den Programmcode der Simulationen hier im Detail vorstelle und bespreche? Soll ich bei weiteren Artikeln zu dieser Serie weiter so verfahren, oder interessiert sich niemand für den Programmcode?
Kommentare (18)