Advanced Games Physics
2. Kapitel

Maßstabsgerechtes Programmieren

Geeignete Maßstäbe für Ort und Zeit

Das Entwickeln von Computerspielen oder -simulationen bedingt ein maßstabsgerechtes Programmieren!
Vor der Entwicklung eines Computerspiels ebenso wie bei der Computersimulation muss man sich nicht nur über die Größenverhältnisse des beabsichtigten Playgrounds Gedanken machen, sondern auch über die Zeitverhältnisse. Dass z.B. ein Fußballfeld nicht ohne maßstäbliche Anpassung auf einem Bildschirm von 23" Diagonale Platz findet, ist ganz klar. Aber auch in zeitlicher Hinsicht können Anpassungen erforderlich werden, wenn z.B. kosmische Verhältnisse simuliert werden sollen. Kosmische Ereignisse dauern Jahre oder noch länger, so lange will kein Spieler warten. Also müssen die Ereignisse in der Simulation durch einen Zeitraffer auf menschliche Dimensionen beschleunigt werden. Ein Vergleich mit dem Film oder Video zeigt, dass, obwohl die geometrischen Verhältnisse vom realen Fußballfeld auf die Verhältnisse am Bildschirm verkleinert werden mussten, die Zeitverhältnisse unverändert bleiben. Der Flug des Fußballs dauert auf dem Bildschirm genau so lange wie auf dem Fußballfeld! Das liegt daran, dass mit den geometrischen Dimensionen auch die Geschwindigkeiten bzw. Beschleunigungen im gleichen Maßstab verändert wurden.

Die Zeit

Wenden wir uns zuerst dem Thema Zeit zu. Im Alltagsleben ist die Zeit ein analoges Phänomen. Zeitabschnitte können beliebig klein "gestückelt" werden. Anders in der digitalen Welt des Computers. Hier wird die Zeit zwangsläufig diskretisiert, d.h. in Zeitquanten zerlegt. Wie die Abb. deutlich macht, werden die Darstellung eines Bildinhalts und das Durchlaufen der draw-Routine synchronisiert. Wie oft dieser Vorgang je Sekunde wiederholt wird, wird in der setup-Routine durch die Funktion frameRate() festgelegt. Rufen wir diese Funktion mit der Constanten frmRate auf, dann erfolgen die Bildwechsel und das Durchlaufen der draw-Routine mit diesem voreingestellten Wert (). Gleichzeitig stellt der Kehrwert der Constanten frmRate die Zeitdauer der Existenz eines Bildes dar.
Egal, ob Normalzeit, Zeitraffer oder Zeitlupe: die Zeit ist der zentrale Parameter unserer Programme! Der geübte Programmierer ist geneigt, die dem Computer eigene Zeitbasis für unsere Applikationen zu verwenden. Aber das ist nicht vorteilhaft! Denn die Computerzeit ist eine "absolute" Zeit, die nicht auf die Bildwechsel Rücksicht nimmt. Deshalb verwenden wir die Zeit zwischen zwei Bildwechseln als zeitliche Differenz oder Zeitquant Δt
()
Δ t = 1 f r m R a t e

und berechnen unsere Zeit daraus incrementell, relativ zu den Bildwechseln:

()
t = t + Δ t


Das hat zwei Vorteile:
  1. Zeit und Bildwechsel verlaufen synchron. Damit stehen die Rechenergebnisse immer dem jeweiligen Zeitpunkt, der durch den Bildwechsel bestimmt ist, korrekt zur Anzeige zur Verfügung.
  2. Der Operator dt, der in der Differentiation und der Integration auftritt, kann angenähert durch die Zeitspanne Δt zwischen zwei Bildwechseln ersetzt werden. Dies spielt insbesondere bei den numerischen Lösungsmethoden eine wichtige Rolle.
Mit dem Befehl frameRate() wird die Bildwechselrate auf einen vordefinierten Wert eingestellt. Im Beispiel () sind dies 50 Bildwechsel je Sekunde.

