Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

31.05.2017, 10:33

TreeSet statt ArrayList + compareTo überschreiben für A* Algorithmus

Hallo mal wieder,

zur Zeit programmiere ich an einem 2D Strategiespiel und komme gut voran.

Ich habe ein Spielfeld 50x100 mit 32x32großen Tiles, sowie an die 30 Einheiten, die ich steuern kann ohne das es laggt. Werden es aber mehr als 30 dann laggt das Ganze immer mehr und schließlich tut sich gar nichts mehr aber nur, wenn ich alle z.B. 36 Einheiten gleichzeitig bewegen will (Mausrahmen-markieren-bewegen).

Also habe ich gegoogelt und herausgefunden, dass ein TreeSet insgesamt eindeutig besser wäre als eine ArrayList um die openList sortiert zu halten. Für die closedList nehme ich ein HashSet(ich denke das ist schnell genug). Mit einem TreeSet muss ich aber die Werte vergleichen mit Comparable oder einem Comparator, den das TreeSet im Konstruktor aufnehmen kann. Aber meine Implementierung mit einem von beiden klappte nicht und ich weiß nicht warum?

Ein weiteres Problem das ich habe ist, dass ich während ich den A* aufrufe zur Laufzeit, das ganze Spielfeld inklusive der Hindernisse ständig berechnen lasse, und das kostet denke ich auch Performance, aber nur so funktioniert alles. Das Problem dabei ist wenn ich das Spielfeld nur einmal im Konstruktor lade laufen die Einheiten nur ein Feld und das Programm stürzt ab und ich verstehe nicht ganz warum?

Wenn jemand da draußen was damit anfangen kann bitte melden.

Wenn nötig poste ich auch den Code.

Gruß Javaist

Nox

Supermoderator

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

2

31.05.2017, 11:09

1) bitte erwähn explizit die genutzt Sprache (auch wenn alles für Java spricht).
2) glauben heißt nicht wissen. Bevor du anfängst wild drauf los zu optimieren, profile erstmal. Am Ende ist dein Flaschenhals noch ganz wo anders.
3) google sowie Forensuche sind dein Freunde. Zum Thema Pathfinding gibt es sehr gute Quellen und auch fertige Lösungen. u.U. ist A* garnicht das ideale für deinen Anwendungsfall.
4) Da wir den Code nicht kennen, können wir nur raten warum dein Programm abstürzt. Was dir in dem Fall weiterhilft ist der Debugger. Falls du den noch nicht benutzt hast, solltest du die Zeit definitiv investieren.
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

Schorsch

Supermoderator

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

3

31.05.2017, 13:20

Ohne deinen Code zu kennen können wir natürlich nicht sagen warum dein Programm abstürzt. Ansonsten möchte ich anmerken dass A* ein Algorithmus auf einem statischen Graphen ist. Zur Laufzeit sollte sich da nichts ändern. Das bedeutet dynamische Hindernisse solltest du erst gar nicht mit einfließen lassen. Was aktuell den Weg versperrt, eine halbe Sekunde aber nicht mehr interessiert dich ja an der Stelle erst mal nicht. Dafür nimmt man dann normalerweise andere Systeme. Dazu kannst du mal nach "local pathfinding" googlen. Ansonsten gibt es einige Ansätze zum optimieren, ob die allerdings entscheident sind kann man nicht sagen. A* sollte auch für mehr als 30 parallele Instanzen laufen können, solange deine Welt nicht unfassbar groß ist. Hast du mal überlegt den Algorithmus nicht für jede Einheit gesondert sondern für mehrere gleichzeitig zu berechnen? Wenn sich alle Einheiten in einem Bereich befinden und zum gleichen Zielort navigiert werden sollen, dann musst du den Weg ja nicht für jeden Individuell berechnen. Wichtig ist wie gesagt hierbei dass am Ende nicht zu 100% wichtig sein muss wie sich die Einheit Pixel für Pixel bewegt sondern nur dass ein grober Pfad gefunden wird. Die eigentliche Bewegung entlang des Pfades und das ausweichen von dynamischen Hindernissen wie anderen Einheiten findet erst nach A* statt.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

4

31.05.2017, 14:43

