Advanced Games Physics
Inhaltsverzeichnis zum
Inhalt

Grundlagen von Computerspielen

Basics der Programmierung

Programmieren mit processing oder p5

In diesem Kapitel möchte ich Dir die Basics der Programmierung meiner ausführbaren Programmbeispiele vermitteln, um Dir die Nachverfolgung oder eigenständigen Entwicklung solcher Programme zu ermöglichen.
Als die Entwickler von processing sich an die Arbeit machten, hatten sie das Ziel, eine Entwicklungsumgebung für "jedermann" zu schaffen. Die Ansprüche an Programmier­kenntnisse sollten möglichst gering gehalten werden. Zu Zeiten der Konzipierung der ersten Version von Processing spielten die Themen "Internet" bzw. "Mobile Endgeräte" noch keine Rolle, was zur Folge hatte, dass die von den Nutzern entwickelten Lösungen in erster Linie lokal auf dem Desktop liefen. In der zweiten Version wurde eine Interpreter zur Verfügung gestellt, der JAVA in JAVASCRIPT convertierte und damit Auftritte im Internet ermöglichte. Allerdings mit dem Mangel, dass sie auf mobilen Endgeräten nicht bedienbar sind, weil die Behandlung von touch-events nicht hinreichend möglich ist. In der dritten Version von Processing steht dieser Converter nun nicht mehr zur Verfügung. Statt dessen wurde eine neue Entwicklungsumgebung, nämlich p5.js ins Leben gerufen. Wie der Appendix '.js' deutlich macht, handelt es sich hierbei um ein javascript-basiertes Tool. Jetzt sind auch Internet-Auftritte auf mobilen Endgeräten möglich. Natürlich verlangt der Umstieg auf p5.js einiges an Umstellungen, die aber relativ schnell zu erlernen sind. In p5.js werden die Unterschiede erläutert und mit Programmbeispielen hinterlegt. Auf meiner Homepage stelle ich beide Programmversionen als download zur Verfügung, auch wenn die Processing-Lösungen nicht immer dem letzten Stand entsprechen. Alle aktuellen Programmbeispiele sind in p5.js geschrieben.

Processing

Processing hat verschiedene Vorteile, von denen die wichtigsten hier genannt sein:
  1. Processing ist ein open source Projekt,
  2. Processing ist Java-basiert und kooperiert mit JavaScript,
  3. Dank JavaScript können web-Applikationen geschrieben werden,
  4. Dank der komfortablen Matrix-Befehle können voneinander unabhängige Koordinatensysteme kreiert werden
  5. Processing verfügt über viele features, die eine Vielzahl von Bewegungsmodi unterstützt
  6. Processing verfügt über multimedia-Fähigkeiten

Es ist nicht meine Absicht, eine detailierten Einführung in "processing" zu geben - dafür gibt es gute Tutorials. Nein, ich möchte nur auf eine Besonderheit eingehen, die manchmal zu Verwirrung führt. zeigt den prinzipiellen Pogrammlauf eines in "processing" geschriebenen Programmes. Zu Beginn des Programms wird, wie üblich, eine Deklaration von Variablen und Klassen ausgeführt. Daran schließt sich eine setup()-Sequenz an, die nur einmal durchlaufen wird. Im Setup werden Variablen mit Startwerten versehen oder Klassen instanziiert. Ist dies erfolgt, beginnt der eigentliche Programmlauf. Und hier liegt der Unterschied zu anderen Objekt orientierten Programmiersprachen: diese als draw() bezeichnete Sequenz wird, wenn nicht das Programm absichtlich gestoppt wird, fortlaufend wiederholt. Und zwar im Takt des Bildschirm-Refreshs. Damit werden bewegte Bilder einfach wie bei einem Film oder Video möglich. Ein Film besteht ja auch aus aufeinander folgenden Bildern, genau das bewirkt die draw() Routine!
Allerdings, der Programmierer muss sich der Tatsache stets bewußt sein, dass die draw() Sequenz ohne sein Zutun immer wieder durchlaufen wird. Alle Nutzerinputs, die eigentlich nur bei Bedarf abgefragt werden, müssen immer durch geeignete Verzweigungen aus dem steten Zyklus heraus vorgenommen werden.
Die Bildschirm-Refresh-Rate kann im setup() voreingestellt werden. Damit wird für die Programmausführung ein festes Zeitraster vorgegeben. In der Regel orientiert sich diese Rate an der Physiologie unseres Sehens. Also 25 - 100 mal pro Sekunde wird das Bild auf dem Bildschirm erneuert, also wird die draw() Routine 25 - 100 mal pro Sekunde durchlaufen. Sollte allerdings der in draw() enthaltene Code so umfangreich oder so anspruchsvoll sein, dass die CPU die erforderliche Leistung nicht aufbringen kann, dann kann es zu einer Verringerung der refresh-Rate kommen. Dies hat Konsequenzen für die interne Zeitbasis, die wir zur Berechnung der Zeitfunktionen benötigen.

