/** * eAdventure (formerly <e-Adventure> and <e-Game>) is a research project of the * <e-UCM> research group. * * Copyright 2005-2010 <e-UCM> research group. * * You can access a list of all the contributors to eAdventure at: * http://e-adventure.e-ucm.es/contributors * * <e-UCM> is a research group of the Department of Software Engineering * and Artificial Intelligence at the Complutense University of Madrid * (School of Computer Science). * * C Profesor Jose Garcia Santesmases sn, * 28040 Madrid (Madrid), Spain. * * For more info please visit: <http://e-adventure.e-ucm.es> or * <http://www.e-ucm.es> * * **************************************************************************** * * This file is part of eAdventure, version 2.0 * * eAdventure is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * eAdventure 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with eAdventure. If not, see <http://www.gnu.org/licenses/>. */ package es.eucm.ead.engine.gameobjects.trajectories.dijkstra; import com.google.inject.Singleton; import es.eucm.ead.engine.factories.SceneElementFactory; import es.eucm.ead.engine.game.ValueMap; import es.eucm.ead.engine.gameobjects.sceneelements.SceneElementGO; import es.eucm.ead.model.elements.operations.ElementField; import es.eucm.ead.model.elements.scenes.SceneElement; import es.eucm.ead.model.elements.trajectories.Node; import es.eucm.ead.model.elements.trajectories.NodeTrajectory; import es.eucm.ead.model.elements.trajectories.Side; import es.eucm.ead.model.params.util.Position; import es.eucm.ead.model.params.util.Rectangle; import java.util.*; /** * Dijkstra's algorithm based {@link es.eucm.ead.engine.gameobjects.trajectories.TrajectoryGO} for {@link NodeTrajectory} * trajectory types. * */ @Singleton public class DijkstraNodeTrajectoryGenerator { private SceneElementFactory gameObjectFactory; /** * The values in the game */ private ValueMap valueMap; public DijkstraNodeTrajectoryGenerator( SceneElementFactory gameObjectFactory, ValueMap valueMap) { this.gameObjectFactory = gameObjectFactory; this.valueMap = valueMap; } public Path getTrajectory(NodeTrajectory trajectoryDefinition, SceneElement movingElement, float x, float y) { return pathToNearestPoint(trajectoryDefinition, movingElement, x, y, null); } public Path getTrajectory(NodeTrajectory trajectoryDefinition, SceneElement movingElement, float x, float y, SceneElementGO sceneElement) { return pathToNearestPoint(trajectoryDefinition, movingElement, x, y, sceneElement); } public boolean canGetTo(NodeTrajectory trajectoryDefinition, SceneElement movingElement, SceneElementGO sceneElement) { return pathToNearestPoint(trajectoryDefinition, movingElement, sceneElement.getX(), sceneElement.getY(), sceneElement) .isGetsTo(); } /** * Returns a {@link Path} from the a point to another. * <p> * Uses an algorithm based on Dijkstra's * (http://en.wikipedia.org/wiki/Dijkstra's_algorithm), where all relevant * points in the representation (including those in each side that are * closest to the target) are turned into nodes. * * @param toX * The current position along the x-axis * @param toY * The current position along the y-axis * @return The path to the destination */ private Path pathToNearestPoint(NodeTrajectory trajectoryDefinition, SceneElement movingElement, float toX, float toY, SceneElementGO sceneElement) { Map<String, DijkstraNode> nodeMap = new HashMap<String, DijkstraNode>(); List<DijkstraNode> nodeList = new ArrayList<DijkstraNode>(); for (Node node : trajectoryDefinition.getNodes()) { DijkstraNode dNode = new DijkstraNode(new Position(node.getX(), node.getY())); dNode.setScale(node.getScale()); dNode.calculateGoalDistance(toX, toY); nodeMap.put(node.getId(), dNode); nodeList.add(dNode); dNode.setGetsTo(isGetsTo(dNode.getPosition(), sceneElement)); } DijkstraNode currentNode = generateSides(trajectoryDefinition, nodeMap, movingElement, toX, toY, sceneElement); if (currentNode == null) { return new Path(); } Map<DijkstraNode, PathInfo> map = getMap(currentNode); return decodePath(currentNode, map); } /** * Generate the map, used to calculate the best path and distance to each * node * * @param currentNode * @return */ public Map<DijkstraNode, PathInfo> getMap(DijkstraNode currentNode) { Map<DijkstraNode, PathInfo> map = new HashMap<DijkstraNode, PathInfo>(); PathInfo pathInfo = new PathInfo(); pathInfo.length = 0; map.put(currentNode, pathInfo); List<DijkstraNode> visitNodes = new ArrayList<DijkstraNode>(); visitNodes.add(currentNode); List<DijkstraNode> visitedNodes = new ArrayList<DijkstraNode>(); visitedNodes.add(currentNode); while (!visitNodes.isEmpty()) { DijkstraNode node = visitNodes.get(0); visitNodes.remove(0); if (node.isBreakNode()) continue; for (PathSide side : node.getSides()) { DijkstraNode other = side.getOtherNode(node); if (map.get(other) == null) map.put(other, new PathInfo()); if (map.get(node).length + side.getLength() < map.get(other).length) { map.get(other).length = map.get(node).length + side.getLength(); map.get(other).path = side; if (!visitedNodes.contains(other)) { visitNodes.add(other); visitedNodes.add(other); } } } } return map; } /** * Decode the Dijkstra path * * @param currentNode * @param map * @return */ private Path decodePath(DijkstraNode currentNode, Map<DijkstraNode, PathInfo> map) { DijkstraNode bestNode = currentNode; for (DijkstraNode node : map.keySet()) { if (node.isGetsTo() && !bestNode.isGetsTo()) bestNode = node; else if (!node.isGetsTo() && bestNode.isGetsTo()) continue; else if (node.getGoalDistance() < bestNode.getGoalDistance()) bestNode = node; } Path path = new Path(); DijkstraNode lastNode = bestNode; path.setGetsTo(lastNode.isGetsTo()); while (bestNode != currentNode) { PathSide side = map.get(bestNode).path; if (bestNode == lastNode) { // This code is needed so that the avatar stops just before // reaching a barrier or similar // it should not affect anything else. float x1 = bestNode.getPosition().getX(); float y1 = bestNode.getPosition().getY(); float x21 = side.getOtherNode(bestNode).getPosition().getX() - x1; float y21 = side.getOtherNode(bestNode).getPosition().getY() - y1; if (x21 != 0) x1 += x21 / Math.abs(x21); if (y21 != 0) y1 += y21 / Math.abs(y21); side.setEndPosition(new Position(x1, y1)); } else side.setEndPosition(bestNode.getPosition()); side.setEndScale(bestNode.getScale()); bestNode = side.getOtherNode(bestNode); path.addSide(side); } return path; } /** * Class to store information about a DijkstraPath, usded in the Dijkstra * algorithm */ private class PathInfo { public float length = Integer.MAX_VALUE; public PathSide path = null; } /** * Generate the sides in the Dijkstra trajectory representation. The sides * are assigned to the nodes where they start or end. * * @param trajectoryDefinition * @param nodeMap * @param toX * @param toY * @param sceneElement * @return */ private DijkstraNode generateSides(NodeTrajectory trajectoryDefinition, Map<String, DijkstraNode> nodeMap, SceneElement movingElement, float toX, float toY, SceneElementGO sceneElement) { Side currentSide = getCurrentSide(trajectoryDefinition, movingElement); DijkstraNode currentNode = null; for (Side side : trajectoryDefinition.getSides()) { DijkstraNode start = nodeMap.get(side.getIdStart().getId()); DijkstraNode end = nodeMap.get(side.getIdEnd().getId()); Position currentPosition = getCurrentPosition(movingElement); List<DijkstraNode> intersections = new ArrayList<DijkstraNode>(); intersections.add(start); if (side == currentSide) { if (currentPosition.getX() == start.getPosition().getX() && currentPosition.getY() == start.getPosition().getY()) currentNode = start; else if (currentPosition.getX() == end.getPosition().getX() && currentPosition.getY() == end.getPosition().getY()) currentNode = end; else { currentNode = new DijkstraNode(currentPosition); currentNode.setScale(valueMap.getValue(movingElement, SceneElement.VAR_SCALE, 1f)); intersections.add(currentNode); } } intersections.add(end); addClosestPoint(intersections, toX, toY); addBarrierIntersections(trajectoryDefinition, intersections); if (sceneElement != null) addInfluenceAreaIntersections(sceneElement, intersections); for (DijkstraNode newNode : intersections) { newNode.calculateGoalDistance(toX, toY); newNode .setGetsTo(isGetsTo(newNode.getPosition(), sceneElement)); } for (int i = 0; i < intersections.size() - 1; i++) { DijkstraNode s = intersections.get(i); DijkstraNode e = intersections.get(i + 1); double length = getLength(s.getPosition(), e.getPosition()); PathSide pathSide = new PathSide(s, e, side.getLength() * length / side.getRealLength(), length, side); s.addSide(pathSide); e.addSide(pathSide); } } return currentNode; } /** * @param s * a position in the 2D plane * @param e * a position in the 2D plane * @return The length of the side from e to s */ public double getLength(Position s, Position e) { return Math.sqrt(Math.pow(s.getX() - e.getX(), 2) + Math.pow(s.getY() - e.getY(), 2)); } /** * Add the closest point (if different to one of the nodes already in the * intersections list) to the intersections list as a new node * * @param intersections * the list of nodes in the path * @param toX * the target x * @param toY * the target y */ private void addClosestPoint(List<DijkstraNode> intersections, float toX, float toY) { for (int i = 0; i < intersections.size() - 1; i++) { DijkstraNode newNode = getClosestPosition(intersections.get(i) .getPosition(), intersections.get(i).getScale(), intersections.get(i + 1).getPosition(), intersections.get( i + 1).getScale(), toX, toY); if (newNode != null) { intersections.add(i + 1, newNode); break; } } } /** * Inspired by code in * http://www.gamedev.net/topic/444154-closest-point-on-a-line/ * * @param A * one of the nodes in the the segment * @param B * the other node in the segment * @return null if the closest point is one of the nodes, or a position * otherwise */ private DijkstraNode getClosestPosition(Position A, float scaleA, Position B, float scaleB, float toX, float toY) { float APx = toX - A.getX(); float APy = toY - A.getY(); float ABx = B.getX() - A.getX(); float ABy = B.getY() - A.getY(); float ab2 = ABx * ABx + ABy * ABy; float ap_ab = APx * ABx + APy * ABy; float t = ap_ab / ab2; if (ab2 == 0 || t <= 0.01f || t >= 0.99f) return null; float x = A.getX(); x += ABx * t; float y = A.getY(); y += ABy * t; DijkstraNode node = new DijkstraNode(new Position((int) x, (int) y)); node.setScale(scaleA + t * (scaleB - scaleA)); return node; } /** * Add the intersections to the influence area of the scene element with the * current side * * @param sceneElement * the scene element with the influence area * @param intersections * the current intersections or nodes of the side */ private void addInfluenceAreaIntersections(SceneElementGO sceneElement, List<DijkstraNode> intersections) { Rectangle rectangle = valueMap.getValue(new ElementField( (SceneElement) sceneElement.getElement(), NodeTrajectory.VAR_INFLUENCE_AREA), (Rectangle) null); // TODO check if the position of the element isn't relevant (i.e. if the // position of the rectangle is not relative to the element) if (rectangle != null) { Position position = new Position(rectangle.getX(), rectangle.getY()); int i = 0; while (i < intersections.size() - 1) { List<DijkstraNode> newIntersections = getIntersections( intersections.get(i), intersections.get(i + 1), rectangle.getWidth(), rectangle.getHeight(), position); for (DijkstraNode newNode : newIntersections) newNode.setGetsTo(true); intersections.addAll(i + 1, newIntersections); i += newIntersections.size() + 1; } } } private boolean isGetsTo(Position position, SceneElementGO sceneElement) { if (sceneElement == null) return false; Rectangle rectangle = valueMap.getValue(new ElementField( (SceneElement) sceneElement.getElement(), NodeTrajectory.VAR_INFLUENCE_AREA), (Rectangle) null); if (rectangle == null) return false; // TODO check if the position of the element isn't relevant (i.e. if the // position of the rectangle is not relative to the element) if (rectangle.getX() < position.getX() && rectangle.getY() <= position.getY() && rectangle.getX() + rectangle.getWidth() >= position.getX() && rectangle.getY() + rectangle.getHeight() >= position.getY()) return true; return false; } /** * Add the intersections to the barriers to the list of intersections of the * current side * * @param trajectoryDefinition * The trajectory definition, to get the barriers * @param intersections * The current intersections or nodes of the side */ private void addBarrierIntersections(NodeTrajectory trajectoryDefinition, List<DijkstraNode> intersections) { for (SceneElement barrier : trajectoryDefinition.getBarriers()) { SceneElementGO go = gameObjectFactory.get(barrier); ElementField barrierOn = new ElementField(barrier, NodeTrajectory.VAR_BARRIER_ON); if (valueMap.getValue(barrierOn, false)) { Position position = new Position(go.getX(), go.getY(), go .getDispX(), go.getDispY()); int i = 0; while (i < intersections.size() - 1) { List<DijkstraNode> newIntersections = getIntersections( intersections.get(i), intersections.get(i + 1), (int) (go.getWidth() * go.getScaleX()), (int) (go .getHeight() * go.getScaleY()), position); for (DijkstraNode newNode : newIntersections) newNode.setBreakNode(true); intersections.addAll(i + 1, newIntersections); i += newIntersections.size() + 1; } } } } /** * Returns a list with the {@link DijkstraNode}s representing the * intersections of a line from start to end with the rectangle in the given * {@link Position} with a width and height * * @param start * The start {@link DijkstraNode} * @param end * The end {@link DijkstraNode} * @param width * The width of the rectangle * @param height * The height of the rectangle * @param position * The {@link Position} of the rectangle * @return A list of the intersections as {@link DijkstraNode}s */ private List<DijkstraNode> getIntersections(DijkstraNode start, DijkstraNode end, int width, int height, Position position) { ArrayList<DijkstraNode> intersections = new ArrayList<DijkstraNode>(); int x = position.getJavaX(width); int y = position.getJavaY(height); float startX = start.getPosition().getX(); float startY = start.getPosition().getY(); float endX = end.getPosition().getX(); float endY = end.getPosition().getY(); float startScale = start.getScale(); float endScale = end.getScale(); DijkstraNode temp = getIntersection(startX, startY, startScale, endX, endY, endScale, x, y, x + width, y); if (temp != null) intersections.add(temp); temp = getIntersection(startX, startY, startScale, endX, endY, endScale, x + width, y, x + width, y + height); if (temp != null) intersections.add(temp); temp = getIntersection(startX, startY, startScale, endX, endY, endScale, x, y + height, x + width, y + height); if (temp != null) intersections.add(temp); temp = getIntersection(startX, startY, startScale, endX, endY, endScale, x, y, x, y + height); if (temp != null) intersections.add(temp); Collections.sort(intersections, new Comparator<DijkstraNode>() { @Override public int compare(DijkstraNode arg0, DijkstraNode arg1) { if (arg0.getLinePosition() < arg1.getLinePosition()) return -1; return 1; } }); return intersections; } /** * Using formula from http://paulbourke.net/geometry/lineline2d/ * * @return */ private DijkstraNode getIntersection(float x1, float y1, float scale1, float x2, float y2, float scale2, int x3, int y3, int x4, int y4) { float den = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if (den == 0) return null; float x13 = x1 - x3; float y13 = y1 - y3; float numA = (x4 - x3) * y13 - (y4 - y3) * x13; float numB = (x2 - x1) * y13 - (y2 - y1) * x13; float uA = numA / den; float uB = numB / den; if (uA < 0 || uB < 0 || uA > 1 || uB > 1) return null; int x = (int) (x1 + uA * (x2 - x1)); int y = (int) (y1 + uA * (y2 - y1)); float scale = scale1 + uA * (scale2 - scale1); DijkstraNode node = new DijkstraNode(new Position(x, y), uA); node.setScale(scale); return node; } /** * Get current side from variables or choose a side from the closest node if * none is available * * @param nodeTrajectoryDefinition * @return */ private Side getCurrentSide(NodeTrajectory nodeTrajectoryDefinition, SceneElement movingElement) { Side side = valueMap.getValue(movingElement, NodeTrajectory.VAR_CURRENT_SIDE, null); if (!nodeTrajectoryDefinition.getSides().contains(side)) side = null; if (side == null) { int distance = Integer.MAX_VALUE; for (Node node : nodeTrajectoryDefinition.getNodes()) { int d = (int) Math.sqrt(Math.pow(node.getX() - valueMap.getValue(movingElement, SceneElement.VAR_X, 0f), 2) + Math.pow(node.getY() - valueMap.getValue(movingElement, SceneElement.VAR_Y, 0f), 2)); if (d < distance) { for (Side side2 : nodeTrajectoryDefinition.getSides()) if (side2.getIdEnd().getId().equals(node.getId()) || side2.getIdStart().getId().equals( node.getId())) { side = side2; distance = d; } } } } return side; } private Position getCurrentPosition(SceneElement element) { float x = valueMap.getValue(element, SceneElement.VAR_X, 0); float y = valueMap.getValue(element, SceneElement.VAR_Y, 0); return new Position(x, y); } }