Umgekehrt ist aber mit der Bildwechselfrequenz die Gültigkeitsdauer eines Bildes (frames) festgelegt. Die Gültigkeitsdauer, oder gleichbedeutend die Zeitspanne zwischen zwei Bildwechseln, ist der elementare Zeitquant, den wir als die Zeitbasis für alle künftigen Berechnungen verwenden werden. Er ergibt sich als Kehrwert dt aus der frameRate. Siehe dazu die setup()-Prozedur in .
Geistesblitz




Abb. Einstellung der Bildwiederholrate frameRate und Berechnung der Zeitbasis t

Auf der Basis des Zeitquants dt kann nun die eigentliche Zeit incrementell berechnet werden. Siehe dazu die draw()-Prozedur in

Die Wahl des Variablennamens für den Zeitquant dt in Anlehnung an den Differentialquotienten ist nicht zufällig. Eigentlich handelt es sich hier um ein Delta (weil Zeitquant), da die damit verbundene Zeitspanne als die Zeit zwischen zwei Bildwechseln verstanden wird. Andererseits wird zukünftig genau dieses dt die o.g. Rolle bei den numerischen Lösungsmethoden für Differentialgleichungen spielen.


Manipulation der Zeitbasis

Wie schon erwähnt gibt es Sujets, die mit unseren normalen Zeitmaßstäben nicht gut zu erfassen sind. Siehe die kosmischen Vorgänge. Indem wir den Zeitquant dt mit einem scaling-Faktor timeScale multiplizieren, können wir die Zeitbasis verändern:

Zeitempfinden timeScale Zeitbasis
1. Normalzeit: = 1 unverändert
2. Zeitraffer: > 1 beschleunigt
3. Zeitlupe: < 1 verlangsamt

Bleibt der Zeitskalierungsfaktor innerhalb des Programms unverändert, kann der Wert des elementaren Zeitquants auch im setup() verändert werden, so dass in der draw()-Routine von nun an nur noch mit dem korrigierten Wert gerechnet wird.



Abb. Manipulation der Zeitbasis

Das in gezeigte Beispiel verdeutlicht die Wirkung einer geeigneten Zeitskalierung. Die für die drei Beispiele Fußgänger, kosmisches Objekt und Geschoss gewählte Zeitskalierung entspricht den Zahlenwerten des Programmauszugs von

Bitte einen Augenblick Geduld
während das Programm geladen wird!

Abb. Manipulation der Zeitbasis


Hier werden Dir die zwei Programmvarianten bereit gestellt. Es steht Dir frei, die passende Version herunter zu laden!
download processing
download p5.js

Ort, Weg, Geschwindigkeit und Beschleunigung

Objektgrößen werden in der Realität in Metern [m] gemessen, auf dem Bildschirm in [Pixeln]. Also besteht die maßstäbliche Anpassung darin, einen vernüftigen Umrechnungsfaktor, den Maßstab [M], zwischen Metern und Pixeln zu finden.

()
l i n P i x e l = l i n M e t e r · M
Geistesblitz
on/off


Diese Definition eines Maßstabes, darüber sollten wir uns bewusst sein, ist genau reziprok zu der bekannten Maßstabskonvention, die auf Landkarten gebräuchlich ist! Die Darstellung großer Entfernungen bedingt dort große Maßstäbe, bei uns hingegen kleine Maßstäbe!

Der zuverlässigste Weg, eine maßstäbliche Abbildung vorzunehmen, ist die Deklaration einer Variablen M (), die generell für die maßstabsgerechte Umrechnung von Metern in Pixel verantwortlich ist.


Abb. Maßstabsgerechte Programmierung - Maßstabsdeklaration

Wenn wir uns noch einmal die Definitionen für Ort, Geschwindigkeit und Beschleunigung anschauen, stellen wir fest, dass alle drei Größen die Ortsvariable s linear enthalten:

()
s [ m ]
v = d s d t [ m s ]
a = d 2 s d t 2 [ m s 2 ]