Prinzipieller Programmablauf

Abb. prinzipieller Programm­ablauf


Noch ein Hinweis zur zeitabhängigen Darstellung eines bewegten Objektes. Da sich die draw()-Sequenz zyklisch wiederholt, werden ohne geeignete Maßnahmen für alle errechneten Orte das Objekt bleibend(!) dargestellt. Ein Spur entstünde dabei. Um das zu verhindern wird bei zu Beginn jedes draw()-Zyklus das letzte Bild vollständig gelöscht, um anschließend mit den neuen Werten neu gezeichnet zu werden. Der Befehl, der dies ausführt, ist der background()-Befehl.

Viele der hier vorgestellten Programme verwenden Funktionen oder Klassen von Funktionen, die in den unterschiedlichsten Situationen Anwendung finden. Sind diese Programme hinreichend allgemein gehalten, werden sie sinnvoller Weise nicht im Hauptprogramm (draw()) untergebracht, sondern separat in einer Library. So stehen sie allen Programmen zur Verfügung. In Processing können solche Programme mittels Tabs eingebunden werden (siehe ).

Hauptprogramm und Tabs

Abb. Einbindung von Tabs in Processing

Tabs sind die in oben beschrifteten Reiter (z.B. Basics_v2.0). Zweckmäßig ist in diesem Zusammenhang die Einrichtung eines Ordners "Libraries", in dem alle Programme und ihre Versionen allgemeingültiger Aufgaben abgelegt sind. In processing ist es allerdings notwendig, dass die erforderlichen Library-Programme in den Ordner des aufrufenden Hauptprogramms kopiert werden, erst dann stehen Sie als Tab zur Verfügung ()!

In processing müssen alle Unterprogramme physisch im Ordner des Hauptprogramms verfügbar sein! Das bedeutet, größte Sorgfalt beim Editieren der Unterprogramme walten zu lassen, da die Unterprogramme zunächst nur lokal in diesem Ordner abgespeichert sind. Erst, wenn diese Änderung hinreichend getestet wurde, kann sie als neue Version in die Library übernommen werden. Aus diesem Grunde stelle ich keine geschlossene Library für die processing-Versionen meiner Beispielprogramme zum Download zur Verfügung - alle erforderlichen Unterprogramme (Tabs) sind in jedes downloadbaren zip-File eingeschlossen!

Ordnerhierarchie

Abb. Verfügbarmachung von Tabs in Processing



p5.js

Der grundsätzliche Programmablauf von "p5.js" unterscheidet sich nicht von dem in gezeigten Ablauf in "processing". Auch für "p5.js" gibt es ein geeignetes Tutorial.
p5.js erzeugt ein script (javaScript), welches in eine html-Seite eingebunden - eingebettet - wird (). Im Beispiel ist dies das Programm "pGrund_0010mini.js". Des weiteren werden aber auch die Includes, die ebenfalls als scripte vorliegen, geladen. Hier ist zu unterscheiden in die "libraries", die von p5.js zur Verfügung gestellt und als Basis der Programmentwicklung eingefügt werden und die Includes, die den Tabs von Processing gleich zu setzen sind. Also entspricht z.B. pControls_v0.0.js, die alle Buttons, Schieberegler und sensitiven Elemente beinhaltet dem Tab controls_vX.X.pde der processing-Lösungen. Neu ist das file "canCSS.css", welches für das Styling der html-Seite verantwortlich ist.
Mit dem abschließenden <div id="pGrund_0010mini" class="canvasStyle"></div> wird die Ausführung des scriptes gestylt und auf der html-Seite positioniert.