OK, den Code posten ist so ne Sache, denn es sind viele Klassen, die hier alle reinzustellen, ist mir echt Zuviel.(Kann man verstehen oder?).
Ich bin gerade noch mit dem Debugger beschäftigt, ABER das mit dem Laden und Erzeugen aller Nodes und Hindernisse zur A* - Laufzeit kann man doch beheben, oder? Eigentlich gibt's da gar nicht so viele Möglichkeiten, denn entweder man lädt beides in einem Konstruktor oder initialisiert alles und ruft eine Art init() Methode vor der eigentlichen Spielschleife auf oder man erzeugt beides in der main Methode, die ja wie mein A* statisch ist. Die ersten beiden Möglichkeiten haben beide nur soweit funktioniert, dass die Einheit sich nur ein Feld bewegt hat oder gar nicht und die Möglichkeit mit der main() hat erst gar nicht funktioniert, wobei ich aber hier ehrlicherweise gestehen muss, dass ich da nicht alle mir bekannten Möglichkeiten versucht habe.

So ich poste jetzt einfach mal den A* inklusive der Heuristik, sowie die update() - Methode der Einheit, damit man das Mal sehen kann und ja ich weiß, dass das wahrscheinlich nicht ausreichen wird, um mir konkrete Lösungen anbieten zu können, aber ich denke man versteht dann besser was ich meine und wie die Zusammenhänge sind kann man ja erfragen. Noch eine kleine Ergänzung: Die Einheiten sind alle in einer Liste und werden mittels repaint() mit update() und render() in der Spielschleife aufgerufen, also zuerst update() dann render(). Ich hab mal irgendwo gelesen, das man in Java Swing und AWT nicht vermischen sollte, deshalb habe ich ein JLabel auf einem JFrame und keine Canvas (wobei ich beide anwenden kann).

Und ich muss noch etwas erwähnen: Normalerweise nimmt man Nodes für den A*. Ich habe aber gleich das Tile genommen statt dem Node. Also die Nodeklasse ist bei mir die Tileklasse mit dem unterschied eben, dass die Tiles im Gegensatz zu den Nodes tatsächlich gemalt werden.

So hier mal der Code:

Das ist die Einheiten update():

Quellcode

1
2
3
4
5
6
7
8
9
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public void update(int xoffset,int yoffset,AffineTransform scale) {
        
        timer++;
        if(timer>100){
            timer=0;
        }
        
        peasantBox.x = (int) x;
        peasantBox.y = (int) y;
        
        mx = Mouse.mousex;
        my = Mouse.mousey;
                
        in = new Point(mx, my);
        out = null;

        try {
            out = scale.inverseTransform(in,out);
        } catch (NoninvertibleTransformException e) {e.printStackTrace();}
        
        if(Mouse.right&&marked){
            endx = (int) out.getX()+(xoffset/width);
            endy = (int) out.getY()+(yoffset/height);
        }
        
        if(marked==false||marked){
            path = Level.findPath((int)startx,(int)starty,(int)endx,(int)endy);
        }
        
        if (path!=null) {
            Tile node = path.get(path.size() - 1);
            //Rechts
            if (node.x * width > x && node.y * height == y) {
                x+=speed;
                figure = getFrame(figure,10,14,5);
            }
            //Links
            if (node.x * width < x && node.y * height == y) {
                x-=speed;
                figure = getFrame(figure,15,19,5);
            }
            //Unten
            if (node.x * width== x && node.y * height > y) {
                y+=speed;
                figure = getFrame(figure,5,9,5);
            }
            //Oben
            if (node.x * width == x && node.y * height < y) {
                y-=speed;
                figure = getFrame(figure,0,4,5);
            }
            //Rechts unten
            if (node.x * width > x && node.y * height > y) {
                x+=speed;
                y+=speed;
                figure = getFrame(figure,30,34,5);
            }
            //Links unten
            if (node.x * width < x && node.y * height > y) {
                x-=speed;
                y+=speed;
                figure = getFrame(figure,35,39,5);
            }
            //Rechts oben
            if (node.x * width > x && node.y * height < y) {
                x+=speed;
                y-=speed;
                figure = getFrame(figure,20,24,5);
            }
            //Links oben
            if (node.x * width < x && node.y * height < y) {
                x-=speed;
                y-=speed;
                figure = getFrame(figure,25,29,5);
            }
            if (x == node.x * width) {
                startx = node.x;
            }
            if (y == node.y * height) {
                starty = node.y;
            }
        }
    }


Und das ist der A*:

Quellcode

