JGU Logo JGU Logo JGU Logo JGU Logo

Institut für Informatik

Markus Blumenstock
Sommersemester 2024

Angewandte Mathematik am Rechner

Übungsblatt 6: Abschlussprojekt — angewandte Differentialgleichungen
Letzte Änderung : 09:12 Uhr, 04 July 2024
Abnahmetermin : 18. Juli 2024



„Artillery“ (aka. Gorillas, Worms, etc.)


Spielablauf:
Beim „Artillery“-Spiel steuern die Mitspieler je eine Kanone in einer Landschaft und versuchen durch ballistische Schätzungen die Einheiten der anderen Spieler zu treffen und diesem dadurch Lebenspunkte abzuziehen. Dabei können die Spieler lediglich den Winkel des Schusses sowie die eingesetzte Feuerkraft des Schusses angeben. Um das Spiel zu erschweren, ist oftmals der Stärkefaktor an einen Geschicklichkeitsmechanismus (halten zum Aufladen und loslassen zum Abfeuern) geknüpft oder ein zufällig wechselnder Wind geht in die ballistische Simulation ein, um eine wiederholte optimale Einstellung zu verhindern. Es gewinnt der Spieler, der am längsten überlebt.


Historischer Hintergrund

Das Spielprinzip ist ein alter Klassiker — erste Varianten erschienen schon Mitte der siebziger Jahre (wenige Jahre nach Pong). Es gab danach unzählige Variationen. Besonders bekannt ist wohl das zu MSDOS 5.0 gehörende „Gorillas.bas )“, mit wütenden Gorillas, oder die Worms)-Reihe (besonders bekannt ist die legendäre Episode „Worms Armageddon“).


Hinweis: Der Übersichtlichkeit halber haben wir diesmal Aufgabentext und Erklärungen getrennt. Tipps und Erläuterungen finden sich in einem gesonderten Abschnitt, nach den Aufgabenstellungen. Beachten Sie auch, dass in Aufgabe 6.2 viel Wahlfreiheit besteht (man muss nicht alles implementieren).


Aufgabe 6.1: Implementieren Sie das Grundgerüst des Spiels.

[55 Punkte]


Implementieren Sie ein Spiel nach dem „Artillery“-Spielprinzip, bei dem zwei (oder mehr) Spieler versuchen, den/die Gegenspieler mit einem Projektil zu treffen.

Aufgabe 6.2: Erweiterungen

[45 Punkte]


Wählen Sie drei der nachfolgend vorgeschlagenen Erweiterungen aus und bauen Sie dies in Ihre Implementierung ein. Jede Ergänzung gibt (unabhängig vom Schwierigkeitsgrad) 15 Punkte; maximal sind 45 Punkte erreichbar. Es dürfen beliebig viele Ergänzungen implementiert werden.

Für die Abschlußpräsentation sind natürlich auch weiterführende Erweiterungen (Sound/Graphik/Spielerweiterungen) gerne gesehen; formale Punkte gibt es aber keine.

Tipps zur Implementierung (mit PyQt)

Zeichnen auf Bildern
An sich können Sie ähnlich wie beim ersten Übungsblatt („Snake“) auch erst ein Bild erstellen und dann die einzelnen Pixel manipulieren. Dies hat einige Nachteile: Zum einen müssen Sie so zu jedem Zeitschritt das komplette Bild neu zeichnen lassen. (z.B. ändert sich der Hintergrund sehr selten). Zum anderen ist es so schwieriger komplexere Strukturen wie Kreise zu zeichnen oder bestehende Grafiken einzubinden. Unsere Empfehlung ist es, mit der Klasse QPainter zu arbeiten. Wir haben dazu das Skript um einige Grundfunktionen wie das Zeichnen von Ellipsen/Kreisen, Rechtecken und Text erweitert (Kapitel 1.4.9 „PyQt — Arbeiten mit Bildern“). Für alles Weitere empfehlen wir Ihnen einen Blick in die Dokumentation (Der dortige Code ist zwar C++, das Python-Äquivalent sieht jedoch bis auf etwas anderen „syntaktischen Zucker“ genauso aus).