Abb. Verfügbarmachung von Tabs und Aufruf eines p5.js- scripts in einer HTML-Seite


zeigt die zu gehörende Ordnerstruktur. Anders als in Processing werden hier die Includes bzw. die Libraries zentral in einem Ordner gelagert. Das erleichtert die Pflege der Includes, da eine Änderung alle vergangenen und künftigen Entwicklungen gleichermaßen betrifft.

Ordnerstruktur in p5

Abb. Ablage von Includes und Libraries
download p5.js
p5 Library


Strukturierung meiner Programmbeispiele

Die meisten meiner Beispielprogramme sind nach dem gleichen Strickmuster gearbeitet. Sieh Dir noch einmal die , die den Grundaufbau der processing oder p5.js-Programmabläufe zeigt, an. Da siehst Du, dass der unter der draw()-Sequenz befindliche Code maßgeblich für das gesamte Programm ist. Also sollten wir uns die draw()-Sequenz etwas genauer anschauen. Hier befinden sich alle Voreinstellungen, alle Berechnungsschritte, alle User-Interaktionen und alle Darstellungen der Objekte.
Bevor wir auf die draw()-Sequenz näher eingehen, will ich aber noch ein Wort zur setup()-Routine sagen. Dort wird nach der Festlegung des Darstellungsbereiches (size(xmax, ymax) bzw. canvas(xmax, ymax)) mit Hilfe der Funktion evaluateConstants(xpos, ypos) die Position des START-Buttons festgelegt. Die Werte für xpos und ypos bewegen sich in einem Rahmen von 0 bis 100, wobei der Wert 100 dem rechten (xmax) bzw. dem unteren Rand (ymax) entsprechen. Die wirkliche Position des Buttons ergibt sich durch Multiplikation der normierten Werte xGrid bzw. yGrid mit den angegebenen Positionswerten xpos und ypos. Die Werte xGrid und yGrid spannen ein 100 x 100 - Gitternetz auf, dessen Maschen wie im Kapitel 2. Darstellung bewegter Objekte beschrieben, unter Berücksichtigung der Display-Auflösung berechnet worden sind. Viele der nachfolgend benutzten Funktionen greifen auf dieses Gitter zurück.
Und nun zur draw()-Routine. Bei der Analyse meiner Programmbeispiele stellt sich heraus, dass ich drei Grundvarianten in Benutzung habe. Diese Varianten unterscheiden sich eigentlich nur in ihrem Start-/Reset-Verhalten und im Zeitpunkt der Freischaltung von Mitteln zur Nutzerinteraktion.

processing: Start/Reset

Abb. a)
Processing: START/RESET des Programmablaufs

p5.js: Start/Reset

Abb. b)
p5.js: START/RESET des Programmablaufs

p5.js: automatischer Start

Abb. c)
Processing bzw. p5.js: automatischer Programmstart
run program
run program