Für die Maßstabsberechnung bedeutet das, dass zwei Möglichkeiten für deren Ausführung bestehen:
  1. Alle Berechnungen und alle Parameter werden im Originalmaß berechnet bzw. angegeben und erst am Ende der Berechnung werden allein die Ortsgrößen maßstäblich behandelt.
    Also werden z.B. der Startort in m, die aktuelle Geschwindigkeit in m/s usw. angegeben. Die anschließende Rechnung liefert den aktuellen Ort des darzustellenden Objektes auch im Originalmaß m. Erst, wenn dieses Ergebnis vorliegt, erfolgt die Umrechnung in Pixel maßstabsgerecht.

    • Nachteilig an dieser Variante ist, dass die maßstäbliche Umrechnung von m in Pixel bei jedem Bildwechsel erneut erfolgen muss. D.h. Rechenzeit konsumiert.

  2. Diese Variante ist Rechenzeit neutral. Alle Maßstäbe werden vor der eigentlichen Rechnung in der draw()-Routine im setup() ausgeführt. Also werden z.B. der Startort in Pixeln, die Anfangsgeschwindigkeit in Pixel/s, die Beschleunigung in Pixel/s² usw. angegeben. Nachteilig an dieser Variante sind,

    • dass die eigentlichen Rechnungen wenig transparent sind, also eine erhebliche Fehlerquelle darstellen und

    • die für ein responsives Design erforderliche Fensterkorrektur bezieht sich auf die Systemvariablen width und height, um den Maßstab M dynamisch in Abhängigkeit von den Fenstermaßen berechnen zu können. Diese Variablen stehen aber erst nach der Festlegung der canvas-Größe mit der Anweisung createCanvas(width, height) (p5.js) bzw. size(width, height) (processing) in der setup()-Routine zur Verfügung. Daher verbietet sich, zu mindest für ein responsives Design, die vorab-Festlegung eines bestimmten Maßstabes vor Ausführung der setup()-Routine.


zeigt im Programmauszug die wesentlichen Unterschiede beider Methoden. Während Methode 2 die Maßstabskorrektur bereits bei der Variablendeklaration vornimmt, also nur einmalig zum Programmstart, rechnet die 1. Methode durchweg mit den realen Größen und führt erst in der draw-Routine die Maßstabskorrektur bei jedem loop durch. Das kostet natürlich Rechenzeit. Dafür kann hier der Maßstab aber leicht dynamisch verändert werden, wenn sich die Fensterabmessungen ändern.


Abb. Maßstabsgerechte Programmierung - Methodenunterschiede

Hier nun das vollständige Programm in action:

Bitte einen Augenblick Geduld
während das Programm geladen wird!

Abb. Methodenvergleich in der Simulation

Das Programm, das hinter läuft, verfügt absichtlich über keine dynamische Maßstabsanpassung, um die Wirkung einer Maßstabsänderung zu demonstrieren. Betätigung des Buttons hat eine Maßstabsänderung zur Folge, was sich in einer Größen- und Ortsänderung der beiden Objekte Kreis und Linie zum Ausdruck kommt.
Da die dynamische Maßstabsanpassung fehlt, kann das bei einer Änderung der Monitorauslösung zu einer extrem veränderten Anordnung der maßstäblich dargestellten Objekte führen.

Optimale Wahl von Maßstab und Zeitbasis

Bei eigenen Programmversuchen wirst Du feststellen, dass die Wahl von geometrischem Maßstab und Zeitskalierung nicht unabhängig von einander sind. Zum Beispiel müssen in kosmischen Szenarien die geometrischen Dimensionen sehr stark verkleinert werden, um z.B. die Entfernung Erde - Mond auf dem Bildschirm darstellen zu können. Im gleichen Maß wird, ob Du das willst oder nicht, auch die Geschwindigkeit der Objekte reduziert. Das liegt daran, dass die Geschwindigkeit ja als Quotient von Ortsdifferenz zu Zeitdifferenz definiert ist. Schrupft die Pixel bezogene Ortsdifferenz maßstäblich, wird automatisch im gleichen Maß auch die Pixel bezogene Geschwindigkeit verkleinert. So weit so gut. Aber die Umlaufdauer der Erde um die Sonne beträgt immer noch ein Jahr! Wer will am Monitor schon so lange warten?
Was wir brauchen, ist ein Kriterium, nach dem die gewählten Maßstäbe für Ort und Zeit beurteilt werden können. Dieses Kriterium kann die scheinbare Geschwindigkeit eines Objektes gemessen in Pixel/Sekunde auf dem Bildschirm sein. Das hängt mit unseren jahrelangen Erfahrungen mit Fernseh-Angeboten oder anderen Monitor basierten Eindrücken zusammen. Wir wissen zum Beispiel, wie schnell ein Fußgänger, ein Fußballspieler oder ein Flugzeug über den Bildschirm läuft bzw. fliegt. Aus diesen Erfahrungen können wir die am Bildschirm wahrgenommene Geschwindigkeitsempfindung etwa so zusammenfassen:

