A*-Implementierung flexibler machen

Kar

Mitglied
Hallo,

ich habe wie folgt den A*-Algorithmus implementiert:
Java:
package astar;

import java.awt.Point;
import java.util.*;

public abstract class AStar {
	public static interface World {
		public List<Node> getNeighbours(Node node);
	}

	public static interface CostsCalculator {
		public int getMovementCosts(Node end);
		public int getEstimatedCosts(Node start, Node end);
	}

	public static CostsCalculator costsCalculator;
	public static Comparator<Node> costsComparator;

	public static Stack<Point> getPath(World world, Node start, Node end) {
		List<Node> openList = new ArrayList<Node>();
		List<Node> closedList = new ArrayList<Node>();
		openList.add(start);
		while(!closedList.contains(end) || openList.isEmpty()) {
			Collections.sort(openList, costsComparator);
			start = openList.get(0);
			openList.remove(start);
			closedList.add(start);
			for(Node neighbour : world.getNeighbours(start)) {
				if(!closedList.contains(neighbour)) {
					neighbour.previousNode = start;
					neighbour.calculateCosts(costsCalculator, end);
					if(!openList.contains(neighbour))
						openList.add(neighbour);
				}
			}
		}
		Stack<Point> path = new Stack<Point>();
		Node node = closedList.get(closedList.indexOf(end));
		while(node.previousNode != null) {
			path.push(node);
			node = node.previousNode;
		}
		return path;
	}

	static {
		costsCalculator = new CostsCalculator() {
			@Override
			public int getMovementCosts(Node end) {
				int costs = 10;
				if(end.previousNode != null)
					costs += end.getCosts(Node.Costs.MOVEMENT);
				return costs;
			}

			@Override
			public int getEstimatedCosts(Node start, Node end) {
				return (Math.abs(start.x - end.x) 
						+ Math.abs(start.y - end.y)) * 10;
			}
		};

		costsComparator = new Comparator<Node>() {
			@Override
			public int compare(Node a, Node b) {
				if(a.getCosts(Node.Costs.ALL) < b.getCosts(Node.Costs.ALL)) 
					return -1;
				else if(a.getCosts(Node.Costs.ALL) > b.getCosts(Node.Costs.ALL))
					return 1;
				return 0;
			}
		};
	}

	static class Node extends Point {
		public static enum Costs {
			ALL, MOVEMENT, ESTIMATED
		}

		public Node previousNode;

		private int movementCosts;
		private int estimatedCosts;

		public Node(int x, int y) {
			super(x, y);
			previousNode = null;
			movementCosts = 0;
			estimatedCosts = 0;
		}

		public void calculateCosts(CostsCalculator c, Node node) {
			movementCosts = c.getMovementCosts(node);
			estimatedCosts = c.getEstimatedCosts(this, node);
		}

		public int getCosts(Costs costs) {
			switch(costs) {
			case ALL:
				return movementCosts + estimatedCosts;
			case MOVEMENT:
				return movementCosts;
			case ESTIMATED:
				return estimatedCosts;
			default:
				return -1;
			}
		}
	}
}

Zum Algorithmus an sich habe ich keine Frage. Der funktioniert soweit super.
Mir geht es darum, dass ich diese Implementierung flexibel halten möchte, sodass er öfter einsetzbar ist. Das habe ich durch die Schnittstellen World, CostsCalculator und Comparator<Node> versucht. Für die letzteren beiden wird eine Defaultimplementierung mitgeliefert.

Geht es auch anders? Es gibt keine Methoden- bzw. Funktionszeiger in Java soweit ich weiß und mit den Schnittstellen wollte ich mir einen Workaround basteln. Allerdings finde ich das nicht sehr sauber.
 
Comparator<Node> extern finde ich unnötig, Node selber kann doch Comparable sein,
dann sieht es für Subklassen schlecht aus mit eigener Sortierung, aber das sollte auch kaum nötig sein,
Sortierung nach Kosten Hauptkriterium bleiben,

