de._13ducks.cor.game.server.movement.SectorPathfinder.java Source code

Java tutorial

Introduction

Here is the source code for de._13ducks.cor.game.server.movement.SectorPathfinder.java

Source

/*
 *  Copyright 2008, 2009, 2010, 2011:
 *   Tobias Fleig (tfg[AT]online[DOT]de),
 *   Michael Haas (mekhar[AT]gmx[DOT]de),
 *   Johannes Kattinger (johanneskattinger[AT]gmx[DOT]de)
 *
 *  - All rights reserved -
 *
 *
 *  This file is part of Centuries of Rage.
 *
 *  Centuries of Rage is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Centuries of Rage is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Centuries of Rage.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package de._13ducks.cor.game.server.movement;

import de._13ducks.cor.game.SimplePosition;
import java.util.*;
import org.apache.commons.collections.buffer.PriorityBuffer;

/**
 * Der Serverpathfinder.
 * Sucht Wege zwischen Knoten (Nodes)
 * Um einen echten Weg zu bekommen, muss der Weg danach noch berarbeitet werden, aber das ist nicht Aufgabe des
 * Pathfinders.
 * @author tfg
 */
public final class SectorPathfinder {

    /**
     * Niemand kann einen Pathfinder erstellen, dies ist eine Utilityclass
     */
    private SectorPathfinder() {
    }