1. langsame Bewegung: 10 - 20 pixel/s,
2. mittlere Bewegung: 50 - 100 pixel/s
3. schnelle Bewegung: mehr als 100 pixel/s


Damit ist uns eine perceptive Geschwindigkeit vperc zur Beurteilung der getroffenen Wahl von geometrischem Maßstab M und zeitlicher Skalierung timeScale an die Hand gegeben.

Der Zusammenhang zwischen wahrgenommener Geschwindigkeit vperc und wirklicher Geschwindigkeit vreal wird bei gegebenem geometrischem Maßstab M und gegebener zeitlicher Skalierung timeScale durch ausgedrückt:
()
v p e r c = M · t i m e S c a l e · v r e a l [ p i x e l s ]
Bei der Konzipierung eines Playgrounds sind die geometrischen Größen und die Bildschirmauflösung vorgegeben - daraus folgt zwangsläufig der geometrische Maßstab M. Weiterhin sind realen Objektgeschwindigkeiten vreal und die zugeordneten perceptiven Geschwindigkeiten vperc (bei der Einordnung der wahrzunehmenden Geschwindigkeiten kann Dir die obige Liste behilflich sein) gegeben. So kann jetzt der dimensionslose zeitliche Maßstab als einzig verbleibende Wahlgröße nach abgeschätzt werden:

()
t i m e S c a l e = v p e r c v r e a l · M 1


Beachte!: dies ist eine Abschätzung und keine Rechenvorschrift für die Programmierung eines Szenariums! Insbesondere bedeutet das für dynamische Maßstäbe sorgfältige Überlegungen zur geeigneten Wahl des Maßstabes!

Zwangsbedingungen in der Darstellung bewegter Objekte berücksichtigen

Zwangsbedingungen zwingen einen Körper, eine durch die Zwangsbedingung vorgeschriebenen Bahn nicht zu verlassen. Insbesondere beim Übergang von einer Bewegungsform zu einer anderen kann dies zu Problemen bei der Darstellung führen.
Wie schon erwähnt führt die Diskretisierung der Zeit dazu, dass auch die Ortsvariablen diskretisiert berechnet werden. So werden im Laufe eine Bewegungsberechnung nicht alle Orte "getroffen" werden. Das hat Folgen für die Kollisionserkennung im weitesten Sinn. So wird beispielsweise der Übergang vom schrägen Wurf in die Bewegung auf der Ebene (die ja eine Zwangsbedingung y = 0 zur Folge hat) durch das Erreichen der Kollisionsbedingung y ≤ 0 eingeleitet. Da infolge der Diskretisierung der y-Wert in der Regel nicht ≡ 0 sein wird, würde das Objekt auf der Ebene unter der Nulllinie weiterlaufen. Um diese Unnatürlichkeit zu vermeiden, ist eine nachträgliche Ortskorrektur notwendig. Dies demonstriert das Beispielprogramm in :

Bitte einen Augenblick Geduld
während das Programm geladen wird!

Abb. Darstellung unter Beachtung der Zwangsbedingung

Hier kannst Du sehen, dass das Erreichen des Bodes nicht exakt bei y = 0 erfolgt, sondern sichtbar darunter. Ohne Korrektur läuft die Kugel zum Teil im Boden, was unnatürlich wirkt. Die Korrektur besteht in diesem einfachen Fall darin, dass nach dem Eintreten der Kollisionsbedingung, der Wert für y = 0 zwangsweise gesetzt und für die folgende Bewegung am Boden auch beibehalten wird. Etwas höher leigt der Aufwand, wenn eine über die demnächst zu erwartende Kollision gemacht wird. Dies ist dann sinnvoll, wenn das Über- oder Unterschreiten der Zwangsbedingung vermieden werden muss.

Umgang mit variierenden Fensterabmessungen - Unterstützung eines responsive Design

processing