1
2
3
4
5
6
7
8
9
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public static List<Tile> findPath(int startx,int starty,int endx,int endy){
        
        for(int i = 0; i < width; i++){
            for(int j = 0;j < height;j++){
                tiles[i][j] = new Tile(i,j,size,size,obstacles[i][j],false);
            }
        }
        
        for(int i = 0; i < width; i++){
            for(int j = 0;j < height;j++){
                tiles[i][j].addNeighbours(tiles,width,height);
            }
        }
        
        List<Tile> openList = new ArrayList<Tile>(); // Here i want a TreeSet
        HashSet<Tile> closedList = new HashSet<Tile>();
        List<Tile> path = null;
        
        Tile start = tiles[startx][starty];
        Tile end = tiles[endx][endy];
        
        Tile closest = start;
        closest.h = heuristic(closest,end);

        openList.add(start);

        while(!openList.isEmpty()) {
            
            int winner = 0;
            for (int i = 0; i < openList.size(); i++) {
                if (openList.get(i).f < openList.get(winner).f) {
                    winner = i;
                }
            }
    
            Tile current = openList.get(winner);
            
            openList.remove(current);
            
            if (current == end) {
                path = new ArrayList<Tile>();
                Tile tmp = current;
                path.add(tmp);
                while (tmp.previous != null) {
                    path.add(tmp);
                    tmp = tmp.previous;
                }
                return path;
            }
            
            closedList.add(current);
            
            List<Tile> neighbours = current.neighbours;
    
            for (int i = 0; i < neighbours.size(); i++) {
                Tile neighbour = neighbours.get(i);
                int cost = current.g + heuristic(current,neighbour);
                if (openList.contains(neighbour) && cost < neighbour.g) {
                    openList.remove(neighbour);
                }
                if (closedList.contains(neighbour) && cost < neighbour.g) {
                    closedList.remove(neighbour);
                }
                int newcost = heuristic(neighbour, end);
                if (!openList.contains(neighbour) && !closedList.contains(neighbour) && !neighbour.obstacle) {
                    neighbour.h = newcost;
                    if (neighbour.h < closest.h) {
                        closest = neighbour;
                    }
                }
                if (!openList.contains(neighbour) && !closedList.contains(neighbour) && !neighbour.obstacle) {
                    neighbour.g = cost;
                    openList.add(neighbour);
                    neighbour.f = neighbour.g + neighbour.h;
                    neighbour.previous = current;
                }
            }
        }
        Tile tmp = closest;
        path = new ArrayList<Tile>();
        path.add(tmp);
        while (tmp.previous != null) {
            path.add(tmp);
            tmp = tmp.previous;
        }
        return path;
    }

    public static int heuristic(Tile A,Tile B) {
        int dx = Math.abs(A.x - B.x);
        int dy = Math.abs(A.y - B.y);
        return 1 * (dx + dy) + (1 - 2 * 1) * Math.min(dx,dy);
    }


Ich hoffe das verbessert das Verständnis ein wenig.

Also nochmal die Ausgangsfrage, wo lädt man die Tiles?

5

31.05.2017, 14:50

Noch was vergessen:
Der A* nimmt Tile Koordinaten um meine Einheiten dann überhaupt laufen lassen zu können transformiere ich die Einheitenposition in Double Werte und weise die dann der Einheitenposition zu. Sorry das ist wichtig habs beinahe vergessen.

Schorsch

Supermoderator

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

6

31.05.2017, 15:38

Ich gehe mal davon aus dass dein A* soweit korrekt funktioniert. Habe den Code dazu grad nur halb überflogen. Was mir auffällt, in deiner update-Methode in Zeile 26 hast du eine unnötige Bedingung marked==false||marked ist das selbe wie !marked||marked was gleichzusetzen mit true ist da eine Seite immer false und die andere immer true ergeben wird. Entweder hast du dich hier vertan oder benötigst die Bedingung überhaupt nicht. Dann erstellst du deine Tiles an der Stelle wo du den Pfad berechnen möchtest. Das ist natürlich teuer. Vor allem wenn du für 30 Entitäten deinen Graphen erstellst und jedes mal wieder verwirfst. Brauchst du die Tileklasse an dieser Stelle überhaupt? Ich habe die letzten male einfach den Index zum jeweiligen Tile genommen und im Algorithmus damit gearbeitet. Die Nachbarn eines Knoten sind bei einer Tilemap implizit gegeben. Das einzige was interessant für dich ist ist ob ein Tile begehbar und wenn ja, wie hoch die Kosten dafür sind. Die nötigen Informationen dazu kannst du dir normalerweise mithilfe des Index aus deiner Tilemap ziehen. Jetzt hast du in deinem Fall nicht nur einen Index sondern X und Y Koordinaten. Zwischen den beiden Systemen kannst du aber ganz einfach umrechnen. Ein Beispiel:

Quellcode

1
2
3
0 1 2
3 4 5
6 7 8