    public static synchronized List<Node> findPath(SimplePosition start, SimplePosition target,
            FreePolygon startSector, MovementMap moveMap) {

        if (start == null || target == null) {
            System.out.println("FixMe: SPathfinder, irregular call: " + start + "-->" + target);
            return null;
        }

        FreePolygon targetSector = moveMap.containingPoly(target.x(), target.y());

        if (targetSector == null) {
            // Ziel ungltig abbrechen
            System.out.println("Irregular target. Aborting");
            return null;
        }
        FakeNode startNode = new FakeNode(start.x(), start.y(), startSector);
        Node targetNode = new FakeNode(target.x(), target.y(), targetSector);
        targetNode.addPolygon(targetSector);

        // Der Startknoten muss die Member seines Polys kennen
        startNode.setReachableNodes(computeDirectReachable(startNode, startSector));
        // Der Zielknoten muss den Membern seines Polys bekannt sein
        // Die Movement-Map darf aber nicht verndert werden. Des halb mssen einige Aufrufe intern abgefangen werden und das reingedoktert werden.
        List<Node> preTargetNodes = Arrays.asList(computeDirectReachable(targetNode, targetSector));

        PriorityBuffer open = new PriorityBuffer(); // Liste fr entdeckte Knoten
        LinkedHashSet<Node> containopen = new LinkedHashSet<Node>(); // Auch fr entdeckte Knoten, hiermit kann viel schneller festgestellt werden, ob ein bestimmter Knoten schon enthalten ist.
        LinkedHashSet<Node> closed = new LinkedHashSet<Node>(); // Liste fr fertig bearbeitete Knoten

        double cost_t = 0; //Movement Kosten (gerade 5, diagonal 7, wird spter festgelegt)

        startNode.setCost(0); //Kosten fr das Startfeld (von dem aus berechnet wird) sind natrlich 0
        open.add(startNode); //Startfeld in die openlist
        containopen.add(startNode);
        targetNode.setParent(null); //"Vorgngerfeld" vom Zielfeld noch nicht bekannt

        for (int j = 0; j < 40000; j++) { //Anzahl der maximalen Durchlufe, bis Wegfindung aufgibt

            if (open.isEmpty()) { //Abbruch, wenn openlist leer ist => es gibt keinen Weg
                return null;
            }

            // Sortieren nicht mehr ntig, PriorityBuffer bewahrt die Felder in der Reihenfolge ihrer Priority - also dem F-Wert auf
            Node current = (Node) open.remove(); //der Eintrag aus der openlist mit dem niedrigesten F-Wert rausholen und gleich lschen
            containopen.remove(current);
            if (current.equals(targetNode)) { //Abbruch, weil Weg von Start nach Ziel gefunden wurde
                targetNode.setParent(current.getParent()); //"Vorgngerfeld" von Ziel bekannt
                break;
            }

            // Aus der open wurde current bereits gelscht, jetzt in die closed verschieben
            closed.add(current);

            List<Node> neighbors = computeNeighbors(current, targetNode, preTargetNodes);

            for (Node node : neighbors) {

                if (closed.contains(node)) {
                    continue;
                }

                // Kosten dort hin berechnen
                cost_t = current.movementCostTo(node);

                if (containopen.contains(node)) { //Wenn sich der Knoten in der openlist befindet, muss berechnet werden, ob es einen krzeren Weg gibt

                    if (current.getCost() + cost_t < node.getCost()) { //krzerer Weg gefunden?

                        node.setCost(current.getCost() + cost_t); //-> Wegkosten neu berechnen
                        node.setValF(node.getCost() + node.getHeuristic()); //F-Wert, besteht aus Wegkosten vom Start + Luftlinie zum Ziel
                        node.setParent(current); //aktuelles Feld wird zum Vorgngerfeld
                    }
                } else {
                    node.setCost(current.getCost() + cost_t);
                    node.setHeuristic(Math.sqrt(Math.pow(Math.abs((targetNode.getX() - node.getX())), 2)
                            + Math.pow(Math.abs((targetNode.getY() - node.getY())), 2))); // geschtzte Distanz zum Ziel
                    //Die Zahl am Ende der Berechnung ist der Aufwand der Wegsuche
                    //5 ist schnell, 4 normal, 3 dauert lange

                    node.setParent(current); // Parent ist die RogPosition, von dem der aktuelle entdeckt wurde
                    node.setValF(node.getCost() + node.getHeuristic()); //F-Wert, besteht aus Wegkosten vom Start aus + Luftlinie zum Ziel
                    open.add(node); // in openlist hinzufgen
                    containopen.add(node);
                }

            }
        }

        if (targetNode.getParent() == null) { //kein Weg gefunden
            return null;
        }

        ArrayList<Node> pathrev = new ArrayList<Node>(); //Pfad aus parents erstellen, von Ziel nach Start
        while (!targetNode.equals(startNode)) {
            pathrev.add(targetNode);
            targetNode = targetNode.getParent();
        }
        pathrev.add(startNode);

        ArrayList<Node> path = new ArrayList<Node>(); //Pfad umkehren, sodass er von Start nach Ziel ist
        for (int k = pathrev.size() - 1; k >= 0; k--) {
            path.add(pathrev.get(k));
        }

        // Der folgende Algorithmus braucht Polygon-Infos, diese also hier einfgen
        startNode.addPolygon(startSector);
        targetNode.addPolygon(targetSector);

        /**
         * An dieser Stelle muss der Weg nocheinmal berarbeitet werden.
         * Es kann nmlich durch neue Tweaks sein, dass dies die Knoten nicht direkt
         * verbunden sind (also keinen gemeinsamen Polygon haben)
         * Das tritt z.B. bei der Start- und Zieleinsprungpunkt-Variierung auf.
         */
        for (int i = 0; i < path.size() - 1; i++) {
            Node n1 = path.get(i);
            Node n2 = path.get(i + 1);
            FreePolygon commonSector = commonSector(n1, n2);
            if (commonSector == null) {
                // Das hier ist der interessante Fall, die beiden Knoten sind nicht direkt verbunden, es muss ein Zwischenknoten eingefgt werden:
                // Dessen Punkt suchen
                Edge direct = new Edge(n1, n2);
                Node newNode = null;
                // Die Polygone von n1 durchprobieren
                for (FreePolygon currentPoly : n1.getPolygons()) {
                    List<Edge> edges = currentPoly.calcEdges();
                    for (Edge testedge : edges) {
                        // Gibts da einen Schnitt?
                        SimplePosition intersection = direct.intersectionWithEndsNotAllowed(testedge);
                        if (intersection != null) {
                            // Kandidat fr den nchsten Polygon
                            FreePolygon nextPoly = null;
                            // Kante gefunden
                            // Von dieser Kante die Enden suchen
                            nextPoly = getOtherPoly(testedge.getStart(), testedge.getEnd(), currentPoly);

                            newNode = intersection.toNode();
                            newNode.addPolygon(currentPoly);
                            newNode.addPolygon(nextPoly);
                            break;
                        }
                    }
                    if (newNode != null) {
                        break;
                    }
                }

                if (newNode == null) {
                    // Das drfte nicht passieren, der Weg ist legal gefunden worden, muss also eigentlich existieren
                    System.out.println("[Pathfinder][ERROR]: Cannot insert Nodes into route, aborting!");
                    return null;
                } else {
                    path.add(i + 1, newNode);
                }
            }
        }

        return path; //Pfad zurckgeben
    }