die Kostenberechnung ermöglicht die privaten Kosten-Attribute, durchaus alles machbar, theoretisch auch in die Klasse hineinzuziehen,
World mit getNeighbours() passt

für mich eigentlich nichts zu meckern, als ich das Posting angefangen hatte hatte ich noch bisschen mehr im Sinn,
durch CostsCalculator aber doch kein Fehler 😉

hier noch ein Link zu ähnlichem, aber ich glaube nicht so flexibel wie deins, nicht genau angeschaut
http://www.java-forum.org/spiele-multimedia-programmierung/140117-pfadfindung.html
 
Das sieht (beim Überfliegen) schon sehr allgemein aus. Ich bin mir nicht sicher, ob man es vielleicht eleganter machen könnte, aber da müßte man genauer drüber nachdenken. Als erstes würde ich überlegen, ob man "Node" nicht auch als Interface machen könnte...

Nur hier muss ich nachhaken:

Comparator<Node> extern finde ich unnötig, Node selber kann doch Comparable sein,

Ich finde, es gibt nur SEHR wenige Fälle, wo man eine Implementierung von "Comparable" vorschreiben sollte. Ganz allgemein ist es ja so, dass man für Comparable-Objekte leicht einen generischen "ComparableComparator" schreiben kann (und dass es sowas in der Standard-API nicht gibt, obwohl es an einigen Stellen viel Code und Arbeit hätte sparen können, finde ich ziemlich :autsch: ). Umgekehrt, wenn davon ausgegangen wird, dass die übergebenen Objekte Comparable sind, hat man keinen Einfluß mehr auf die interne Arbeitsweise.

Hier dachte ich als konkretes Beispiel zuerst daran, dass vielleicht jemand nicht die euklidische, sondern z.B. die Manhattan-Distanz verwenden wollen könnte, aber aber das geht hier ja im "CostsCalculator" unter - deswegen ist das nicht direkt anwendbar (solange man die Node-Klasse als solche beibehalten will...)
 
Vielen Dank für die Antworten.

Der Gedanke um den Comparator<Node> war der, dass ich dem Anwender ermöglichen wollte, nicht nur den kostengünstigen Pfad, sondern auch - aus welchem Grund auch immer - den kostenintensivsten Pfad zu ermitteln. Ich habe das aber jetzt dahingehend geändert, dass der Anwender einfach die öffentliche Variable compareMode ändern. Die kann entweder CompareMode.ASCENDING (Standard) oder CompareMode.DESCENDING annehmen.
Den CostsCalculator habe ich in die Klasse Node ausgelagert. Ich finde, dort passt es besser hin. Dass dieses Interface austauschbar ist kommt daher, dass der Algorithmus für viele Fälle einsetzbar sein soll. Wenn man an Spiele denkt, dessen Welten unterschiedliche Bodenbeschaffenheiten besitzen, könnte man die Bewegungskosten und/oder geschätzte Kosten leicht anpassen.

Hier ist der neue Code:
Java:
package astar;

import java.awt.Point;
import java.util.*;

public abstract class AStar {
	public static interface World {
		public List<AStar.Node> getNeighbours(AStar.Node node);
	}
	
	public static enum CompareMode {
		ASCENDING, DESCENDING
	}
	
	public static CompareMode compareMode = CompareMode.ASCENDING;

	public static Stack<Point> getPath(AStar.World world, AStar.Node start, AStar.Node end) {
		List<Node> openList = new ArrayList<Node>();
		List<Node> closedList = new ArrayList<Node>();
		openList.add(start);
		while(!closedList.contains(end) || openList.isEmpty()) {
			Collections.sort(openList, getCostsComparator(compareMode));
			start = openList.get(0);
			openList.remove(start);
			closedList.add(start);
			for(Node neighbour : world.getNeighbours(start)) {
				if(!closedList.contains(neighbour)) {
					neighbour.previousNode = start;
					neighbour.calculateCosts(end);
					if(!openList.contains(neighbour))
						openList.add(neighbour);
				}
			}
		}
		Stack<Point> path = new Stack<Point>();
		Node node = closedList.get(closedList.indexOf(end));
		while(node.previousNode != null) {
			path.push(node);
			node = node.previousNode;
		}
		return path;
	}