Krater & Kollision mit der Landschaft


Generieren Sie die Landschaft als Hintergrundbild in einem QImage und radieren Sie das Hintergrundbild nach und nach aus. (In Wirklichkeit setzen wir den Alpha-Kanal auf volle Transparenz).


# Dies muss nur einmalig ausgeführt werden:

# numpy-Darstellung des Bildes (wie bei Snake, siehe Skript)
world = ...

# Darstellung als Bild
world_img = QImage(world.data, width, height, QImage.Format_RGBA8888)

# Raddiergummi auf dem Bild
mappainter = QPainter(world_img)
mappainter.setCompositionMode(QPainter.CompositionMode_Clear)
mappainter.setPen(Qt.black)
mappainter.setBrush(Qt.black)


# später im Code ...

# Krater zeichnen in Position (12,34) mit dem Radius 50
mappainter.drawEllipse(QPoint(12,34),50,50)

Mithilfe der QImage-Klasse (dem Objekt auf dem der QPainter zeichnet) können Sie auch stets prüfen, ob die Karte an einer gegebenen Position durchsichtig ist.

# alpha-Wert in der Position (25,25) prüfen
world_img.pixel(25,25).alpha()

Abstand zur Oberfläche (wie auf dem Screenshot)
Auf dem Screenshot sind Punkte, die näher an der Oberfläche sind heller. Wir haben dies mithilfe des Pakets scikit-fmm gelöst. Dieses berechnet den Abstand von einem Wert zur nächsten 0.

import skfmm

# Karte ist ein Boolsches Array mit True = Pixel ist Land, False = Pixel ist kein Land
map = np.zeros([width,height], dtype=np.bool)

# Distanzen zur 0 (also False)
map_dist = skfmm.distance(map)

# Normieren auf 0,1
map_dist = map_dist / np.max(map_dist)

# Mit Matplotlib in Farben Konvertieren
world = plt.cm.copper_r(map_dist)

# Nicht-Land auf Transparent setzen (True = 1, False = 0)
world[:,:,3] = map

# Konvertierung in ein 8-Bit Array, das in ein QImage konvertiert werden kann
world = np.asarray(world*255,np.uint8)

Zufällige Karte und Windvektorfeld
Eine weit verbreitete Möglichkeit zufällige Gebirgslandschaften zu generieren verwendet die Sinus-Funktion:
Wir summieren eine feste Anzahl von zufällig verschobenen und gestreckten/gestauchten Sinusfunktionen und gewichten die Sinusfunktionen, die stark gestaucht worden stärker. \[\sum_{w\in W} \frac{1}{\sqrt{w}} \cdot sin\left(w\cdot x + \text{rand}[0..2\pi] \right)\cdot \text{rand}[-1..1]\]
Beispielsweise hat in unserer Musterlösung \(W = \,\) np.linspace(0.001,0.05,20) gut funktioniert.
Mithilfe dieser Kurve können Sie nun ein Boolsches-Array erstellen, das wie in der Erklärung der Krater die Karte definiert.
Hinweis: Diese Kurve bewegt sich nun in welchem Intervall? Womit müssen Sie die Kurve multiplizieren, damit Sie sich stets in Ihrem Bildausschnitt bewegt?


Ganz ähnlich können Sie nun ein Windvektorfeld erstellen. Verwenden Sie dazu zwei Mengen \(W_x\) und \(W_y\) für die x- bzw. die y-Koordinate und Summieren Sie die beiden Ergebnisse für das Ergebnis \(\alpha_{x,y}\). Der daraus resultierende Wert kann den Winkel der Windrichtung beschreiben. Führen Sie dies ein zweites mal aus, um die Stärke \(s_{x,y}\) der jeweiligen Windvektoren zu definieren. Die Beschleunigung durch den Wind ist dann am Punkt \(\begin{pmatrix}x\\y\end{pmatrix}\) durch \(s_{x,y}\cdot \begin{pmatrix}\cos \alpha_{x,y}\\\sin \alpha_{x,y}\end{pmatrix}\) gegeben.
Alternativ: Statt die beiden Werte als Richtung und Stärke zu interpretieren können sie die Werte auch \(x_{x,y}\) und \(y_{x,y}\) nennen und damit die Gesamtbeschleunigung durch \(\begin{pmatrix}x\\y\end{pmatrix}\) durch \(\begin{pmatrix}x_{x,y}\\y_{x,y}\end{pmatrix}\). Tatsächlich kann man die beiden Darstellungen in einander überführen. Die erste nennt sich kartesische Koordinatendarstellung, die zweite nennt sich Polarkoordinatendarstellung. Das Umrechnen zwischen diesen beiden ist beim Rechnen mit komplexen Zahlen üblich. Beispielsweise haben wir Ihnen bereits die Umrechnung aus Polakoordinaten \(\alpha,r\) in kartesische Koordinaten bereits genannt.