Ein im eigentlichen Sinne responsives Design unterstützt Processing nicht. Es gibt keine Anpassmöglichkeit an die Abmessungen des viewports. Dennoch gibt es eine Möglichkeit, die dem responsives Design angenähernd gerecht wird: nämlich die automatische Anpassung an die Auflösung des clientseitigen Monitors. Willst Du also ein von den Monitotabmessungen unabhängiges Programm zu schreiben, dann müssen für die unterschiedlichen Fensterabmessungen geeignete Vorkehrungen getroffen werden. Damit ein Objekt oder eine Entfernung stets die zur Monitorauflösung relativ gleichen Maße bekommen sind zwei Maßnahmen zu ergreifen:

  1. Beim Entwurf der Szenerie sollte nicht mehr von einer absoluten Zuordung von Pixel-Anzahl zu Metern ausgegangen werden. Statt dessen beziehen wir uns jetzt bei der Maßzuordnung auf die Fensterbreite oder -höhe und ordnen den realen Maßen einen uns sinnvoll erscheinenden Prozentsatz der Fensterabmessung(en). So erhalten wir einen relativen Maßstab!
  2. Aus einer uns sinnvoll erscheinenden Zuordnung von realem Maß (z.B. eines bestimmten Objektes) zu prozentualer Fensterabmessung berechnen wir einen Maßstab M, der zwar nicht mehr fix ist, aber dafür sorgt, dass eine Darstellung der Szenerie unabhängig von den Fensterabmessungen möglich wird.


Mit dem Kommando size(wPixel, hPixel), das als erstes Kommando in der setup()- Routine stehen sollte - und ab Processing 3 stehen muss, wird die Größe des Darstellungsbereiches eingestellt. Die Variable wPixel gibt die horizontale Ausdehnung und hPixel die vertikale Ausdehnung des Darstellungsfensters in Pixeln an. Beide Größen werden vom Programmierer nach seinen Überlegungen festgelegt! Werden wPixel und hPixel kleiner als die am Monitor eingestellte Auflösung gewählt, ist die Welt in Ordnung. Das bedeutet allerdings, dass die verfügbare Bildfläche nicht voll ausgenutzt werden kann. Das ist unschön, denn wir wollen Spiele entwickeln! Da sollte schon das Maximum an Fenstergröße zur Verfügung stehen!

Deshalb werden wir die Fenstergröße nicht feststehend in die setup()-Routine eintragen, sondern (ab processing 3) statt der size()-Routine das Kommando fullScreen() verwenden. Unabhängig von der Wahl dieser Kommandos gibt es die Systemvariablen width und height, die genau die gewünschte Information über die Fensterbreite und -höhe in Pixeln tragen ().

Nun tut sich aber ein anderes Problem auf. Unser Spiel soll ja portabel sein, also auch auf einem VGA-Monitor in voller Schönheit und vor allem vollständig zu sehen sein. Deshalb suchen wir uns jetzt ein charakteristische Objekt (z.B. die Erde) oder eine charakteristische Distanz (z.B. die Entfernung Erde - Mond) aus, die wir unserem Szenarium entsprechend in einer bestimmten, prozentual ausdrückbaren Relation zur Monitorgröße, dargestellt sehen wollen. Zum Beispiel könnten wir wollen, dass die Erde, dargestellt als Kreis, 30% der Bildschirmbreite einnehmen soll, dann kann der Maßstab für jede beliebige Fensterbreite ausgerechent werden:

()
M = 0,3 · w i d t h 12730000


Auf diese Weise wird die Erde unabhängig von der jeweiligen Fensterbreite stets in der richtigen Relation dargestellt werden. Sicher hast Du bemerkt, dass ich mich bei der Nennung der Fensterabmessung vorsichtig ausgedrück habe; denn es gibt ja auch noch die Fensterhöhe! Solange bei der Änderung der Fensterabmessung das Verhältnis von Breite zu Höhe unverändert bleibt, ist die Welt in Ordnung. Wir wissen aber, dass es 4:3 - und 16:9 - Monitore gibt. Der Wechsel von einem Typ zum anderen bringt unzweifelhaft Probleme mit sich. Bei der Wahl der Fensterabmessung, die zur dynamischen Berechnung des Maßstabes Verwendung finden soll, muss also unbedingt gefragt werden, auf welche der späteren Objektabmessungen das Gewicht gelegt werden soll. Wenn z.B. ein vertikales Szenarium dargestellt werden soll, dann sollte auch die Fensterhöhe und nicht die Fensterbreite den Maßstab bestimmen.