Stell dir vor dass ist eine Tilemap wobei die Zahl in jeder Stelle der Index ist. Um jetzt von X und Y Koordinate auf den Index zu schließen kannst du folgende Formeln nutzen:
y * breite + x

als Beispiel:
x = 2, y = 1
=> 1 * 3 + 2 = 5
passt also

Um jetzt vom Index zurück auf x und y zu schließen geht folgendes:
x = Index % breite
y = Index / breite (abgerundet)

selbes Beispiel:
Index = 5:
5 % 3 = 2
5 / 3 = 1 (abgerundet)
passt wieder.

Ich denke das ganze sollte recht klar sein. Jetzt kannst du einfach in deinem Algorithmus den Index in deine Listen stecken und wenn du Daten aus deiner Tilemap benötigst rechnest du dir fix die Koordinaten aus und holst dir das nötige Tile.

Solltest du deine Tiles da unbedingt benötigen dann erzeuge diese an einer Stelle und arbeite ab da mit diesen Daten. An welcher Stelle du das machst sollte da egal sein. Solltest du aus irgendeinem merkwürdigen Grund bei jedem Durchlauf neue Tiles benötigen so erstell einmal die Instanzen und steck sie in einen Puffer. Wenn du ein neues Tile brauchst holst du dir eins aus dem Puffer und wenn du fertig bist steckst du es wieder zurück. Dann erstellst du zumindest nicht dauernd neue Objekte und gibst diese wieder frei.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

7

31.05.2017, 16:02

Danke für die sehr detaillierte schnelle Antwort.

Also das mit den Double Positionen habe ich komplett weggelassen. Ich habe überhaupt mit diesen Transformationen gearbeitet, weil ich diese, als einzige für die Bewegung auf einer isometrischen Karte, verstanden habe. Ich hab mir halt gedacht was isometrisch geht, funktioniert orthogonal auch, denn ich hab dummerweise mit dem schwierigeren Problem angefangen statt mit dem leichteren :pillepalle: . Und jetzt weiß ich auch, dass das selbst bei isometrischen Karten auch ohne Double Umrechnung funktioniert, denn man muss nur die Nachbarn anders berechnen, glaub ich zumindest, ABER jetzt funktioniert so alles perfekt mit Ganzzahlen zumindest auf einer orthonalen Karte.
Und ich habe selbst bei mehr als 30 Einheiten keine gravierenden Ruckler mehr. Aber Du hast komplett Recht mit Deiner Antwort. Denn dieses ständige gelade der ganzen Map zur Laufzeit ist sehr teuer, vor allem bei größeren Karten.

Ich werde nun ein Weilchen Brauchen bis ich Deine Antwort verdaut hab. Und werde mich wieder melden.

Was halt noch bleibt ist die Ausgangsfrage: Wie implementiert man für ein TreeSet die compareTo() Methode so, dass es weniger Laufzeit kostet als mit einer ArrayList????

Denn wie man sieht hab ich ja den gewinner direkt berechnet und nicht mittels compareTo().

8

31.05.2017, 16:09

Nachtrag: Das mit dem if(!marked||marked) braucht man schon, denn ich klicke auf eine Einheit, die läuft los und während die läuft, hebe ich die Markierung auf, aber die Einheit läuft weiter bis zum Ziel, während man eine andere Einheit anklicken kann und diese dann rumschickt.
Denn vorher frage ich ab, ob eine Einheit auch markiert ist bevor ich den Zielpunkt berechnen lasse mit der Maus. So habe ich mir das halt gedacht.

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

9

31.05.2017, 16:12

Der Code heißt effektiv: Wenn die Einheit markiert ist oder nicht markiert ist, mache was. Folglich tut man immer was, weil die Einheit immer markiert oder nicht markiert ist. Eins von beiden gilt immer. Kannste das IF auch weglassen und den Code danach immer ausführen, hat denselben Effekt.
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

10

31.05.2017, 23:43

OK hast Recht. Gleich und sofort rausgelöscht.


Noch ne prinzipielle Frage zum A* : Sollten die Kosten ganzzahlig oder mit Fließkommazahlen definiert sein?
Das wäre nämlich wichtig zu wissen, wegen diesem Überschreiben von compareTo() beim TreeSet(), denn Integers direkt mit compareTo zu vergleichen geht nicht. Außerdem: Braucht man in meinem Fall wo immer nur Objekte vom selben Typ im TreeSet<Tile>() sind, sowas wie einen sortierKey oder sowas ähnliches, oder nicht?

Werbeanzeige