Flüssigere Steuerung des Kanonenrohrs


Da die Simulation der Partikel häufig und lange laufen muss bietet es sich an sie die ganze Zeit laufen zu lassen (ganz ähnlich wie in der Event-basierten Programmierung). Beim Tastendruck setzen wir die Beschleunigung, die auf den Winkel des Kanonenrohrs wirkt auf einen festen Wert und lösen die Differentialgleichung \(F=ma\) wie gewohnt.


Partikeleffekte


Erweitern Sie Ihre Simulation, indem sie (theoretisch) eine beliebige Anzahl von Partikeln simulieren können; Kanonenkugel, Splitter und Rauch sind dann nur noch Spezialfälle des abstrakten Partikelobjektes, die sich lediglich durch die Art der Visualisierung und der internen Parameter (wie das Gewicht, die Beschleunigung oder die Stärke der Dämpfung) unterscheiden.
Implementieren Sie nun Splitter-Partikel: Sobald eine Kollision beobachtet wurde sollen einige Splitterpartikel mit randomisierten Anfangsgeschwindigkeiten und Richtungen an den aktuellen Kollisionspunkt gesetzt werden. Färben Sie die Splitter-Partikel abhängig von der Länge des Geschwindigkeitsvektors. Dazu eignet sich zum Beispiel eines der vielen Farbschemata von Matplotlib (siehe letztes Blatt). Dieses Vorgehen kann bei der richtigen Wahl dazu führen, dass die Explosion im Zentrum „heißer“ aussieht und nach außen hin abzukühlen scheint.
Implementieren Sie nun Rauchpartikel, die eine leichte Beschleunigung nach oben aufweisen und eine starke Dämpfung der Geschwindigkeit. Sobald die Geschwindigkeit eines Splitterpartikels einen bestimmen Wert überschreitet soll an dessen Stelle zusätzlich ein Rauchpartikel (Startgeschwindigkeit 0) erscheinen.


Hinweis: Achten Sie darauf. die Partikel wieder zu entfernen, wenn sie den Bildschirmrand verlassen oder kollidieren. Ansonsten wird Ihr Code sehr schnell darunter leiden, dass es nur in Python geschrieben wurde. Sie können dies um ein vielfaches herauszögern, wenn sie die Differentialgleichungen vektorisiert lösen. Mehr Details sowie Tipps & Tricks zu diesen Partikelsystemen gibt es in der letzten Vorlesung!


Partikeleffekte 2

Die folgenden zwei Tipps führen dazu, dass die Partikeleffekte noch realistischer Aussehen:
Zeichnen Sie die Rauchpartikel als Kreise mit dunkelgrauer halbtransparenter Mitte und gänzlich Transparentem Rand. Sind genug Rauchpartikel in der Nähe und haben Sie Windfluktuationen eingeführt, führt dies zu einem realistischer Rauch-Effekt.
Zeichnen Sie die Splitter-Partikel auf eine separate Ebene. Übermalen Sie diese Ebene im nächsten Simulationsschritt mit einer leichten Transparenz-Maske (wie der Radiergummi-Effekt beim Visualisieren der Krater) — verwenden Sie zum Übermalen der Ebene statt Schwarz einen Grauton. Dies bewirkt, dass die letzten Simulationsschritte noch leicht sichtbar sind. Visuell bewirkt dies, dass die Explosion mehr Partikel zu haben scheint.