	private static Comparator<AStar.Node> getCostsComparator(final CompareMode cmpMode) {
		return new Comparator<Node>() {
			@Override
			public int compare(Node a, Node b) {
				if(a.getCosts(Node.Costs.ALL) < b.getCosts(Node.Costs.ALL)) 
					return cmpMode == CompareMode.ASCENDING ? -1 : 1;
				else if(a.getCosts(Node.Costs.ALL) > b.getCosts(Node.Costs.ALL))
					return cmpMode == CompareMode.ASCENDING ? 1 : -1;
				return 0;
			}
		};
	}

	public static class Node extends Point {
		public static interface CostsCalculator {
			public int getMovementCosts(AStar.Node end);
			public int getEstimatedCosts(AStar.Node start, AStar.Node end);
		}
		
		public static enum Costs {
			ALL, MOVEMENT, ESTIMATED
		}
		
		public CostsCalculator costsCalculator;

		public Node previousNode;

		private int movementCosts;
		private int estimatedCosts;

		public Node(int x, int y) {
			super(x, y);
			costsCalculator = getDefaultCostsCalculator();
			previousNode = null;
			movementCosts = 0;
			estimatedCosts = 0;
		}

		public void calculateCosts(Node node) {
			movementCosts = costsCalculator.getMovementCosts(node);
			estimatedCosts = costsCalculator.getEstimatedCosts(this, node);
		}

		public int getCosts(Costs costs) {
			switch(costs) {
			case ALL:
				return movementCosts + estimatedCosts;
			case MOVEMENT:
				return movementCosts;
			case ESTIMATED:
				return estimatedCosts;
			default:
				return -1;
			}
		}
		
		public CostsCalculator getDefaultCostsCalculator() {
			return new CostsCalculator() {
				@Override
				public int getMovementCosts(AStar.Node end) {
					int costs = 10;
					if(end.previousNode != null)
						costs += end.getCosts(Node.Costs.MOVEMENT);
					return costs;
				}

				@Override
				public int getEstimatedCosts(AStar.Node start, AStar.Node end) {
					return (Math.abs(start.x - end.x) 
							+ Math.abs(start.y - end.y)) * 10;
				}
			};
		}
	}
}

Wenn man die Nodeklasse zu einem Interface machen würde, müssten zum Beispiel Spielobjekte dieses zwangsläufig implementieren. Und mir gefällt es nicht, wenn ein Spielobjekt ein Knoten für A* repräsentiert. Um den Pfad zu ermitteln reicht es, eine Nodeinstanz mit den jeweiligen Koordinaten des "echten" Objektes zu erzeugen. Das finde ich persönlich besser.

Nun einmal zu einer Stilfrage:
Ich habe ja überwiegend statische Member, weil die AStar-Klasse abstrakt ist. Wäre es "besser" oder "schöner", diese Klasse instanziibar zu machen? Wenn ja, wieso?
Dazu hatte ich mir mal überlegt, der Klasse bei Instanziierung ein World-Objekt zu übergehen und vorarb schonmal zu untersuchen (z.B auf begehbare Flächen), sodass die Pfadermittlung möglicherweise zügiger vonstatten geht.
 
mit dem statischen CompareMode bist zu etwas angreifbar, was ist wenn jemand das mitten in der Suche ändert?
lege dir lieber eine lokale Kopie zu Beginn deiner Methode an,

mit Objekten hättest du etwas mehr Möglichkeiten, etwa zwei Suchen gleichzeitig mit unterschiedlichen CompareMode,
ginge als weiterer statischer Parameter natürlich auch,

dann noch alle allgemeinen OO-Features, Untermethoden ohne Endlos-Parameter,
Rückgabewert muss nicht alle Infos als Stack ausgedrückt enthalten, das Objekt könnte nach der Berechnung noch weiterbestehen und befragt werden, getNearestNode(), wieSchwerFandenSieDieseAufgabe() usw.
 

Neue Themen


Zurück
Oben