    /**
     * Der Start und Zielknoten sind von weit mehr als nur den Knoten ihres Polygons erreichbar.
     * Dies muss bereits whrend der Basic-Berechnung beachtet werden, das kann die Pfadoptimierung nachtrglich nichtmehr leisten.
     * Also alle Nodes suchen, die ohne Hinderniss direkt erreichbar sind
     * @param from alle vollstndig im Mesh liegenden Kanten von hier zu Nachbarknoten suchen
     * @param basicPolygon Der Polygon, in dem der Node drinliegt.
     * @return alle direkt erreichbaren Knoten (natrlich sind die des basicPolygons auch dabei)
     */
    private static Node[] computeDirectReachable(Node from, FreePolygon basicPolygon) {
        // Das ist eine modifizierte Breitensuche:
        LinkedList<FreePolygon> open = new LinkedList<FreePolygon>(); // Queue fr zu untersuchende Polygone
        LinkedHashSet<FreePolygon> openContains = new LinkedHashSet<FreePolygon>(); // Welche Elemente die open enthlt (schnellerer Test)
        LinkedHashSet<FreePolygon> closed = new LinkedHashSet<FreePolygon>();
        LinkedHashSet<Node> testedNodes = new LinkedHashSet<Node>();
        LinkedList<Node> result = new LinkedList<Node>();
        open.offer(basicPolygon); // Start-Polygon
        openContains.add(basicPolygon);

        while (!open.isEmpty()) {
            // Diesen hier bearbeiten wir jetzt
            FreePolygon poly = open.poll();
            openContains.remove(poly);
            closed.add(poly);

            boolean containsreachableNodes = false;
            // Alle Nodes dieses Knotens untersuchen
            for (Node node : poly.getNodes()) {
                // Schon bekannt?
                if (result.contains(node)) {
                    // Bekannt und ok
                    containsreachableNodes = true;
                } else {
                    if (testedNodes.contains(node)) {
                        // Der geht nicht
                    } else {
                        // Testen!
                        FreePolygon currentPoly = basicPolygon;
                        // Testweise Kante zwischen from und node erstellen
                        Edge edge = new Edge(from, node);
                        // Im Folgenden wird untersucht, ob der neue Weg "edge" passierbar ist.
                        // Damit wir beim Dreieckwechsel nicht wieder zurck gehen:
                        Node lastNode = null;

                        boolean routeAllowed = true;

                        // Jetzt so lange weiter laufen, bis wir im Ziel-Polygon sind
                        while (!node.getPolygons().contains(currentPoly)) {
                            // Untersuchen, ob es eine Seite des currentPolygons gibt, die sich mit der alternativRoute schneidet
                            List<Edge> edges = currentPoly.calcEdges();
                            Edge intersecting = null;
                            SimplePosition intersection = null;
                            for (Edge testedge : edges) {
                                // Gibts da einen Schnitt?
                                intersection = edge.intersectionWithEndsNotAllowed(testedge);
                                if (intersection != null && !intersection.equals(lastNode)) {
                                    intersecting = testedge;
                                    break;
                                }
                            }
                            // Kandidat fr den nchsten Polygon
                            FreePolygon nextPoly = null;
                            // Kante gefunden
                            if (intersecting != null) {
                                // Von dieser Kante die Enden suchen
                                nextPoly = getOtherPoly(intersecting.getStart(), intersecting.getEnd(),
                                        currentPoly);
                            }
                            if (intersecting != null && nextPoly != null) {
                                // Wir haben einen Schnittpunkt und eine Kante gefunden, sind jetzt also in einem neuen Polygon
                                // Extra Node bentigt
                                Node extraNode = intersection.toNode();

                                extraNode.addPolygon(nextPoly);
                                extraNode.addPolygon(currentPoly);
                                lastNode = extraNode;
                                currentPoly = nextPoly;
                                // Der nchste Schleifendurchlauf wird den nchsten Polygon untersuchen
                            } else {
                                // Es gab leider keinen betretbaren Polygon hier.
                                // Das bedeutet, dass wir die Suche abbrechen knnen, es gibt hier keinen direkten Weg
                                routeAllowed = false;
                                break;
                            }

                        }

                        // Wenn der neue Weg gltig war, einbauen. Sonst weiter mit dem nchsten Knoten
                        if (routeAllowed) {
                            // In die erlaubt-Liste:
                            result.add(node);
                            testedNodes.add(node);
                            containsreachableNodes = true;
                        } else {
                            testedNodes.add(node);
                        }
                    }
                }
            }

            // Nur weiter in die Tiefe gehen, wenn mindestens einer erreichbar war
            if (containsreachableNodes) {
                // Alle Nachbarn untersuchen:
                for (FreePolygon n : poly.getNeighbors()) {
                    // Schon bekannt/bearbeitet?
                    if (!openContains.contains(n) && !closed.contains(n)) {
                        // Nein, also auch zur Bearbeitung vorsehen
                        open.add(n);
                        openContains.add(n);
                    }
                }
            }
        }
        return result.toArray(new Node[0]);
    }

