algorithmus „Jump Point Search“

Transcription

algorithmus „Jump Point Search“
Implementierung des Wegfindungsalgorithmus „Jump Point Search“ im
Spieleentwicklungs-Framework Unity 3D
Kai Hillenbrand
Bachelor-Thesis
Studiengang Informatik
Fakultät für Informatik
Hochschule Mannheim
15.07.13
Durchgeführt bei der Firma Elaspix
Betreuer: Dr. Tobias Günther
Zweitkorrektor: Prof. Dr. Thomas Ihme
Bachelorarbeit
Kai Hillenbrand
Eigenständigkeitserklärung
Hiermit bestätige ich, dass ich den vorliegenden Bericht selbstständig verfasst und keine anderen als
die angegebenen Hilfsmittel benutzt habe.
Datum
Kai Hillenbrand
_________________________
_________________________
15.07.2013
1
Bachelorarbeit
Kai Hillenbrand
Abstract
In dieser Arbeit werden die Grundlagen der Wegfindung sowie die verbreitetsten Methoden und
Algorithmen in diesem Themenbereich erklärt. Wegfindung wird in Spielen genutzt, um einen
optimalen Weg zwischen einem Start- und Endpunkt zu finden, welcher von Charakteren und
Objekten verwendet wird. Es wird ein kurzer Überblick über in bekannten Spiele-Engines verwendete
Arten der Wegfindung gegeben. Der Fokus liegt dabei auf dem Spiele-Framework Unity 3D. Den Kern
der Arbeit bildet die Implementierung des Jump Point Search Algorithmus in Unity 3D, welcher eine
Erweiterung des gängigen A*-Wegfindungs-Algorithmus ist. Hierbei wird auf eine effiziente
Implementierung Wert gelegt. Ebenfalls werden Tipps zu Datenstrukturen und weiteren
Optimierungen gegeben. Jump Point Search wird erklärt und abschließend dessen Implementierung
ausführlich evaluiert, indem er mit einer Implementierung von A* verglichen wird. Die Ergebnisse
zeigen, dass dieser neue Algorithmus tatsächlich in den meisten Anwendungsfällen, insbesondere auf
weiten Flächen, A* deutlich überlegen ist.
15.07.2013
2
Bachelorarbeit
Kai Hillenbrand
Inhaltsverzeichnis
Abstract ................................................................................................................................................... 2
Inhaltsverzeichnis .................................................................................................................................... 3
Abbildungsverzeichnis ............................................................................................................................. 5
Begriffsverzeichnis................................................................................................................................... 6
1
2
3
4
5
6
Einleitung ......................................................................................................................................... 7
1.1
Motivation ............................................................................................................................... 7
1.2
Problembeschreibung ............................................................................................................. 7
1.3
Aufgabenstellung..................................................................................................................... 7
1.4
Aufbau der Arbeit .................................................................................................................... 7
Grundlagen ...................................................................................................................................... 9
2.1
Allgemeiner Ablauf .................................................................................................................. 9
2.2
Graphentheorie ..................................................................................................................... 10
2.3
Wegfindungsalgorithmen ...................................................................................................... 13
2.4
Spiele-Engines........................................................................................................................ 17
Wegfindung in verbreiteten Spiele-Engines .................................................................................. 19
3.1
Unity 3D ................................................................................................................................. 19
3.2
Unreal Development Kit (UDK).............................................................................................. 19
3.3
CryENGINE ............................................................................................................................. 20
3.4
Source Engine ........................................................................................................................ 20
3.5
Weitere Engines .................................................................................................................... 21
Repräsentation eines Levels als Graph.......................................................................................... 22
4.1
Rectangular Grid .................................................................................................................... 22
4.2
Quadtree ............................................................................................................................... 22
4.3
Waypoints.............................................................................................................................. 23
4.4
Navmesh ................................................................................................................................ 23
Häufige Wegfindungsalgorithmen in Spielen ................................................................................ 25
5.1
Hierarchical Pathfinding A* (HPA*) ....................................................................................... 25
5.2
Iterative Deepening A* (IDA*)............................................................................................... 26
5.3
Simplified Memory Bounded A* (SMA*)............................................................................... 26
5.4
D* .......................................................................................................................................... 26
Der Jump Point Search Algorithmus .............................................................................................. 27
6.1
Einleitung ............................................................................................................................... 27
15.07.2013
3
Bachelorarbeit
7
8
Kai Hillenbrand
6.2
Algorithmus-Erklärung .......................................................................................................... 27
6.3
Evaluierung ............................................................................................................................ 30
6.4
JPS+ ........................................................................................................................................ 30
Jump Point Search in Unity 3D ...................................................................................................... 31
7.1
Übersicht ............................................................................................................................... 31
7.2
Implementierung des Algorithmus........................................................................................ 33
7.3
Datenstrukturen und Optimierungen ................................................................................... 36
7.4
Evaluierung ............................................................................................................................ 37
Fazit ............................................................................................................................................... 42
8.1
Ergebnis ................................................................................................................................. 42
8.2
Ausblick.................................................................................................................................. 42
Literaturverzeichnis ............................................................................................................................... 43
Bücher und Paper .............................................................................................................................. 43
Webseiten ......................................................................................................................................... 43
15.07.2013
4
Bachelorarbeit
Kai Hillenbrand
Abbildungsverzeichnis
Abbildung 1 – Allgemeiner Ablauf der Wegfindung................................................................................ 9
Abbildung 2 - Ein nicht zusammenhängender Graph............................................................................ 10
Abbildung 3 - Ein Graph mit Weg und Kreis .......................................................................................... 10
Abbildung 4 - Ein Baum-Graph .............................................................................................................. 11
Abbildung 5 - Ein Graph mit Mehrfachkanten und Schlinge ................................................................. 11
Abbildung 6 - Ein bewerteter Graph mit zwei Wegen von A nach B..................................................... 11
Abbildung 7 - Ein gerichteter Graph ...................................................................................................... 12
Abbildung 8 - Ein Wurzelbaum .............................................................................................................. 12
Abbildung 9 - Reihenfolge der Expansionen bei der Breitensuche ....................................................... 13
Abbildung 10 - Reihenfolge der Expansionen bei der Tiefensuche....................................................... 14
Abbildung 11 - Die Unity 3D Entwicklungsumgebung ........................................................................... 18
Abbildung 12 - Beispiele der Levelrepräsentationen [11] ..................................................................... 24
Abbildung 13 - Symmetrische Wege mit jeweils 9 horizontalen und vertikalen Schritten [14] .......... 27
Abbildung 14 - Pruning der Nachfolger eines Feldes beim Expandieren [15]....................................... 28
Abbildung 15 - Beispiel einer Suche mit JPS.......................................................................................... 30
Abbildung 16 - Beispiel eines gefundenen besten Weges in der VisualTiles-Demo ............................. 31
Abbildung 17 - A*-Benchmark in der TileExporter-Demo anhand eines Levels aus Baldur's Gate ...... 32
Abbildung 18 - Ergebnisse der Vergleichstests zwischen A* und JPS ................................................... 38
Abbildung 19 - Tests 1-3 (links: A*, rechts: JPS) .................................................................................... 39
Abbildung 20 - Tests 4-6 (links: A*, rechts: JPS) .................................................................................... 40
Abbildung 21 - Tests 7-9 (links: A*, rechts: JPS) .................................................................................... 41
15.07.2013
5
Bachelorarbeit
Kai Hillenbrand
Begriffsverzeichnis
Agent
Ein Objekt (z.B. Spielercharakter), welches Wegfindung
anfordert
Closed List
Eine Liste von bereits besuchten Knoten während der
Wegfindung.
Division Scheme
Ein Abbildungsschema, welches beschreibt, wie ein SpieleLevel auf einen mathematischen Graphen abgebildet wird.
Engine
Ein eigenständiger Teil eines Computerprogramms, welcher
für eine bestimmte Aufgabe zuständig ist. Eine Grafik-Engine
kümmert sich beispielsweise um die Darstellung der Grafik in
einem Spiel.
Expansion
Einen Knoten in einem Wegfindungsalgorithmus zu
expandieren, bedeutet, seine Nachfolger zu bestimmen und
der Open List hinzuzufügen.
Frame
Ein Einzelbild aus einer Videosequenz, bzw. in diesem Fall der
visuellen Ausgabe des Spiels
Game Loop
Die Schleife, in der sich ein Spiel befindet, während es läuft
Graph
Ein mathematischer Graph ist eine Datenstruktur, welche
mehrere Objekte („Knoten“) und deren Verbindungen
untereinander („Kanten“) beschreibt.
Heuristik
In Bezug auf die Wegfindung beschreibt eine HeuristikFunktion einen Schätzwert von der aktuellen Position zum
Ziel.
Open List
Eine sortierte Liste von Knoten, aus welcher in jedem Schritt
des Wegfindungsalgorithmus der als nächstes zu
expandierende Knoten bezogen wird.
Polygonfläche
Die Fläche, welche ein Polygon (Vieleck) bildet
Suchraum
Die Menge an Objekten, unter denen der beste Weg zwischen
zweien dieser Objekte gesucht wird
Thread
Ein Thread ist ein Teil eines Prozesses. Eine Anwendung kann
in mehrere parallel laufende Threads aufgeteilt werden, um
gleichzeitig Aktionen durchzuführen.
15.07.2013
6
Bachelorarbeit
Kai Hillenbrand
1 Einleitung
1.1 Motivation
Als Wegfindungsalgorithmus bezeichnet man einen Algorithmus, welcher den besten Weg zwischen
zwei Punkten A und B sucht. Hierbei ist allerdings nicht nur eine möglichst kurze Weglänge wichtig.
Liegt beispielsweise ein Fluss zwischen Start und Ziel, ist es im Normalfall vorzuziehen, den Umweg
über die Brücke zu wählen, statt den direkten Weg durch den Fluss zu beschreiten. Wie „gut“ ein
Weg ist, ist also ebenfalls etwas, das von einem Wegfindungsalgorithmus beachtet werden muss.
Wegfindung wird primär im Bereich der Spieleentwicklung verwendet. Sie wird beispielsweise
eingesetzt, um einen Charakter in einem Strategiespiel von A nach B laufen zu lassen, ohne, dass
dieser an möglichen Hindernissen hängenbleibt. Ein weiteres offensichtliches Anwendungsgebiet ist
die Routenplanung. Navigationssysteme greifen auf Wegfindungsalgorithmen zurück, um dem Nutzer
den besten Weg zu seinem Ziel zu präsentieren. Auch in der Robotik wird Wegfindung eingesetzt, um
Roboter intelligent zum Ziel zu führen und Hindernissen ausweichen zu lassen.
1.2 Problembeschreibung
Vor allem in der Spieleentwicklung gibt es weiterhin großen Verbesserungsbedarf an Wegfindungsalgorithmen. Selbst bei großen Spieleproduktionen mit hohem Budget lassen sich häufig Charaktere
erkennen, die an einer Wand hängen bleiben, gegen unsichtbare Hindernisse stoßen oder scheinbar
grundlos einen Umweg laufen.
Durch immer detaillierter werdende Levels erhöht sich die Komplexität der Wegfindung und somit
oft auch die Anzahl der Fehler. Der Bedarf von Ressourcen (CPU-Leistung und Arbeitsspeicher) steigt
ebenfalls, was sich negativ auf die Spieleperformance auswirken kann. Somit ist es wichtig, dass
Wegfindungsalgorithmen so effektiv wie möglich arbeiten, um auch in komplexeren Levels schnell
einen optimalen Weg zu finden.
1.3 Aufgabenstellung
Ziel dieser Arbeit ist es, einen neuartigen Wegfindungsalgorithmus namens „Jump Point Search“ im
Spieleentwicklungs-Framework Unity 3D zu implementieren. Diese Erweiterung von Unity 3D soll
Entwicklern helfen, schnelle Wegfindung auch in komplexen Projekten anzuwenden. Jump Point
Search ist bisher noch kaum verbreitet und es existiert noch keine Implementierung für das Unity 3D
Framework, welches derzeit eines der beliebtesten Werkzeuge zur Spieleentwicklung ist.
1.4 Aufbau der Arbeit
Im Grundlagenkapitel wird zunächst der allgemeine Ablauf der Wegfindung erklärt sowie eine
Einführung in zum Verständnis notwendige Themen wie Graphentheorie, grundlegende
Wegfindungsalgorithmen sowie die Unity 3D Engine und Spiele-Engines im Allgemeinen gegeben. Im
darauf folgenden Kapitel werden verbreitete Spiele-Engines vorgestellt und bezüglich ihrer
verwendeten Wegfindungstechnologien analysiert. Daraufhin werden mehrere Methoden
vorgestellt, welche beschreiben, wie sich ein Level in Form eines zur Wegfindung verwendbaren
15.07.2013
7
Bachelorarbeit
Kai Hillenbrand
Graphen repräsentieren lässt. Während im Grundlagenkapitel die Basisalgorithmen wie A* erklärt
wurden, werden im Kapitel „Häufige Wegfindungsalgorithmen in Spielen“ einige auf A* basierende
und häufig in Spielen verwendete Algorithmen präsentiert. Die folgenden beiden Kapitel behandeln
das Kernthema der Arbeit: Jump Point Search und dessen Implementierung in Unity 3D. Zunächst
wird der Algorithmus und dessen Ablauf detailliert erklärt. Dann wird ein Überblick über die im Zuge
dieser Arbeit entstandene Implementierung in der Spiele-Engine Unity 3D gegeben. Dabei wird
außerdem die Implementierung in Form einer Evaluation mit A* verglichen und es werden mögliche
Optimierungen vorgeschlagen. Im abschließenden Kapitel wird ein Fazit gegeben, welches sowohl
das Ergebnis zusammenfasst, als auch einen Ausblick auf die Zukunft gibt.
15.07.2013
8
Bachelorarbeit
Kai Hillenbrand
2 Grundlagen
2.1 Allgemeiner Ablauf
Der allgemeine Ablauf bei der Suche nach dem besten Weg zwischen einem Start- und Endpunkt
läuft im Regelfall wie folgt ab:
Level / Karte des Suchraums
(1) konvertieren/ abbilden
Repräsentation als Graph
Optimaler Weg von A nach B
(2) anwenden (Startpunkt, Endpunkt)
Wegfindungs-Algorithmus
Abbildung 1 – Allgemeiner Ablauf der Wegfindung
Zunächst wird der Suchraum in einen mathematischen Graph konvertiert. Möchte man beispielsweise den besten Weg von München nach Berlin herausfinden, wäre der Suchraum eine Karte von
Deutschland, welche sämtliche Straßen beinhaltet. In einem Spiel ist der Suchraum das Level, in dem
die Wegfindung durchgeführt werden soll. Bei der Konvertierung werden nur die zur Wegfindung
essentiellen Informationen übertragen – die Farbe einer Wand oder der Name einer Straße ist für
den Algorithmus nicht von Bedeutung. Auf Basis dieses mathematischen Graphs wird nun der
Wegfindungs-Algorithmus angewendet, welcher als Resultat den besten Weg zurückgibt.
Das Konvertieren in einen Graphen geschieht gewöhnlicher Weise nicht zur Laufzeit, sondern wird
schon vorher berechnet und gespeichert. Der eigentliche Algorithmus wird dann zur Laufzeit auf
Anfrage ausgeführt. Bei dynamischen, sich verändernden Levels wird der Graph bei einer Änderung
im Level sofort angepasst, sodass der Algorithmus jederzeit auf die aktuellen Wegfindungsdaten
zurückgreifen kann. Manche Wegfindungsalgorithmen können sogar während der Suche noch auf
Veränderungen im Level reagieren und den gefundenen Weg entsprechend anpassen.
In einem Spiel ist die Wegfindung besonders zeitkritisch, da es mit einer bestimmten Anzahl an
„Frames pro Sekunde“ (FPS) abläuft. Als Frame wird ein einzelnes Standbild der konstanten Folge an
Bildern (im Regelfall zwischen 30 und 60 Bildern pro Sekunde, d.h. ein neues Bild alle 16-33ms)
bezeichnet. Grafisch aufwendigere Spiele haben oft weniger FPS, da die visuellen Berechnungen
umfangreicher sind. Wenn nun viele Agenten – Objekte, die vom Spieler oder der KI gesteuert
werden – gleichzeitig Wegfindungen anfordern, kann das in komplexen Levels dazu führen, dass die
Algorithmen zu lange benötigen, um innerhalb eines Frames eine Lösung zu finden. Deshalb müssen
sie in eigene Threads ausgelagert werden. Generell sollte der Algorithmus allerdings dahingehend
optimiert werden, dass auch viele Wege in nur wenigen Frames gefunden werden können.
15.07.2013
9
Bachelorarbeit
Kai Hillenbrand
2.2 Graphentheorie
Im Folgenden werden die Grundlagen der Graphentheorie erläutert, welche für diese Arbeit
vorausgesetzt sind.
2.2.1
Was ist ein Graph?
Abbildung 2 - Ein nicht zusammenhängender Graph
Ein Graph ist eine mathematische Struktur, welche verschiedene Objekte und deren Zusammenhänge darstellt. Die Objekte werden als Knoten (blau dargestellt in der Grafik) und die Verbindungen
zwischen ihnen als Kanten (grün) bezeichnet. Ein reales Beispiel hierzu wäre ein U-Bahn-Plan:
Stationen würden durch Knoten repräsentiert werden und jene, die Direktverbindungen zueinander
besitzen, wären mit Kanten verbunden. Der Graph in der obigen Abbildung ist „nicht
zusammenhängend“, da nicht jeder Knoten von jedem anderen Knoten erreichbar ist.
2.2.2
Wege und Kreise
Abbildung 3 - Ein Graph mit Weg und Kreis
Eine Folge von Kanten wird als „Weg“ bezeichnet (oben in rot). Ein „Kreis“ ist ein Weg in einem
Graph, der von einem Ausgangsknoten über andere Knoten zum Ausgansknoten zurück führt
(orange).
15.07.2013
10
Bachelorarbeit
2.2.3
Kai Hillenbrand
Bäume
Abbildung 4 - Ein Baum-Graph
Als „Baum“ wird ein zusammenhängender Graph bezeichnet, welcher keine Kreise enthält. Diese Art
von Graph ist eines der zentralen Elemente in einem Wegfindungsalgorithmus.
2.2.4
Mehrfachkanten und Schlingen
Abbildung 5 - Ein Graph mit Mehrfachkanten und Schlinge
Es ist auch möglich, mehrere Kanten zwischen zwei Knoten zu haben (oben in rot). Das kann
beispielsweise notwendig sein, wenn es mehrere U-Bahn-Linien zwischen zwei Stationen gibt. Als
„Schlinge“ wird eine Kante bezeichnet, die von einem Knoten zu sich selbst führt (orange).
2.2.5
Bewertete Graphen
5
7
B
A
2
3
Abbildung 6 - Ein bewerteter Graph mit zwei Wegen von A nach B
Wie würde man in einem Graph beschreiben, dass zwei U-Bahn-Stationen besonders nah
beieinander liegen? Hier kommen bewertete Graphen ins Spiel: Sämtliche Kanten bekommen eine
Bewertung zugefügt – ihre Kosten. Die Kosten eines Weges lassen sich berechnen, in dem man die
einzelnen Kosten seiner Kanten aufaddiert. Aus einer Vielzahl von Wegen zwischen zwei Knoten ist
jener der „beste“, welcher die geringsten Kosten besitzt – und dieser wird vom WegfindungsAlgorithmus gesucht. In der obigen Grafik wäre der orangefarbene Weg von A nach B also günstiger
als der rote. Zur Berechnung der Kosten einer Kante werden nicht nur die Distanz, sondern auch
15.07.2013
11
Bachelorarbeit
Kai Hillenbrand
weitere Parameter, die beschreiben, ob eine Kante „besser“ oder „schlechter“ ist als eine andere,
beachtet. Eine ICE-Verbindung zwischen zwei Städten wäre beispielsweise besser als eine
Busverbindung und hätte somit geringere Kosten, obwohl beide dieselbe Distanz zurücklegen.
2.2.6
Gerichtete Graphen
B
A
Abbildung 7 - Ein gerichteter Graph
Enthält ein Graph Informationen darüber, in welche Richtung zwei Knoten miteinander verbunden
sind, spricht man von einem „gerichteten Graphen“. Die gerichteten Kanten in diesem Graphen
werden mithilfe von Pfeilen repräsentiert. Im obigen Bild kommt man also von A nach B, aber nicht
mehr zurück. In einem Spiel könnte das beispielsweise bedeuten, dass man von einem Felsen zwar
herunterspringen, jedoch nicht mehr hinaufklettern kann. Als „ausgehende Kanten“ bezeichnet man
solche Kanten eines Knoten, die von diesem zu anderen Knoten führen. Das Gegenteil davon wird als
„eingehende Kante“ bezeichnet.
2.2.7
Wurzelbäume
A
B
Abbildung 8 - Ein Wurzelbaum
Ein „Wurzelbaum“ ist ein gerichteter Baum, welcher eine Wurzel besitzt, d.h. einen Knoten, der
ausschließlich ausgehende Kanten hat (Knoten A im obigen Bild). Besteht eine Kante von einem
Knoten A zu einem Knoten B, wird B als „Nachfolger“ oder „Kind“ von A bezeichnet. Gleichzeitig ist A
„Vorgänger“ oder „Vater“ von B.
15.07.2013
12
Bachelorarbeit
Kai Hillenbrand
2.3 Wegfindungsalgorithmen
2.3.1
Der Suchbaum
Um einen Wegfindungsalgorithmus auf einen Graphen anzuwenden, wird aus diesem während der
Suche automatisch ein Wurzelbaum aufgebaut und ständig erweitert. Der Startknoten der Suche
bildet die Wurzel des Baumes. Dann wird dieser Knoten „expandiert“ – das bedeutet, man sucht
dessen Nachfolgeknoten. Die Nachfolger eines Knoten sind jene Knoten, die von ihm aus erreicht
werden können – in der Regel wird dabei der Vorgängerknoten ausgelassen. Durch dieses Ausfächern
entsteht der Wurzelbaum. Wird der Zielknoten gefunden, lässt sich über die Hierarchie des Baumes
der Weg zum Startknoten zurückverfolgen.
Doch was passiert, wenn man auf einen Knoten stößt, welcher bereits (über einen anderen Weg
durch den Baum) expandiert wurde? Ein erneutes Expandieren würde zu einer Endlosschleife führen.
Aus diesem Grund wird eine sogenannte „Closed List“ geführt, welche sämtliche bereits expandierte
Knoten speichert. Trifft man auf einen Knoten aus dieser Liste, wird er einfach ignoriert und nicht
expandiert. Somit werden die Kreise des Graphen eliminiert und spätere Endlosschleifen vermieden.
Die Entstehung des Suchbaums gleicht also dem Ablauf jedes Suchalgorithmus: Der Startknoten wird
expandiert, indem dessen Nachfolger in eine „Open List“ hinzugefügt werden. In jedem Schritt der
Suche wird nun ein Knoten aus dieser Liste ausgewählt, expandiert und dessen Nachfolger wiederum
zur Liste hinzugefügt. Das wird solange fortgeführt, bis der Zielknoten gefunden wurde. Die Auswahl
des nächsten zu expandierenden Knotens aus der Open List gehört zu den größten Unterscheidungsmerkmalen der Algorithmen. Im Folgenden werden die grundlegenden Wegfindungsalgorithmen
kurz vorgestellt und erklärt.
2.3.2
Breitensuche
Die Breitensuche ist eine uninformierte Suche, welche einen Suchbaum Ebene für Ebene aufbaut und
nach dem Ziel durchsucht. „Uninformiert“ bedeutet, dass keine weiteren Kenntnisse über die
Position des Ziels beachtet werden. Stattdessen werden für jeden Knoten erst alle direkten
Nachfolger expandiert (d.h. nach dem Zielknoten geschaut), dann wiederum alle deren direkten
Nachfolger usw., bis das Ziel gefunden oder der komplette Baum durchsucht wurde.
1
3
2
4
5
6
7
Abbildung 9 - Reihenfolge der Expansionen bei der Breitensuche
15.07.2013
13
Bachelorarbeit
Kai Hillenbrand
Abbildung 9 stellt die Reihenfolge dar, in der vorgegangen wird. Der Startknoten ist grün markiert,
der Zielknoten rot. Wäre der orangefarbene Knoten das Ziel, hätte die Breitensuche es bereits nach 3
Schritten gefunden und müsste den restlichen Baum gar nicht erst aufbauen – Knoten 4 bis 7 würden
also wegfallen.
2.3.3
Tiefensuche
Die Tiefensuche ist ebenfalls eine uninformierte Suche und der Breitensuche sehr ähnlich. Sie
unterscheiden sich lediglich in der Reihenfolge, in der der Baum aufgebaut und durchsucht wird. Es
wird in der Tiefe gesucht, was bedeutet, dass zunächst der erste Nachfolger expandiert wird, dann
dessen erster Nachfolger usw., bis kein Nachfolger mehr vorhanden ist. Erst dann wird in der Breite
nach einem Nachbarknoten geschaut. Ist hier keiner mehr vorhanden, wird wieder eine Ebene nach
oben gegangen und weitergesucht. Sobald wieder ein Knoten mit Nachfolgern gefunden wurde, wird
dieser Ast komplett nach unten durchgegangen.
1
5
2
3
4
6
7
Abbildung 10 - Reihenfolge der Expansionen bei der Tiefensuche
Wäre der orangefarbene Knoten das Ziel, hätte es die Tiefensuche erst nach 5 Schritten gefunden. Je
nachdem, wo das Ziel liegt, findet also einer der beiden Algorithmen es schneller als der andere. In
diesem Fall würden demnach Knoten 6 und 7 wegfallen.
2.3.4
Bestensuche
Die Bestensuche ist eine informierte Suche, welche eine Heuristikfunktion verwendet, um das Ziel
schneller zu finden. Das bedeutet, dass gezielt Knoten expandiert werden, welche gemäß der
Heuristikfunktion mit einer höheren Wahrscheinlichkeit zum Ziel führen. Das steht im Gegensatz zu
den bereits vorgestellten uninformierten Breiten- und Tiefensuchen, welche lediglich nach einer
vorgegebenen Reihenfolge Knoten expandieren. Durch diese zielstrebigere (statt willkürliche) Suche
verringert sich im Normalfall die Anzahl der zu expandierenden Knoten, bis das Ziel gefunden wurde,
deutlich. Die Bestensuche funktioniert nur auf einem bewerteten Graphen, da die Kosten der Kanten
in die Suche (und die Berechnung der Heuristik) mit einbezogen werden.
Beim Expandieren werden die Nachfolger eines Knotens anhand ihrer heuristischen Bewertung in die
Open List einsortiert. In jedem Schritt wird der Knoten mit der besten Bewertung aus dieser Liste
expandiert. Trifft man beim Expandieren auf einen Knoten aus der Closed List, wird er nur erneut in
die Open List hinzugefügt, wenn ein besserer Weg zu ihm gefunden wurde als zuvor. Ansonsten wird
er ignoriert, um Endlosschleifen zu vermeiden. Im Folgenden wird der Algorithmus zur besseren
Veranschaulichung im Pseudo-Code gezeigt.
15.07.2013
14
Bachelorarbeit
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
Kai Hillenbrand
Open List = {Startknoten}
Closed List = {}
while OPEN not empty
{
1. Verschiebe Knoten x mit der besten Bewertung aus der Open List
in die Closed List (im 1. Durchgang ist das der Startknoten).
2. Wenn x == Zielknoten, gebe zurückverfolgten Pfad als Ergebnis
Zurück und terminiere den Algorithmus.
3. Expandiere die Nachfolger von x. Für jeden Nachfolger n:
a. Wenn n nicht in Closed List, berechne f(n), setze x als
Parent von n und füge n zur Open List hinzu.
b. Wenn n in Closed List und neue Bewertung besser als alte,
aktualisiere vorhandenen Knoten mit x als Parent.
}
Codelisting 1 - Die Bestensuche im Pseudo-Code
2.3.5
A*
A* ist eine Variante der Bestensuche und gleichzeitig der zur Wegfindung am häufigsten verwendete
Algorithmus. Er expandiert die Knoten anhand einer Bewertungsfunktion, welche wie folgt aufgebaut
ist:
( )
( )
( )
( ) beschreibt die benötigten Kosten vom Startknoten bis zum Knoten und ( ) die geschätzten
Kosten von
zum Zielknoten. Die Summe dieser beiden Funktionen bildet die geschätzten
Gesamtkosten ( ), anhand derer die Open List sortiert wird. Abgesehen von der erweiterten
Bewertungsfunktion funktioniert der restliche Algorithmus exakt wie die Bestensuche.
2.3.6
Dijkstra
Der Dijkstra-Algorithmus ist eine Art von A*, bei welcher ( )
gilt. Das Bedeutet, dass keine
Heuristik verwendet wird, sondern nur die aktuellen Kosten eines Knotens beachtet werden. Der
Algorithmus berechnet die kürzesten Wege von einem gegebenen Startknoten zu allen anderen
Knoten im Graphen. Er beschränkt sich nicht auf einen bestimmten Zielknoten, sondern sucht
gleichmäßig in alle Richtungen, abhängig von den Kosten der jeweiligen Knoten. Daher wird er im
Normalfall nicht verwendet, um den kürzesten Weg zwischen zwei bestimmten Knoten zu finden, wie
es bei normalem A* mit Heuristik der Fall ist. Es würden zu viele kürzeste Wege zu uninteressanten
Knoten gefunden werden, ohne benötigt zu werden.
Für spezielle Szenarien ist er aber auch in Spielen sehr nützlich. Beispielsweise kann er zur Suche des
am schnellsten erreichbaren Power-Ups genutzt werden. In dem Fall wird der Algorithmus so lange
ausgeführt, bis das erste Power-Up gefunden wurde, und dann abgebrochen, da es keinen kürzeren
Weg als diesen dorthin gibt. Auch für taktische Entscheidungen wie die Suche nach Deckung wird er
oft verwendet.
15.07.2013
15
Bachelorarbeit
2.3.7
Kai Hillenbrand
Heuristik
Eine Heuristikfunktion wird verwendet, um die Nähe eines Knotens zum Ziel abzuschätzen. Im
normalen Fall nutzt man hierzu eine „zulässige Heuristik“. Dies bedeutet, dass die geschätzten Kosten
immer geringer sein müssen, als die tatsächlichen Kosten – die Voraussetzung für ein optimales
Ergebnis im Algorithmus. Auch bei leichter Überschätzung kommen meist noch gute oder sogar
optimale Ergebnisse heraus. Zu hohe Überschätzung kann sich allerdings schlecht auf die
Performance des Algorithmus auswirken. Die folgenden Heuristikfunktionen sind weit verbreitet:
-
Euklidischer Abstand: Entspricht der Luftlinie zwischen Knoten und Zielknoten.
Manhatten-Abstand: Addiert die horizontalen und vertikalen Abstände zwischen Knoten und
Zielknoten.
Chebyshev-Abstand: Sucht aus horizontalem und vertikalem Abstand zwischen Knoten und
Zielknoten den Größeren heraus.
15.07.2013
16
Bachelorarbeit
Kai Hillenbrand
2.4 Spiele-Engines
2.4.1
Aufbau einer Spiele-Engine
Eine Spiele-Engine (auch Spiele-Framework genannt) besteht aus den wesentlichen Komponenten,
die benötigt werden, um ein Spiel zu entwickeln. Dazu gehören unter anderem:
-
Grafik-Engine (zur visuellen Darstellung – dies ist die Komponente, in der sich
unterschiedliche Spiele-Engines oft am meisten unterscheiden)
Sound-Engine (zur Ausgabe von Sound-Effekten, Sprache und Musik)
Input-Engine (setzt Eingabebefehle des Nutzers durch Tastendrücke, Mausbewegung und
weitere in Spielbefehle um)
Physik-Engine (stellt sicher, dass sich Objekte physikalisch korrekt verhalten, miteinander
kollidieren etc.)
Netzwerk-Engine (für Spiele mit Online-Funktionalitäten, um mehrere Spieler miteinander zu
verbinden)
Um die Entwicklung eines Spiels möglichst leicht zu gestalten, beinhalten die meisten Spiele-Engines
eine Entwicklungsumgebung. Mithilfe dieser lassen sich häufige Arbeitsschritte deutlich verkürzen
und vereinfachen. Während manche Engines „nur“ einen Editor inklusive einer Scriptsprache (oft
Lua) zur Verfügung stellen, erhält man in anderen kompletten Zugriff auf den Quelltext der Engine,
um somit tiefgreifendere Anpassungen vornehmen zu können. Einige Spiele-Engines sind außerdem
auf spezielle Genres ausgelegt und lassen sich nur mit umfangreichen Veränderungen für andere
Spiele nutzen.
2.4.2
Unity 3D - Einführung
In dieser Arbeit liegt der Fokus auf Unity 3D, welche momentan eine der beliebtesten freien SpieleEngines auf dem Markt ist. Erstmals im Jahr 2005 präsentiert, wurde sie doch erst 2010 durch
Version 3 populär. Das entwickelte Spiel kann auf viele verschiedene Plattformen exportiert werden:
-
Windows, OS X, Linux
mobile Geräte (Android, IOS, Windows Phone)
aktuelle Spielekonsolen (Xbox 360, PlayStation 3, Wii, WiiU)
kommende Spielekonsolen (Xbox One und PlayStation 4)
Tragbare Spielekonsolen (PlayStation Vita)
Web (Adobe Flash, Unity Web Player)
Die Engine wird stetig verbessert und liegt bereits in Version 4 vor. Neben weiteren Plattformen
werden auch regelmäßig neue Features hinzugefügt (u.a. verbesserte Animationen, DirectX 11
Unterstützung, NavMesh Wegfindung, HDR-Beleuchtung). Zusätzlich können Erweiterungen aus dem
„Asset Store“ heruntergeladen werden. Dort liegen sowohl kostenlose als auch kostenpflichtige
Assets vor, die im eigenen Projekt verwendet werden können.
Neben einer kostenlosen Version gibt es auch Unity Pro, welches zusätzliche Features bietet
(vorallem in der Grafik-Engine), sowie die Möglichkeit, den Quelltext der Engine durch Plug-Ins zu
erweitern. [1]
15.07.2013
17
Bachelorarbeit
2.4.3
Kai Hillenbrand
Unity 3D – Wichtige Begriffe
Unity besitzt einen sehr umfangreichen und leicht bedienbaren Editor, um ein Projekt zu verwalten.
Den Kern bilden die sogenannten „Assets“ – einzelne Ressourcen, wie z.B. Modelle, Materialien oder
Scripts. Mehrere Assets können für den Import und Export in ein „Package“ zusammengefasst
werden. Eine „Scene“ wird mit solchen Assets ausgestattet und dann ausgeführt – sie repräsentiert
ein Level in einem Spiel.
„GameObjects“ werden Assets genannt, wenn sie sich als eigenständige Objekte in einer Scene
befinden – beispielweise eine Kamera, ein Licht oder der Spielercharakter. Diese können wiederum
mit anderen Assets wie Scripts oder Materialien ausgestattet werden, welche dann „Components“
genannt werden und das GameObject, dem sie zugewiesen wurden, erweitern. Ein GameObject wird
als „Prefab“ bezeichnet, wenn es als Vorlage für weitere GameObjects dient – beispielsweise ein
Gegnertyp. Von diesem Prefab werden dann in der Scene mehrere Instanzen erzeugt und angepasst.
Diese Anpassungen geschehen mithilfe des „Inspectors“, welcher ein Teil der Benutzeroberfläche ist
und auch aus anderen Anwendungen bekannt ist. Mit ihm können Position, Rotation und sonstige
Eigenschaften eines GameObjects eingestellt werden.
Selbstverständlich wird auch ein Script-Editor mitgeliefert. Ein sehr nützliches Feature dabei ist, dass
vorgenommene Änderungen an Scripts oder im Inspector „live“ ins laufende Spiel übertragen
werden können und so das regelmäßige Neustarten des Spiels zum Testen wegfällt.
Abbildung 11 - Die Unity 3D Entwicklungsumgebung
15.07.2013
18
Bachelorarbeit
Kai Hillenbrand
3 Wegfindung in verbreiteten Spiele-Engines
Die meisten großen Spiele-Engines haben bereits eine oder mehrere Methoden zur Wegfindung
integriert. Alternativ werden auch oft extern Plug-Ins angeboten, welche diese Methoden
nachliefern. Wenn beides nicht der Fall ist, muss der Entwickler die Wegfindung selbst
implementieren, was einiges an Zeit und Aufwand kostet. Alle der unten erläuterten Lösungen zur
Wegfindung nutzen den Algorithmus A* zur Suche. Die meisten 3D-Engines nutzen außerdem einen
Navmesh zur Repräsentierung des Level-Graphen.
3.1 Unity 3D
Die Unity 3D Engine bietet in der kostenlosen Version keine Wegfindung an. Es besteht aber die
Möglichkeit, auf eine der vielen Erweiterungen aus dem Asset Store zurückzugreifen. Zu den
bekanntesten – zu einem Preis von etwa 100 US$ aber auch teuersten – Wegfindungs-Plug-Ins zählt
„A* Pathfinding Project Pro“ von Aron Granberg. Damit lassen sich u.a. Grids, Waypoint-Graphen und
Navmeshs erstellen, die Pfade mit Smoothing-Algorithmen natürlicher machen und tausende Knoten
pro ms durchsuchen. Weitere Plug-Ins wären beispielsweise „SimplePath“ von Alex Kring (60 US$),
„Simply A*“ von BFGames (20 US$) und „Pathfinder“ von Song-GameDev (35 US$). [2]
In der Pro-Version von Unity 3D ist seit Version 3.5 ein Navmesh-System enthalten. Im Editor lässt
sich ein Navmesh automatisch aus einem Levelmesh erzeugen. Den Agenten-GameObjects wird die
„NavMesh Agent“-Component hinzugefügt, welche dann zur Laufzeit dafür sorgt, dass Wegfindung
für dieses Objekt berechnet werden kann. Dabei können u.a. Geschwindigkeit und Höhe des Agenten
festgelegt werden. [3]
3.2 Unreal Development Kit (UDK)
Die Unreal Engine 3 ist eine der in großen Spieleproduktionen am häufigsten verwendete SpieleEngines seit ihrer Veröffentlichung im Jahr 2006. Zu den bekanntesten Titeln, welche die Engine
verwenden, gehören „Unreal Tournament 3“, „Gears of War“, „Mass Effect“, „Batman: Arkham
Asylum“ und „Bioshock“. Seit 2009 bietet das Unreal Development Kit größtenteils
uneingeschränkten Zugriff auf die Engine. Mit dem UDK lassen sich nicht-kommerzielle Spiele
kostenlos veröffentlichen.
Das UDK verwendet die NavMesh-Methode zur Wegfindung, während ältere Versionen noch
Waypoints nutzten. Zusätzlich zum automatisch generierten NavMesh wird noch ein „Obstacle
Mesh“ erzeugt, welcher die „Trennwände“ um Flächen im NavMesh beschreibt. Somit lässt sich
schnell testen, ob der direkte Weg zwischen zwei Punkten hindernisfrei ist: man erzeugt einen Strahl
zwischen diesen beiden Punkten und schaut, ob er den Obstacle Mesh kreuzt. Wenn ja, liegt ein
Hindernis vor und es muss ein Weg um das Hindernis herum berechnet werden.
Die Erzeugung des NavMeshs ist in mehrere Schritte aufgeteilt. Zunächst wird ein rasterartiger Mesh
erzeugt, ausgehend von den vom Entwickler gesetzten Startpositionen. Dieser umfasst die von
diesen Positionen aus erreichbaren Flächen. Die vielen kleinen, quadratischen Flächen werden nun
im nächsten Schritt zusammengefasst, wodurch eine vereinfachte und optimierte Version des
Navmeshs entsteht. Im letzten Schritt erst werden die Kanten des Graphen erzeugt, welche zwischen
den Knoten (Flächen) im Navmesh vorliegen, die miteinander verbunden sind. [4]
15.07.2013
19
Bachelorarbeit
Kai Hillenbrand
3.3 CryENGINE
Die CryENGINE entstand für den im Jahre 2004 erschienenen Ego-Shooter „Far Cry“ bei
Entwicklungsstudio Crytek aus Frankfurt. Seitdem wurden neuere Versionen der Engine vor allem
durch den indirekten Nachfolger „Crysis“ und dessen Fortsetzungen bekannt. Sie wird primär für
Actionspiele verwendet. Mittlerweile steht die CryENGINE 3 – die aktuellste Version der Engine – zur
nicht-kommerziellen Nutzung kostenlos zur Verfügung. [5]
Das Wegfindungssystem nutzt einen NavMesh, welcher in einem ungerichteten Graphen
repräsentiert wird. Ein Knoten in diesem Graphen repräsentiert eine frei begehbare Fläche im Level
ohne Hindernisse. Der Graph wird automatisch vom Editor mithilfe von Triangulierung erstellt. Dabei
werden jeweils drei nah beieinander liegende Hindernisse gewählt und ein Dreieck erzeugt, welches
die freie Fläche zwischen diesen Hindernissen repräsentiert. Angrenzende Flächen werden im Graph
automatisch mit Kanten verbunden. Alternativ lassen sich die Knoten (und somit die Flächen) auch
manuell im Editor festlegen. [6]
Zur Berechnung des kürzesten Weges wird A* verwendet. Die Kosten der Kanten im Graph
entsprechen üblicherweise dem tatsächlichen Abstand im 3D-Raum zwischen den Mittelpunkten der
zu verbindenden Flächen. Diese Kosten lassen sich allerdings auch anpassen: Beispielsweise könnten
die Kosten zwischen zwei Knoten auf einer Straße mit 0.1 multipliziert werden, um somit bevorzugt
zu werden. [7]
3.4 Source Engine
Die Source Engine von Spieleentwickler Valve kam erstmals bei den Spielen „Counter Strike Source“
sowie „Half-Life 2“ zum Einsatz. Obwohl sie seitdem ständig mit neuen und verbesserten Features
überarbeitet wurde, sieht man ihr im Vergleich zur Unreal Engine 3.0 oder CryENGINE 3 ihr Alter
bereits an. Obwohl die Source Engine primär für Ego-Shooter konzipiert wurde, findet sie
mittlerweile auch in anderen Genres Anwendung, wie beispielsweise Puzzle („Portal“), Strategie
(„DotA 2“) und Top-Down-Action („Alien Swarm“).
Wie die meisten 3D-Spiele-Engines setzt auch die Source Engine auf NavMeshs. Durchschnittlich fünf
Minuten benötigt der Editor, um für ein normal großes Level den NavMesh zu generieren. Dabei wird
ausgehend vom Startpunkt des Spielercharakters das Level erfasst und anschließend werden
erreichbare Flächen zum NavMesh hinzugefügt. Nicht begehbare Flächen lassen sich im Editor
manuell verbieten. Auf Wunsch lassen sich auch spezielle Orte wie Verstecke oder SniperAussichtspunkte automatisch berechnen. Ebenfalls wird die Größe der Agenten beachtet und so
verhindert, dass beispielsweise ein Auto durch eine Gasse fährt, die nicht breit genug ist. [8]
15.07.2013
20
Bachelorarbeit
Kai Hillenbrand
3.5 Weitere Engines
Zu weiteren recht verbreiteten 3D-Spiele-Engines zählen die „Blender Game Engine“ (BGE), sowie die
„Java Monkey Engine“ (JME). Beide werden nur selten für größere Projekte verwendet und bieten
sich eher für kleinere Spiele oder Prototypen an. Während BGE NavMesh nativ unterstützt, bietet
JME keine Wegfindungsmethoden ab. Dafür muss man auf verschiedene von der Community
entwickelte Klassen zurückgreifen.
Die HTML5-Engine „Construct 2“ beinhaltet Wegfindung auf einem Grid. Parameter wie Tilegröße
und Geschwindigkeit der Agenten lassen sich anpassen. Andere große 2D-Engines, die auf HTML5
basieren, sind die „Impact Engine“ und „EaselJS“. Beide haben keine Wegfindungsmethoden
integriert, aber auch hier existieren entsprechende Erweiterungen von der Community. [9]
15.07.2013
21
Bachelorarbeit
Kai Hillenbrand
4 Repräsentation eines Levels als Graph
Wie bereits im Grundlagenkapitel erklärt (Abbildung 1 – Allgemeiner Ablauf der Wegfindung auf
Seite 9), lautet der erste Schritt der Wegfindung, den Suchraum in einen Graph zu konvertieren,
welcher vom Wegfindungs-Algorithmus verwendet wird. Im Fall eines Spiels wird ein Level in
begehbare Bereiche oder Wegpunkte unterteilt (Knoten), welche miteinander verbunden werden
(Kanten). Das geschieht auf Grundlage eines „Division Schemes“, also einem Abbildungsschema. Im
Verlauf dieses Kapitels werden die verbreitetsten Division Schemes kurz vorgestellt. Es sei gesagt,
dass jede dieser Repräsentationen ihre spezifischen Anwendungsbereiche, Stärken und Schwächen
besitzt und es die Aufgabe des Entwicklers ist, das für sein Spiel am besten geeignete Division
Scheme zu wählen. [10] [11]
4.1 Rectangular Grid
Ein Grid (oft auch „Tilemap“ genannt) besteht ähnlich wie ein Schachbrett aus vielen einzelnen
quadratischen Feldern („Tiles“). Ein Tile ist üblicherweise entweder begehbar oder nicht begehbar,
was bei der Wegfindung berücksichtigt wird. Die meisten Strategiespiele verwenden diese
zweidimensionalen Grids, welche dem eigentlichen, deutlich detaillierteren 3D-Level unterliegen. Je
höher die Tile-Dichte, desto präziser wird der Algorithmus arbeiten – viele Algorithmen werden
dadurch aber spürbar verlangsamt. Ein Tile repräsentiert einen Knoten im Graph, angrenzende Tiles
sind mit Kanten verbunden.
Problematisch wird es in 3D-Spielen, die wie die meisten aktuellen Actionspiele aus mehreren
Ebenen bestehen. Für eine Brücke, die man überqueren, aber auch darunter durch laufen kann,
bräuchte man schon ein weiteres Grid und einen speziell angepassten Algorithmus. Ein Vorteil der
Grid-Repräsentation besteht darin, dass sie schnell auf sich dynamisch ändernde Levels angepasst
werden können. Tiles lassen sich direkt über ihre x- und y-Koordinate ansprechen und auf
„begehbar“ oder „nicht begehbar“ setzen.
4.2 Quadtree
Ähnlich einem Grid, wird auch hier das Level in viele Quadrate unterteilt. Während beim Grid aber
sämtliche Tiles dieselbe Größe besitzen, wird in einem Quadtree je nach notwendiger Genauigkeit
die Tilegröße variiert. Das ist gerade bei größeren begehbaren Flächen von Vorteil, da zum einen
weniger Speicher benötigt, als auch die Algorithmus-Laufzeit verringert wird. Die einzelnen Tiles
können allerdings nicht frei in ihrer Größe geändert werden, sondern müssen einem bestimmten
Schema folgen: Die komplette Ursprungsfläche wird zunächst in 4 gleichgroße Quadrate unterteilt,
welche wiederum selbst so weit wie benötigt unterteilt werden.
Auch dieses Division Scheme eignet sich weniger für 3D-Levels mit mehreren Ebenen und somit vor
allem für Strategie- und Rollenspiele. Für diesen Zweck gibt wird ein „Octree“ verwendet. Hierbei
läuft das Verfahren wie beim Quadtree ab, mit dem Unterschied, dass von einem Würfel
ausgegangen wird, welcher jeweils in 8 gleich große Würfel unterteilt wird. Von hier an werden die
einzelnen Würfel jeweils so oft unterteilt wie gewünscht.
15.07.2013
22
Bachelorarbeit
Kai Hillenbrand
4.3 Waypoints
Ein in aktuellen 3D-Spielen häufig verwendetes Division Scheme ist das „Waypoints“-Schema (auch
„Points of View“) genannt. Hierbei werden Wegpunkte (Knoten) im Level bestimmt und jene
miteinander verbunden, die sich gegenseitig sehen können (Kanten). Dieses Schema ähnelt im
Aufbau am deutlichsten dem von einem Graphen und eignet sich auch für Actionspiele mit mehreren
Ebenen. Im oben genannten Beispiel würde man einen Wegpunkt sowohl auf als auch unter die
Brücke setzen. Nachteil ist, dass die Wegfindung dann nur einen begrenzten Überblick über das Level
besitzt – es weiß nicht, was sich abseits dieser Wegpunktverbindungen befindet. Um ein Level
optimal zu repräsentieren, wären also extrem viele Wegpunkte notwendig. Ansonsten ist der
resultierende Weg sehr „eckig“ und oft nicht optimal (Abbildung 12, links unten).
Dieses Schema wird oft zusammen mit dem aufwendigeren Navmesh-Schema verwendet und
besonders bei Nicht-Spieler-Charakteren (NPCs) genutzt. In Rollenspielen werden damit
beispielsweise Wegrouten für unwichtige NPCs erstellt, auf denen sich diese kontinuierlich (zufällig)
bewegen, um den Eindruck einer lebendigen Welt zu geben. Da sich diese Charaktere niemals zu
einem nicht auf dieser Route befindlichen Ort begeben müssen, reicht das Waypoint-Schema also für
sie (auch mit wenigen Wegpunkten) aus.
4.4 Navmesh
Ein sogenannter „Navigation Mesh“ wird direkt aus den 3D-Informationen des Levels erstellt. Statt
aus quadratischen Flächen besteht der Navmesh aus vielen unterschiedlich großen Polygonflächen
(also Vielecken). Hierbei werden natürlich nur jene Flächen in den Navmesh übernommen, die auch
begehbar sind. Welche das sind, kann der Entwickler anhand verschiedener Parameter bestimmen.
Alternativ lassen sich Flächen auch manuell aus dem Navmesh ausschließen. Die übrig gebliebenen
Flächen repräsentieren dann die Knoten im Graph, während aneinander angrenzende Flächen durch
Kanten verbunden werden. Obwohl Navmeshs eine relativ neue Entwicklung sind, werden sie bereits
von den meisten modernen Engines unterstützt und verwendet (siehe Kapitel „Wegfindung in
verbreiteten Spiele-Engines“).
15.07.2013
23
Bachelorarbeit
Kai Hillenbrand
Abbildung 12 - Beispiele der Levelrepräsentationen [11]
Der gefundene Weg ist in Gelb eingezeichnet, die expandierten Knoten (d.h. Felder, Punkte oder
Polygone) in Rosa.
Links oben: Rectangular Grid
Rechts oben: Quadtree
Links unten: Waypoints
Rechts unten: Navmesh
15.07.2013
24
Bachelorarbeit
Kai Hillenbrand
5 Häufige Wegfindungsalgorithmen in Spielen
Die meisten Wegfindungsalgorithmen in Spielen basieren auf dem A*-Algorithmus. Hierbei wird in
der Regel eine erweiterte, speziell auf das Spiel angepasste Form verwendet und nur selten
unveränderter A*. In diesem Kapitel werden einige der verbreitetsten A*-Varianten vorgestellt. Dies
ist nur ein kleiner Auszug aus einer Vielzahl von Algorithmen. Keine dieser Varianten ist allumfassend
oder besser als eine andere - jede ist für spezielle Situationen und Spieltypen besser geeignet als eine
andere und hat demnach ihre Daseinsberechtigung.
Ein Wegfindungsalgorithmus bekommt als Eingabeparameter den Graphen sowie Start- und
Zielknoten. Er liefert – wenn vorhanden – den besten Weg zwischen diesen beiden Knoten zurück.
Bei Graphen ohne Mehrfachkanten ist dies für gewöhnlich eine Liste der Knoten dieses Weges. Gibt
es allerdings Mehrfachkanten, wird es notwendig, stattdessen eine Liste der Wegkanten
zurückzuliefern. [10]
5.1 Hierarchical Pathfinding A* (HPA*)
Sehr detaillierte Levels benötigen in der Regel auch einen sehr umfangreichen Graphen zur
Wegfindung. Die Berechnung des kompletten Weges kann unter Umständen zu lange benötigen, um
noch innerhalb eines Frames berechnet zu werden, was zu störenden Verzögerungen führt. Bei
weiten Wegen ist auch die Wahrscheinlichkeit höher, dass sich die Spielwelt in der Zwischenzeit
schon wieder geändert hat: eventuell ist der vorherbestimmte Weg mittlerweile blockiert oder muss
gar nicht mehr begangen werden. Das führt dazu, dass Rechenzeit unnötig verschwendet wurde, da
nur ein Teil des gefundenen Wegs benutzt wurde, da der Rest mittlerweile ungültig geworden ist.
Diese Probleme versucht HPA* zu umgehen. Der ursprüngliche, detaillierte Wegfindungsgraph wird
in einzelne Teilgraphen unterteilt. Zusätzlich wird ein abstrakterer Graph erzeugt, welcher diese
Teilgraphen miteinander verbindet, ohne die Detailinformationen dieser Graphen zu kennen. Wird
nun ein Weg gesucht, geschieht dies zunächst auf dem abstrakten Graph mit der Information als
Ergebnis, welche Teilgraphen (in welcher Reihenfolge) für den kompletten Weg benötigt werden.
Nun kann der Anfang des kompletten Weges durch den ersten Teilgraphen schnell gefunden werden
und es können bei Bedarf weitere Teile des Weges in den folgenden Teilgraphen „on the fly“
berechnet werden. Dadurch wird sichergestellt, dass immer nur die relevanten Teilprobleme
berechnet werden. Der eigentliche Suchalgorithmus dafür ist im Regelfall – aber nicht zwangsweise Standard-A*.
Angenommen, man suche den besten Weg von Mannheim nach Berlin. Statt nun in einem einzelnen,
komplexen Graphen zu suchen, welcher sämtliche Straßen Deutschlands beinhaltet, wird zunächst
auf einem abstrakteren Graphen nach dem groben Weg gesucht. Dadurch ergeben sich Orte
(Zwischenstationen), die auf dem besten Weg liegen, z.B. Auf- und Abfahrten der zu befahrenen
Autobahnen. Wie man von der aktuellen Position zur nächsten Autobahn oder von der
abschließenden Autobahn zum Ziel gelangt, wird schließlich aus entsprechenden detaillierteren
Graphen entnommen. Der große Vorteil dabei ist, dass jeweils nur der Weg zur nächsten
Zwischenstation und nicht der komplette Weg zum Ziel gesucht werden muss. [12]
15.07.2013
25
Bachelorarbeit
Kai Hillenbrand
5.2 Iterative Deepening A* (IDA*)
Diese Variante von A* kombiniert die „Iterative Tiefensuche“ mit A*. Bei der iterativen Tiefensuche
wird eine normale Tiefensuche wiederholt bis zu einer bestimmten Tiefe ausgeführt, welche mit
jedem Schritt erhöht wird, bis eine Maximaltiefe erreicht oder zuvor das Ziel gefunden wird. Bei IDA*
werden statt der Tiefe die maximalen Kosten des Knotens als Limit gesetzt und iterativ erhöht. Der
Vorteil hierbei ist, dass die bereits besuchten Knoten nicht gespeichert werden müssen, es durch das
Kostenlimit aber trotzdem nicht zur Endlosschleife kommt. Das führt zu einem geringeren
Speicherverbrauch. Ein Nachteil ist allerdings, dass IDA* etwas langsamer als Standard-A* ist, da
einige Knoten zu einem gewissen Grad mehrfach besucht werden.
5.3 Simplified Memory Bounded A* (SMA*)
Der SMA*-Algorithmus bekommt nur begrenzten Speicher zur Verfügung gestellt, welcher nicht
überschritten wird. Das ist in Situationen nützlich, in denen der Speicher knapp und die Anzahl der zu
findenden Wege hoch ist. Bei ausreichend Speicher verhält sich SMA* exakt wie A*. Ist der Speicher
des Algorithmus allerdings aufgebraucht, schneidet er diejenigen Äste ab, welche die höchsten
Kosten haben, da vermutet wird, dass diese später sowieso nicht mehr benötigt werden. Stellen sich
die nun expandierten „besseren“ Äste als schlechter als zunächst vermutet heraus, müssen die zuvor
abgeschnittenen Äste wieder rekonstruiert werden. Hierzu wird in jedem Vaterknoten der
Kostenwert des abgeschnittenen Kind-Astes mit den geringsten Kosten gespeichert, um diesen später
wiederherstellen zu können.
5.4 D*
D* wurde speziell für sich dynamisch stark ändernde oder für den Agenten unbekannte Levels
konzipiert. Im Gegensatz zu A* wird hierbei die Suche beim Zielknoten begonnen und rückwärts der
Weg zum Start gesucht. Weiterhin ist nur die direkte Umgebung des Agenten bekannt – vom
restlichen Level wird ausgegangen, dass es frei begehbar ist. Erst wenn neue Umgebung erkundet
und Hindernisse gefunden werden, wird der Algorithmus entsprechend angepasst und die
entdeckten Hindernisse mit einberechnet. D* wird bevorzugt in der Robotersteuerung verwendet, da
er unter Berücksichtigung von vielen Agenten in einem Spiel aufgrund der häufigen
Neuberechnungen oft zu langsam ist.
15.07.2013
26
Bachelorarbeit
Kai Hillenbrand
6 Der Jump Point Search Algorithmus
6.1 Einleitung
„Jump Point Search“ (JPS) ist ein relativ neuer Wegfindungsalgorithmus. Er wurde im Jahr 2011 von
Daniel Harabor und Alban Grastien entwickelt. JPS ist eine Erweiterung von A* und benötigt eine
Grid-Repräsentation des Levels zur Anwendung. Dabei muss der Graph unbewertet sein, d.h. alle
Felder besitzen dieselben Schrittkosten zu ihren angrenzenden Feldern.
Der Algorithmus führt Symmetriereduktion durch, was bedeutet, dass mehrfache Pfade derselben
Länge zwischen zwei Feldern entfernt werden, um so die Anzahl der möglichen Wege zu reduzieren
und die Wegfindung zu beschleunigen. Außerdem werden sogenannte „Jump Points“ berechnet:
diese führen dazu, dass weite Flächen, die von A* Feld für Feld erkundet werden würden, einfach
übersprungen werden können.
Jump Point Search lässt sich leicht mit anderen Optimierungstechniken verbinden, wie beispielsweise
Abstraktion – der Ansatz, welcher auch von HPA* umgesetzt wird. [13]
6.2 Algorithmus-Erklärung
6.2.1
Symmetriereduktion
Der JPS-Algorithmus basiert zum Großteil auf der Idee der Symmetriereduktion. In einem Grid
existieren stets mehrere gleich lange Wege zwischen zwei Feldern, welche sich nur in der
Reihenfolge ihrer Schritte unterscheiden. Durch Symmetriereduktion werden solche symmetrischen
Wege entfernt, sodass nur noch ein einzelner Weg übrig bleibt. Das führt dazu, dass deutlich weniger
Wege betrachtet werden müssen und die Wegfindung entsprechend schneller abläuft. [14]
Abbildung 13 - Symmetrische Wege mit jeweils 9 horizontalen und vertikalen Schritten [14]
15.07.2013
27
Bachelorarbeit
6.2.2
Kai Hillenbrand
Expansion von Feldern
Während Jump Point Search im Wesentlichen wie A* abläuft, unterscheiden sie sich am meisten
beim Expandieren der Felder. Statt sämtliche direkten Nachbarn des Feldes zur Open List
hinzuzufügen, wie A* es tut, versucht Jump Point Search, Symmetrien zu erkennen, zu vermeiden
und entsprechende Felder/Knoten zu ignorieren. Das Ignorieren bzw. „Abschneiden“ der Kanten zu
diesen Knoten wird auch „Pruning“ genannt. Dadurch, dass somit die meisten Felder lediglich
überprüft, aber nicht sofort in die Open List einsortiert werden müssen (da sie übersprungen
werden), ergibt sich ein großer Geschwindigkeitsvorteil. Die Felder, die dann übrig bleiben, werden
„Jump Points“ genannt und bilden die Nachfolger des Knotens, von dem aus gesprungen wurde.
Dabei wird zwischen horizontalem/vertikalem und diagonalem Springen unterschieden. Die
Vorgehensweise wird in den nächsten beiden Unterkapiteln anhand von Abbildung 14 gezeigt. [15]
Abbildung 14 - Pruning der Nachfolger eines Feldes beim Expandieren [15]
(a)
(b)
(c)
(d)
zeigt die Felder, welche bei einer horizontalen Expansion ignoriert werden (grau)
zeigt einen „Forced Neighbor“ auf Feld 3, da Feld 2 blockiert ist (schwarz)
zeigt die Felder, welche bei einer diagonalen Expansion ignoriert werden (grau)
zeigt einen „Forced Neighbor“ auf Feld 1, da Feld 4 blockiert ist (schwarz)
6.2.3
Horizontaler und vertikaler Sprung
Angenommen, man kommt von Feld 4 nach x und möchte nun x expandieren (vgl. Abbildung 14 Teil
a). Da wir von 4 kommen, kann das Feld 4 direkt ignoriert werden. Felder 1, 2, 6 und 7 sind von 4 aus
direkt zu erreichen und somit schneller als über x, daher werden sie auch ignoriert. Als letztes lassen
sich noch Felder 3 und 8 ignorieren, da diese ebenfalls von 4 aus – ohne über x zu gehen – genauso
schnell zu erreichen sind. Somit bleibt nur Feld 5 übrig, d.h. das nächste Feld, welches in der
Laufrichtung liegt. Solange keine Hindernisse vorliegen, wird also in diese Richtung gesprungen, was
auch als „horizontaler Sprung“ bezeichnet wird.
Liegt nun aber ein Hindernis auf Feld 2 oder 7 vor (vgl. Abbildung 14 Teil b), wird der Sprung beendet
und das aktuelle Feld x der Open List hinzugefügt. Der Grund dafür liegt darin, dass nun Feld 3 nicht
mehr ignoriert werden kann, da nun der Weg durch x der (einzige) schnellste Weg dorthin ist. Feld 3
wird nun „Forced Neighbor“ genannt, da wir es genauer betrachten müssen, statt es wie zuvor
ignorieren zu können. Als nächstes besteht nun die Möglichkeit, von Feld x aus sowohl in Richtung 5
(nach rechts), als auch in Richtung 3 (nach rechts oben) zu springen.
15.07.2013
28
Bachelorarbeit
Kai Hillenbrand
Sollte Feld 5 blockiert sein, wird der komplette Sprung verworfen, da er in einer Sackgasse enden
würde. Ein vertikaler Sprung funktioniert analog dazu.
6.2.4
Diagonaler Sprung
Bei einem diagonalen Sprung wird ähnlich vorgegangen (vgl. Abbildung 14 Teil c). Hier werden die
Felder 1, 4, 6, 7 und 8 ignoriert, da der Weg von 6 aus ohne x zu nutzen schneller oder gleich schnell
wäre. Ist Feld 4 oder 7 blockiert, werden Feld 1 bzw. 8 zum Forced Neighbor (vgl. Abbildung 14 Teil
c). Im Gegensatz zum horizontalen bzw. vertikalen Sprung kann man hier aber nicht einfach diagonal
weiterspringen, bis ein Forced Neighbor auftritt – schließlich gibt es 3 Felder, welche potentielle
Nachfolger sind (2, 3 und 5).
Von jedem Feld, das man nach einem diagonalen Schritt expandiert, führt man also zuerst einen
horizontalen, dann einen vertikalen Sprung aus. Wird hier ein Forced Neighbor gefunden, fügt man
die aktuelle Position des expandierenden Feldes der Open List hinzu und beendet den Sprung somit.
Ansonsten führt man einen weiteren diagonalen Schritt zum nächsten Feld in der bestehenden
Richtung aus und expandiert es.
Das wiederholt man solange, bis entweder ein Forced Neighbor durch einen der horizontalen oder
vertikalen Sprünge gefunden wurde, oder bis der diagonale Sprung auf ein direktes Hindernis trifft. In
letzterem Fall wird der komplette Sprung verworfen, da er wieder in einer Sackgasse endete und kein
geeignetes Nachbarfeld gefunden werden konnte.
6.2.5
Ablauf
Das allgemeine Vorgehen bei JPS unterscheidet sich nicht von A*: Man expandiert einen Knoten und
fügt dessen Nachfolger in eine Open List hinzu. Dann nimmt man von dieser den mit der besten
(niedrigsten) Bewertung und expandiert ihn usw. Lediglich bei der Suche der Nachfolger
unterscheidet er sich. Statt der angrenzenden freien Felder werden Jump Points des zu
expandierenden Knotens gefunden und in die Open List eingefügt. Dabei wird beachtet, aus welcher
Richtung man auf das expandierende Feld gelangt ist. Gesprungen wird dann in die Richtung jeder
Felder, welche nicht durch Pruning wegfallen (vgl. Abbildung 14).
Horizontal/vertikal wird in derselben Richtung sowie in Richtung des Forced Neighbors
weitergesprungen. Es muss hier nämlich einen Forced Neighbor geben, denn sonst hätte der
vorangegangene Sprung zu diesem Feld nicht gestoppt. Diagonal wird analog dazu vorgegangen.
Zur Wiederholung und Zusammenfassung der Suche nach einem Jump Point: Bei einem horizontalen
oder vertikalen Sprung wird so lange gesprungen, bis ein Feld mit einem Forced Neighbor gefunden
wurde. Dieses Feld ergibt dann den Jump Point. Wird ein direktes Hindernis in Sprungrichtung
gefunden, ist der Sprung ungültig und ergibt keinen Jump Point. Bei einem diagonalen Sprung wird
für jedes Feld auf dem Weg zunächst horizontal und vertikal nach einem Jump Point gesucht. Wird
einer gefunden, wird das diagonal angesprungene Feld zum Jump Point. Ansonsten wird
weitergesprungen, bis ein Hindernis in diagonaler Richtung auftaucht (oder vorher ein Jump Point in
einer der horizontalen bzw. vertikalen Suche gefunden wurde). Beim Treffen auf ein Hindernis wird
auch hier der Sprung ungültig und es gibt somit keinen Jump Point in dieser Richtung.
15.07.2013
29
Bachelorarbeit
Kai Hillenbrand
Die folgende Grafik verdeutlicht die Vorgehensweise des Algorithmus anhand eines Beispiels. Das
grüne Feld ist der Startpunkt, das rote Feld das Ziel. Graue Felder sind gefundene Jump Points, das
gelbe Feld ist der zuletzt gefundene Jump Point, von dem aus direkt ins Ziel gesprungen wird. Die
Pfeile zeigen die versuchten Sprünge. Die blauen Pfeile im rechten Teil der Grafik entsprechen den
erfolgreichen Sprüngen und stellen den optimalen Weg zum Ziel dar. [16]
Abbildung 15 - Beispiel einer Suche mit JPS
Es wird vom grünen Startfeld aus ein Weg zum roten Feld gesucht. Die Pfeile stellen die Sprünge dar,
graue Felder sind Jump Points. Die tatsächlich vollzogenen Sprünge sind blau markiert.
6.3 Evaluierung
Im Vergleich zu normalem A* und Erweiterungen wie HSPA* hat der JPS-Algorithmus den Nachteil,
dass er nur auf unbewerteten Grids funktioniert. Es können somit keine Orte schlechter oder besser
zu begehen sein als andere. Es besteht jedoch die Möglichkeit, den Algorithmus so zu erweitern, dass
auch bewertete Grids verwendet werden können – das würde die Performance allerdings mit hoher
Wahrscheinlichkeit verschlechtern.
Jump Point Search benötigt zur Suche des besten Weges im schlimmsten Fall etwas länger als A*, ist
im optimalen Fall aber um ein Vielfaches schneller. Gerade weite Flächen können schnell
übersprungen werden, sodass die Anzahl der expandierten Knoten am Schluss viel geringer ist als bei
A*. Ein genauerer Vergleich zwischen JPS und A* folgt unter „Evaluierung“ im Kapitel „Jump Point
Search in Unity 3D“.
6.4 JPS+
Die Erfinder von JPS haben im Jahr 2012 einen weiteren Ansatz präsentiert: „JPS+“. Durch das
Springen zu den Jump Points im JPS-Algorithmus werden viele zeitaufwändige Operationen, welche
die Open List betreffen (hinzufügen, sortieren, löschen), vermieden. Das Berechnen dieser Jump
Points ist allerdings ein Flaschenhals (wenn auch ein nicht so kritischer), der an diese Stelle tritt. JPS+
lagert diese Berechnungen in einen Pre-Processing-Schritt aus: Statt der tatsächlichen direkten
Nachbarn eines Feldes werden dessen mögliche Jump Points im Graphen gespeichert und können zur
Laufzeit dann direkt zum Sprung verwendet werden. [17]
15.07.2013
30
Bachelorarbeit
Kai Hillenbrand
7 Jump Point Search in Unity 3D
7.1 Übersicht
Im Rahmen der Bachelorarbeit wurde ein Package für Unity entwickelt, welches die folgenden Assets
beinhaltet:
-
7.1.1
Mehrere C#-Scripts, in denen sowohl A* als auch Jump Point Search implementiert sind,
inklusive benötigter Helferklassen (Tile, Point) und Datenstrukturen (PriorityQueue)
Scripts, die beim Ausführen, Überwachen und Auswerten der Algorithmen helfen, ebenfalls
in C# implementiert (Finder, FinderData)
Zwei Scenes, welche die Funktionalitäten der Algorithmen demonstrieren sollen, inklusive
zugehöriger Scripts und Prefabs: VisualTiles und TileExporter
VisualTiles
Die „Visual Tiles“ Demo umfasst ein interaktives Level, bestehend aus einem Grid von 70x70 Feldern
(„Tiles“), in dem die beiden Wegfindungsalgorithmen ausprobiert werden können. Mithilfe der Maus
werden Start- und Endpunkt gesetzt. Anschließend wird dann wahlweise von A* oder Jump Point
Search der beste Weg zwischen diesen beiden Punkten gesucht. Die Tiles, die der Weg
durchschreitet, werden grün markiert. Der gelbe Bereich stellt dar, welche Felder der Algorithmus
expandiert hat, d.h. wo er nach einem Weg gesucht hat. Je größer die Anzahl der gelben Tiles, desto
zeitaufwändiger war die Suche. Ebenfalls wird dem Nutzer die Dauer mitgeteilt, die der Algorithmus
benötigt hat, um den Weg zu finden.
Abbildung 16 - Beispiel eines gefundenen besten Weges in der VisualTiles-Demo
15.07.2013
31
Bachelorarbeit
7.1.2
Kai Hillenbrand
TileExporter
Die „Tile Exporter“ Demo dient dem Benchmark der beiden Algorithmen. Hierbei wird vor dem Start
ein Level ausgewählt, das bis zu 512x512 Tiles umfasst, was einen deutlich höheren Rechen- und
somit Zeitaufwand im Vergleich zur „Visual Tiles“ Demo bedeutet. Somit lassen sich Zeitunterschiede
sehr viel einfacher herausstellen. Analog zur anderen Demo werden auch hier der optimale Weg (in
blau) und die expandierten Tiles (rot) eingezeichnet. Das gewünschte Level kann vor dem Start im
Inspektor von Unity ausgewählt werden. Die Grafik, die in der Mitte des Bildschirms dargestellt wird,
kann auf Wunsch automatisch als PNG-Datei exportiert werden.
Die in der Demo enthaltenen Levels sind von einer Benchmark-Seite der „Moving AI Lab“ [18]
bezogen. Diese bietet eine umfangreiche Auswahl an unterschiedlichen Testdateien zum Auswerten
von Grid-basierten Algorithmen. Neben speziell für Tests hergestellten Maps (Labyrinthe, Räume,
zufällige Hindernisse) werden auch Levels aus Rollen- und Strategiespielen von BioWare (Baldur’s
Gate, Dragon Age) sowie Blizzard (Starcraft, Warcraft 3) angeboten. Mithilfe von diesen lässt sich gut
abschätzen, wie ein Algorithmus tatsächlich in einem Spiel abschneiden würde.
Abbildung 17 - A*-Benchmark in der TileExporter-Demo anhand eines Levels aus Baldur's Gate
15.07.2013
32
Bachelorarbeit
Kai Hillenbrand
7.2 Implementierung des Algorithmus
Der Quelltext wurde in C# geschrieben. Zur späteren Evaluierung, vor allem in Bezug auf die
Performance von JPS, wurde zusätzlich eine normale Version von A* implementiert. Da ein Spiel
nicht einfach pausieren kann (selbst wenn es nur 100ms wären), läuft die Wegfindung in einem
separaten Thread. Hierzu wurden zwei Helper-Klassen erstellt: Finder und FinderData.
FinderData kapselt sämtliche Parameter, die der Algorithmus zur Berechnung des besten Weges
benötigt – u.a. die komplette Tilemap, Startpunkt und Endpunkt. Bei Abschluss wird die FinderDataInstanz mit Informationen über das Resultat – also den gefundenen Weg – aufgefüllt, welches dann
vom Spiel abgefragt werden kann.
Der eigentliche Aufruf wird über die Finder-Klasse stark vereinfacht. Es wird eine Finder-Instanz als
Klassenvariable angelegt, mit welcher dann bei Bedarf der Algorithmus ausgeführt werden kann. Der
Finder benötigt hierzu lediglich ein FinderData-Objekt mit den entsprechenden Informationen. In der
Game Loop (die Schleife, in der sich ein Spiel befindet, während es läuft) wird kontinuierlich
überprüft, ob die Suche abgeschlossen wurde. Wenn dies der Fall ist, kann man von der FinderInstanz das Resultat abfragen. Hierbei sollte man allerdings nicht vergessen, zu prüfen, ob es
eventuell zu einem Fehler kam.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// create the class variable
private Finder finder;
(...)
// run pathfinder
finder = new Finder(new FinderData(tileMap, start, end)).RunJPS();
(...)
// in the game loop, check if it has finished
if ((finder != null) && (finder.IsFinished()))
{
// check for error
if (finder.HasError())
{
finder = null;
return;
}
// use the pathfinder result
MoveCharacter(finder.GetResult().GetCompleteBestPath());
finder = null;
}
Codelisting 2 - Verwendung der Finder-Klasse zur Ausführung der Wegfindung
15.07.2013
33
Bachelorarbeit
Kai Hillenbrand
Es folgen zwei Quelltext-Auszüge, welche die Implementierungen der beiden Algorithmen
gegenüberstellen. Es wird jeweils die While-Schleife gezeigt, die den zentralen Aspekt ausmacht und
solange läuft, bis das Ziel gefunden wurde.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// JUMP POINT SEARCH IMPLEMENTATION
// add start tile to open list
openHeap.Push(start);
// continue until open list is empty
while (!openHeap.IsEmpty)
{
// get first (most primising) tile from the open
Tile currentTile = openHeap.Pop();
closed[currentTile.pos.x, currentTile.pos.y] = true;
// check for goal in current tile
if (currentTile.pos.Equals(data.endPoint))
return data.CalculateReturnData(currentTile, closed);
// find and add successors
foreach (Point neighbor in GetNeighbors(currentTile, data.tileMap))
{
Point jumpPoint = Jump(neighbor, currentTile.pos, data);
// skip this successor, if it is already on the closed list
// or no jump point could be found
if ((jumpPoint == null) || (closed[jumpPoint.x, jumpPoint.y]))
continue;
Tile jumpNode = new Tile(jumpPoint.x,jumpPoint.y,currentTile);
jumpNode.g = currentTile.g
+ Tile.EuclideanDistance(currentTile.pos, jumpPoint);
jumpNode.CalcH(data.endPoint);
jumpNode.CalcFOnly();
openHeap.Push(jumpNode);
}
}
Codelisting 3 - Auszug aus dem Quelltext der Jump Point Search Implementierung
15.07.2013
34
Bachelorarbeit
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Kai Hillenbrand
// A* IMPLEMENTATION
// add start tile to open queue
openHeap.Push(start);
// continue until open queue is empty
while (!openHeap.IsEmpty)
{
// get first (most primising) tile from the open
Tile currentTile = openHeap.Pop();
// if current tile is in closed list, ignore it
if (closed[currentTile.pos.x, currentTile.pos.y])
continue;
// check for goal in current tile
if (currentTile.pos.Equals(data.endPoint))
return data.CalculateReturnData(currentTile, closed);
// add current tile to closed list
closed[currentTile.pos.x, currentTile.pos.y] = true;
// go through successors of current tile
foreach (Point suc in currentTile.GetNeighbors(data.tileMap))
{
// calculate total cost and add it to the queue
Tile sucTile = new Tile(suc.x, suc.y, currentTile);
sucTile.CalcFGH(data.endPoint);
openHeap.Push(sucTile);
}
}
Codelisting 4 - Auszug aus dem Quelltext der A* Implementierung
Im Vergleich lassen sich leicht die vielen Ähnlichkeiten erkennen und es wird schnell sichtbar, dass
JPS eine Erweiterung von A* ist. Der wesentliche Unterschied besteht in der Suche der Nachfolger.
Dazu nutzt JPS eine speziell angepasste Version der GetNeighbors()-Funktion und führt auf diesen
Nachbarn die Jump()-Funktion aus, um einen Jump Point zu finden. Die Kosten des Nachfolgers
müssen ebenfalls gesondert berechnet werden, da hier der Abstand zwischen aktuellem und
nachfolgendem Tile beachtet werden muss. Dieser ist bei A* immer gleich 1, da dort nicht
gesprungen wird – daher fällt eine entsprechende Rechnung weg.
15.07.2013
35
Bachelorarbeit
Kai Hillenbrand
7.3 Datenstrukturen und Optimierungen
Es ist nicht nur wichtig, dass der Algorithmus an sich effizient und schnell arbeitet. Ein wesentlicher
Teil der Performance wird dadurch beeinflusst, auf welche Art und Weise er implementiert wird. Den
wohl größten Unterschied macht hier die Wahl der Datenstrukturen aus, welche für die Open- und
Close-Listen verwendet werden.
Normale, unsortierte Arrays oder verknüpfte Listen sind die langsamste Variante. Das Hinzufügen
eines Knotens ist zwar sehr schnell, die Suche nach einem vorhandenen Knoten oder dem mit den
niedrigsten Kosten hingegen ist sehr zeitintensiv. Im schlimmsten Fall müssten hier alle Elemente
durchlaufen werden, bis der gesuchte Knoten gefunden wird.
Nach Kosten sortierte Arrays beschleunigen die Abfrage des kostengünstigsten Knoten (welcher sich
an erster Stelle befindet), sowie die eines bestimmten Knoten (mithilfe von Binärsuche). Das
Einfügen ist jedoch zeitaufwendiger, da hierzu alle nachfolgenden Elemente verschoben werden
müssen.
Eine weitere Möglichkeit – vor allem bei grid-basierten Suchalgorithmen - sind indexierte Arrays.
Hierbei wird für jedes Tile ein Speicherplatz reserviert, wodurch man sehr schnell auf ein bestimmtes
Feld zugreifen kann, da direkt über den bekannten Index des Tiles im Array zugegriffen werden kann.
Das Einfügen und Löschen eines Feldes läuft ebenfalls schnell ab. Die Suche nach dem
kostengünstigsten Knoten ist allerdings aufwendig, da das komplette Array durchsucht werden muss.
Für die Open List in dieser Arbeit wurde eine eigene, simple Datenstruktur erstellt. Diese besteht im
Wesentlichen aus einer verknüpften Liste und einem Comparer-Objekt, welches zwei Knoten
miteinander vergleicht und bestimmt, welcher die niedrigeren Kosten besitzt. Beim Einfügen in die
Liste wird das Element direkt an die entsprechende Position geschoben, sodass die Liste jederzeit
sortiert bleibt. Die Suche nach dieser Position wird von einer Binärsuche mithilfe des Comparers
relativ schnell durchgeführt. Der kostengünstigste Knoten ist schnell abzufragen, da er immer an der
ersten Stelle steht. Diese Lösung bietet somit einen sinnvollen Kompromiss zur Anwendung bei
Wegfindungsalgorithmen. Sie ist hier besonders effektiv, da in dieser Jump Point Search
Implementierung keine Suche nach einem bestimmten Tile notwendig ist, abgesehen von der Suche
nach dem kostengünstigsten Tile, welche durch die Sortierung schnell passiert.
Für die Closed List wird ein simples multidimensionales Boolean-Array verwendet. Für jedes Tile im
Grid kann direkt abgefragt werden, ob es bereits expandiert wurde, oder nicht. Da hier nur Booleans
gespeichert werden und nicht die eigentlichen Tiles, ist der höhere Speicherbedarf im Vergleich zu
einer Liste auch in größeren Grids vernachlässigbar, jedoch ist der Geschwindigkeitsunterschied
erheblich.
Es gibt keine perfekte Datenstruktur, die für alle Anwendungsfälle optimal ist – jede hat ihre
Schwächen und Stärken. Daher muss man bei der Implementierung darauf achten, die für die
verwendeten Operationen performanteste Datenstruktur zu wählen, beziehungsweise sich seine
eigene zusammenzustellen. Idealerweise vermeidet man die für viele Datenstrukturen
zeitaufwendigen Operationen (wie beispielsweise das Prüfen, ob ein Element vorhanden ist) so weit
wie möglich.
15.07.2013
36
Bachelorarbeit
Kai Hillenbrand
Einen kleinen Performanceschub kann es geben, wenn man das 2D-Grid auf 1D-Koordinaten
abbildet:
1D-Position = X + Grid-Breite * Y
Somit müssen die Koordinaten nicht immer erst von der Garbage Collection erzeugt und später
wieder entfernt werden, sondern können direkt als Integer weitergegeben werden. Auch der
Speicherbedarf sinkt etwas, da nur noch ein einzelner Integer gespeichert wird (statt zwei). Das klingt
zunächst vernachlässigbar, kann sich aber bei großen Grids und parallel laufenden
Wegfindungsalgorithmen um mehrere MB handeln.
7.4 Evaluierung
Der Jump Point Search Algorithmus wurde speziell dazu entwickelt, Wegfindung auf einem Grid mit
einheitlichen Kosten zu beschleunigen. Mithilfe der TileExporter-Demo wurden auf neun
unterschiedlichsten Test-Levels sowohl A* als auch JPS ausgeführt und deren Laufzeiten in
Millisekunden aufgezeichnet. In jedem Level wurden mehrere Durchläufe pro Algorithmus
ausgeführt und die Ergebnisse gemittelt. Die Standardabweichungen innerhalb der einzelnen
Testfälle betrugen nie mehr als 5ms und die Laufzeiten somit sehr konstant. Daher wurde auf eine
ausführliche Auflistung der Resultate verzichtet und es werden lediglich die Mittelwerte pro
Testergebnis angegeben. Getestet wurde auf einem Intel i5-3450-Prozessor. Die folgende Tabelle
beschreibt die neun verschiedenen Testgrids und das Diagramm die Ergebnisse der Tests.
TestID Levelbeschreibung (512x512 Tiles)
1
Innengebiet aus Baldur’s Gate, bestehend aus einer größeren Halle mit verschiedenen
angrenzenden Räumen
2
Labyrinth, das das komplette Grid umfasst und dessen Gänge nie breiter als 1 Tile sind
3
Labyrinth, das das komplette Grid umfasst und dessen Gänge 32 Tiles breit sind
4
zu 10% mit zufällig verteilten Wänden besetztes Level
5
zu 35% mit zufällig verteilten Wänden besetztes Level
6
Level bestehend aus 64x64 Räumen, die jeweils 7x7 Tiles groß sind und Verbindungen zu
1-4 angrenzenden Räumen besitzen
7
Level bestehend aus 8x8 Räumen, die jeweils 63x63 Tiles groß sind und Verbindungen zu
1-4 angrenzenden Räumen besitzen
8
Innengebiet aus Baldur’s Gate, bestehend aus vielen Räumen und einigen verstreuten
Hindernissen sowie langen Fluren
9
Außengebiet aus Baldur’s Gate mit weiten Flächen und großen Hindernissen und einer
Stadtmauer, um die ein Weg herum gesucht werden muss
15.07.2013
37
Bachelorarbeit
Kai Hillenbrand
33
1
728
85
2
319
32
3
1397
109
4
7
166
185
5
JPS (in ms)
A* (in ms)
81
62
6
44
7
450
8
8
145
27
9
764
0
200
400
600
800
1000
1200
1400
1600
Abbildung 18 - Ergebnisse der Vergleichstests zwischen A* und JPS
Wie man sehen kann, ist JPS tatsächlich in den meisten Fällen deutlich schneller. Gerade bei den
realen Spielbeispielen (Tests 1, 8 und 9) kann ein Weg mit der 20-30fachen Geschwindigkeit
berechnet werden. Das liegt daran, dass Jump Point Search bei weiten Flächen seine Stärken zeigen
kann, da diese größtenteils übersprungen werden können. Tests 3 und 7 verdeutlichen das.
Nur in Grids mit sehr vielen kleinen Hindernissen (Test 4) befindet sich A* im Vorteil: der Algorithmus
bewegt sich zielstrebiger zum Ziel, während JPS breit gefächert nach vielen Jump Points sucht, die
sich durch die Menge der Hindernisse ergeben. Hierbei muss allerdings beachtet werden, dass A* nur
deshalb so schnell zum Ziel gelangt, weil der Weg beinahe geradlinig und ohne größere Umwege zu
erreichen ist. Wäre dies nicht der Fall, könnte JPS einen entsprechenden Umweg wiederum etwas
schneller finden. Das wird in Test 2 gezeigt, dessen Level ebenfalls ein Grid mit vielen Hindernissen,
jedoch ohne direkt offensichtlichen Weg zum Ziel ist.
In diesen neun (sehr unterschiedlichen) Tests benötigte A* im Schnitt 448ms und Jump Points Search
nur 65ms. Das bedeutet, dass JPS im Schnitt etwa 7x so schnell war. Die Standardabweichung betrug
569ms bei A* und nur 47ms bei JPS. Damit lässt sich erkennen, dass JPS konstanter in seiner
Performance ist, während die Suchdauer von A* stärker vom jeweiligen Level abhängt.
Um die Unterschiede besser nachvollziehen zu können, folgen nun einige grafische Ausgaben aus der
TileExporter-Demo.
15.07.2013
38
Bachelorarbeit
Kai Hillenbrand
Abbildung 19 - Tests 1-3 (links: A*, rechts: JPS)
Expandierte Tiles sind rot markiert, der beste Pfad blau. Je mehr rot, desto mehr Tiles wurden
expandiert und desto mehr Zeit hat der Algorithmus benötigt.
15.07.2013
39
Bachelorarbeit
Kai Hillenbrand
Abbildung 20 - Tests 4-6 (links: A*, rechts: JPS)
Schwächeres rot bedeutet, dass die Dichte der expandierten Felder geringer ist.
15.07.2013
40
Bachelorarbeit
Kai Hillenbrand
Abbildung 21 - Tests 7-9 (links: A*, rechts: JPS)
15.07.2013
41
Bachelorarbeit
Kai Hillenbrand
8 Fazit
8.1 Ergebnis
In dieser Arbeit wurde ein Einstieg in die Wegfindung mit dem Schwerpunkt auf Spieleentwicklung
gegeben. Vorhandene Methoden und Algorithmen wurden erklärt, sowie deren Anwendung in
verbreiteten Spiele-Engines betrachtet. Der Jump Point Search Algorithmus wurde ausführlich
beschrieben und in der Unity-Engine implementiert. Die Implementierung wurde mit A* verglichen
und evaluiert.
Wie beschrieben, fehlt es der Unity-Engine momentan an schnellen, kostenlosen Wegfindungsimplementierungen. Für Entwickler, die solche benötigen, ist das Ergebnis dieser Arbeit interessant,
da in vielen Fällen eine um ein Vielfaches höhere Geschwindigkeit erreicht werden kann. Während
bei vertikalen 3D-Levels mit mehreren Ebenen mittlerweile im Regelfall auf eine Variante von
Navmesh in Verbindung mit A* zurückgegriffen wird, nutzen Strategiespiele und ähnliche Genres mit
flacheren Gebieten weiterhin noch oft die simplere Grid-Repräsentation und können somit von Jump
Point Search stark profitieren. Gerade in besonders dynamischen Levels ist es viel effizienter, die
entsprechenden Felder in einem Grid zu blockieren/freizugeben, als einen Navmesh zur Laufzeit
anzupassen und neue Polygone zu berechnen.
8.2 Ausblick
Die Implementierung von Jump Point Search in Unity ist lediglich eine leicht optimierte Basisform des
Algorithmus. Weitere mögliche Optimierungen wurden im entsprechenden Kapitel angesprochen
und lassen sich auf Basis des vorhandenen Quelltextes umsetzen. Die Implementierung könnte
beispielsweise um die Variante JPS+ erweitert werden, welche die Jump Points im Voraus berechnet.
Weiterhin lässt sich die Implementierung mit anderen der vorgestellten Wegfindungsmethoden
kombinieren, wie beispielsweise HPA*. Dass JPS nur auf nicht-bewerteten Grids funktioniert, wäre
ebenfalls noch eine Einschränkung, die man durch eine Erweiterung des Algorithmus aufheben
könnte.
Abschließend kann gesagt werden, dass Wegfindung ein elementarer Bestandteil in Spielen ist und es
auch immer bleiben wird. Durch immer größer und dynamischer werdende Levels müssen auch die
Wegfindungsalgorithmen verbessert werden, um weiterhin schnellstmöglich den besten Weg zu
finden.
15.07.2013
42
Bachelorarbeit
Kai Hillenbrand
Literaturverzeichnis
Bücher und Paper
[10] I. Millington and J. Funge, Artificial Intelligence for Games (Second Edition), 2009.
[11] Goodwin, Menon and Price, "Pathfinding in Open Terrain".
[12] A. Botea, M. Müller and J. Schaeffer, "Near Optimal Hierarchical Path-Finding".
[14] D. Harabor, "Fast Pathfinding via Symmetry Breaking".
[15] D. Harabor and A. Grastien, "Online Graph Pruning for Pathfinding on Grid Maps".
[17] D. Harabor and A. Grastien, "The JPS Pathfinding System".
[20] D. Kastenholz, "3D Pathfinding," 2006.
[21] X. Cui and H. Shi, "A*-based Pathfinding in Modern Computer Games," 2011.
[22] M. Buckland, "Programming Game AI by Example," 2005.
[23] H. Waldschmidt, "Vergleich von Pathfinding-Algorithmen," 2008.
Webseiten
[1] „Unity 3D,“ Unity Technologies, [Online]. Verfügbar unter: http://unity3d.com/unity/.
[2] „Unity 3D Asset Store,“ Unity Technologies, [Online]. Verfügbar unter:
https://www.assetstore.unity3d.com.
[3] „Unity 3D Docs - Navmesh and Pathfinding,“ Unity Technologies, [Online]. Verfügbar unter:
http://docs.unity3d.com/Documentation/Manual/NavmeshandPathfinding.html.
[4] „Unreal Developer Network - Navigation Mesh Reference,“ Epic Games, [Online]. Verfügbar
unter: http://udn.epicgames.com/Three/NavigationMeshReference.html.
[5] „My CryEngine,“ CryTek, [Online]. Verfügbar unter: http://mycryengine.com.
[6] „CryDev - Triangulation-based Navigation,“ CryTek, [Online]. Verfügbar unter:
15.07.2013
43
Bachelorarbeit
Kai Hillenbrand
http://freesdk.crydev.net/display/SDKDOC4/Triangulation-based+Navigation.
[7] „CryDev - Pathfinding Costs,“ CryTek, [Online]. Verfügbar unter:
http://freesdk.crydev.net/display/SDKDOC4/Pathfinding+Costs.
[8] „Valve Developer Community - Navigation Meshes,“ Valve, [Online]. Verfügbar unter:
https://developer.valvesoftware.com/wiki/Navigation_Meshes.
[9] „Construct 2 - Pathfinding,“ Scirra, [Online]. Verfügbar unter:
https://www.scirra.com/manual/154/pathfinding.
[13] D. Harabor, „Shortest Path,“ [Online]. Verfügbar unter:
http://harablog.wordpress.com/2011/09/07/jump-point-search.
[16] N. Witmer, „zerowidth positive lookahead - Jump Point Search Explained,“ [Online]. Verfügbar
unter: http://zerowidth.com/2013/05/05/jump-point-search-explained.html.
[18] „MovingAI Benchmarks,“ Moving AI Lab at the University of Denver, [Online]. Verfügbar unter:
http://movingai.com/benchmarks/.
[19] „Unity 3D Script Reference,“ Unity Technologies, [Online]. Verfügbar unter:
http://docs.unity3d.com/Documentation/ScriptReference/index.html.
15.07.2013
44