Abb. viewport-angepasste Programmierung


Nun gibt es nicht nur bewegte oder auch feststehende Objekte, die zur Szenerie gehören. Sondern auch andere Elemente wie z.B. Buttons, Bilder oder Texte, die unabhängig von der Szenerie dargestellt werden sollen. Während erstere der maßstäblichen Korrektur unterliegen, sollen jene Elemente vom Maßstab unbeeinflusst bleiben. Trotzdem wollen wir, dass diese Elemente auf Monitoren verschiedenster Auflösungen in Relation immer gleich groß und am gleichen Ort erscheinen.

Für Schriften verwende ich normPixel (normierte Pixel), die die Schrifthöhe und -breite auf den 1000-ten Teil des geometrischen Mittels der Fensterabmessungen beziehen:
()
n o r m P i x e l = w i d t h · h e i g h t 1000

Hingegen wird die Plazierung von administrativen Elementen (Hinweistexte, Buttons, ...) durch den 100-ten Teil der Fensterabmessungen in beide Dimensionen getrennt vorgenommen. Diese Basismaße (GridX bzw. GridY ) spannen ein Gitter auf (daher auch der Name), das eine Positionierung in Prozenten der jeweiligen Fensterabmessung erlaubt:
()
G r i d X = w i d t h 100
()
G r i d Y = h e i g h t 100

Untenstehender Programmauszug () zeigt, wie unverrückbare Elemente, in diesem Fall zwei Hinweistexte, plaziert werden. Darüber hinaus wird auch die Fondsgröße auf die aktuellen Fenstermaße adaptiert.


Abb. viewport-gerechte Normierung und Plazierung von Texten

p5.js

Grundsätzlich unterscheiden sich die Berechnung von Maßstab M (), Gitternetz gridX und gridY () sowie der normierten Pixel normPixel () nicht von der Berechnung in processing
p5.js stellt aber statt der Methode fullScreen() eine javascript-typische Methode () zur Verfügung. Diese Methode ist weder Bestandteil des setup()- noch der draw()-Routine! Sie wird durch das "Betriebssystem" von p5.js zur Verfügung gestellt und ausgewertet. Bei jeder Änderung der Fensterabmessungen liefert diese Funktion einen neuen Satz der Fenstermaße width, height und übergibt diese Werte an die canvas-Funktion. So erhält der Darstellungsbereich seine neuen Abmessungen:


Abb. Variante 1 automatische Fensteranpassung


Nachteilig an dieser Methode (Variante 1) ist, dass jegliche Normierung für Maßstab, Gitternetz oder normierte Pixel in der draw()-Routine ausgeführt werden müssen. Das kostet Rechenleistung!

Aber ist denn in der Spielepraxis eine unmittelbare Änderung der Fensterabmessungen überhaupt benötigt? Eigentlich wünscht sich jeder Spieler, dass das Spiel den ganzen Bildschirm ausfüllt! Also sollte es doch ausreichend sein, die Fensterabmessungen anzupassen, wenn z.B. die Orientierung von "portrait" zu "landscape" oder umgekehrt geändert wird. Es genügt daher, die setup()-Routine durch das html-event onresize aufzurufen, nachdem ein Orientierungswechsel stattgefunden hat (Variante 2). Dies kann im body-tag der html-Seite (siehe ) erfolgen.



Abb. Variante 2 automatische Fensteranpassung


Im body-tag der html-Seite () steht noch die Anweisung onload="startTouchEventListener()", die die Voraussetzungen für die touch-Bedienung schafft.


Im folgenden Beispiel () kannst Du die Wirkung Fenster abhängiger Maßstäbe bei Änderung der Fensterabmessungen überprüfen. Dazu musst Du das Browser-Fenster mit der Maus verkleinern oder vergrößern. Damit simulierst Du die Darstellung des Programmgegenstandes auf verschiedenen Endgeräten (PC, Handheld, Tablet etc.).
download p5.js
download p5.js
run program