    private static List<Node> computeNeighbors(Node current, Node target, List<Node> preTargetNodes) {
        List<Node> originalNodes = current.getReachableNodes();
        if (preTargetNodes.contains(current)) {
            originalNodes.add(target);
        }
        return originalNodes;
    }

    public static List<SimplePosition> optimizePath(List<Node> path, SimplePosition startPos, SimplePosition endPos,
            MovementMap moveMap) {
        // Besseres, iteratives Vorgehen

        FreePolygon startPolygon = moveMap.containingPoly(startPos.x(), startPos.y());
        if (startPolygon == null) {
            System.out.println("ERROR! Target unreachable (no poly found)");
            return null;
        }

        boolean improved = true;

        while (improved) {
            improved = false;

            FreePolygon currentPoly = startPolygon;

            // Weg durchgehen

            for (int i = 1; i < path.size() - 1; i++) {
                Node pre = path.get(i - 1);
                Node cur = path.get(i);
                Node nxt = path.get(i + 1);

                // Testweise Kante zwischen pre und nxt erstellen

                Edge edge = new Edge(pre, nxt);

                // Im Folgenden wird untersucht, ob der neue Weg "edge" passierbar ist.
                // Eventuell mssen fr Polygonwechsel neue Nodes eingefgt werden

                LinkedList<Node> extraNodes = new LinkedList<Node>();
                // Damit wir beim Dreieckwechsel nicht wieder zurck gehen:
                Node lastNode = null;

                boolean routeAllowed = true;

                // Jetzt so lange weiter laufen, bis wir im Ziel-Polygon sind
                while (!nxt.getPolygons().contains(currentPoly)) {
                    // Untersuchen, ob es eine Seite des currentPolygons gibt, die sich mit der alternativRoute schneidet
                    List<Edge> edges = currentPoly.calcEdges();
                    Edge intersecting = null;
                    for (Edge testedge : edges) {
                        // Gibts da einen Schnitt?
                        SimplePosition intersection = edge.intersectionWithEndsNotAllowed(testedge);
                        if (intersection != null && !intersection.equals(lastNode)) {
                            intersecting = testedge;
                            break;
                        }
                    }
                    // Kandidat fr den nchsten Polygon
                    FreePolygon nextPoly = null;
                    // Kante gefunden
                    if (intersecting != null) {
                        // Von dieser Kante die Enden suchen
                        nextPoly = getOtherPoly(intersecting.getStart(), intersecting.getEnd(), currentPoly);
                    }
                    if (intersecting != null && nextPoly != null) {
                        // Wir haben einen Schnittpunkt und eine Kante gefunden, sind jetzt also in einem neuen Polygon
                        // Extra Node einfgen
                        Node extraNode = intersecting.intersectionWithEndsNotAllowed(edge).toNode();

                        if (extraNode.equals(cur)) {
                            // Abbruch, das ist eine Gerade, hier kann man nicht abkrzen!
                            FreePolygon currentCand = commonSector(cur, nxt);
                            if (currentCand != null) {
                                currentPoly = currentCand;
                            }
                            routeAllowed = false;
                            break;
                        }

                        extraNode.addPolygon(nextPoly);
                        extraNode.addPolygon(currentPoly);
                        extraNodes.add(extraNode);
                        lastNode = extraNode;
                        currentPoly = nextPoly;
                        // Der nchste Schleifendurchlauf wird den nchsten Polygon untersuchen
                    } else {
                        // Es gab leider keinen betretbaren Polygon hier.
                        // Das bedeutet, dass wir die Suche abbrechen knnen, der derzeit untersuchte Wegpunkt (cur)
                        // Ist also unverzichtbar.
                        // Es soll also der nchste Punkt untersucht werden, also die for einfach weiter laufen
                        // Eventuell muss aber der currentPoly gendert werden.
                        // CurrentPoly ndern, wenn in neuem Sektor:
                        FreePolygon currentCand = commonSector(cur, nxt);
                        if (currentCand != null) {
                            currentPoly = currentCand;
                        }
                        routeAllowed = false;
                        break;
                    }

                }

                // Wenn der neue Weg gltig war, einbauen. Sonst weiter mit dem nchsten Knoten
                if (routeAllowed) {
                    // Den ursprnglichen Knoten lschen und die neuen Einbauen
                    path.remove(i);
                    path.addAll(i, extraNodes);
                    // Der Weg wurde gendert, die for muss neu starten
                    improved = true;
                    break;
                }

                // Wenn wir hier hinkommen, soll der nchste Knoten getestet werden.
                extraNodes.clear();
            }

        }

        // Hier ist der Weg fertig optimiert
        // Start wieder lschen und zurckgeben
        path.remove(0);

        LinkedList<SimplePosition> retList = new LinkedList<SimplePosition>();
        for (Node n : path) {
            retList.add(n);
        }
        return retList;
    }

    private static FreePolygon getOtherPoly(Node n1, Node n2, FreePolygon myself) {
        for (FreePolygon poly : n1.getPolygons()) {
            if (poly.equals(myself)) {
                continue;
            }
            if (n2.getPolygons().contains(poly)) {
                return poly;
            }
        }
        return null;
    }

    /**
     * Findet einen Sektor, den beide Knoten gemeinsam haben
     * @param n1 Knoten 1
     * @param n2 Knoten 2
     */
    private static FreePolygon commonSector(Node n1, Node n2) {
        for (FreePolygon poly : n1.getPolygons()) {
            if (n2.getPolygons().contains(poly)) {
                return poly;
            }
        }
        return null;
    }
}