Meine Programme liegen in den meisten Fällen in zwei Varianten vor: Variante 1 (a) (Processing) bzw. (b)) (p5.js)) und Variante 2 (c)) für beide Entwicklungs­umgebungen. Warum zwei Varianten? Weil unterschiedliche Start­situationen vorliegen können:
  1. Variante: Hier wird nach dem Start des Programms auf eine Nutzereingabe, die die Berechnung der Bewegungsabläufe startet, gewartet. Der Nutzer löst die Berechnungen mit der Betätigung des START-Buttons aus. Vor dem START kann der Nutzer per Eingabe mit der Maus bestimmte Bewegungsparameter, wie z.B. die Fallhöhe beim freien Fall oder die Startgeschwindigkeit des zu bewegenden Objektes voreinstellen. Nach dem START ist das nicht immer möglich, weil eine nachträgliche Veränderung der Start­parameter ggf. zu falschen Resultaten führen würde. Nach dem Start des Programms, wechselt der START-Button seine Bedeutung und wird zum RESET-Button. Eine Betätigung des RESET-Buttons führt zu einem erneuten Bedeutungswechsel dieses Buttons. Er wird wieder zum START-Button und eine Wiederholung das Experiment mit den alten oder aber auch mit neuen Startparametern wird möglich. Ist das Experiment regulär beendet, weil eine Endbedingung erfüllt ist, wird die Bewegungsberechnung beendet. Diese wird in der Routine finaltest() bzw. endProgram() ausgetestet. Bei Erfüllung der Bedingung wird der Zeitquant dt = 0 gesetzt, was zu einem Einfrieren der Bewegung führt. Mit dem START wird u.a. der Zeitquant dt = 1/frmRate gesetzt, womit eine erneute Bewegung möglich wird.
    Zwischen den beiden Lösungen a) und b) bestehen kleine Unterschiede, die auf die unterschiedlichen Behandlungen von Variablen in Klassen (Processing) bzw. Objekten (p5.js) zurück zu führen sind.
  2. Variante: Das ist die Spielvariante. Mit dem Programmstart wird auch sofort die Berechnung von Bewegungen möglich. Ein "Ende" gibt es nicht, darum auch kein RESET. Die Steuerung des Spielablaufs wird ausschließlich durch Nutzereingaben bestimmt, sofern nicht das Programm selbst Abläufe bestimmt.
Programmtechnisch unterscheiden sich die beiden Varianten darin, dass bei der
  1. Variante mit dem Aufruf der Funktion startProgram() der Startbutton aktiviert und die Berechnung der Bewegungsabläufe beginnt. Diese endet, wenn entweder die Endbedingung (Processing: finalTest() bzw. p5.js: endProgram()) erfüllt ist oder der RESET-Button betätigt wurde.
  2. Variante dieser Aufruf nicht erfolgt, da die Bewegungsberechnung sofort mit dem Start des Programms beginnt. Nach enthält sie die Programmabläufe der 1. Variante nur rudimentär. Als Beispiel sein hier das Programm Flipper genannt. Das zwar die Abfrage der START-Variablen beinhaltet, aber keinen solchen Button hat. Hier wird mit dem START-Ablauf nur eine Parametrisierung des Spiels vorgenommen, die aber ebensogut in der setup()-Routine erledigt werden könnte. Auch eine Abbruchbedingung gibt es, nämlich das Einlochen der Kugel, sie beendet aber nicht das Spiel, sondern stellt den Ausgangszustand wieder her. Hat die Kugel nämlich das Startloch wieder erreicht, kann sie per handle wieder losgeschickt werden.

Aus grundsätzlichen Überlegungen gibt es eine weitere Gliederung der Aufgaben innerhalb der draw()-Funktion. Diese Gliederung ergibt sich u.a. aus der Tatsache, dass
  1. Bewegungsabläufe sinnvoller Weise in einem Abschnitt Bewegungsberechnung in den realen Größen wie Meter m, Geschwindigkeit m/s oder Beschleunigung m/s2 berechnet werden sollten. Diese werden dann
  2. in einem Darstellungsabschnitt mittels einer Maßstab-Umrechnung und anschließender Koordinatentransformation maßstäblich und örtlich korrekt plaziert und dargestellt.
  3. es schließlich noch administrative Aufgaben zu erledigen gibt. Dazu gehören z.B. textliche Ausgaben (Überschriften, Zahlenangaben) aber auch die unterschiedlichsten Bedienelemente (Buttons oder Scrollbars). Diese Elemente haben ja mit der Spiel-Szenerie nichts zu tun. Daher unterliegen sie keiner maßstäblichen oder örtlichen Transformation.
Und hier stehen die Beispielprogramme nach den Abbildungen a, b und c) zum Download bereit:
download p5.js
download p5.js